${mkSvg(icons.playCenter)}
00:00 / 00:00
[${curListIdx + 1}/${totalInList}] ${esc(item.name)}
${mkSvg(icons.pip)}
${mkSvg(icons.search)}
${mkSvg(icons.close)}
${curListIdx + 1} / ${totalInList}
`;
const v = d.querySelector('#pk_video');
const box = d.querySelector('#pk_p_box');
const btnPlay = d.querySelector('#pk_p_play');
const btnVol = d.querySelector('#pk_p_vol');
const slideVol = d.querySelector('#pk_p_vol_slide');
const btnFull = d.querySelector('#pk_p_full');
const btnClose = d.querySelector('#pk_p_close');
const btnSearch = d.querySelector('#pk_p_search');
const tCur = d.querySelector('#pk_t_cur');
const tDur = d.querySelector('#pk_t_dur');
const progArea = d.querySelector('#pk_p_prog_area');
const progFilled = d.querySelector('#pk_p_filled');
const resList = d.querySelector('#pk_p_res_list');
const resTxt = d.querySelector('#pk_p_res_txt');
const spdList = d.querySelector('#pk_p_spd_list');
const plist = d.querySelector('#pk_p_plist');
const pTab = d.querySelector('#pk_p_plist_tab');
const pScroll = d.querySelector('#pk_p_plist_scroll');
pScroll.onwheel = (e) => {
e.preventDefault();
pScroll.scrollBy({ left: e.deltaY > 0 ? 300 : -300, behavior: 'smooth' });
};
let pTip = document.getElementById('pk_p_plist_tip_global');
if (!pTip) {
pTip = document.createElement('div');
pTip.id = 'pk_p_plist_tip_global';
pTip.className = 'pk-p-plist-tip';
document.body.appendChild(pTip);
}
d.querySelector('#pk_p_side_L').onclick = (e) => {
e.stopPropagation();
const prevIdx = (curListIdx - 1 + totalInList) % totalInList;
softSwitch(prevIdx);
};
d.querySelector('#pk_p_side_R').onclick = (e) => {
e.stopPropagation();
const nextIdx = (curListIdx + 1) % totalInList;
softSwitch(nextIdx);
};
pTab.onmouseenter = () => box.classList.add('pk-tab-hover');
pTab.onmouseleave = () => box.classList.remove('pk-tab-hover');
const strip = d.querySelector('.pk-p-plist-strip');
if (strip) {
strip.onmouseenter = () => box.classList.add('pk-tab-hover');
strip.onmouseleave = () => box.classList.remove('pk-tab-hover');
}
pTab.onclick = (e) => {
e.stopPropagation();
plist.classList.toggle('open');
box.classList.toggle('plist-active');
pTab.setAttribute('data-pk-tip', plist.classList.contains('open') ? L.tip_plist_close : L.tip_plist_open);
if (plist.classList.contains('open')) {
const activeItem = pScroll.querySelector('.active');
if (activeItem) activeItem.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'center' });
}
};
pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => {
el.onmouseenter = (e) => {
if (!plist.classList.contains('open')) return;
pTip.innerHTML = `
${e.currentTarget.dataset.name} ${e.currentTarget.dataset.size}`;
pTip.style.display = 'block';
};
el.onmousemove = (e) => {
if (pTip.style.display === 'block') {
const tW = pTip.offsetWidth || 150;
pTip.style.left = (e.clientX - (tW / 2)) + 'px';
pTip.style.top = (e.clientY - 60) + 'px';
}
};
el.onmouseleave = () => pTip.style.display = 'none';
el.onclick = (e) => {
e.stopPropagation();
if (pTip) pTip.style.display = 'none';
const idx = parseInt(e.currentTarget.dataset.idx);
if (idx === curListIdx) return;
softSwitch(idx);
};
});
const updatePlistNav = () => {
const sl = pScroll.scrollLeft;
const sw = pScroll.scrollWidth;
const cw = pScroll.clientWidth;
d.querySelector('#pk_p_plist_L').style.display = sl <= 5 ? 'none' : 'flex';
d.querySelector('#pk_p_plist_R').style.display = (sl + cw >= sw - 5) ? 'none' : 'flex';
};
d.querySelector('#pk_p_plist_L').onclick = (e) => {
e.stopPropagation();
pScroll.scrollBy({ left: -400, behavior: 'smooth' });
setTimeout(updatePlistNav, 300);
};
d.querySelector('#pk_p_plist_R').onclick = (e) => {
e.stopPropagation();
pScroll.scrollBy({ left: 400, behavior: 'smooth' });
setTimeout(updatePlistNav, 300);
};
pScroll.addEventListener('scroll', updatePlistNav, { passive: true });
setTimeout(updatePlistNav, 50);
document.body.appendChild(d);
const initL = d.querySelector('#pk_p_side_L');
const initR = d.querySelector('#pk_p_side_R');
if (initL) initL.style.display = curListIdx === 0 ? 'none' : 'flex';
if (initR) initR.style.display = curListIdx === totalInList - 1 ? 'none' : 'flex';
let hideTimer = null;
let isMouseOverUI = false;
const resetHideTimer = () => {
box.classList.remove('ui-hidden');
if (hideTimer) clearTimeout(hideTimer);
if (isMouseOverUI) return;
hideTimer = setTimeout(() => {
if (!v.paused) box.classList.add('ui-hidden');
}, 3000);
};
const protectedUIs = [
d.querySelector('.pk-player-top'),
d.querySelector('.pk-player-controls'),
d.querySelector('.pk-p-prog-wrap')
];
protectedUIs.forEach(ui => {
if (!ui) return;
ui.addEventListener('mouseenter', () => { isMouseOverUI = true; if (hideTimer) clearTimeout(hideTimer); });
ui.addEventListener('mouseleave', () => { isMouseOverUI = false; resetHideTimer(); });
});
box.addEventListener('mouseleave', () => { if (!isMouseOverUI) box.classList.add('ui-hidden'); });
box.addEventListener('mouseenter', resetHideTimer);
box.addEventListener('mousemove', resetHideTimer);
box.addEventListener('click', resetHideTimer);
box.addEventListener('keydown', resetHideTimer);
resetHideTimer();
const handleVideoError = (e) => {
if (isSwitching) return;
if (!v.getAttribute('src') && !pkHls) return;
if (v.networkState === 2 && !v.error && !e.force) return;
const errCode = v.error ? v.error.code : (e.force ? 4 : 0);
const errMsg = v.error ? v.error.message : "";
console.warn(`[VideoError] Code: ${errCode}, Msg: ${errMsg}, Src: ${v.src || 'HLS'}`);
if (currentLink) failedUrls.add(currentLink);
if (lastWorkingLink && lastWorkingLink !== currentLink && !failedUrls.has(lastWorkingLink)) {
console.log(`[Compatibility] Target stream failed, rolling back to: ${lastWorkingResName}`);
if (resTxt) resTxt.textContent = `${L.str_compat_mode} (${lastWorkingResName})`;
const toast = document.createElement('div');
toast.style.cssText = "position:absolute;top:20px;left:50%;transform:translateX(-50%);background:rgba(217, 48, 37, 0.9);color:#fff;padding:6px 12px;border-radius:20px;font-size:12px;z-index:100;animation:pkFadeIn 0.5s;";
toast.textContent = L.str_switch_compat.replace('{n}', lastWorkingResName);
box.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
currentResName = lastWorkingResName;
currentLink = lastWorkingLink;
const savedTime = v.currentTime;
loadSource(currentLink, savedTime);
v.play().catch(()=>{});
const resList = d.querySelector('#pk_p_res_list');
if (resList) {
resList.innerHTML = renderQualityMenu(qualityList, currentResName);
bindResEvents();
}
return;
}
const nextCandidate = qualityList.find(q => {
const url = q.link || q.url;
return url !== currentLink && !failedUrls.has(url);
});
if (nextCandidate) {
console.log(`[Compatibility] Auto-switching to next available source: ${nextCandidate.name}`);
if (resTxt) resTxt.textContent = `${L.str_compat_mode} (${nextCandidate.name})`;
const toast = document.createElement('div');
toast.style.cssText = "position:absolute;top:20px;left:50%;transform:translateX(-50%);background:rgba(33, 150, 243, 0.9);color:#fff;padding:6px 12px;border-radius:20px;font-size:12px;z-index:100;animation:pkFadeIn 0.5s;";
toast.textContent = L.msg_fallback_report.replace('{n}', nextCandidate.name);
box.appendChild(toast);
setTimeout(()=>toast.remove(), 4000);
currentResName = nextCandidate.name;
currentLink = nextCandidate.link || nextCandidate.url;
const savedTime = v.currentTime;
loadSource(currentLink, savedTime);
v.play().catch(()=>{});
const resList = d.querySelector('#pk_p_res_list');
if (resList) {
resList.innerHTML = renderQualityMenu(qualityList, currentResName);
bindResEvents();
}
return;
}
if (errCode !== 4 && !e.force) {
console.log("[Video] Recoverable error detected, staying in loader.");
return;
}
const loader = d.querySelector('.pk-p-loading');
if (loader) loader.style.display = 'none';
showSadBox(currentResName);
};
v.addEventListener('error', handleVideoError);
v.addEventListener('loadstart', () => {
const errOv = box.querySelector('.pk-err-ov');
if (errOv) errOv.remove();
const loader = d.querySelector('.pk-p-loading');
if (loader) loader.style.display = 'block';
});
(async () => {
try {
const targetApiId = getPhysicalId(item);
const newData = await apiGet(targetApiId);
const freshData = getBestSource(newData);
qualityList = freshData.list;
resList.innerHTML = renderQualityMenu(qualityList, currentResName);
bindResEvents();
if (v.error || !currentLink || (currentResName === L.str_original && freshData.name !== L.str_original)) {
const savedTime = v.currentTime;
currentLink = freshData.src;
currentResName = freshData.name;
resTxt.textContent = currentResName;
loadSource(currentLink);
v.currentTime = savedTime;
v.play().catch(()=>{});
resList.innerHTML = renderQualityMenu(qualityList, currentResName);
bindResEvents();
}
} catch (e) { }
})();
const fmtT = (s) => {
s = Math.max(0, s || 0);
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sc = Math.floor(s % 60);
return String(h).padStart(2, '0') + ":" + String(m).padStart(2, '0') + ":" + String(sc).padStart(2, '0');
};
const fmtFullT = fmtT;
const togglePlay = () => { if (v.paused) v.play(); else v.pause(); };
const ThumbnailEngine = (() => {
let shadowV = null;
let canvas = null;
let ctx = null;
let isInit = false;
let cacheStore = null;
let currentReqId = 0;
const BASE_HEIGHT = 180;
const DISPLAY_HEIGHT = 120;
const previewBox = d.querySelector('#pk_p_preview');
const imgBox = d.querySelector('#pk_p_img_box');
const previewTime = previewBox.querySelector('.pk-prev-time');
const init = async () => {
if (isInit) return;
if (window.localforage) {
cacheStore = window.localforage.createInstance({ name: 'pk_thumbs', storeName: 'snapshots' });
}
shadowV = document.createElement('video');
shadowV.muted = true;
shadowV.crossOrigin = 'anonymous';
shadowV.style.display = 'none';
shadowV.preload = 'auto';
shadowV.src = currentLink;
canvas = document.createElement('canvas');
canvas.width = 160; canvas.height = 90;
ctx = canvas.getContext('2d');
shadowV.onerror = () => console.warn("[Thumb] Shadow player error");
isInit = true;
};
const getCacheKey = (time) => `${getPhysicalId(item)}_${Math.floor(time)}`;
const generate = async (time) => {
if (!isInit) await init();
if (cacheStore) {
const cachedBlob = await cacheStore.getItem(getCacheKey(time));
if (cachedBlob) return URL.createObjectURL(cachedBlob);
}
return new Promise((resolve, reject) => {
const seekHandler = () => {
try {
const vw = shadowV.videoWidth || 160;
const vh = shadowV.videoHeight || 90;
const ratio = vw / vh;
const renderH = BASE_HEIGHT;
const renderW = Math.floor(renderH * ratio);
if (canvas.width !== renderW || canvas.height !== renderH) {
canvas.width = renderW;
canvas.height = renderH;
}
ctx.drawImage(shadowV, 0, 0, renderW, renderH);
canvas.toBlob((blob) => {
if (cacheStore) cacheStore.setItem(getCacheKey(time), blob).catch(()=>{});
resolve(URL.createObjectURL(blob));
}, 'image/jpeg', 0.8);
} catch (e) {
reject(e);
} finally {
shadowV.removeEventListener('seeked', seekHandler);
}
};
shadowV.addEventListener('seeked', seekHandler);
shadowV.currentTime = time;
});
};
const show = async (clientX, rect) => {
const boxRect = box.getBoundingClientRect();
const pos = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const targetTime = pos * v.duration;
if (!isFinite(targetTime)) return;
const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1;
let left = (clientX - boxRect.left) / scale;
const halfWidth = (imgBox.offsetWidth / 2) || 80;
const minX = halfWidth + 10;
const maxX = (boxRect.width / scale) - halfWidth - 10;
left = Math.max(minX, Math.min(maxX, left));
previewBox.style.left = `${left}px`;
previewTime.textContent = fmtT(targetTime);
previewBox.classList.add('show');
const myId = ++currentReqId;
try {
await sleep(50);
if (myId !== currentReqId) return;
const url = await generate(targetTime);
if (myId !== currentReqId) return;
const img = document.createElement('img');
img.src = url;
const onImgReady = () => {
if (myId !== currentReqId) return;
if (img.naturalWidth && img.naturalHeight) {
const ratio = img.naturalWidth / img.naturalHeight;
imgBox.style.width = `${DISPLAY_HEIGHT * ratio}px`;
imgBox.style.height = `${DISPLAY_HEIGHT}px`;
}
imgBox.style.display = 'flex';
img.classList.add('active');
imgBox.appendChild(img);
const oldImages = imgBox.querySelectorAll('img');
if (oldImages.length > 1) {
setTimeout(() => {
for (let i = 0; i < oldImages.length - 1; i++) {
oldImages[i].remove();
}
}, 150);
}
};
if (img.complete) onImgReady();
else img.onload = onImgReady;
} catch (e) {
}
};
const hide = () => {
previewBox.classList.remove('show');
imgBox.style.display = 'none';
currentReqId++;
setTimeout(() => {
if (!previewBox.classList.contains('show')) {
const imgs = imgBox.querySelectorAll('img');
imgs.forEach(i => i.remove());
}
}, 500);
};
const resetSource = (newUrl) => {
if (shadowV) shadowV.src = newUrl;
};
return { show, hide, resetSource };
})();
const updateState = () => {
if (v.paused) {
box.classList.add('paused');
btnPlay.innerHTML = mkSvg(icons.play);
box.classList.remove('ui-hidden');
if (hideTimer) clearTimeout(hideTimer);
}
else {
box.classList.remove('paused');
btnPlay.innerHTML = mkSvg(icons.pause);
resetHideTimer();
}
};
const updateVolUI = () => {
slideVol.value = v.muted ? 0 : v.volume;
btnVol.innerHTML = mkSvg((v.muted || v.volume === 0) ? icons.mute : icons.vol);
};
let transcodeTimer = null;
const destroyPlayer = () => {
if (isPlayerDestroyed) return;
isPlayerDestroyed = true;
document.removeEventListener('keydown', playerKeyHandler);
if (document.pictureInPictureElement) {
document.exitPictureInPicture().catch(() => {});
}
window.removeEventListener('resize', onResizeTransform);
if (transcodeTimer) { clearInterval(transcodeTimer); transcodeTimer = null; }
if (v.duration > 0 && v.currentTime > 5 && v.duration - v.currentTime > 5) {
gmSet('pk_progress_' + getPhysicalId(item), {
t: v.currentTime,
d: v.duration,
ts: Date.now()
});
}
v.pause();
v.muted = true;
if (pkHls) {
pkHls.stopLoad();
pkHls.detachMedia();
pkHls.destroy();
pkHls = null;
}
const targetId = item.id;
const targetIdx = S.display.findIndex(x => x.id === targetId);
if (targetIdx !== -1) {
S.sel.clear();
S.sel.add(targetId);
S.activeId = targetId;
const rowTop = targetIdx * CONF.rowHeight;
const vpHeight = UI.vp.clientHeight;
UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2));
renderVisible();
updateStat();
}
v.src = "";
v.load();
if (styleEl) styleEl.remove();
d.remove();
};
v.addEventListener('play', updateState);
v.addEventListener('pause', updateState);
const markStarted = () => {
if (isPlayerDestroyed) return;
if (box) {
box.classList.add('pk-v-started');
stopSpinner();
updateState();
lastWorkingLink = currentLink;
lastWorkingResName = currentResName;
}
};
v.addEventListener('playing', markStarted);
v.addEventListener('seeked', () => { if(v.currentTime > 0.1) markStarted(); });
v.addEventListener('click', () => {
markStarted();
togglePlay();
});
v.addEventListener('dblclick', (e) => {
e.stopPropagation();
btnFull.click();
});
const posterEl = d.querySelector('#pk_p_poster');
const loaderEl = d.querySelector('.pk-p-loading');
let shutterTargetTime = 0;
let isPiPDesired = false;
const stopSpinner = (force = false) => {
const isErrorVisible = !!box.querySelector('.pk-err-dialog');
if ((shutterTargetTime > 0 || isErrorVisible) && !force) return;
box.classList.remove('buffering');
if (v.paused && v.readyState >= 2) {
box.classList.add('pk-v-started');
updateState();
}
if (loaderEl) loaderEl.style.display = 'none';
if (posterEl) {
posterEl.style.pointerEvents = 'none';
posterEl.style.opacity = '0';
setTimeout(() => {
if(posterEl.style.opacity === '0') {
posterEl.style.display = 'none';
posterEl.style.pointerEvents = 'auto';
}
}, 450);
}
};
const showSpinner = () => {
box.classList.add('buffering');
if (loaderEl) loaderEl.style.display = 'block';
};
v.addEventListener('waiting', showSpinner);
v.addEventListener('stalled', showSpinner);
v.addEventListener('playing', stopSpinner);
v.addEventListener('seeked', stopSpinner);
v.addEventListener('canplaythrough', stopSpinner);
let lastTextUpdate = 0;
const updateTimeUI = () => {
requestAnimationFrame(() => {
if (isDragSeek) return;
if (v.currentTime > 0.1) stopSpinner();
const dur = v.duration;
const cur = v.currentTime;
const now = performance.now();
if (dur > 0) {
const pct = (cur / dur) * 100;
progFilled.style.width = `${pct}%`;
}
if (now - lastTextUpdate > 250) {
tCur.textContent = fmtT(cur);
if (dur > 0 && isFinite(dur)) tDur.textContent = fmtT(dur);
lastTextUpdate = now;
}
});
};
v.addEventListener('timeupdate', updateTimeUI);
v.addEventListener('durationchange', updateTimeUI);
v.addEventListener('timeupdate', () => {
if (shutterTargetTime > 0) {
if (v.currentTime >= shutterTargetTime - 0.5 && v.readyState >= 3) {
v._isRestarting = false;
console.log(`[Shutter] Target reached: ${v.currentTime}, Unlocking...`);
shutterTargetTime = 0;
stopSpinner(true);
}
}
});
let hasCheckedProgress = false;
let lastSaveTime = 0;
const applyProgress = () => {
if (hasCheckedProgress || v.duration <= 0) return;
hasCheckedProgress = true;
triggerResume(v, item);
};
v.addEventListener('canplay', applyProgress, { once: true });
v.addEventListener('playing', () => {
const now = Date.now();
const pId = getPhysicalId(item);
const existing = gmGet('pk_progress_' + pId);
let data = { t: v.currentTime, d: v.duration || 0, ts: now };
if (existing && typeof existing === 'object') {
data.t = existing.t;
if (!data.d) data.d = existing.d;
}
gmSet('pk_progress_' + pId, data);
});
v.addEventListener('timeupdate', () => {
const now = Date.now();
if (now - lastSaveTime > 3000) {
const curT = v.currentTime;
const totalT = v.duration || 0;
const progressData = { t: curT, d: totalT, ts: now };
const pId = getPhysicalId(item);
if (totalT > 0 && (totalT - curT < 5)) {
progressData.t = 0;
gmSet('pk_progress_' + pId, progressData);
} else if (curT > 1) {
gmSet('pk_progress_' + pId, progressData);
}
lastSaveTime = now;
}
});
v.addEventListener('loadedmetadata', () => {
triggerResume(v, item);
updateTimeUI();
const dur = v.duration;
if (dur > 0 && isFinite(dur)) {
const seconds = Math.round(dur);
const pId = getPhysicalId(item);
gmSet('pk_duration_' + pId, seconds);
S.durationMap.set(pId, seconds);
if (item.params) item.params.duration = seconds;
if (typeof renderVisible === 'function') renderVisible();
}
});
const enableMediaControls = () => {
if (v.readyState >= 2) {
if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important');
const btnPip = d.querySelector('#pk_p_pip');
if (btnPip && document.pictureInPictureEnabled && !v.disablePictureInPicture) {
btnPip.style.setProperty('display', 'flex', 'important');
if (isPiPDesired && !document.pictureInPictureElement) {
v.requestPictureInPicture().catch(() => { isPiPDesired = false; });
}
}
}
};
v.addEventListener('loadeddata', enableMediaControls);
v.addEventListener('canplay', enableMediaControls);
if (v.readyState >= 2) enableMediaControls();
let isDragSeek = false;
let hasUserSeeked = false;
let initialTimeBeforeDrag = 0;
let progRectCache = null;
let lastSeekRequestTime = 0;
let lastMouseX = 0;
const seekIndicator = d.querySelector('#pk_p_seek_indicator');
const updateVisualOnly = (clientX) => {
if (!progRectCache || !v.duration) return 0;
const pos = Math.max(0, Math.min(1, (clientX - progRectCache.left) / progRectCache.width));
const targetTime = pos * v.duration;
progFilled.style.setProperty('width', `${pos * 100}%`, 'important');
seekIndicator.textContent = `${fmtFullT(targetTime)} / ${fmtFullT(v.duration)}`;
if (tCur) tCur.textContent = fmtT(targetTime);
return targetTime;
};
const updateVideoOnDemand = (targetTime) => {
const now = performance.now();
if (v.paused && (now - lastSeekRequestTime > 40)) {
v.currentTime = targetTime;
lastSeekRequestTime = now;
}
};
const stopDragging = (isCancel = false) => {
if (!isDragSeek) return;
if (isCancel) {
v.currentTime = initialTimeBeforeDrag;
const revertPos = (initialTimeBeforeDrag / v.duration) * 100;
progFilled.style.setProperty('width', `${revertPos}%`, 'important');
} else {
const finalT = updateVisualOnly(lastMouseX);
v.currentTime = finalT;
if (finalT > 5) {
gmSet('pk_progress_' + getPhysicalId(item), {
t: finalT,
d: v.duration,
ts: Date.now()
});
}
}
isDragSeek = false;
progRectCache = null;
document.body.classList.remove('pk-dragging');
box.classList.remove('pk-is-seeking');
if (typeof ThumbnailEngine !== 'undefined') {
if (progArea && !progArea.matches(':hover')) {
ThumbnailEngine.hide();
}
}
const thumb = box.querySelector('.pk-player-progress-thumb');
if (thumb) {
thumb.classList.remove('pk-look-r', 'pk-look-l');
thumb.classList.add('pk-blink-anim', 'pk-blink-hold');
setTimeout(() => thumb.classList.remove('pk-blink-anim'), 200);
setTimeout(() => thumb.classList.remove('pk-blink-hold'), 300);
}
seekIndicator.style.display = 'none';
updateState();
};
const onMouseMove = (e) => {
if (!isDragSeek) return;
const topBar = d.querySelector('.pk-player-top');
if (topBar) {
const barRect = topBar.getBoundingClientRect();
if (e.clientY >= barRect.top && e.clientY <= barRect.bottom) {
stopDragging(true);
return;
}
}
const thumb = box.querySelector('.pk-player-progress-thumb');
if (thumb) {
if (e.clientX > lastMouseX + 1) {
thumb.classList.add('pk-look-r'); thumb.classList.remove('pk-look-l');
} else if (e.clientX < lastMouseX - 1) {
thumb.classList.add('pk-look-l'); thumb.classList.remove('pk-look-r');
}
}
lastMouseX = e.clientX;
const targetT = updateVisualOnly(e.clientX);
updateVideoOnDemand(targetT);
if (typeof ThumbnailEngine !== 'undefined' && progRectCache) {
ThumbnailEngine.show(e.clientX, progRectCache);
}
};
progArea.addEventListener('mousedown', (e) => {
if (!v.duration || isNaN(v.duration)) return;
if (typeof ThumbnailEngine !== 'undefined') ThumbnailEngine.hide();
isDragSeek = true;
hasUserSeeked = true;
lastMouseX = e.clientX;
initialTimeBeforeDrag = v.currentTime;
progRectCache = progArea.getBoundingClientRect();
document.body.classList.add('pk-dragging');
box.classList.add('pk-is-seeking');
seekIndicator.style.display = 'flex';
const targetT = updateVisualOnly(e.clientX);
v.currentTime = targetT;
e.preventDefault();
});
progArea.addEventListener('mousemove', (e) => {
const rect = progArea.getBoundingClientRect();
ThumbnailEngine.show(e.clientX, rect);
});
progArea.addEventListener('mouseleave', () => {
ThumbnailEngine.hide();
});
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', () => stopDragging(false));
box.addEventListener('mouseleave', (e) => {
if (isDragSeek) {
const rect = box.getBoundingClientRect();
if (e.clientX <= rect.left || e.clientX >= rect.right || e.clientY <= rect.top || e.clientY >= rect.bottom) {
stopDragging(true);
}
}
});
btnPlay.onclick = togglePlay;
btnClose.onclick = destroyPlayer;
btnFull.onclick = () => {
if (!document.fullscreenElement) { box.requestFullscreen(); btnFull.innerHTML = mkSvg(icons.exitFull); }
else { document.exitFullscreen(); btnFull.innerHTML = mkSvg(icons.full); }
};
const subPanel = d.querySelector('#pk_sub_panel');
if (subPanel) {
btnFull.addEventListener('mouseenter', () => subPanel.style.setProperty('display', 'none', 'important'));
btnFull.addEventListener('mouseleave', () => subPanel.style.removeProperty('display'));
}
const btnPip = d.querySelector('#pk_p_pip');
if (btnPip) {
if (!document.pictureInPictureEnabled || v.disablePictureInPicture) {
btnPip.style.display = 'none';
} else {
btnPip.onclick = async (e) => {
e.stopPropagation();
try {
if (document.pictureInPictureElement) {
isPiPDesired = false;
await document.exitPictureInPicture();
} else {
isPiPDesired = true;
await v.requestPictureInPicture();
window.focus();
}
} catch (err) { console.error("PiP Error:", err); }
};
}
}
btnSearch.onclick = (e) => {
e.stopPropagation();
if (v) {
v.pause();
setTimeout(() => { if (v) v.pause(); }, 100);
}
const posterImg = d.querySelector('#pk_p_poster img');
if (v.readyState >= 2 && v.videoWidth > 0) {
startImageSearch(v, item.name, d, null);
}
else if (posterImg && posterImg.style.display !== 'none' && posterImg.src) {
startImageSearch(posterImg, item.name, d, posterImg.src);
}
else {
startImageSearch(v, item.name, d, null);
}
};
const initPosterImg = d.querySelector('#pk_p_poster img');
if (initPosterImg) {
if (initPosterImg.complete && initPosterImg.src && initPosterImg.src.startsWith('http')) {
if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important');
} else {
initPosterImg.addEventListener('load', () => {
if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important');
});
}
}
const savedMute = gmGet('pk_vol_muted', false);
const savedVol = parseFloat(gmGet('pk_vol_level', 1.0));
v.muted = savedMute;
v.volume = (Number.isFinite(savedVol) && savedVol >= 0 && savedVol <= 1) ? savedVol : 1.0;
if (slideVol) slideVol.value = v.volume;
updateVolUI();
btnVol.onclick = () => {
v.muted = !v.muted;
updateVolUI();
gmSet('pk_vol_muted', v.muted);
};
slideVol.oninput = (e) => {
v.muted = false;
const val = parseFloat(e.target.value);
v.volume = val;
updateVolUI();
gmSet('pk_vol_muted', false);
gmSet('pk_vol_level', val);
};
spdList.querySelectorAll('.pk-p-item').forEach(item => {
item.onclick = () => {
const s = parseFloat(item.dataset.spd);
v.playbackRate = s;
spdList.querySelector('.active').classList.remove('active');
item.classList.add('active');
d.querySelector('#pk_p_spd_txt').textContent = item.textContent;
};
});
function bindResEvents() {
resList.querySelectorAll('.pk-p-item').forEach(item => {
item.onclick = () => {
const link = item.dataset.link;
if(link === currentLink) return;
const curT = v.currentTime;
const isPaused = v.paused;
const curRate = v.playbackRate;
box.classList.add('buffering');
if (loaderEl) loaderEl.style.display = 'block';
if (posterEl) {
if (v.readyState >= 2 && v.currentTime > 0) {
try {
const canvas = document.createElement('canvas');
canvas.width = v.videoWidth; canvas.height = v.videoHeight;
canvas.getContext('2d').drawImage(v, 0, 0);
const posterImg = posterEl.querySelector('img');
if (posterImg) {
posterImg.src = canvas.toDataURL('image/jpeg', 0.7);
posterImg.style.filter = 'brightness(0.8)';
}
} catch (e) { console.warn("[ClaritySwitch] Frame capture failed."); }
}
posterEl.style.transition = 'none';
posterEl.style.display = 'flex';
posterEl.style.opacity = '1';
posterEl.style.pointerEvents = 'auto';
}
currentLink = link;
currentResName = item.textContent.trim();
shutterTargetTime = curT > 0.1 ? curT : 0;
v.pause();
loadSource(link, curT);
v.playbackRate = curRate;
if(!isPaused) v.play().catch(()=>{});
resList.innerHTML = renderQualityMenu(qualityList, currentResName);
if(resTxt) resTxt.textContent = currentResName;
bindResEvents();
};
});
}
bindResEvents();
const subState = {
hasSub: false,
size: 24,
pos: 10,
offset: 0,
bgOpacity: 0.6,
track: null,
blobUrl: null
};
const processSubtitleFile = (file) => {
if (!file) return;
const ext = file.name.split('.').pop().toLowerCase();
if (!['srt', 'vtt', 'ass', 'ssa'].includes(ext)) {
showToast(L.err_sub_drop_type);
return;
}
const reader = new FileReader();
reader.onload = (evt) => {
const buffer = evt.target.result;
const encodings = ['utf-8', 'gbk', 'big5', 'utf-16le', 'shift_jis'];
let text = "";
for (let enc of encodings) {
try {
const decoder = new TextDecoder(enc, { fatal: true });
text = decoder.decode(buffer);
break;
} catch (e) { if(enc === 'shift_jis') text = new TextDecoder('utf-8').decode(buffer); }
}
if (subState.blobUrl) URL.revokeObjectURL(subState.blobUrl);
let vttText = "";
if (ext === 'ass' || ext === 'ssa') {
vttText = convertAssToVtt(text);
} else {
vttText = convertSrtToVtt(text);
}
const blob = new Blob([vttText], { type: 'text/vtt' });
subState.blobUrl = URL.createObjectURL(blob);
let targetTrack = null;
if (v.textTracks) {
for (let i = 0; i < v.textTracks.length; i++) {
const t = v.textTracks[i];
if (t.label === 'pk-subs') targetTrack = t;
t.mode = 'disabled';
}
}
if (!targetTrack) targetTrack = v.addTextTrack("subtitles", "pk-subs", "en");
targetTrack.oncuechange = () => {
const cues = targetTrack.activeCues;
const txtEl = d.querySelector('#pk_sub_text');
if (cues && cues.length > 0 && txtEl) {
const text = cues[cues.length - 1].text;
txtEl.innerHTML = text.replace(/<[^>]+>/g, '').replace(/\n/g, '
');
txtEl.style.display = 'block';
} else if (txtEl) {
txtEl.style.display = 'none';
}
};
const vttLines = vttText.split('\n');
let cueStart = null, cueEnd = null, cueText = [];
const timeReg = /(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[.,](\d{3})/;
while (targetTrack.cues && targetTrack.cues.length > 0) targetTrack.removeCue(targetTrack.cues[0]);
vttLines.forEach(line => {
const match = line.match(timeReg);
if (match) {
if (cueStart !== null && cueText.length > 0) {
try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){}
}
cueStart = parseInt(match[1])*3600 + parseInt(match[2])*60 + parseInt(match[3]) + parseInt(match[4])/1000;
cueEnd = parseInt(match[5])*3600 + parseInt(match[6])*60 + parseInt(match[7]) + parseInt(match[8])/1000;
cueText = [];
} else if (line.trim() !== '' && !line.trim().startsWith('WEBVTT') && !/^\d+$/.test(line.trim())) {
if (cueStart !== null) cueText.push(line.trim());
}
});
if (cueStart !== null && cueText.length > 0) {
try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){}
}
subState.hasSub = true;
subState.track = targetTrack;
subState.track.mode = 'hidden';
updateSubStyle();
d.querySelector('#pk_sub_toggle').checked = true;
d.querySelector('#pk_sub_name').textContent = file.name;
updateSubStyle();
console.log(L.msg_sub_drop_load.replace('{n}', file.name));
};
reader.readAsArrayBuffer(file);
};
box.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
box.style.boxShadow = "inset 0 0 50px var(--pk-pri)";
});
box.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
box.style.boxShadow = "0 25px 50px rgba(0,0,0,0.5)";
});
box.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
box.style.boxShadow = "0 25px 50px rgba(0,0,0,0.5)";
const file = e.dataTransfer.files[0];
processSubtitleFile(file);
});
const subStyleTag = document.createElement('style');
document.head.appendChild(subStyleTag);
const updateSubStyle = () => {
const txt = d.querySelector('#pk_sub_text');
const layer = d.querySelector('#pk_sub_render_layer');
const toggle = d.querySelector('#pk_sub_toggle');
if (txt && layer) {
txt.style.fontSize = `${subState.size}px`;
txt.style.backgroundColor = `rgba(0,0,0,${subState.bgOpacity})`;
layer.style.paddingBottom = `${subState.pos / 2}%`;
const isShow = toggle ? toggle.checked : true;
layer.style.display = isShow ? 'flex' : 'none';
}
if (subState.track) {
subState.track.mode = 'hidden';
}
};
const fileInp = d.querySelector('#pk_sub_file');
d.querySelector('#pk_sub_local_btn').onclick = (e) => { e.stopPropagation(); fileInp.click(); };
fileInp.onchange = (e) => {
processSubtitleFile(e.target.files[0]);
};
const cleanSubText = (text) => {
return text.replace(/\{[^}]*?\}/g, '')
.replace(/<\/?i>/g, '')
.replace(/\\N/gi, '\n')
.replace(/\r\n/g, '\n')
.trim();
};
const convertAssToVtt = (assText) => {
let vtt = "WEBVTT\n\n";
const lines = assText.split('\n');
let inEvents = false;
let count = 1;
const fmtTime = (t) => {
if (!t) return "00:00:00.000";
const parts = t.trim().split('.');
const hms = parts[0].split(':');
const ms = (parts[1] || '00').padEnd(3, '0');
return `${hms[0].padStart(2,'0')}:${hms[1]}:${hms[2]}.${ms}`;
};
for (let line of lines) {
line = line.trim();
if (line.startsWith('[Events]')) { inEvents = true; continue; }
if (!inEvents || !line.startsWith('Dialogue:')) continue;
const parts = line.split(',');
if (parts.length < 10) continue;
const start = fmtTime(parts[1]);
const end = fmtTime(parts[2]);
const rawText = parts.slice(9).join(',');
const text = cleanSubText(rawText);
if (text) {
vtt += `${count++}\n${start} --> ${end}\n${text}\n\n`;
}
}
return vtt;
};
const convertSrtToVtt = (srtText) => {
if (/^WEBVTT/i.test(srtText)) return srtText;
let vtt = "WEBVTT\n\n";
const text = srtText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const regex = /(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{3})/g;
let match;
const cues = [];
while ((match = regex.exec(text)) !== null) {
cues.push({
start: match[1],
end: match[2],
index: match.index,
endOfLine: regex.lastIndex
});
}
let count = 1;
for (let i = 0; i < cues.length; i++) {
const cue = cues[i];
const nextCue = cues[i + 1];
const contentStart = cue.endOfLine;
const contentEnd = nextCue ? nextCue.index : text.length;
let rawContent = text.substring(contentStart, contentEnd);
rawContent = rawContent.replace(/\n+\s*\d+\s*$/, '');
const cleanContent = cleanSubText(rawContent);
const vttStart = cue.start.replace(/,/g, '.');
const vttEnd = cue.end.replace(/,/g, '.');
if (cleanContent) {
vtt += `${count++}\n${vttStart} --> ${vttEnd}\n${cleanContent}\n\n`;
}
}
return vtt;
};
const autoMatchSubtitle = async (videoItem) => {
const parentId = videoItem.parent_id;
if (!parentId) return;
const videoNameBase = videoItem.name.substring(0, videoItem.name.lastIndexOf('.'));
let files = [];
if (typeof globalCache !== 'undefined' && globalCache.has(parentId)) {
files = globalCache.get(parentId);
} else {
try {
files = await apiList(parentId, 100);
} catch(e) { return; }
}
if (!files || files.length === 0) return;
const subFiles = files.filter(f =>
!f.trashed && f.kind !== 'drive#folder' &&
/\.(srt|vtt|ass|ssa)$/i.test(f.name)
);
if (subFiles.length === 0) return;
let targetSub = subFiles.find(f => {
const subBase = f.name.substring(0, f.name.lastIndexOf('.'));
return subBase === videoNameBase;
});
if (!targetSub) {
targetSub = subFiles.find(f => f.name.includes(videoNameBase));
}
if (targetSub) {
console.log(`[AutoSub] Matched: ${targetSub.name}`);
const box = d.querySelector('#pk_p_box');
const toast = document.createElement('div');
toast.style.cssText = "position:absolute;top:80px;right:20px;background:rgba(0,0,0,0.6);color:#fff;padding:6px 12px;border-radius:4px;font-size:12px;pointer-events:none;animation:pkFadeIn 0.5s;z-index:90;";
toast.textContent = L.msg_auto_sub_load.replace('{n}', targetSub.name);
box.appendChild(toast);
setTimeout(()=>toast.remove(), 4000);
try {
let link = targetSub.web_content_link;
if (!link) {
const detail = await apiGet(targetSub.id);
link = detail.web_content_link;
}
const res = await fetch(link);
const buffer = await res.arrayBuffer();
const encodings = ['utf-8', 'gbk', 'big5', 'utf-16le', 'shift_jis'];
let text = "";
for (let enc of encodings) {
try {
const decoder = new TextDecoder(enc, { fatal: true });
text = decoder.decode(buffer);
break;
} catch (e) { if(enc === 'shift_jis') text = new TextDecoder('utf-8').decode(buffer); }
}
let vttText = "";
const subExt = targetSub.name.split('.').pop().toLowerCase();
if (subExt === 'ass' || subExt === 'ssa') {
vttText = convertAssToVtt(text);
} else {
vttText = convertSrtToVtt(text);
}
const blob = new Blob([vttText], { type: 'text/vtt' });
if (subState.blobUrl) URL.revokeObjectURL(subState.blobUrl);
subState.blobUrl = URL.createObjectURL(blob);
let targetTrack = null;
if (v.textTracks) {
for (let i = 0; i < v.textTracks.length; i++) {
const t = v.textTracks[i];
if (t.label === 'pk-subs') targetTrack = t;
t.mode = 'disabled';
}
}
if (!targetTrack) targetTrack = v.addTextTrack("subtitles", "pk-subs", "en");
targetTrack.oncuechange = () => {
const cues = targetTrack.activeCues;
const txtEl = d.querySelector('#pk_sub_text');
if (cues && cues.length > 0 && txtEl) {
const text = cues[cues.length - 1].text;
txtEl.innerHTML = text.replace(/<[^>]+>/g, '').replace(/\n/g, '
');
txtEl.style.display = 'block';
} else if (txtEl) {
txtEl.style.display = 'none';
}
};
const vttLines = vttText.split('\n');
let cueStart = null, cueEnd = null, cueText = [];
const timeReg = /(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[.,](\d{3})/;
while (targetTrack.cues && targetTrack.cues.length > 0) targetTrack.removeCue(targetTrack.cues[0]);
vttLines.forEach(line => {
const match = line.match(timeReg);
if (match) {
if (cueStart !== null && cueText.length > 0) {
try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){}
}
cueStart = parseInt(match[1])*3600 + parseInt(match[2])*60 + parseInt(match[3]) + parseInt(match[4])/1000;
cueEnd = parseInt(match[5])*3600 + parseInt(match[6])*60 + parseInt(match[7]) + parseInt(match[8])/1000;
cueText = [];
} else if (line.trim() !== '' && !line.trim().startsWith('WEBVTT') && !/^\d+$/.test(line.trim())) {
if (cueStart !== null) cueText.push(line.trim());
}
});
if (cueStart !== null && cueText.length > 0) {
try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){}
}
subState.hasSub = true;
subState.track = targetTrack;
subState.track.mode = 'hidden';
updateSubStyle();
const toggle = d.querySelector('#pk_sub_toggle');
if(toggle) toggle.checked = true;
const nameLabel = d.querySelector('#pk_sub_name');
if(nameLabel) nameLabel.textContent = targetSub.name;
updateSubStyle();
} catch (e) {
console.warn("[AutoSub] Load failed", e);
}
}
};
const btnCloudSub = d.querySelector('#pk_sub_cloud_btn');
btnCloudSub.onclick = async (e) => {
e.stopPropagation();
const subPanel = d.querySelector('#pk_sub_panel');
const subTrigger = d.querySelector('#pk_sub_trigger');
if (subPanel && subTrigger) {
subPanel.style.setProperty('display', 'none', 'important');
const clearDisplay = () => {
subPanel.style.removeProperty('display');
subTrigger.removeEventListener('mouseleave', clearDisplay);
};
subTrigger.addEventListener('mouseleave', clearDisplay);
}
if (v && !v.paused) v.pause();
const originalText = btnCloudSub.textContent;
btnCloudSub.textContent = L.loading;
btnCloudSub.style.opacity = "0.6";
btnCloudSub.style.pointerEvents = "none";
let startPath = [{ id: '', name: L.btn_nav_home }];
let targetFolderId = '';
try {
let targetItem = item;
if (S.offlineMode || S.uploadMode || S.recentMode || item.kind === 'drive#task') {
const realFileId = (item.kind === 'drive#task' || S.offlineMode || S.uploadMode)
? (item.file_id || (item.params && item.params.file_id))
: item.id;
if (realFileId) {
try {
targetItem = await apiGet(realFileId);
} catch(err) {
console.warn("[CloudSub] Failed to resolve task file:", err);
}
}
}
if (targetItem.parent_id && targetItem.parent_id !== 'root') {
targetFolderId = targetItem.parent_id;
}
if (targetItem._lineage && Array.isArray(targetItem._lineage) && targetItem._lineage.length > 0) {
startPath = targetItem._lineage.map(x => ({ id: x.id || '', name: x.name }));
if (startPath.length > 0 && startPath[0].id !== '' && startPath[0].id !== 'root') {
startPath.unshift({ id: '', name: L.btn_nav_home });
}
}
else if (targetFolderId) {
const trace = [];
let curr = targetFolderId;
let safety = 6;
while (curr && curr !== 'root' && safety > 0) {
try {
const f = await apiGet(curr);
trace.unshift({ id: f.id, name: f.name });
curr = f.parent_id;
} catch(e) {
break;
}
safety--;
}
if (trace.length > 0) {
startPath = [{ id: '', name: L.btn_nav_home }, ...trace];
}
}
else if (S.path && S.path.length > 0) {
const cleanPath = S.path.filter(p => !p.id.startsWith('virtual_') && !p.id.includes('_root') && p.id !== 'analyze_root');
if (cleanPath.length > 0) {
startPath = cleanPath;
if (startPath[0].id !== '' && startPath[0].id !== 'root') {
startPath.unshift({ id: '', name: L.btn_nav_home });
}
}
}
} catch (error) {
console.error("[CloudSub] Path resolve error:", error);
targetFolderId = '';
startPath = [{ id: '', name: L.btn_nav_home }];
} finally {
btnCloudSub.textContent = originalText;
btnCloudSub.style.opacity = "1";
btnCloudSub.style.pointerEvents = "auto";
}
showFolderSelector(
targetFolderId,
async (id, name, subItem) => {
const box = d.querySelector('#pk_p_box');
const toast = document.createElement('div');
toast.style.cssText = "position:absolute;top:80px;right:20px;background:rgba(0,0,0,0.8);color:#fff;padding:8px 16px;border-radius:4px;font-size:13px;pointer-events:none;z-index:90;display:flex;align-items:center;gap:8px;";
toast.innerHTML = `
${L.msg_dl_sub}`;
box.appendChild(toast);
try {
let link = subItem.web_content_link;
if (!link) {
const detail = await apiGet(subItem.id);
link = detail.web_content_link;
}
const res = await fetch(link);
const buffer = await res.arrayBuffer();
const encodings = ['utf-8', 'gbk', 'big5', 'utf-16le', 'shift_jis', 'windows-1252'];
let text = "";
for (let enc of encodings) {
try {
const decoder = new TextDecoder(enc, { fatal: true });
text = decoder.decode(buffer);
break;
} catch (e) { if(enc === 'windows-1252') text = new TextDecoder('utf-8').decode(buffer); }
}
let vttText = "";
const subExt = subItem.name.split('.').pop().toLowerCase();
if (subExt === 'ass' || subExt === 'ssa') {
vttText = convertAssToVtt(text);
} else {
vttText = convertSrtToVtt(text);
}
const blob = new Blob([vttText], { type: 'text/vtt' });
if (subState.blobUrl) URL.revokeObjectURL(subState.blobUrl);
subState.blobUrl = URL.createObjectURL(blob);
let targetTrack = null;
if (v.textTracks) {
for (let i = 0; i < v.textTracks.length; i++) {
const t = v.textTracks[i];
if (t.label === 'pk-subs') targetTrack = t;
t.mode = 'disabled';
}
}
if (!targetTrack) targetTrack = v.addTextTrack("subtitles", "pk-subs", "en");
targetTrack.oncuechange = () => {
const cues = targetTrack.activeCues;
const txtEl = d.querySelector('#pk_sub_text');
if (cues && cues.length > 0 && txtEl) {
const text = cues[cues.length - 1].text;
txtEl.innerHTML = text.replace(/<[^>]+>/g, '').replace(/\n/g, '
');
txtEl.style.display = 'block';
} else if (txtEl) {
txtEl.style.display = 'none';
}
};
const vttLines = vttText.split('\n');
let cueStart = null, cueEnd = null, cueText = [];
const timeReg = /(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[.,](\d{3})/;
while (targetTrack.cues && targetTrack.cues.length > 0) targetTrack.removeCue(targetTrack.cues[0]);
vttLines.forEach(line => {
const match = line.match(timeReg);
if (match) {
if (cueStart !== null && cueText.length > 0) {
try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){}
}
cueStart = parseInt(match[1])*3600 + parseInt(match[2])*60 + parseInt(match[3]) + parseInt(match[4])/1000;
cueEnd = parseInt(match[5])*3600 + parseInt(match[6])*60 + parseInt(match[7]) + parseInt(match[8])/1000;
cueText = [];
} else if (line.trim() !== '' && !line.trim().startsWith('WEBVTT') && !/^\d+$/.test(line.trim())) {
if (cueStart !== null) cueText.push(line.trim());
}
});
if (cueStart !== null && cueText.length > 0) {
try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){}
}
subState.hasSub = true;
subState.track = targetTrack;
subState.track.mode = 'hidden';
updateSubStyle();
const toggle = d.querySelector('#pk_sub_toggle');
if(toggle) toggle.checked = true;
const nameLabel = d.querySelector('#pk_sub_name');
if(nameLabel) nameLabel.textContent = subItem.name;
updateSubStyle();
toast.innerHTML = `✅ ${L.msg_auto_sub_load.replace('{n}', subItem.name)}`;
setTimeout(() => toast.remove(), 3000);
} catch (err) {
console.error(err);
toast.innerHTML = `❌ ${L.err_sub_dl_fail}`;
setTimeout(() => toast.remove(), 4000);
}
},
startPath,
(f) => /\.(srt|vtt|ass|ssa)$/i.test(f.name),
L.title_sel_sub
);
};
const loadJSZip = () => {
if (window.JSZip) return Promise.resolve(window.JSZip);
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
script.onload = () => resolve(window.JSZip);
script.onerror = () => reject(new Error(L.msg_jszip_fail));
document.head.appendChild(script);
});
};
const cleanFilename = (name) => {
let n = name.toLowerCase();
n = n.replace(/\.[^/.]+$/, "");
n = n.replace(/^(\d{2,4}|[a-z]\d{2,4})[\.\s\-\_]+/, "");
const garbage = [
/\b(1080p|720p|2160p|4k|uhd|hd)\b.*/,
/\b(bluray|web-dl|webrip|remux|hdtv)\b.*/,
/\b(x264|x265|hevc|h264|aac|dts|ac3)\b.*/,
/\[.*?\]/g,
/\(.*?\)/g,
/\{.*?\}/g
];
garbage.forEach(g => n = n.replace(g, ''));
n = n.replace(/[\._\+]/g, ' ').trim();
const episodeMatch = n.match(/(.*?)s\d+e\d+/);
if (episodeMatch) return episodeMatch[0];
return n;
};
const gmxRequest = (url, type='text') => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
responseType: type,
anonymous: false,
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Cache-Control": "no-cache",
"Pragma": "no-cache"
},
onload: (res) => {
if (res.status === 200) {
resolve(res.response);
} else if (res.status === 404) {
resolve(null);
} else {
console.warn(`[Subtitle] HTTP ${res.status} from ${url}`);
resolve(null);
}
},
onerror: (e) => reject(new Error(L.err_req_blocked)),
ontimeout: () => reject(new Error(L.err_req_timeout))
});
});
};
const btnSearchSub = d.querySelector('#pk_sub_search_btn');
btnSearchSub.onclick = async (e) => {
e.stopPropagation();
const subPanel = d.querySelector('#pk_sub_panel');
const subTrigger = d.querySelector('#pk_sub_trigger');
if (subPanel && subTrigger) {
subPanel.style.setProperty('display', 'none', 'important');
const clearDisplay = () => {
subPanel.style.removeProperty('display');
subTrigger.removeEventListener('mouseleave', clearDisplay);
};
subTrigger.addEventListener('mouseleave', clearDisplay);
}
const searchOv = document.createElement('div');
searchOv.className = 'pk-sub-search-modal';
searchOv.style.cssText = `
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(20,20,20,0.95); border: 1px solid #444; border-radius: 8px;
width: 380px; height: 450px; display: flex; flex-direction: column;
box-shadow: 0 10px 40px rgba(0,0,0,0.8); z-index: 60; padding: 15px; backdrop-filter: blur(10px);
`;
box.appendChild(searchOv);
searchOv.onclick = (evt) => evt.stopPropagation();
const keyword = cleanFilename(item.name);
searchOv.innerHTML = `
${L.btn_close}
`;
const resultList = searchOv.querySelector('#pk_sub_search_list');
const input = searchOv.querySelector('#pk_sub_search_input');
const doSearch = (query) => {
const cleanQ = query.trim();
if (!cleanQ) return;
const plusQ = encodeURIComponent(cleanQ).replace(/%20/g, '+');
const spaceQ = encodeURIComponent(cleanQ);
const curLang = L.lang_code;
let engines = [];
if (curLang === 'zh') {
engines = [
{ name: 'Assrt', url: `https://assrt.net/sub/?searchword=${plusQ}` },
{ name: 'SubHD', url: `https://subhd.tv/search/${spaceQ}` },
{ name: 'OpenSubtitles', url: `https://www.opensubtitles.org/zh/search2/sublanguageid-kor/moviename-${plusQ}` },
];
} else if (curLang === 'tc') {
engines = [
{ name: 'R3Sub', url: `https://r3sub.com/search.php?s=${plusQ}` },
{ name: 'SubHD', url: `https://subhd.tv/search/${spaceQ}` },
{ name: 'Assrt', url: `https://assrt.net/sub/?searchword=${plusQ}` }
];
} else if (curLang === 'ko') {
engines = [
{ name: 'iSubtitles', url: `https://isubtitles.org/search?q=${plusQ}` },
{ name: 'OpenSubtitles', url: `https://www.opensubtitles.org/ko/search2/sublanguageid-kor/moviename-${plusQ}` },
{ name: 'SubtitleCat', url: `https://www.subtitlecat.com/index.php?search=${plusQ}` }
];
} else if (curLang === 'ja') {
engines = [
{ name: 'OpenSubtitles', url: `https://www.opensubtitles.org/ja/search2/sublanguageid-jpn/moviename-${plusQ}` },
{ name: 'MovieSubtitles', url: `https://www.moviesubtitles.org/search.php?q=${plusQ}` },
{ name: 'Anime Tosho', url: `https://animetosho.org/search?q=${plusQ}` }
];
} else {
engines = [
{ name: 'OpenSubtitles', url: `https://www.opensubtitles.org/en/search2/sublanguageid-eng/moviename-${plusQ}` },
{ name: 'MovieSubtitles', url: `https://www.moviesubtitles.org/search.php?q=${plusQ}` },
{ name: 'iSubtitles', url: `https://isubtitles.org/search?q=${plusQ}` }
];
}
let html = `
`;
resultList.innerHTML = html;
};
try {
await loadJSZip();
doSearch(input.value);
} catch (err) {
resultList.innerHTML = `
${L.msg_jszip_fail}
`;
}
input.oninput = () => doSearch(input.value);
input.onkeydown = (ev) => {
ev.stopPropagation();
};
searchOv.querySelector('#pk_sub_search_close').onclick = () => searchOv.remove();
};
d.querySelector('#pk_sub_toggle').onchange = (e) => {
updateSubStyle();
if (v.textTracks) {
Array.from(v.textTracks).forEach(t => {
if (t !== subState.track) t.mode = 'disabled';
});
}
};
if (v.textTracks) {
v.textTracks.addEventListener('addtrack', (e) => {
if (subState.hasSub && e.track !== subState.track) {
e.track.mode = 'disabled';
}
});
}
const sizeVal = d.querySelector('#pk_sub_size_val');
d.querySelector('#pk_sub_size_dec').onclick = (e) => {
e.stopPropagation();
subState.size = Math.max(12, subState.size - 2);
sizeVal.textContent = subState.size;
updateSubStyle();
};
d.querySelector('#pk_sub_size_inc').onclick = (e) => {
e.stopPropagation();
subState.size = Math.min(80, subState.size + 2);
sizeVal.textContent = subState.size;
updateSubStyle();
};
d.querySelector('#pk_sub_pos').oninput = (e) => {
e.stopPropagation();
subState.pos = parseInt(e.target.value);
updateSubStyle();
};
d.querySelector('#pk_sub_bg_opacity').oninput = (e) => {
e.stopPropagation();
subState.bgOpacity = parseInt(e.target.value) / 100;
updateSubStyle();
};
const timeVal = d.querySelector('#pk_sub_time_val');
const adjustOffset = (delta) => {
subState.offset += delta;
timeVal.textContent = subState.offset.toFixed(1) + " " + L.unit_sec;
if (subState.track && subState.track.cues) {
const cues = Array.from(subState.track.cues);
cues.forEach(cue => {
cue.startTime += delta;
cue.endTime += delta;
});
}
};
d.querySelector('#pk_sub_time_dec').onclick = (e) => { e.stopPropagation(); adjustOffset(-0.5); };
d.querySelector('#pk_sub_time_inc').onclick = (e) => { e.stopPropagation(); adjustOffset(0.5); };
d.querySelector('#pk_sub_panel').onclick = (e) => e.stopPropagation();
d.querySelectorAll('.pk-sub-tab').forEach(t => {
t.onclick = () => {
d.querySelectorAll('.pk-sub-tab, .pk-sub-pane').forEach(el => el.classList.remove('active'));
t.classList.add('active');
d.querySelector('#' + t.dataset.target).classList.add('active');
};
});
const curPMode = gmGet('pk_play_mode', 'stop');
const pRadios = d.querySelectorAll('input[name="pk_pmode"]');
pRadios.forEach(r => {
if (r.value === curPMode) r.checked = true;
r.onchange = () => gmSet('pk_play_mode', r.value);
});
let transformState = { rotate: 0, flipH: 1, flipV: 1, ratio: 'default' };
const applyTransform = () => {
if (document.pictureInPictureElement === v) {
v.style.transform = 'none';
return;
}
if (transformState.ratio === 'default') {
v.style.objectFit = 'contain';
v.style.width = '100%';
v.style.height = box.classList.contains('plist-active') ? (document.fullscreenElement ? 'calc(100% - 84px)' : '100%') : '100%';
v.style.aspectRatio = 'auto';
v.style.margin = '0';
v.style.inset = 'auto';
} else {
v.style.objectFit = 'fill';
v.style.aspectRatio = transformState.ratio;
v.style.width = 'auto';
v.style.height = 'auto';
v.style.maxWidth = '100%';
v.style.maxHeight = '100%';
v.style.margin = 'auto';
v.style.inset = '0';
}
let autoScale = 1;
if (Math.abs(transformState.rotate) % 180 !== 0) {
const boxW = box.clientWidth;
const boxH = box.clientHeight;
const vW = v.offsetWidth || boxW;
const vH = v.offsetHeight || boxH;
if (vW > 0 && vH > 0) {
const scaleW = boxW / vH;
const scaleH = boxH / vW;
autoScale = Math.min(scaleW, scaleH);
}
}
v.style.transform = `translateZ(0) rotate(${transformState.rotate}deg) scale(${autoScale}) scale(${transformState.flipH}, ${transformState.flipV})`;
};
const onResizeTransform = () => requestAnimationFrame(applyTransform);
window.addEventListener('resize', onResizeTransform);
d.querySelectorAll('#pk_ratio_opts .pk-size-btn').forEach(btn => {
btn.onclick = (e) => {
d.querySelectorAll('#pk_ratio_opts .pk-size-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
transformState.ratio = btn.dataset.ratio;
applyTransform();
};
});
d.querySelector('#pk_btn_rot_l').onclick = () => { transformState.rotate -= 90; applyTransform(); };
d.querySelector('#pk_btn_rot_r').onclick = () => { transformState.rotate += 90; applyTransform(); };
d.querySelector('#pk_btn_flip_h').onclick = () => { transformState.flipH *= -1; applyTransform(); };
d.querySelector('#pk_btn_flip_v').onclick = () => { transformState.flipV *= -1; applyTransform(); };
const opVal = d.querySelector('#pk_op_val');
const edVal = d.querySelector('#pk_ed_val');
let valOp = parseInt(gmGet('pk_skip_intro', 0)) || 0;
let valEd = parseInt(gmGet('pk_skip_outro', 0)) || 0;
opVal.textContent = valOp + " " + L.unit_sec;
edVal.textContent = valEd + " " + L.unit_sec;
const updateSkip = (type, delta) => {
if (type === 'op') {
valOp = Math.max(0, valOp + delta);
opVal.textContent = valOp + " " + L.unit_sec;
gmSet('pk_skip_intro', valOp);
} else {
valEd = Math.max(0, valEd + delta);
edVal.textContent = valEd + " " + L.unit_sec;
gmSet('pk_skip_outro', valEd);
}
};
d.querySelector('#pk_op_dec').onclick = () => updateSkip('op', -5);
d.querySelector('#pk_op_inc').onclick = () => updateSkip('op', 5);
d.querySelector('#pk_ed_dec').onclick = () => updateSkip('ed', -5);
d.querySelector('#pk_ed_inc').onclick = () => updateSkip('ed', 5);
d.querySelector('#pk_op_mark').onclick = (e) => {
e.stopPropagation();
const markTime = Math.max(0, Math.floor(v.currentTime));
valOp = markTime;
opVal.textContent = valOp + " " + L.unit_sec;
gmSet('pk_skip_intro', valOp);
};
d.querySelector('#pk_ed_mark').onclick = (e) => {
e.stopPropagation();
if (!v.duration) return;
const markTime = Math.max(0, Math.floor(v.duration - v.currentTime));
valEd = markTime;
edVal.textContent = valEd + " " + L.unit_sec;
gmSet('pk_skip_outro', valEd);
};
let hasTriggeredEnd = false;
v.addEventListener('timeupdate', () => {
const skipEd = parseInt(gmGet('pk_skip_outro', 0)) || 0;
if (skipEd > 0 && v.duration > 0 && !hasTriggeredEnd) {
if (v.duration - v.currentTime <= skipEd) {
hasTriggeredEnd = true;
console.log(`[AutoSkip] Outro skipped at ${v.currentTime}`);
v.onended();
}
}
});
v.addEventListener('play', () => hasTriggeredEnd = false);
v.addEventListener('seeking', () => hasTriggeredEnd = false);
v.addEventListener('enterpictureinpicture', applyTransform);
v.addEventListener('leavepictureinpicture', () => {
if (typeof isSwitching !== 'undefined' && !isSwitching) {
isPiPDesired = false;
}
applyTransform();
});
v.onended = () => {
const mode = gmGet('pk_play_mode', 'stop');
if (mode === 'single_loop') {
v.currentTime = 0; v.play().catch(()=>{});
} else if (mode === 'list_loop') {
const nextIdx = (curListIdx + 1) % totalInList;
if (totalInList > 1) softSwitch(nextIdx);
else { v.currentTime = 0; v.play().catch(()=>{}); }
}
};
const makeEditable = (el, type, callback) => {
el.ondblclick = (e) => {
e.stopPropagation();
const oldText = el.textContent;
const oldVal = type === 'offset' ? parseFloat(oldText) : parseInt(oldText);
el.innerHTML = `
`;
const input = el.querySelector('input');
input.focus();
input.select();
const finish = () => {
const val = parseFloat(input.value);
if (isNaN(val)) {
el.textContent = oldText;
} else {
let finalVal = type === 'offset' ? val : Math.round(val);
if (type !== 'offset') finalVal = Math.max(0, finalVal);
if (type === 'offset') el.textContent = finalVal.toFixed(1) + " " + L.unit_sec;
else el.textContent = finalVal;
callback(finalVal);
}
el.ondblclick = (evt) => { evt.stopPropagation(); makeEditable(el, type, callback).ondblclick(evt); };
};
input.onblur = finish;
input.onkeydown = (ev) => {
ev.stopPropagation();
if (ev.key === 'Enter') {
input.blur();
}
};
input.onclick = (ev) => ev.stopPropagation();
};
};
makeEditable(d.querySelector('#pk_sub_size_val'), 'int', (val) => {
subState.size = val; updateSubStyle();
});
makeEditable(d.querySelector('#pk_sub_time_val'), 'offset', (val) => {
const delta = val - subState.offset;
adjustOffset(delta);
});
makeEditable(d.querySelector('#pk_op_val'), 'int', (val) => {
valOp = val; gmSet('pk_skip_intro', val);
d.querySelector('#pk_op_val').textContent = val + " " + L.unit_sec;
});
makeEditable(d.querySelector('#pk_ed_val'), 'int', (val) => {
valEd = val; gmSet('pk_skip_outro', val);
d.querySelector('#pk_ed_val').textContent = val + " " + L.unit_sec;
});
updateSubStyle();
const playerKeyHandler = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (!document.getElementById('pk-player-ov')) return;
e.stopPropagation();
e.preventDefault();
resetHideTimer();
switch(e.key) {
case ' ':
case 'k':
togglePlay();
break;
case 'ArrowRight':
if (e.ctrlKey || e.metaKey) {
const nextIdx = (curListIdx + 1) % totalInList;
softSwitch(nextIdx);
} else {
const targetTime = Math.min(v.duration, v.currentTime + 10);
v.currentTime = targetTime;
if (tCur) tCur.textContent = fmtT(targetTime);
if (progFilled && v.duration) progFilled.style.setProperty('width', `${(targetTime / v.duration) * 100}%`, 'important');
}
break;
case 'ArrowLeft':
if (e.ctrlKey || e.metaKey) {
const prevIdx = (curListIdx - 1 + totalInList) % totalInList;
softSwitch(prevIdx);
} else {
const targetTime = Math.max(0, v.currentTime - 10);
v.currentTime = targetTime;
if (tCur) tCur.textContent = fmtT(targetTime);
if (progFilled && v.duration) progFilled.style.setProperty('width', `${(targetTime / v.duration) * 100}%`, 'important');
}
break;
case 'p':
case 'P':
const btnPip = d.querySelector('#pk_p_pip');
if (btnPip && btnPip.style.display !== 'none') btnPip.click();
break;
case 'e':
case 'E':
if (pTab) pTab.click();
break;
case 'ArrowUp':
case 'ArrowDown':
v.muted = false;
const delta = (e.key === 'ArrowUp' ? 0.05 : -0.05);
v.volume = Math.max(0, Math.min(1, v.volume + delta));
updateVolUI();
const volInd = d.querySelector('#pk_p_vol_indicator');
const volVal = d.querySelector('#pk_p_vol_val');
if (volInd && volVal) {
volVal.textContent = Math.round(v.volume * 100) + '%';
volInd.style.display = 'flex';
box.classList.add('pk-is-vol-active');
clearTimeout(v._volTimer);
v._volTimer = setTimeout(() => {
volInd.style.display = 'none';
box.classList.remove('pk-is-vol-active');
}, 1000);
}
break;
case 'f':
case 'F':
if (btnSearch && btnSearch.style.display !== 'none') btnSearch.click();
break;
case 'Enter':
btnFull.click();
break;
case 'Escape':
if (document.fullscreenElement) document.exitFullscreen();
else destroyPlayer();
break;
}
};
document.addEventListener('keydown', playerKeyHandler);
const smartLoad = async () => {
if (item.phase === 'PHASE_TYPE_RUNNING' || item.phase === 'PHASE_TYPE_PENDING') {
console.log("[Transcode] Video is processing, entering polling mode.");
const mask = document.createElement('div');
mask.className = 'pk-transcode-mask';
mask.innerHTML = `
${L.msg_transcoding}
${L.msg_transcoding_wait}
${L.btn_force_play}
`;
box.appendChild(mask);
mask.querySelector('#pk_tc_force').onclick = (e) => {
e.stopPropagation();
if (transcodeTimer) clearInterval(transcodeTimer);
mask.remove();
loadSource(currentLink);
v.play();
};
transcodeTimer = setInterval(async () => {
try {
const freshData = await apiGet(item.id);
if (freshData.phase === 'PHASE_TYPE_COMPLETE') {
clearInterval(transcodeTimer);
mask.remove();
item = freshData;
const best = getBestSource(freshData);
currentLink = best.src;
loadSource(currentLink);
if (typeof ThumbnailEngine !== 'undefined') ThumbnailEngine.resetSource(currentLink);
v.play();
qualityList = best.list;
currentResName = best.name;
const resTxt = d.querySelector('#pk_p_res_txt');
const resList = d.querySelector('#pk_p_res_list');
if(resTxt) resTxt.textContent = currentResName;
if(resList) {
resList.innerHTML = renderQualityMenu(qualityList, currentResName);
bindResEvents();
}
const toast = document.createElement('div');
toast.style.cssText = "position:absolute;top:20px;left:50%;transform:translateX(-50%);background:rgba(76, 175, 80, 0.9);color:#fff;padding:8px 16px;border-radius:20px;font-size:13px;z-index:100;animation:pkFadeIn 0.5s;";
toast.textContent = L.msg_transcode_done;
box.appendChild(toast);
setTimeout(()=>toast.remove(), 3000);
}
} catch(e) { console.warn("[TranscodePoll] Error:", e); }
}, 3000);
} else {
const initDur = (item.params && item.params.duration) ||
S.durationMap.get(item.id) ||
gmGet('pk_duration_' + item.id, 0);
if (tDur && initDur > 0) tDur.textContent = fmtT(initDur);
loadSource(currentLink, null);
setTimeout(() => {
if (isPlayerDestroyed) return;
const p = v.play();
if (p !== undefined) { p.catch(() => updateState()); }
if (startFullscreen) {
const box = d.querySelector('#pk_p_box');
const btnFull = d.querySelector('#pk_p_full');
if (box && box.requestFullscreen) {
box.requestFullscreen().then(() => {
if(btnFull) btnFull.innerHTML = mkSvg(icons.exitFull);
}).catch(err => console.warn("Fullscreen auto-resume failed", err));
}
}
}, 100);
}
};
smartLoad();
autoMatchSubtitle(item);
}
let isImageOpening = false;
async function showImage(startItem) {
if (S.trashMode) return;
if (document.querySelector('.pk-img-ov')) return;
isImageOpening = true;
let lastDirection = 1;
const item = startItem;
const imgList = S.display.filter(i => {
if (i.isHeader) return false;
if (S.offlineMode && i.phase !== 'PHASE_TYPE_COMPLETE') return false;
if (S.uploadMode && i.status !== 'DONE') return false;
return i.mime_type && i.mime_type.startsWith('image');
});
if (imgList.length === 0) return;
let curIdx = imgList.findIndex(i => i.id === startItem.id);
if (curIdx === -1) curIdx = 0;
const d = document.createElement('div');
d.className = 'pk-img-ov';
d.tabIndex = 0;
const icons = {
close: '
',
full: '
',
exitFull: '
',
prev: '
',
next: '
',
rotate: '
',
flipH: '
',
flipV: '
',
searchlens: `
`,
leftArr: `
`,
rightArr: `
`,
upArr: `
`
};
const renderImgListItems = () => {
const RANGE = 150;
const start = Math.max(0, curIdx - RANGE);
const end = Math.min(imgList.length, curIdx + RANGE + 1);
return imgList.slice(start, end).map((v, i) => {
const absIdx = start + i;
const imgSrc = v.thumbnail_link || v.icon_link || '';
return `
`}).join('');
};
const listFixStyle = ``;
d.innerHTML = listFixStyle + `
${icons.searchlens}
${icons.flipV}
${icons.flipH}
${icons.rotate}
${icons.full}
${icons.close}
${curIdx + 1} / ${imgList.length}
${icons.leftArr}
${renderImgListItems()}
${icons.rightArr}
`;
document.body.appendChild(d);
const box = d.querySelector('#pk_img_box');
const img = d.querySelector('#pk_img_el');
const viewport = d.querySelector('#pk_img_viewport');
const title = d.querySelector('#pk_img_title');
const loader = d.querySelector('#pk_img_load');
const btnFull = d.querySelector('#pk_img_full');
const btnRot = d.querySelector('#pk_img_rot');
const btnMirror = d.querySelector('#pk_img_mirror');
const btnFlipV = d.querySelector('#pk_img_flip_v');
const btnSearch = d.querySelector('#pk_img_search');
let scale = 1, transX = 0, transY = 0, rotation = 0, flipH = 1, flipV = 1, isDrag = false, startX, startY;
let isLongImageMode = false;
const updateTransform = () => {
if (isLongImageMode) return;
img.style.transform = `translate(${transX}px, ${transY}px) scale(${scale}) rotate(${rotation}deg) scaleX(${flipH}) scaleY(${flipV})`;
};
const resetView = (keepOrientation = false) => {
scale = 1; transX = 0; transY = 0;
if (!keepOrientation) {
rotation = 0; flipH = 1; flipV = 1;
}
isLongImageMode = false;
viewport.classList.remove('pk-long-image-mode');
viewport.classList.remove('pk-fit-mode');
if(viewport.scrollTop) viewport.scrollTop = 0;
img.style.transition = 'none';
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'contain';
img.style.maxWidth = 'none';
img.style.cursor = 'grab';
if (btnRot) btnRot.style.display = 'flex';
if (btnMirror) btnMirror.style.display = 'flex';
if (btnFlipV) btnFlipV.style.display = 'flex';
updateTransform();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
img.style.transition = '';
});
});
};
let imgLoadId = 0;
const loadCurrent = async (scrollMode = 'smooth') => {
if (typeof updateImgPlistUI === 'function') {
updateImgPlistUI(scrollMode);
}
imgLoadId++;
const myId = imgLoadId;
resetView();
img.style.opacity = '0';
const currentItem = imgList[curIdx];
title.textContent = `[${curIdx + 1}/${imgList.length}] ${currentItem.name}`;
btnSearch.style.display = 'none';
btnSearch.onclick = (e) => {
e.stopPropagation();
const thumbUrl = currentItem.thumbnail_link ? currentItem.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE') : (currentItem.icon_link || '');
const thumbImg = new Image();
thumbImg.crossOrigin = 'anonymous';
thumbImg.src = thumbUrl;
startImageSearch(thumbImg, currentItem.name, d, thumbUrl);
};
loader.style.display = 'block';
const checkLongImage = () => {
if (myId !== imgLoadId) return;
btnSearch.style.display = 'flex';
const nw = img.naturalWidth;
const nh = img.naturalHeight;
if (nw > 0 && nh > 0) {
if (nh / nw > 2.5) {
isLongImageMode = true;
viewport.classList.add('pk-long-image-mode');
img.style.transform = 'none';
if (btnRot) btnRot.style.display = 'none';
if (btnMirror) btnMirror.style.display = 'none';
if (btnFlipV) btnFlipV.style.display = 'none';
}
}
img.style.opacity = '1';
};
img.removeAttribute('crossorigin');
if (currentItem.thumbnail_link) {
img.src = currentItem.thumbnail_link;
const handleThumb = () => {
if (myId !== imgLoadId) return;
checkLongImage();
loader.style.display = 'none';
};
img.onerror = () => {
if (myId !== imgLoadId) return;
img.style.opacity = '0';
};
if (img.complete) handleThumb();
else img.onload = handleThumb;
} else {
img.removeAttribute('src');
img.style.opacity = '0';
}
let targetUrl = currentItem.web_content_link;
if (!targetUrl && !currentItem._resolved) {
try {
const targetApiId = ((S.offlineMode && currentItem.kind === 'drive#task') || (S.uploadMode && currentItem.file_id)) ? currentItem.file_id : currentItem.id;
const fullItem = await apiGet(targetApiId);
if (fullItem) {
if (fullItem.thumbnail_link) currentItem.thumbnail_link = fullItem.thumbnail_link;
if (fullItem.icon_link) currentItem.icon_link = fullItem.icon_link;
if (myId === imgLoadId) requestAnimationFrame(() => updateImgPlistUI(false));
}
if (myId !== imgLoadId) return;
targetUrl = fullItem.web_content_link;
currentItem.web_content_link = targetUrl;
currentItem._resolved = true;
} catch (e) { console.warn("API Error", e); }
}
if (myId !== imgLoadId) return;
const performFinalRender = () => {
if (myId !== imgLoadId) return;
if (targetUrl && targetUrl !== currentItem.thumbnail_link) {
img.crossOrigin = 'anonymous';
img.src = targetUrl;
} else {
img.crossOrigin = 'anonymous';
}
if (img.complete && img.naturalWidth > 0) {
checkLongImage();
} else {
img.addEventListener('load', checkLongImage, { once: true });
}
btnSearch.onclick = (e) => {
e.stopPropagation();
const thumbUrl = currentItem.thumbnail_link ? currentItem.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE') : (currentItem.icon_link || '');
const thumbImg = new Image();
thumbImg.crossOrigin = 'anonymous';
thumbImg.src = thumbUrl;
startImageSearch(thumbImg, currentItem.name, d, thumbUrl);
};
const mainDir = lastDirection, sideDir = -lastDirection, DEPTH = 5;
const quickLoad = (url) => { if(!url) return; const p = new Image(); p.src = url; };
const getIdx = (offset) => (curIdx + offset + imgList.length) % imgList.length;
quickLoad(imgList[getIdx(sideDir)].thumbnail_link);
for (let i = 1; i <= DEPTH; i++) {
if (myId !== imgLoadId) return;
const next = imgList[getIdx(i * mainDir)];
quickLoad(next.thumbnail_link);
if (next.web_content_link) {
const p = new Image(); p.crossOrigin = 'anonymous'; p.src = next.web_content_link;
}
}
};
if (targetUrl && targetUrl !== currentItem.thumbnail_link) {
const tempImg = new Image();
tempImg.crossOrigin = 'anonymous';
tempImg.onload = performFinalRender;
tempImg.onerror = performFinalRender;
tempImg.src = targetUrl;
} else {
performFinalRender();
}
};
d.addEventListener('wheel', (e) => {
if (isLongImageMode) return;
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
scale = Math.max(0.1, Math.min(10, scale + delta));
updateTransform();
});
img.onclick = () => {
if (isLongImageMode) {
viewport.classList.toggle('pk-fit-mode');
}
};
img.addEventListener('mousedown', (e) => {
if (e.button !== 0 || (isLongImageMode && !viewport.classList.contains('pk-fit-mode'))) return;
e.preventDefault();
const z = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1;
isDrag = true;
img.style.cursor = 'grabbing';
startX = e.clientX - transX * scale * z;
startY = e.clientY - transY * scale * z;
});
document.addEventListener('mousemove', (e) => {
if (!isDrag || isLongImageMode) return;
const z = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1;
let tx = (e.clientX - startX) / (scale * z);
let ty = (e.clientY - startY) / (scale * z);
if (img.naturalWidth && viewport) {
const vw = viewport.clientWidth;
const vh = viewport.clientHeight;
const iw = img.naturalWidth;
const ih = img.naturalHeight;
const baseRatio = Math.min(vw / iw, vh / ih);
let curW = iw * baseRatio * scale;
let curH = ih * baseRatio * scale;
if (Math.abs(rotation % 180) === 90) [curW, curH] = [curH, curW];
const limitX = curW > vw ? (curW - vw) / (2 * scale) : 0;
const limitY = curH > vh ? (curH - vh) / (2 * scale) : 0;
tx = Math.max(-limitX, Math.min(limitX, tx));
ty = Math.max(-limitY, Math.min(limitY, ty));
}
transX = tx;
transY = ty;
updateTransform();
});
document.addEventListener('mouseup', () => {
isDrag = false;
if (!isLongImageMode && img) img.style.cursor = 'grab';
});
const resizeHandler = () => {
if (isLongImageMode) return;
resetView(true);
};
window.addEventListener('resize', resizeHandler);
d.querySelector('#pk_img_close').onclick = (e) => {
e.stopPropagation();
const currentItem = imgList[curIdx];
if (currentItem) {
const targetId = currentItem.id;
const targetIdx = S.display.findIndex(x => x.id === targetId);
if (targetIdx !== -1) {
S.sel.clear();
S.sel.add(targetId);
S.activeId = targetId;
const rowTop = targetIdx * CONF.rowHeight;
const vpHeight = UI.vp.clientHeight;
UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2));
renderVisible();
updateStat();
}
}
window.removeEventListener('resize', resizeHandler);
d.remove();
};
btnFull.onclick = (e) => {
e.stopPropagation();
box.classList.toggle('full');
const isNowFull = box.classList.contains('full');
btnFull.innerHTML = isNowFull ? icons.exitFull : icons.full;
btnFull.setAttribute('data-pk-tip', isNowFull ? L.tip_minimize : L.tip_maximize);
if (isLongImageMode && viewport) {
viewport.scrollTop = 0;
} else {
setTimeout(() => resetView(true), 210);
}
};
btnRot.onclick = (e) => {
e.stopPropagation();
if (isLongImageMode) return;
rotation += 90;
updateTransform();
};
if (btnMirror) {
btnMirror.onclick = (e) => {
e.stopPropagation();
if (isLongImageMode) return;
flipH *= -1;
img.style.transition = 'none';
updateTransform();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
img.style.transition = '';
});
});
};
}
if (btnFlipV) {
btnFlipV.onclick = (e) => {
e.stopPropagation();
if (isLongImageMode) return;
flipV *= -1;
img.style.transition = 'none';
updateTransform();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
img.style.transition = '';
});
});
};
}
const plist = d.querySelector('#pk_img_plist');
const pTab = d.querySelector('#pk_img_plist_tab');
const pScroll = d.querySelector('#pk_img_plist_scroll');
let pTip = document.getElementById('pk_p_plist_tip_global');
if (!pTip) {
pTip = document.createElement('div');
pTip.id = 'pk_p_plist_tip_global';
pTip.className = 'pk-p-plist-tip';
document.body.appendChild(pTip);
}
const pTxt = d.querySelector('#pk_img_idx_txt');
const updateImgPlistUI = (scrollType = 'smooth') => {
if (pTxt) pTxt.textContent = `${curIdx + 1} / ${imgList.length}`;
pScroll.innerHTML = renderImgListItems();
pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => {
el.onclick = (e) => {
e.stopPropagation();
const idx = parseInt(e.currentTarget.dataset.idx);
if (idx === curIdx) return;
curIdx = idx;
loadCurrent('instant');
};
el.onmouseenter = (e) => {
if (plist.classList.contains('open')) {
const name = e.currentTarget.dataset.name;
const size = e.currentTarget.dataset.size;
pTip.innerHTML = `
${name} ${size}`;
pTip.style.display = 'block';
}
};
el.onmousemove = (e) => {
if (pTip.style.display === 'block') {
const tW = pTip.offsetWidth || 150;
pTip.style.left = (e.clientX - (tW / 2)) + 'px';
pTip.style.top = (e.clientY - 60) + 'px';
}
};
el.onmouseleave = () => {
pTip.style.display = 'none';
};
});
const itemsInDom = pScroll.querySelectorAll('.pk-p-plist-item');
itemsInDom.forEach((el) => {
const absIdx = parseInt(el.dataset.idx);
const isActive = absIdx === curIdx;
el.classList.toggle('active', isActive);
if (scrollType !== false && isActive && plist.classList.contains('open')) {
if (scrollType === 'instant') {
pScroll.style.scrollBehavior = 'auto';
} else {
pScroll.style.scrollBehavior = 'smooth';
}
el.scrollIntoView({
behavior: scrollType === 'instant' ? 'auto' : 'smooth',
block: 'nearest',
inline: 'center'
});
if (scrollType === 'instant') {
setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50);
}
}
});
const sl = Math.ceil(pScroll.scrollLeft);
const sw = pScroll.scrollWidth;
const cw = pScroll.clientWidth;
if (sw <= cw) {
d.querySelector('#pk_img_plist_L').style.setProperty('display', 'none', 'important');
d.querySelector('#pk_img_plist_R').style.setProperty('display', 'none', 'important');
} else {
if (sl <= 5) d.querySelector('#pk_img_plist_L').style.setProperty('display', 'none', 'important');
else d.querySelector('#pk_img_plist_L').style.setProperty('display', 'flex', 'important');
if (sl + cw >= sw - 5) d.querySelector('#pk_img_plist_R').style.setProperty('display', 'none', 'important');
else d.querySelector('#pk_img_plist_R').style.setProperty('display', 'flex', 'important');
}
const btnPrev = d.querySelector('#pk_img_prev');
const btnNext = d.querySelector('#pk_img_next');
if (btnPrev) btnPrev.style.setProperty('display', curIdx === 0 ? 'none' : 'flex', 'important');
if (btnNext) btnNext.style.setProperty('display', curIdx === imgList.length - 1 ? 'none' : 'flex', 'important');
};
pTab.onclick = (e) => {
e.stopPropagation();
const willOpen = !plist.classList.contains('open');
plist.classList.toggle('open');
if (typeof box !== 'undefined') box.classList.toggle('plist-active');
pTab.setAttribute('data-pk-tip', plist.classList.contains('open') ? L.tip_plist_close : L.tip_plist_open);
if (!isLongImageMode) resetView();
if (willOpen) {
updateImgPlistUI(false);
pScroll.style.scrollBehavior = 'auto';
const activeItem = pScroll.querySelector('.active');
if (activeItem) {
const targetLeft = activeItem.offsetLeft - (pScroll.clientWidth / 2) + (activeItem.clientWidth / 2);
pScroll.scrollLeft = targetLeft;
}
setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50);
setTimeout(() => updateImgPlistUI(false), 100);
}
};
const handleListWheel = (e) => {
e.stopPropagation();
e.preventDefault();
pScroll.scrollBy({ left: e.deltaY > 0 ? 300 : -300, behavior: 'smooth' });
};
pScroll.removeEventListener('wheel', handleListWheel);
pScroll.addEventListener('wheel', handleListWheel, { passive: false });
pScroll.addEventListener('scroll', () => {
requestAnimationFrame(() => updateImgPlistUI(false));
}, { passive: true });
const btnListL = d.querySelector('#pk_img_plist_L');
const btnListR = d.querySelector('#pk_img_plist_R');
if (btnListL) {
btnListL.onclick = (e) => {
e.stopPropagation();
pScroll.scrollBy({ left: -400, behavior: 'smooth' });
setTimeout(() => updateImgPlistUI(false), 300);
};
}
if (btnListR) {
btnListR.onclick = (e) => {
e.stopPropagation();
pScroll.scrollBy({ left: 400, behavior: 'smooth' });
setTimeout(() => updateImgPlistUI(false), 300);
};
}
setTimeout(() => updateImgPlistUI(false), 100);
const goPrev = () => {
if (curIdx > 0) {
lastDirection = -1;
curIdx--;
loadCurrent();
}
};
const goNext = () => {
if (curIdx < imgList.length - 1) {
lastDirection = 1;
curIdx++;
loadCurrent();
}
};
d.querySelector('#pk_img_prev').onclick = (e) => { e.stopPropagation(); goPrev(); };
d.querySelector('#pk_img_next').onclick = (e) => { e.stopPropagation(); goNext(); };
d.focus();
d.addEventListener('keydown', (e) => {
if (e.key === 'Escape') d.remove();
else if (e.key === 'ArrowLeft') goPrev();
else if (e.key === 'ArrowRight') goNext();
else if (e.key === 'f' || e.key === 'F') {
if (btnSearch && btnSearch.style.display !== 'none') btnSearch.click();
}
else if (e.key === 'e' || e.key === 'E') {
if (pTab) pTab.click();
}
else if (e.key === 'm' || e.key === 'M') {
e.preventDefault();
if (btnFull) btnFull.click();
}
else if (e.key === 'r' || e.key === 'R') {
e.preventDefault();
if (!isLongImageMode && btnRot) btnRot.click();
}
else if (e.key === 'h' || e.key === 'H') {
e.preventDefault();
if (!isLongImageMode && btnMirror) btnMirror.click();
}
else if (e.key === 'v' || e.key === 'V') {
e.preventDefault();
if (!isLongImageMode && btnFlipV) btnFlipV.click();
}
});
try { await loadCurrent(); } catch (e) { console.error(e); } finally { isImageOpening = false; }
}
const SEARCH_CSS = `
.pk-search-ov {
position: fixed; inset: 0; z-index: 2147483647;
background: rgba(0,0,0,0.5); cursor: crosshair;
}
.pk-crop-box {
position: absolute; border: 2px dashed #fff;
background: rgba(255,255,255,0.1); pointer-events: none;
box-shadow: 0 0 0 9999px rgba(0,0,0,0.5);
}
.pk-search-msg {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(0,0,0,0.8); color: #fff; padding: 10px 20px; border-radius: 5px;
font-size: 14px; pointer-events: none;
}
`;
async function startImageSearch(mediaElement, fileName, containerElement, originalLink) {
if (document.querySelector('.pk-search-running-mask')) return;
try { window.focus(); if (containerElement) containerElement.focus(); } catch (e) {}
const L = getStrings();
const isVideo = mediaElement.tagName === 'VIDEO';
const MAX_SIDE = 1000;
const BLOB_TYPE = 'image/jpeg';
const BLOB_QUALITY = 0.7;
const ov = document.createElement('div');
ov.className = 'pk-search-running-mask';
ov.style.cssText = 'position:absolute; inset:0; z-index:2147483647; background:rgba(0,0,0,0.85); display:flex; align-items:center; justify-content:center; flex-direction:column; gap:20px; border-radius:inherit;';
ov.innerHTML = `
${L.str_processing}
`;
const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
if (fsEl) {
fsEl.appendChild(ov);
} else if (containerElement) {
containerElement.appendChild(ov);
} else {
document.body.appendChild(ov);
}
if (isVideo && !mediaElement.paused) mediaElement.pause();
try {
let finalBlob = null;
const cvs = document.createElement('canvas');
const ctx = cvs.getContext('2d');
const drawScaled = (source, srcW, srcH) => {
let w = srcW, h = srcH;
if (w === 0 || h === 0) throw new Error("Media dimensions not ready");
if (w > MAX_SIDE || h > MAX_SIDE) {
const ratio = Math.min(MAX_SIDE / w, MAX_SIDE / h);
w = Math.floor(w * ratio); h = Math.floor(h * ratio);
}
cvs.width = w; cvs.height = h;
ctx.drawImage(source, 0, 0, w, h);
};
let fetchUrl = originalLink;
const isLocalData = fetchUrl && (fetchUrl.startsWith('blob:') || fetchUrl.startsWith('data:'));
const BLOB_TYPE = 'image/jpeg';
const BLOB_QUALITY = 0.85;
if (isVideo) {
const sourceW = mediaElement.videoWidth;
const sourceH = mediaElement.videoHeight;
drawScaled(mediaElement, sourceW, sourceH);
try {
finalBlob = await new Promise((resolve, reject) => {
cvs.toBlob(b => b ? resolve(b) : reject(new Error("Empty")), BLOB_TYPE, BLOB_QUALITY);
});
} catch (err) {
throw new Error("Tainted: Video CORS Blocked");
}
} else {
let canvasSuccess = false;
try {
const sourceW = mediaElement.naturalWidth || mediaElement.width;
const sourceH = mediaElement.naturalHeight || mediaElement.height;
if (sourceW > 0 && sourceH > 0) {
drawScaled(mediaElement, sourceW, sourceH);
finalBlob = await new Promise((resolve, reject) => {
try {
cvs.toBlob(b => b ? resolve(b) : reject(new Error("Tainted")), BLOB_TYPE, BLOB_QUALITY);
} catch (e) { reject(e); }
});
canvasSuccess = !!finalBlob;
}
} catch (canvasErr) {
console.warn("[ImageSearch] DOM Canvas extraction failed (Tainted/Not Ready), fallback to network.");
}
if (!canvasSuccess && fetchUrl && !isLocalData) {
console.log("[ImageSearch] Fetching image via network as fallback...");
try {
const res = await fetch(fetchUrl, { mode: 'cors', credentials: 'omit' });
if (!res.ok && res.status !== 206) throw new Error(`Fetch HTTP ${res.status}`);
finalBlob = await res.blob();
} catch (fetchErr) {
console.warn(`[ImageSearch] Native fetch failed (${fetchErr.message}), fallback to GM_xhr...`);
let fetchRetry = 0;
let fetchSuccess = false;
while (fetchRetry < 3 && !fetchSuccess) {
try {
finalBlob = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: fetchUrl,
responseType: "blob",
timeout: 30000,
headers: {
"Referer": "https://mypikpak.com/",
"User-Agent": navigator.userAgent
},
onload: (res) => {
if (res.status === 200 || res.status === 206) resolve(res.response);
else reject(new Error(`HTTP ${res.status}`));
},
onerror: () => reject(new Error("Network Error")),
ontimeout: () => reject(new Error("Timeout"))
});
});
fetchSuccess = true;
} catch (err) {
fetchRetry++;
if (fetchRetry < 3) {
console.warn(`[ImageSearch] GM_xhr retry ${fetchRetry}/3...`);
await new Promise(r => setTimeout(r, 1500));
}
}
}
}
}
if (!finalBlob) {
throw new Error(L.err_network_break);
}
if (finalBlob.size > 8 * 1024 * 1024) {
console.log(`[ImageSearch] Image too large (${(finalBlob.size/1024/1024).toFixed(1)}MB), applying compression...`);
try {
const bmp = await createImageBitmap(finalBlob);
drawScaled(bmp, bmp.width, bmp.height);
bmp.close();
finalBlob = await new Promise((resolve, reject) => {
cvs.toBlob(b => b ? resolve(b) : reject(new Error("Compression Failed")), BLOB_TYPE, BLOB_QUALITY);
});
} catch (e) {
console.warn("[ImageSearch] Compression failed, using original blob.", e);
}
}
}
if (!finalBlob) throw new Error(L.err_capture || "Capture failed");
const uploadAndGetUrl = async () => {
if (typeof GM_xmlhttpRequest === 'undefined') throw new Error("Missing GM_xmlhttpRequest");
const txtDiv = ov.lastElementChild;
const FAST_TIMEOUT = 4000;
const uploadTask = (url, formData, parseType, stageText) => {
return new Promise((resolve, reject) => {
if (txtDiv) txtDiv.textContent = stageText;
GM_xmlhttpRequest({
method: "POST", url: url, data: formData,
timeout: FAST_TIMEOUT,
responseType: parseType === 'json' ? 'json' : 'text',
onload: (res) => {
if (res.status === 200) resolve(res);
else reject(new Error(`HTTP ${res.status}`));
},
onerror: () => reject(new Error("Network Error")),
ontimeout: () => reject(new Error("Timeout"))
});
});
};
const tryNode1 = async () => {
const fd = new FormData();
fd.append('files[]', finalBlob, `pk_1.jpg`);
const res = await uploadTask(
"https://uguu.se/upload.php",
fd, 'json',
L.str_upload_1
);
if (res.response && res.response.success && res.response.files?.[0]?.url) {
return res.response.files[0].url;
}
throw new Error("Uguu API Error");
};
const tryNode2 = async () => {
console.warn("Node 1 failed, switching to Litterbox...");
const fd = new FormData();
fd.append('reqtype', 'fileupload');
fd.append('time', '1h');
fd.append('fileToUpload', finalBlob, `pk_2.jpg`);
const res = await uploadTask(
"https://litterbox.catbox.moe/resources/internals/api.php",
fd, 'text',
L.str_upload_2
);
return res.responseText.trim();
};
const tryNode3 = async () => {
console.warn("Node 2 failed, switching to Catbox...");
const fd = new FormData();
fd.append('reqtype', 'fileupload');
fd.append('userhash', '');
fd.append('fileToUpload', finalBlob, `pk_3.jpg`);
const res = await uploadTask(
"https://catbox.moe/user/api.php",
fd, 'text',
L.str_upload_3
);
return res.responseText.trim();
};
try {
return await tryNode1();
} catch (e1) {
try {
return await tryNode2();
} catch (e2) {
try {
return await tryNode3();
} catch (e3) {
console.error("All upload nodes failed:", e1, e2, e3);
throw new Error("All Upload Hosts Failed");
}
}
}
};
try {
const imgUrl = await uploadAndGetUrl();
if (!imgUrl || !imgUrl.startsWith('http')) {
throw new Error("Invalid URL returned.");
}
const currentEngine = gmGet('pk_search_engine', 'google');
const encUrl = encodeURIComponent(imgUrl);
let jumpUrl = '';
let engineName = '';
switch (currentEngine) {
case 'yandex':
jumpUrl = `https://yandex.com/images/search?rpt=imageview&url=${encUrl}`;
engineName = 'Yandex';
break;
case 'saucenao':
jumpUrl = `https://saucenao.com/search.php?db=999&url=${encUrl}`;
engineName = 'SauceNAO';
break;
case 'tracemoe': {
const cleanUrl = imgUrl.replace(/^https?:\/\//, '');
const proxyUrl = `https://wsrv.nl/?url=${cleanUrl}&output=jpg`;
jumpUrl = `https://trace.moe/?url=${encodeURIComponent(proxyUrl)}`;
engineName = 'trace.moe';
break;
}
case 'google':
default:
jumpUrl = `https://lens.google.com/uploadbyurl?url=${encUrl}`;
engineName = 'Google Lens';
break;
}
const spinDiv = ov.querySelector('.pk-spin-lg');
const txtDiv = ov.lastElementChild;
if (spinDiv) spinDiv.style.borderColor = '#4CAF50';
if (txtDiv) txtDiv.textContent = L.str_redirecting.replace('Google Lens', engineName);
await sleep(600);
window.open(jumpUrl, '_blank');
} catch (err) {
console.warn("Auto upload failed, switching to manual fallback:", err);
const txtDiv = ov.lastElementChild;
if (txtDiv) txtDiv.textContent = L.str_upload_fail_copy;
let fallbackBlob = null;
try {
const bmp = await createImageBitmap(finalBlob);
const tmpCvs = document.createElement('canvas');
tmpCvs.width = bmp.width;
tmpCvs.height = bmp.height;
tmpCvs.getContext('2d').drawImage(bmp, 0, 0);
bmp.close();
fallbackBlob = await new Promise(r => tmpCvs.toBlob(r, 'image/png'));
} catch (e) {
console.warn("Bitmap conversion failed, falling back to origin canvas:", e);
fallbackBlob = await new Promise(r => cvs.toBlob(r, 'image/png'));
}
const MAX_CLIPBOARD_SIZE = 19.5 * 1024 * 1024;
if (fallbackBlob.size > MAX_CLIPBOARD_SIZE) {
console.log(`PNG too large, resizing...`);
try {
const ratio = Math.sqrt(MAX_CLIPBOARD_SIZE / fallbackBlob.size);
const bmp = await createImageBitmap(fallbackBlob);
const newW = Math.floor(bmp.width * ratio);
const newH = Math.floor(bmp.height * ratio);
const tmpCvs = document.createElement('canvas');
tmpCvs.width = newW;
tmpCvs.height = newH;
tmpCvs.getContext('2d').drawImage(bmp, 0, 0, newW, newH);
bmp.close();
fallbackBlob = await new Promise(r => tmpCvs.toBlob(r, 'image/png'));
} catch (e) {
console.warn("Resize failed:", e);
}
}
try {
const item = new ClipboardItem({ 'image/png': fallbackBlob });
await navigator.clipboard.write([item]);
} catch (clipErr) {
console.error("Clipboard write failed:", clipErr);
if (txtDiv) txtDiv.textContent = "Clipboard Failed";
await sleep(1000);
}
const ctrlVPhrase = (navigator.platform.toUpperCase().indexOf('MAC') >= 0) ? 'Cmd+V' : 'Ctrl+V';
const hintText = L.msg_manual_paste.replace('{cmd}', `
${ctrlVPhrase} `);
ov.innerHTML = `
⚠️
${L.msg_copy_success}
${hintText}
`;
await sleep(1500);
const currentEngine = gmGet('pk_search_engine', 'google');
let manualUrl = '';
switch (currentEngine) {
case 'yandex': manualUrl = 'https://yandex.com/images/'; break;
case 'saucenao': manualUrl = 'https://saucenao.com/'; break;
case 'tracemoe': manualUrl = 'https://trace.moe/'; break;
case 'google': default: manualUrl = 'https://lens.google.com/upload'; break;
}
window.open(manualUrl, '_blank');
}
} catch (e) {
console.error("Search Error:", e);
const errorMsg = e.message.includes('Tainted') ? "Security Error: CORS Blocked" : e.message;
ov.innerHTML = `
❌ ${errorMsg}
`;
await sleep(2000);
} finally {
ov.remove();
}
}
function dataURLtoBlob(dataurl) {
let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {type:mime});
}
const FILTER_EXTS = {
video: ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','mpg','mpeg','rm','rmvb','asf','vob','dat','divx','f4v','m2ts','mts','tp','trp','ogv','mpe','m2v','m3u8'],
audio: ['mp3','wav','flac','aac','ogg','wma','ape','m4a','amr','opus','m4b','alac','aiff','mid','midi','ra','dts','ac3','dsf','dff'],
image: ['jpg','jpeg','png','gif','bmp','webp','svg','tif','tiff','ico','heic','heif','raw','cr2','nef','arw','dng','orf','avif','psd','ai','eps','jfif','jpe'],
document: ['txt','html','pdf','pptx','chm','docx','xlsx','htm','doc','dwg','mdb','ppt','xls','rtf','odt','ods','odp','epub','mobi','azw3','djvu','cbz','cbr','md','log','csv','xml','json'],
software: ['apk','exe','ipa','dmg','rpm','deb','msi','pkg','xapk','apks','aab','jar','bin','sh','bat','cmd'],
archive: ['zip','rar','7z','tar','gz','iso','cab','bz2','xz','tgz','wim','esd','img','zst','lzh'],
torrent: ['torrent']
};
const FILTER_NAMES = {
video: L.cat_video,
audio: L.cat_audio,
image: L.cat_image,
document: L.cat_document,
software: L.cat_software,
archive: L.cat_archive,
torrent: L.cat_torrent,
other: L.cat_other
};
const fIcons = {
all: `
`,
video: `
`,
audio: `
`,
image: `
`,
document: `
`,
software: `
`,
archive: `
`,
torrent: `
`,
other: `
`
};
const renderActiveFilterUI = () => {
if (!UI.filterBar) return;
const cat = S.filterState.cat;
if (cat === 'all') return;
UI.filterCatLabel.textContent = FILTER_NAMES[cat] || (L.cat_other);
if (cat === 'other') {
UI.filterExtsWrap.style.display = 'none';
} else {
UI.filterExtsWrap.style.display = 'flex';
const exts = FILTER_EXTS[cat] ||[];
const mainExts = exts.slice(0, 3);
const moreExts = exts.slice(3);
let html = `
${L.cat_all} `;
let displayExts = [...mainExts];
if (S.filterState.ext !== 'all' && moreExts.includes(S.filterState.ext)) {
displayExts[2] = S.filterState.ext;
}
displayExts.forEach(e => {
html += `
${e} `;
});
UI.filterExtsMain.innerHTML = html;
if (moreExts.length > 0) {
UI.filterExtsMoreBtn.style.display = 'flex';
} else {
UI.filterExtsMoreBtn.style.display = 'none';
}
UI.filterExtsMain.querySelectorAll('.pk-f-ext').forEach(span => {
span.onclick = (e) => {
e.stopPropagation();
S.filterState.ext = span.dataset.ext;
renderActiveFilterUI();
S.sel.clear();
refresh();
};
});
}
};
const showFilterCatPopup = (triggerEl, e) => {
e.stopPropagation();
const existing = document.querySelector('#pk-filter-cat-pop');
if (existing) { existing.remove(); return; }
const pop = document.createElement('div');
pop.id = 'pk-filter-cat-pop';
pop.style.cssText = `
position: absolute; background: var(--pk-bg); border: 1px solid var(--pk-bd);
border-radius: 8px; padding: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15);
z-index: 2147483647; width: 420px; display: flex; flex-direction: column;
zoom: var(--pk-zoom, 1);
`;
if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark');
pop.innerHTML = `
${L.title_file_filter}
${fIcons.all} ${L.cat_all}
${fIcons.video} ${L.cat_video}
${fIcons.audio} ${L.cat_audio}
${fIcons.image} ${L.cat_image}
${fIcons.document} ${L.cat_document}
${fIcons.software} ${L.cat_software}
${fIcons.archive} ${L.cat_archive}
${fIcons.torrent} ${L.cat_torrent}
${fIcons.other} ${L.cat_other}
`;
document.body.appendChild(pop);
const updatePosition = () => {
if (!pop.isConnected) return;
const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1;
const rect = triggerEl.getBoundingClientRect();
let popLeft = rect.left / scale;
if (popLeft + 420 > window.innerWidth / scale) popLeft = (window.innerWidth / scale) - 430;
pop.style.top = ((rect.bottom / scale) + 5) + 'px';
pop.style.left = popLeft + 'px';
};
updatePosition();
window.addEventListener('resize', updatePosition);
const cleanup = () => {
window.removeEventListener('resize', updatePosition);
document.removeEventListener('mousedown', closer);
pop.remove();
};
pop.querySelectorAll('.pk-fc-btn').forEach(btn => {
btn.onclick = (ev) => {
ev.stopPropagation();
const cat = btn.dataset.cat;
S.filterState.cat = cat;
S.filterState.ext = 'all';
S.filterState.active = (cat !== 'all');
cleanup();
if (S.filterState.active) {
renderActiveFilterUI();
}
S.sel.clear();
refresh();
};
});
const closer = (ev) => {
if (!pop.contains(ev.target) && !triggerEl.contains(ev.target)) {
cleanup();
}
};
setTimeout(() => document.addEventListener('mousedown', closer), 10);
};
if (UI.filterBtn) UI.filterBtn.onclick = (e) => showFilterCatPopup(UI.filterBtn, e);
if (UI.filterCatLabel) UI.filterCatLabel.onclick = (e) => showFilterCatPopup(UI.filterCatLabel, e);
if (UI.filterExtsMoreBtn) {
UI.filterExtsMoreBtn.onclick = (e) => {
e.stopPropagation();
const existing = document.querySelector('#pk-filter-more-pop');
if (existing) { existing.remove(); return; }
const pop = document.createElement('div');
pop.id = 'pk-filter-more-pop';
pop.style.cssText = `
position: absolute; background: var(--pk-bg); border: 1px solid var(--pk-bd);
border-radius: 8px; padding: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15);
z-index: 2147483647; max-width: 340px; display: flex; flex-wrap: wrap; gap: 8px;
zoom: var(--pk-zoom, 1);
`;
if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark');
const exts = FILTER_EXTS[S.filterState.cat] ||[];
const mainExts = exts.slice(0, 3);
const moreExts = exts.slice(3);
let displayExts = [...mainExts];
if (S.filterState.ext !== 'all' && moreExts.includes(S.filterState.ext)) {
displayExts[2] = S.filterState.ext;
}
const dropdownExts = exts.filter(ex => !displayExts.includes(ex));
pop.innerHTML = dropdownExts.map(ex => `
${ex} `).join('');
document.body.appendChild(pop);
const updatePosition = () => {
if (!pop.isConnected) return;
const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1;
const rect = UI.filterExtsWrap.getBoundingClientRect();
let popLeft = rect.left / scale;
if (popLeft + 340 > window.innerWidth / scale) popLeft = (window.innerWidth / scale) - 350;
pop.style.top = ((rect.bottom / scale) + 5) + 'px';
pop.style.left = popLeft + 'px';
};
updatePosition();
window.addEventListener('resize', updatePosition);
const cleanup = () => {
window.removeEventListener('resize', updatePosition);
document.removeEventListener('mousedown', closer);
pop.remove();
};
pop.querySelectorAll('.pk-f-ext').forEach(span => {
span.onclick = (ev) => {
ev.stopPropagation();
S.filterState.ext = span.dataset.ext;
cleanup();
renderActiveFilterUI();
S.sel.clear();
refresh();
};
});
const closer = (ev) => {
if (!pop.contains(ev.target) && !UI.filterExtsMoreBtn.contains(ev.target)) {
cleanup();
}
};
setTimeout(() => document.addEventListener('mousedown', closer), 10);
};
}
if (UI.filterExitBtn) {
UI.filterExitBtn.onclick = () => {
S.filterState = { active: false, cat: 'all', ext: 'all' };
UI.filterBtn.style.display = 'flex';
UI.filterActiveUI.style.display = 'none';
S.sel.clear();
refresh();
};
}
const getHistory = () => {
try { return JSON.parse(gmGet('pk_search_history', '[]')); } catch { return []; }
};
const saveHistory = (txt) => {
if (!txt) return;
let list = getHistory();
list = list.filter(x => x !== txt);
list.unshift(txt);
if (list.length > 3) list = list.slice(0, 3);
gmSet('pk_search_history', JSON.stringify(list));
};
const renderHistory = () => {
const list = getHistory();
if (list.length === 0) {
UI.searchHist.style.display = 'none';
return;
}
let html = `
${L.title_search_hist} ${L.btn_clear_hist}
`;
list.forEach(txt => {
html += `
`;
});
UI.searchHist.innerHTML = html;
UI.searchHist.style.display = 'flex';
UI.searchHist.querySelector('#pk-hist-del').onclick = (e) => {
e.stopPropagation();
gmSet('pk_search_history', '[]');
UI.searchHist.style.display = 'none';
};
UI.searchHist.querySelectorAll('.pk-select-item').forEach(el => {
el.onclick = (e) => {
const val = el.querySelector('span').textContent;
UI.searchInput.value = val;
performSearch(val);
UI.searchHist.style.display = 'none';
};
});
};
const performSearch = (val) => {
const txt = val.trim();
if (!txt) {
if (S.search && UI.searchClear) UI.searchClear.click();
return;
}
const isGlobal = UI.chkGlobal && UI.chkGlobal.checked && !S.uploadMode;
if (txt) {
saveHistory(txt);
if (isGlobal && !S.preSearchPath) {
S.preSearchPath = [...S.path];
}
if (isGlobal) {
S.sort = 'modified_time'; S.dir = 1;
S.path = [
{ id: '', name: L.btn_nav_home },
{ id: 'virtual_search_root', name: L.str_search_results }
];
renderCrumb();
}
}
S.search = txt;
if (S.dupMode) {
if (S.pinnedDupPath) {
S.pinnedDupPath = null;
S.sel.clear();
UI.selDupFolder.value = "";
const invertChk = document.getElementById('pk-dup-invert');
if(invertChk) {
invertChk.checked = false;
invertChk.disabled = true;
invertChk.parentNode.style.opacity = '0.5';
}
}
renderDupView();
updateStat();
}
else if (isGlobal) {
if (globalNeedsSync) {
setLoad(true);
updateLoadTxt(L.str_analyzing);
setTimeout(async () => {
if (!S.scanning) {
S.scanning = true;
UI.stopBtn.onclick = () => {
S.scanning = false;
updateLoadTxt(L.str_stopping);
if (UI.chkGlobal) UI.chkGlobal.checked = false;
};
try {
await runFlattenScanOperation(true, [], true);
globalNeedsSync = false;
} catch (e) {
console.error("[GlobalSearch] Sync Error:", e);
} finally {
S.scanning = false;
}
}
load(false, true).finally(() => setLoad(false));
}, 50);
} else {
load(false, false);
}
} else {
refresh();
}
UI.searchClear.style.display = txt ? 'flex' : 'none';
UI.searchHist.style.display = 'none';
UI.searchInput.blur();
};
if (UI.searchInput) {
UI.searchInput.oninput = (e) => {
const val = e.target.value.trim();
UI.searchClear.style.display = val ? 'flex' : 'none';
if (!val && S.search && UI.searchClear) {
UI.searchClear.click();
}
};
UI.searchInput.onfocus = () => {
renderHistory();
if (getHistory()?.length > 0) UI.searchHist.style.display = 'flex';
};
document.addEventListener('click', (e) => {
if (!UI || !UI.searchInput || !UI.searchHist) return;
if (!UI.searchInput.contains(e.target) && !UI.searchHist.contains(e.target)) {
UI.searchHist.style.display = 'none';
}
});
UI.searchInput.onkeydown = (e) => {
e.stopPropagation();
if (e.key === 'Enter') {
performSearch(e.target.value);
}
};
if (UI.searchBtn) {
UI.searchBtn.onclick = () => {
performSearch(UI.searchInput.value);
};
}
if (UI.searchClear) {
UI.searchClear.onclick = async () => {
const wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false;
if (!S.search && UI.searchInput.value) {
UI.searchInput.value = '';
UI.searchClear.style.display = 'none';
UI.searchInput.focus();
return;
}
const searchWasActive = !!S.search;
const isInVirtualStack = S.path.some(node => node.id === 'virtual_search_root');
let requiresReload = false;
UI.searchInput.value = '';
S.search = '';
S.lastGlobalResults = [];
try {
const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}'));
if (prefStore['virtual_search_root']) {
delete prefStore['virtual_search_root'];
gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore));
}
} catch(e) {}
if ((wasGlobalChecked || isInVirtualStack) && searchWasActive) {
const currentFolder = S.path[S.path.length - 1];
if (currentFolder.id !== 'virtual_search_root' && currentFolder.id !== '') {
const traceStack = [];
let ptrId = currentFolder.id;
let ptrName = currentFolder.name;
let safety = 100;
traceStack.unshift({ id: ptrId, name: ptrName });
while (ptrId && ptrId !== 'root' && safety > 0) {
if (globalParentIndex.has(ptrId)) {
const parent = globalParentIndex.get(ptrId);
ptrId = parent.id;
ptrName = parent.name;
if (ptrId === 'root' || ptrId === '') break;
traceStack.unshift({ id: ptrId, name: ptrName });
} else {
const node = S.itemMap.get(ptrId);
if (node && node._lineage && node._lineage.length > 0) {
const ancestors = node._lineage.filter(x => x.id !== '' && x.id !== 'root');
for (let k = ancestors.length - 1; k >= 0; k--) {
traceStack.unshift(ancestors[k]);
}
}
break;
}
safety--;
}
const realPath = [{ id: '', name: L.btn_nav_home }, ...traceStack];
S.path = realPath;
}
else if (isInVirtualStack || S.preSearchPath) {
S.path = S.preSearchPath ? [...S.preSearchPath] : [{ id: '', name: L.btn_nav_home }];
}
requiresReload = true;
}
S.preSearchPath = null;
UI.searchClear.style.display = 'none';
UI.searchHist.style.display = 'none';
if (S.dupMode && !isInVirtualStack) {
if (S.pinnedDupPath) {
S.pinnedDupPath = null;
S.sel.clear();
UI.selDupFolder.value = "";
const invertChk = document.getElementById('pk-dup-invert');
if(invertChk) {
invertChk.checked = false;
invertChk.disabled = true;
invertChk.parentNode.style.opacity = '0.5';
}
}
renderDupView();
updateStat();
} else if (S.isFlattened && !isInVirtualStack) {
refresh();
updateStat();
} else {
if (requiresReload) {
const targetNode = S.path[S.path.length - 1];
const targetKey = S.getRealCacheKey(targetNode.id);
const cachedData = (typeof globalCache !== 'undefined') ? (globalCache.get(targetKey) || globalCache.get('')) : null;
if (cachedData) {
if (cachedData.items) S.items = [...cachedData.items];
else if (Array.isArray(cachedData)) S.items = [...cachedData];
S.itemMap.clear();
for (const it of S.items) S.itemMap.set(it.id, it);
}
refresh();
setLoad(true);
const p = load(false, true);
if (UI.chkGlobal) UI.chkGlobal.checked = wasGlobalChecked;
await p;
} else {
refresh();
updateStat();
}
}
};
}
}
UI.btnHelp.onclick = () => {
const m = showModal(`
${L.modal_help_title}
${L.btn_close}
`);
const modalBox = m.querySelector('.pk-modal');
if (modalBox) {
Object.assign(modalBox.style, {
display: 'flex', flexDirection: 'column',
maxHeight: '85vh', overflow: 'hidden', padding: '30px'
});
const closeBtn = m.querySelector('.pk-modal-close');
if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' });
}
const scrollEl = m.querySelector('.pk-help-scroll');
const fadeEl = m.querySelector('#pk_help_fade');
if (scrollEl) {
scrollEl.style.maxHeight = 'none';
scrollEl.style.flex = '1';
scrollEl.style.minHeight = '0';
}
if (scrollEl && fadeEl) {
scrollEl.style.paddingBottom = "24px";
const updateFade = () => {
if (scrollEl.scrollHeight <= scrollEl.clientHeight + 5 || Math.ceil(scrollEl.scrollTop + scrollEl.clientHeight) >= scrollEl.scrollHeight - 30) {
fadeEl.style.opacity = '0';
} else {
fadeEl.style.opacity = '1';
}
};
scrollEl.addEventListener('scroll', updateFade, { passive: true });
const resizeObserver = new ResizeObserver(() => updateFade());
resizeObserver.observe(scrollEl);
const _orgRemove = m.remove.bind(m);
m.remove = () => {
resizeObserver.disconnect();
_orgRemove();
};
setTimeout(updateFade, 50);
}
m.querySelector('#help_close').onclick = () => m.remove();
m.tabIndex = 0;
setTimeout(() => m.focus(), 10);
m.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); e.stopPropagation();
m.remove();
}
});
};
UI.chkGlobal.onchange = async (e) => {
if (e.target.checked) {
if (S.movingIds && S.movingIds.size > 0) {
e.target.checked = false;
showAlert(L.msg_global_index_blocked_moving);
return;
}
const isSuppressed = gmGet('pk_suppress_global_warn', false);
if (!isSuppressed && !hasShownGlobalWarnSession) {
const userChoice = await new Promise((resolve) => {
const m = showModal(`
${L.title_confirm}
${esc(L.msg_global_warn).replace(/\n/g, ' ')}
${L.lbl_dont_show}
${L.btn_cancel}
${L.btn_ok}
`);
const modalBox = m.querySelector('.pk-modal');
if (modalBox) {
Object.assign(modalBox.style, { width: '420px', padding: '30px', height: 'auto', minHeight: 'auto' });
const closeBtn = m.querySelector('.pk-modal-close');
if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' });
}
m.querySelector('#cfm_cancel').onclick = () => { m.remove(); resolve({ ok: false }); };
m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve({ ok: false }); };
m.tabIndex = 0;
setTimeout(() => m.focus(), 10);
m.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); e.stopPropagation();
m.querySelector('#cfm_ok').click();
}
});
m.querySelector('#cfm_ok').onclick = () => {
const isChecked = m.querySelector('#pk_warn_ignore').checked;
m.remove();
resolve({ ok: true, suppress: isChecked });
};
});
if (!userChoice.ok) {
e.target.checked = false;
return;
}
hasShownGlobalWarnSession = true;
if (userChoice.suppress) {
gmSet('pk_suppress_global_warn', true);
}
}
if (!isGlobalIndexReady || globalNeedsSync) {
S.scanning = true;
UI.stopBtn.onclick = () => {
S.scanning = false;
updateLoadTxt(L.str_stopping);
UI.chkGlobal.checked = false;
};
await runFlattenScanOperation(true);
}
refresh();
} else {
if (S.scanning) {
S.scanning = false;
updateLoadTxt(L.str_stopping);
}
refresh();
}
};
const runFlattenScanOperation = async (isSyncOnly = false, specificTargets =[], isSilent = false) => {
S.scanId = (S.scanId || 0) + 1;
const myScanId = S.scanId;
let fileMap = new Map();
let processedFolders = 0;
if (S.scanAbortController) S.scanAbortController.abort();
S.scanAbortController = new AbortController();
const signal = S.scanAbortController.signal;
setLoad(true);
const isPartialScan = specificTargets && specificTargets.length > 0;
let rootNodes =[];
if (isPartialScan) {
updateLoadTxt(L.msg_init_scan_sel);
specificTargets.forEach(item => {
if (item.kind === 'drive#folder') {
rootNodes.push({
id: item.id,
name: item.name,
lineage:[{ id: item.id, name: item.name }],
retryCount: 0
});
} else if (!isSyncOnly) {
item._lineage =[];
fileMap.set(item.id, item);
}
});
} else {
updateLoadTxt(`${L.str_scanning} 0`);
const startNode = isSyncOnly ? { id: '', name: 'Root' } : S.path[S.path.length - 1];
rootNodes =[{ id: startNode.id || '', name: startNode.name || 'Root', lineage: [], retryCount: 0 }];
}
UI.stopBtn.onclick = () => {
S.scanning = false;
if (S.scanAbortController) S.scanAbortController.abort();
updateLoadTxt(L.str_stopping);
if (S.isFlattened) {
UI.scan.style.display = 'none';
UI.btnExit.style.display = 'flex';
if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none';
if (UI.btnExport) UI.btnExport.style.display = 'none';
} else {
UI.scan.style.display = 'flex';
UI.btnExit.style.display = 'none';
if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex';
if (UI.btnExport) UI.btnExport.style.display = 'flex';
if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex';
if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex';
if(UI.chkGlobal) UI.chkGlobal.checked = false;
S.isFlattened = false;
setTimeout(() => {
if (typeof resumeBackgroundDiscovery === 'function') {
console.log("♻️ Scan interrupted: Forcing background crawler to resume.");
resumeBackgroundDiscovery();
}
}, 1000);
}
};
S.scanning = true;
try {
await coreRecursiveEngine(rootNodes, {
signal: signal,
onFile: (f, parent) => {
if (!isSyncOnly) {
f._lineage = parent.lineage ||[];
fileMap.set(f.id, f);
}
},
onFolder: (folder, filesInFolder) => {
processedFolders++;
indexParents(folder.id, folder.name, filesInFolder);
if (typeof globalLineageMap !== 'undefined') {
globalLineageMap.set(folder.id, folder.lineage);
}
},
onProgress: (st) => {
const folderText = isPartialScan
? L.status_scanning_selection.replace('{n}', st.folders + " " + L.unit_folders)
: `${L.str_scanning} ${st.folders} ${L.unit_folders}`;
const retryTag = st.isRetrying ? `\n[ ${L.str_retries} ]` : "";
const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`;
updateLoadTxt(folderText + statusInfo + retryTag);
}
});
if (S.scanning && !signal.aborted && myScanId === S.scanId) {
globalNeedsSync = false;
isGlobalIndexReady = true;
if (!isSyncOnly) {
updateLoadTxt(L.str_merging);
let tempItems = Array.from(fileMap.values());
if (S.scanFilter && !isSyncOnly) {
const { minBytes, maxBytes, keyword } = S.scanFilter;
const kwList = keyword ? keyword.toLowerCase().split(/[,,]/).map(k => k.trim()).filter(k => k) : [];
tempItems = tempItems.filter(item => {
if (kwList.length > 0) {
const fullLowerName = (item.name || "").toLowerCase();
const lastDot = fullLowerName.lastIndexOf('.');
const nameWithoutExt = (item.kind !== 'drive#folder' && lastDot > 0)
? fullLowerName.substring(0, lastDot)
: fullLowerName;
if (kwList.some(k => nameWithoutExt.includes(k))) return false;
}
const sz = parseInt(item.size || 0);
if (sz < minBytes) return false;
if (maxBytes > 0 && sz > maxBytes) return false;
return true;
});
}
const total = tempItems.length;
S.items = new Array(total);
S.itemMap.clear();
let lastYield = performance.now();
for (let i = 0; i < total; i++) {
const item = tempItems[i];
S.items[i] = item;
S.itemMap.set(item.id, item);
if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) {
S.starredSet.add(item.id);
}
if (i % 5000 === 0 && performance.now() - lastYield > 16) {
updateLoadTxt(`${L.str_merging} ${Math.round((i / total) * 100)}%`);
await sleep(0);
lastYield = performance.now();
}
}
S.isFlattened = true;
S.sort = 'modified_time'; S.dir = 1;
UI.chkAll.checked = false;
S.sel.clear();
UI.scan.style.display = 'none';
UI.btnExit.style.display = 'flex';
if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'none';
if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none';
if (UI.lblGlobal) UI.lblGlobal.style.display = 'none';
if (UI.crumb) UI.crumb.style.setProperty('display', 'none', 'important');
updateLoadTxt(L.str_rendering);
await refresh();
const msg = L.msg_scan_done.replace('{n}', total).replace('{f}', processedFolders);
if (!isSilent) showAlert(msg);
}
}
} catch (e) {
if (e.name !== 'AbortError' && myScanId === S.scanId) {
showAlert(`${L.str_error_crit}: ${e.message}`);
}
if (!isSyncOnly && myScanId === S.scanId) {
UI.scan.style.display = 'flex';
UI.btnExit.style.display = 'none';
if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex';
if (UI.btnExport) UI.btnExport.style.display = 'flex';
UI.lblGlobal.style.display = 'flex';
}
if (myScanId === S.scanId) UI.chkGlobal.checked = false;
} finally {
if (myScanId === S.scanId) {
setLoad(false);
S.scanning = false;
S.scanAbortController = null;
if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun();
}
}
};
const openScanDupModal = async (initialTab) => {
if (S.loading || S.scanning) return;
const curFolderId = S.path[S.path.length - 1].id || '';
if (isPathBusy(curFolderId)) {
showAlert(L.msg_flatten_blocked_moving);
return;
}
S.wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false;
const selectedTargets =[];
if (S.sel.size > 0) {
S.sel.forEach(id => {
const item = S.itemMap.get(id);
if (item) selectedTargets.push(item);
});
}
const lastMin = gmGet('pk_scan_last_min', 0);
const lastMax = gmGet('pk_scan_last_max', '');
const lastUnit = gmGet('pk_scan_last_unit', 'MB');
const lastKeyword = gmGet('pk_scan_last_keyword', '');
let currentStrict = gmGet('pk_dup_strictness', 'strict');
const scanTxt = L.btn_scan;
const dupTxt = L.tip_dup;
const L_min = L.lbl_ana_min;
const L_max = L.lbl_ana_max;
let scanTargetDesc = selectedTargets.length > 0 ? L.lbl_scan_selected.replace('{n}', selectedTargets.length) : L.lbl_scan_current;
let dupTargetDesc = selectedTargets.length > 0 ? L.lbl_dup_selected.replace('{n}', selectedTargets.length) : L.lbl_dup_current;
const m = showModal(`
${L.title_file_analysis}
${scanTargetDesc}
${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}
${L.btn_cancel}
${L.btn_ok}
`);
const modalBox = m.querySelector('.pk-modal');
if (modalBox) {
Object.assign(modalBox.style, { width: 'auto', padding: '0', overflow: 'visible', height: 'auto', minHeight: 'auto' });
const closeBtn = m.querySelector('.pk-modal-close');
if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' });
}
m.querySelectorAll('.pk-s-tab').forEach(tab => {
tab.onclick = () => {
m.querySelectorAll('.pk-s-tab').forEach(t => t.classList.remove('act'));
tab.classList.add('act');
const curMode = tab.dataset.val;
m.querySelector('#pane_scan').style.display = curMode === 'scan' ? 'block' : 'none';
m.querySelector('#pane_dup').style.display = curMode === 'dup' ? 'flex' : 'none';
};
});
const inpMin = m.querySelector('#sc_val_min');
const inpMax = m.querySelector('#sc_val_max');
const unitBtn = m.querySelector('#sc_unit_btn');
const unitMenu = m.querySelector('#sc_unit_menu');
const unitTxt = m.querySelector('#sc_unit_txt');
let currentUnit = lastUnit;
m.querySelector('#sc_inc_min').onclick = (e) => { e.stopPropagation(); inpMin.value = (parseInt(inpMin.value) || 0) + 1; };
m.querySelector('#sc_dec_min').onclick = (e) => { e.stopPropagation(); inpMin.value = Math.max(0, (parseInt(inpMin.value) || 1) - 1); };
m.querySelector('#sc_inc_max').onclick = (e) => { e.stopPropagation(); inpMax.value = (parseInt(inpMax.value) || 0) + 1; };
m.querySelector('#sc_dec_max').onclick = (e) => { e.stopPropagation(); inpMax.value = Math.max(0, (parseInt(inpMax.value) || 1) - 1); };
unitBtn.onclick = (e) => { e.stopPropagation(); unitMenu.style.display = unitMenu.style.display === 'block' ? 'none' : 'block'; };
m.querySelectorAll('.pk-ana-item').forEach(item => {
item.onclick = () => {
m.querySelectorAll('.pk-ana-item').forEach(i => i.classList.remove('act'));
item.classList.add('act');
currentUnit = item.dataset.v;
unitTxt.textContent = currentUnit;
unitMenu.style.display = 'none';
};
});
const closeMenu = () => { if (unitMenu) unitMenu.style.display = 'none'; };
setTimeout(() => document.addEventListener('click', closeMenu), 0);
const _orgRemove = m.remove.bind(m);
m.remove = () => {
document.removeEventListener('click', closeMenu);
_orgRemove();
};
if (S.dupConfig) {
m.querySelector('#scan_video').checked = S.dupConfig.video;
m.querySelector('#scan_image').checked = S.dupConfig.image;
m.querySelector('#scan_other').checked = S.dupConfig.other;
}
const scStrictTrigger = m.querySelector('#cs_sc_strict .pk-select-trigger');
const scStrictMenu = m.querySelector('#cs_sc_strict .pk-select-menu');
const scStrictTxt = m.querySelector('#txt_sc_strict');
scStrictTrigger.onclick = (e) => {
e.stopPropagation();
scStrictMenu.style.display = scStrictMenu.style.display === 'block' ? 'none' : 'block';
};
m.querySelectorAll('#cs_sc_strict .pk-select-item').forEach(item => {
item.onclick = (e) => {
e.stopPropagation();
m.querySelectorAll('#cs_sc_strict .pk-select-item').forEach(i => i.classList.remove('act'));
item.classList.add('act');
currentStrict = item.dataset.val;
scStrictTxt.textContent = item.textContent;
scStrictMenu.style.display = 'none';
};
});
const saveScanInputs = () => {
gmSet('pk_scan_last_min', parseInt(inpMin.value) || 0);
gmSet('pk_scan_last_max', inpMax.value.trim());
gmSet('pk_scan_last_unit', currentUnit);
gmSet('pk_scan_last_keyword', m.querySelector('#sc_keyword').value.trim());
gmSet('pk_dup_strictness', currentStrict);
};
m.querySelector('#sc_cancel').onclick = () => { saveScanInputs(); m.remove(); };
m.querySelector('.pk-modal-close').onclick = () => { saveScanInputs(); m.remove(); };
m.tabIndex = 0;
setTimeout(() => m.focus(), 10);
m.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); e.stopPropagation();
m.querySelector('#sc_start').click();
}
});
m.querySelector('#sc_start').onclick = async () => {
const mode = m.querySelector('.pk-s-tab.act').dataset.val;
saveScanInputs();
if (mode === 'scan') {
const vMin = parseInt(inpMin.value) || 0;
const vMax = parseInt(inpMax.value) || 0;
const kw = m.querySelector('#sc_keyword').value.trim();
if (vMin < 0 || (vMax > 0 && vMin > vMax)) {
inpMin.style.borderColor = '#d93025';
if (vMax > 0 && vMin > vMax) inpMax.style.borderColor = '#d93025';
return;
}
gmSet('pk_scan_last_min', vMin);
gmSet('pk_scan_last_max', vMax > 0 ? vMax : '');
gmSet('pk_scan_last_unit', currentUnit);
gmSet('pk_scan_last_keyword', kw);
let mult = 1;
if (currentUnit === 'MB') mult = 1024 * 1024;
else if (currentUnit === 'GB') mult = 1024 * 1024 * 1024;
else if (currentUnit === 'TB') mult = 1024 * 1024 * 1024 * 1024;
S.scanFilter = {
minBytes: Math.floor(vMin * mult),
maxBytes: vMax > 0 ? Math.floor(vMax * mult) : 0,
keyword: kw
};
m.remove();
S.search = '';
if (UI.searchInput) UI.searchInput.value = '';
if (UI.searchClear) UI.searchClear.style.display = 'none';
if (UI.chkSearchPath) UI.chkSearchPath.checked = false;
S.scanning = true;
UI.scan.style.display = 'none';
UI.btnExit.style.display = 'flex';
if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none';
if (UI.lblGlobal) UI.lblGlobal.style.display = 'none';
if (UI.chkGlobal) UI.chkGlobal.checked = false;
UI.stopBtn.onclick = () => {
S.scanning = false;
if (S.scanAbortController) S.scanAbortController.abort();
updateLoadTxt(L.str_stopping);
if (S.isFlattened) {
UI.scan.style.display = 'none';
UI.btnExit.style.display = 'flex';
if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none';
if (UI.btnExport) UI.btnExport.style.display = 'none';
} else {
UI.scan.style.display = 'flex';
UI.btnExit.style.display = 'none';
if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex';
if (UI.btnExport) UI.btnExport.style.display = 'flex';
if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex';
if (UI.lblGlobal) UI.lblGlobal.style.display = 'flex';
if (UI.chkGlobal) UI.chkGlobal.checked = false;
S.isFlattened = false;
setTimeout(() => {
if (typeof resumeBackgroundDiscovery === 'function') {
resumeBackgroundDiscovery();
}
}, 1000);
}
};
S.lastScanTargets = selectedTargets;
await runFlattenScanOperation(false, selectedTargets, false);
} else {
S.dupConfig = {
video: m.querySelector('#scan_video').checked,
image: m.querySelector('#scan_image').checked,
other: m.querySelector('#scan_other').checked
};
if (!S.dupConfig.video && !S.dupConfig.image && !S.dupConfig.other) return;
m.remove();
S.scanning = true;
S.scanId = (S.scanId || 0) + 1;
const myScanId = S.scanId;
let fileMap = new Map();
let processedFolders = 0;
if (S.scanAbortController) S.scanAbortController.abort();
S.scanAbortController = new AbortController();
const signal = S.scanAbortController.signal;
setLoad(true);
const isPartialScan = selectedTargets.length > 0;
let rootNodes =[];
if (isPartialScan) {
updateLoadTxt(L.msg_init_scan_sel);
selectedTargets.forEach(item => {
if (item.kind === 'drive#folder') {
rootNodes.push({
id: item.id,
name: item.name,
lineage:[{ id: item.id, name: item.name }],
retryCount: 0
});
} else {
item._lineage =[];
fileMap.set(item.id, item);
}
});
} else {
updateLoadTxt(`${L.str_scanning} 0`);
const startNode = S.path[S.path.length - 1];
rootNodes =[{ id: startNode.id || '', name: startNode.name || 'Root', lineage: [], retryCount: 0 }];
}
UI.stopBtn.onclick = () => {
S.scanning = false;
if (S.scanAbortController) S.scanAbortController.abort();
updateLoadTxt(L.str_stopping);
setLoad(false);
};
try {
await coreRecursiveEngine(rootNodes, {
signal: signal,
onFile: (f, parent) => {
f._lineage = parent.lineage ||[];
fileMap.set(f.id, f);
},
onFolder: (folder, filesInFolder) => {
processedFolders++;
if (typeof globalCache !== 'undefined' && !globalCache.has(folder.id)) {
globalCache.set(folder.id, [...filesInFolder]);
}
indexParents(folder.id, folder.name, filesInFolder);
if (typeof globalLineageMap !== 'undefined') {
globalLineageMap.set(folder.id, folder.lineage);
}
},
onProgress: (st) => {
const folderText = isPartialScan
? L.status_scanning_selection.replace('{n}', st.folders + " " + L.unit_folders)
: `${L.str_scanning} ${st.folders} ${L.unit_folders}`;
const retryTag = st.isRetrying ? `\n[ ${L.str_retries} ]` : "";
const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`;
updateLoadTxt(folderText + statusInfo + retryTag);
}
});
if (S.scanning && !signal.aborted && myScanId === S.scanId) {
updateLoadTxt(L.str_merging);
const tempItems = Array.from(fileMap.values());
const cfg = S.dupConfig || { video: true, image: false, other: false };
let candidates = tempItems.filter(i => {
if (!i.mime_type) return false;
const isVideo = i.mime_type.startsWith('video');
const isImage = i.mime_type.startsWith('image');
const isOther = !isVideo && !isImage;
if (isVideo && cfg.video) return true;
if (isImage && cfg.image) return true;
if (isOther && cfg.other) return true;
return false;
});
const preGroups = await computeDuplicateGroups(candidates, cfg, () => S.scanning && !signal.aborted && myScanId === S.scanId);
if (preGroups.length === 0) {
setLoad(false);
showToast(L.msg_dup_none);
S.dupRunning = false;
S.scanning = false;
return;
}
const total = tempItems.length;
S.items = new Array(total);
S.itemMap.clear();
let lastYield = performance.now();
for (let i = 0; i < total; i++) {
const item = tempItems[i];
S.items[i] = item;
S.itemMap.set(item.id, item);
if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) {
S.starredSet.add(item.id);
}
if (i % 5000 === 0 && performance.now() - lastYield > 16) {
updateLoadTxt(`${L.str_merging} ${Math.round((i / total) * 100)}%`);
await sleep(0);
lastYield = performance.now();
}
}
S.dupMode = true;
S.isFlattened = false;
S.sort = 'modified_time'; S.dir = 1;
UI.chkAll.checked = false;
S.sel.clear();
UI.scan.style.display = 'none';
UI.btnExit.style.display = 'flex';
if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none';
UI.lblGlobal.style.display = 'none';
UI.chkGlobal.checked = false;
if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'none';
if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none';
if (UI.crumb) UI.crumb.style.setProperty('display', 'none', 'important');
if (UI.lblSearchPath) UI.lblSearchPath.style.display = 'flex';
updateLoadTxt(L.str_rendering);
S.display =[...S.items];
await refresh();
}
} catch (e) {
if (e.name !== 'AbortError' && myScanId === S.scanId) {
showAlert(`${L.str_error_crit}: ${e.message}`);
}
if (myScanId === S.scanId) {
UI.scan.style.display = 'flex';
UI.btnExit.style.display = 'none';
if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex';
if (UI.btnExport) UI.btnExport.style.display = 'flex';
UI.lblGlobal.style.display = 'flex';
}
} finally {
if (myScanId === S.scanId) {
setLoad(false);
S.scanning = false;
S.scanAbortController = null;
if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun();
}
}
}
};
};
UI.scan.onclick = () => openScanDupModal('scan');
const onOfflineFilterChange = () => {
if (S.offlineMode) {
S.offlineFilters = {
running: UI.chkOffRun.checked,
failed: UI.chkOffFail.checked,
complete: UI.chkOffOk.checked
};
refresh();
updateStat();
}
};
if (UI.chkOffRun) UI.chkOffRun.onchange = onOfflineFilterChange;
if (UI.chkOffFail) UI.chkOffFail.onchange = onOfflineFilterChange;
if (UI.chkOffOk) UI.chkOffOk.onchange = onOfflineFilterChange;
const onUploadFilterChange = () => {
if (S.uploadMode) {
S.uploadFilters = {
running: UI.chkUpRun.checked,
paused: UI.chkUpPause.checked,
complete: UI.chkUpDone.checked
};
refresh();
updateStat();
}
};
if (UI.chkUpRun) UI.chkUpRun.onchange = onUploadFilterChange;
if (UI.chkUpPause) UI.chkUpPause.onchange = onUploadFilterChange;
if (UI.chkUpDone) UI.chkUpDone.onchange = onUploadFilterChange;
const onDupFilterChange = () => {
if(S.dupMode) {
if (S.pinnedDupPath) {
S.pinnedDupPath = null;
S.sel.clear();
UI.selDupFolder.value = "";
const invertChk = document.getElementById('pk-dup-invert');
if(invertChk) {
invertChk.checked = false;
invertChk.disabled = true;
invertChk.parentNode.style.opacity = '0.5';
}
}
renderDupView();
}
};
UI.chkName.onchange = onDupFilterChange;
UI.chkSim.onchange = onDupFilterChange;
UI.chkHash.onchange = onDupFilterChange;
if (UI.chkSearchPath) {
UI.chkSearchPath.onchange = () => {
if (S.dupMode && S.search) {
renderDupView();
} else if ((S.isFlattened || S.analyzeMode) && S.search) {
refresh();
}
};
}
UI.btnExit.onclick = async () => {
S.scanning = false;
S.dupMode = false;
S.suppressClearConfirm = false;
S.isFlattened = false;
S.scanFilter = null;
if (S.filterState) S.filterState = { active: false, cat: 'all', ext: 'all' };
S._sortAppliedForId = null;
S._comicApplied = false;
if (S.analyzeMode) {
S.analyzeMode = false;
S.analyzeResultItems = null;
S.analyzeSimGroups = null;
S.analyzeMap = null;
S.path = [{ id: '', name: L.btn_nav_home }];
}
S.dupRawGroups = [];
S.pinnedDupPath = null;
if (UI.selDupFolder) UI.selDupFolder.value = "";
const invertChk = document.getElementById('pk-dup-invert');
if (invertChk) {
invertChk.checked = false;
invertChk.disabled = true;
invertChk.parentNode.style.opacity = '0.5';
}
if (UI.chkSearchPath) UI.chkSearchPath.checked = false;
isGUISensitive = false;
S.sort = 'modified_time';
S.dir = 1;
if (UI.crumb) UI.crumb.style.display = '';
S.items.sort((a, b) => {
if (a.kind !== b.kind) return a.kind === 'drive#folder' ? -1 : 1;
return a.name.localeCompare(b.name);
});
S.clearSelection();
const targetNode = S.path[S.path.length - 1];
const targetKey = S.getRealCacheKey(targetNode.id);
const cachedData = (typeof globalCache !== 'undefined') ? (globalCache.get(targetKey) || globalCache.get('')) : null;
if (cachedData) {
if (cachedData.items) S.items = [...cachedData.items];
else if (Array.isArray(cachedData)) S.items = [...cachedData];
S.itemMap.clear();
for (const it of S.items) S.itemMap.set(it.id, it);
} else {
S.items = [];
S.itemMap.clear();
}
setLoad(true, true);
refresh();
updateQuotaUI();
await load(false, true);
if (typeof S.wasGlobalChecked !== 'undefined' && UI.chkGlobal) {
UI.chkGlobal.checked = S.wasGlobalChecked;
}
setTimeout(() => {
if (typeof resumeBackgroundDiscovery === 'function') {
console.log("♻️ Sandbox exited: Forcing global discovery to resume.");
resumeBackgroundDiscovery();
}
}, 1500);
};
UI.cols.forEach(c => c.onclick = () => {
if (S.dupMode) return;
const k = c.dataset.k;
if (S.sort === k) {
S.dir *= -1;
}
else {
S.sort = k;
S.dir = 1;
}
refresh();
});
if (UI.btnFolderFirst) {
S.renderFolderFirst = () => {
UI.btnFolderFirst.style.color = S.folderFirst ? 'var(--pk-pri)' : '#666';
};
UI.btnFolderFirst.onmouseenter = () => {
if (!S.folderFirst) UI.btnFolderFirst.style.color = 'var(--pk-fg)';
};
UI.btnFolderFirst.onmouseleave = () => {
if (!S.folderFirst) UI.btnFolderFirst.style.color = '#666';
};
const nameWrap = el.querySelector('#pk-name-text-wrap');
if (nameWrap) {
nameWrap.onmouseenter = () => {
if (S.sort !== 'name') nameWrap.style.color = 'var(--pk-fg)';
};
nameWrap.onmouseleave = () => {
if (S.sort !== 'name') nameWrap.style.color = '#666';
};
}
S.renderFolderFirst();
UI.btnFolderFirst.onclick = (e) => {
e.stopPropagation();
S.folderFirst = !S.folderFirst;
const curNode = S.path[S.path.length - 1];
const isStandard = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!curNode.id.startsWith('virtual_') || curNode.id === 'virtual_search_root');
if (isStandard) {
if (gmGet('pk_sort_independent', false)) {
const folderId = curNode.id || 'root';
try {
const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}'));
const currentPref = prefStore[folderId] || { sort: S.sort, dir: S.dir };
currentPref.folderFirst = S.folderFirst;
currentPref.sort = S.sort;
currentPref.dir = S.dir;
prefStore[folderId] = currentPref;
gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore));
} catch(e) {}
} else {
const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}'));
globalPref.folderFirst = S.folderFirst;
globalPref.sort = S.sort;
globalPref.dir = S.dir;
gmSet('pk_global_sort_pref', JSON.stringify(globalPref));
gmSet('pk_folder_first', S.folderFirst);
}
} else {
gmSet('pk_folder_first', S.folderFirst);
}
S.renderFolderFirst();
refresh();
};
}
const btnInvert = document.getElementById('pk-btn-invert');
if (btnInvert) {
btnInvert.onclick = (e) => {
e.stopPropagation();
if (S.loading || S.display.length === 0) return;
const newSel = new Set();
for (let i = 0; i < S.display.length; i++) {
const item = S.display[i];
if (item && !item.isHeader) {
if (!S.sel.has(item.id)) {
newSel.add(item.id);
}
}
}
S.sel = newSel;
S.lastSelIdx = -1;
S.activeId = null;
renderVisible();
updateStat();
};
btnInvert.onmouseenter = () => btnInvert.style.color = 'var(--pk-pri)';
btnInvert.onmouseleave = () => btnInvert.style.color = 'var(--pk-fg)';
}
S.handleSelectAll = (e) => {
if (!e || !e.target) {
if (UI.chkAll) UI.chkAll.checked = !UI.chkAll.checked;
}
S.activeId = null;
S.lastSelIdx = -1;
let totalVisible = 0;
const allIds = [];
const len = S.display.length;
for (let i = 0; i < len; i++) {
const item = S.display[i];
if (item.isHeader) continue;
if (S.movingIds.has(item.id)) continue;
totalVisible++;
allIds.push(item.id);
}
const isSelectAllAction = (S.sel.size < totalVisible);
UI.chkAll.checked = isSelectAllAction;
if (isSelectAllAction) {
S.sel = new Set(allIds);
} else {
S.sel.clear();
}
requestAnimationFrame(() => {
renderVisible();
updateStat();
UI.chkAll.checked = isSelectAllAction;
});
};
if (UI.btnAnaSelect || UI.btnDupSmart) {
const pop = document.createElement('div');
pop.className = 'pk-ana-pop';
pop.innerHTML = `
${L.opt_keep_new}
${L.opt_keep_old}
${L.opt_keep_large}
${L.opt_keep_small}
${L.opt_keep_short}
${L.opt_keep_long}
`;
UI.win.appendChild(pop);
let activeTargetBtn = null;
const updatePopPos = () => {
if (pop.style.display !== 'flex' || !activeTargetBtn) return;
const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1;
const winRect = UI.win.getBoundingClientRect();
const btnRect = activeTargetBtn.getBoundingClientRect();
let left = (btnRect.left - winRect.left) / scale;
const top = (btnRect.bottom - winRect.top) / scale;
const winWidth = winRect.width / scale;
const popWidth = 340;
if (left + popWidth > winWidth - 10) {
left = (btnRect.right - winRect.left) / scale - popWidth;
}
pop.style.left = Math.max(10, left) + 'px';
pop.style.top = (top + 5) + 'px';
};
const togglePop = (e, btn) => {
e.stopPropagation();
const isVisible = (pop.style.display === 'flex' && activeTargetBtn === btn);
if (isVisible) {
pop.style.display = 'none';
activeTargetBtn = null;
} else {
if (S.analyzeMode && !S.hasShownAnaWarn) {
showToast(L.msg_ana_warn, 'warning', 6000);
S.hasShownAnaWarn = true;
}
pop.style.display = 'flex';
activeTargetBtn = btn;
updatePopPos();
}
};
if (UI.btnAnaSelect) UI.btnAnaSelect.onclick = (e) => togglePop(e, UI.btnAnaSelect);
if (UI.btnDupSmart) UI.btnDupSmart.onclick = (e) => togglePop(e, UI.btnDupSmart);
window.addEventListener('resize', updatePopPos);
pop.querySelectorAll('.pk-ana-opt').forEach(opt => {
opt.onclick = () => {
const op = opt.dataset.op;
S.sel.clear();
const isBetter = (type, curW, curM) => {
if (type === 'new') return new Date(curM.modified_time) > new Date(curW.modified_time);
if (type === 'old') return new Date(curM.modified_time) < new Date(curW.modified_time);
if (type === 'large') return BigInt(curM.size || 0) > BigInt(curW.size || 0);
if (type === 'small') return BigInt(curM.size || 0) < BigInt(curW.size || 0);
if (type === 'short') return curM.name.length < curW.name.length;
if (type === 'long') return curM.name.length > curW.name.length;
return false;
};
if (S.analyzeMode && S.analyzeSimGroups) {
S.analyzeSimGroups.forEach(g => {
const members = g.ids.map(id => S.itemMap.get(id)).filter(Boolean);
if (members.length < 2) return;
let winner = members[0];
members.forEach(m => { if (isBetter(op, winner, m)) winner = m; });
members.forEach(m => { if (m.id !== winner.id) S.sel.add(m.id); });
});
} else if (S.dupMode && S.dupGroups) {
const itemMap = new Map();
S.display.forEach(d => {
if (d.isHeader) return;
const gIdx = S.dupGroups.get(d.id);
if (gIdx !== undefined) {
if (!itemMap.has(gIdx)) itemMap.set(gIdx,[]);
itemMap.get(gIdx).push(d);
}
});
itemMap.forEach(members => {
if (members.length < 2) return;
let winner = members[0];
members.forEach(m => { if (isBetter(op, winner, m)) winner = m; });
members.forEach(m => { if (m.id !== winner.id) S.sel.add(m.id); });
});
}
pop.style.display = 'none';
activeTargetBtn = null;
renderVisible();
updateStat();
};
});
document.addEventListener('mousedown', (e) => {
const isClickInsideBtn = (UI.btnAnaSelect && UI.btnAnaSelect.contains(e.target)) ||
(UI.btnDupSmart && UI.btnDupSmart.contains(e.target));
if (!pop.contains(e.target) && !isClickInsideBtn) {
pop.style.display = 'none';
activeTargetBtn = null;
}
});
}
UI.btnRefresh.onclick = async () => {
updateQuotaUI();
if (S.isFlattened) {
if (!S.scanning) {
S.scanning = true;
UI.scan.style.display = 'none';
UI.btnExit.style.display = 'flex';
UI.stopBtn.onclick = () => {
S.scanning = false;
updateLoadTxt(L.str_stopping);
UI.scan.style.display = 'none';
UI.btnExit.style.display = 'flex';
if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none';
if (UI.btnExport) UI.btnExport.style.display = 'none';
if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none';
if (UI.lblGlobal) UI.lblGlobal.style.display = 'none';
};
runFlattenScanOperation(false, S.lastScanTargets, true).catch(e => {
console.error(e);
S.scanning = false;
});
}
return;
}
const cur = S.path[S.path.length - 1];
const intent = UI.chkGlobal ? UI.chkGlobal.checked : false;
if (cur.id === 'analyze_root') {
setLoad(true);
updateLoadTxt(L.str_refreshing);
await sleep(200);
await load();
return;
}
if (cur.id) S.cache.delete(cur.id);
const p = load(false, true);
if (UI.chkGlobal) UI.chkGlobal.checked = intent;
await p;
};
if (UI.btnAnalyze) {
UI.btnAnalyze.onclick = async () => {
if (S.trashMode) return;
const curFolderId = S.path[S.path.length - 1].id || '';
if (isPathBusy(curFolderId)) {
showAlert(L.msg_op_blocked_analyzing);
return;
}
S.search = '';
if (UI.searchInput) UI.searchInput.value = '';
if (UI.searchClear) UI.searchClear.style.display = 'none';
S.wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false;
const lastMin = gmGet('pk_analyze_last_min', 0);
const lastMax = gmGet('pk_analyze_last_max', '');
const lastUnit = gmGet('pk_analyze_last_unit', 'GB');
const lastKeyword = gmGet('pk_analyze_last_keyword', '');
const lastSim = gmGet('pk_analyze_last_sim', 1.0);
const lastAlgo = gmGet('pk_analyze_last_algo', 'sim');
const result = await new Promise((resolve) => {
const L_min = L.lbl_ana_min;
const L_max = L.lbl_ana_max;
const analyzeTargetDesc = S.sel.size > 0 ? L.lbl_analyze_selected.replace('{n}', S.sel.size) : L.lbl_analyze_current;
const anaSimTargetDesc = S.sel.size > 0 ? L.lbl_ana_sim_selected.replace('{n}', S.sel.size) : L.lbl_ana_sim_current;
const m = showModal(`
${L.btn_analyze}
${L.opt_ana_large}
${L.opt_ana_sim}
${analyzeTargetDesc}
${L_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}
${anaSimTargetDesc}
${L.lbl_threshold}
${(lastSim === 0.01) ? L.opt_loose : L.opt_strict} ${CONF.crumbIcons.down}
${L.btn_cancel}
${L.btn_ok}
`);
let currentMode = 'large';
let currentSim = lastSim;
const updateAlgoLabel = () => {
const lblEl = m.querySelector('#cs_ana_sim .pk-select-label');
if (lblEl) lblEl.textContent = L.lbl_threshold;
};
m.querySelectorAll('input[name="ana_sim_algo"]').forEach(r => r.addEventListener('change', updateAlgoLabel));
updateAlgoLabel();
m.querySelector('#pk_algo_help').onclick = (e) => {
e.stopPropagation();
showAlert(L.algo_help_content, L.title_algo_help);
};
m.querySelectorAll('.pk-s-tab').forEach(tab => {
tab.onclick = () => {
m.querySelectorAll('.pk-s-tab').forEach(t => t.classList.remove('act'));
tab.classList.add('act');
currentMode = tab.dataset.val;
m.querySelector('#ana_pane_large').style.display = currentMode === 'large' ? 'block' : 'none';
m.querySelector('#ana_pane_similar').style.display = currentMode === 'similar' ? 'block' : 'none';
};
});
const simTrigger = m.querySelector('#cs_ana_sim .pk-select-trigger');
const simMenu = m.querySelector('#cs_ana_sim .pk-select-menu');
const simTxt = m.querySelector('#txt_ana_sim');
simTrigger.onclick = (e) => { e.stopPropagation(); simMenu.style.display = simMenu.style.display === 'block' ? 'none' : 'block'; };
m.querySelectorAll('#cs_ana_sim .pk-select-item').forEach(item => {
item.onclick = (e) => {
e.stopPropagation();
m.querySelectorAll('#cs_ana_sim .pk-select-item').forEach(i => i.classList.remove('act'));
item.classList.add('act');
currentSim = parseFloat(item.dataset.val);
gmSet('pk_analyze_last_sim', currentSim);
simTxt.textContent = (currentSim <= 0.5) ? L.opt_loose : L.opt_strict;
simMenu.style.display = 'none';
};
});
const inpMin = m.querySelector('#an_val_min');
const inpMax = m.querySelector('#an_val_max');
const btn = m.querySelector('#an_unit_btn');
m.querySelector('#an_inc_min').onclick = (e) => { e.stopPropagation(); inpMin.value = (parseInt(inpMin.value) || 0) + 1; inpMin.dispatchEvent(new Event('input')); };
m.querySelector('#an_dec_min').onclick = (e) => { e.stopPropagation(); inpMin.value = Math.max(0, (parseInt(inpMin.value) || 1) - 1); inpMin.dispatchEvent(new Event('input')); };
m.querySelector('#an_inc_max').onclick = (e) => { e.stopPropagation(); inpMax.value = (parseInt(inpMax.value) || 0) + 1; inpMax.dispatchEvent(new Event('input')); };
m.querySelector('#an_dec_max').onclick = (e) => { e.stopPropagation(); inpMax.value = Math.max(0, (parseInt(inpMax.value) || 1) - 1); inpMax.dispatchEvent(new Event('input')); };
const menu = m.querySelector('#an_unit_menu');
const txt = m.querySelector('#an_unit_txt');
let currentUnit = lastUnit;
btn.onclick = (e) => { e.stopPropagation(); menu.style.display = menu.style.display === 'block' ? 'none' : 'block'; };
m.querySelectorAll('.pk-ana-item').forEach(item => {
item.onclick = () => {
m.querySelectorAll('.pk-ana-item').forEach(i => i.classList.remove('act'));
item.classList.add('act');
currentUnit = item.dataset.v;
txt.textContent = currentUnit;
menu.style.display = 'none';
};
});
const closeMenu = () => { if(menu) menu.style.display = 'none'; if(simMenu) simMenu.style.display = 'none'; };
setTimeout(() => document.addEventListener('click', closeMenu), 0);
const _orgRemove = m.remove.bind(m);
m.remove = () => {
document.removeEventListener('click', closeMenu);
_orgRemove();
};
setTimeout(() => { inpMin.focus(); inpMin.select(); }, 50);
const kHandler = (e) => {
if (e.key === 'Enter') m.querySelector('#an_confirm').click();
if (e.key === 'Escape') m.querySelector('#an_cancel').click();
};
inpMin.onkeydown = kHandler;
inpMax.onkeydown = kHandler;
const saveAnalyzeInputs = () => {
gmSet('pk_analyze_last_min', parseInt(inpMin.value) || 0);
gmSet('pk_analyze_last_max', inpMax.value.trim());
gmSet('pk_analyze_last_unit', currentUnit);
gmSet('pk_analyze_last_keyword', m.querySelector('#an_keyword').value.trim());
const algo = m.querySelector('input[name="ana_sim_algo"]:checked')?.value;
if (algo) gmSet('pk_analyze_last_algo', algo);
};
m.querySelector('#an_cancel').onclick = () => { saveAnalyzeInputs(); m.remove(); resolve(null); };
m.querySelector('.pk-modal-close').onclick = () => { saveAnalyzeInputs(); m.remove(); resolve(null); };
m.querySelector('#an_confirm').onclick = () => {
if (currentMode === 'large') {
const vMin = parseInt(inpMin.value) || 0;
const vMax = parseInt(inpMax.value) || 0;
const kw = m.querySelector('#an_keyword').value.trim();
if (vMin < 0 || (vMax > 0 && vMin > vMax)) {
inpMin.style.borderColor = '#d93025';
if (vMax > 0 && vMin > vMax) inpMax.style.borderColor = '#d93025';
return;
}
saveAnalyzeInputs();
let mult = 1;
if (currentUnit === 'MB') mult = 1024 * 1024;
else if (currentUnit === 'GB') mult = 1024 * 1024 * 1024;
else if (currentUnit === 'TB') mult = 1024 * 1024 * 1024 * 1024;
m.remove();
resolve({ mode: 'large', minBytes: Math.floor(vMin * mult), maxBytes: vMax > 0 ? Math.floor(vMax * mult) : 0, keyword: kw });
} else {
const algo = m.querySelector('input[name="ana_sim_algo"]:checked').value;
gmSet('pk_analyze_last_algo', algo);
m.remove();
resolve({ mode: 'similar', threshold: currentSim, algo: algo });
}
};
const modalBox = m.querySelector('.pk-modal');
if (modalBox) {
Object.assign(modalBox.style, {
width: '540px',
height: 'auto',
minHeight: 'auto',
overflow: 'visible',
paddingBottom: '30px'
});
const closeBtn = m.querySelector('.pk-modal-close');
if (closeBtn) Object.assign(closeBtn.style, { top: '22px', right: '22px' });
}
});
if (result === null) return;
const isSimMode = result.mode === 'similar';
const minBytes = result.minBytes || 0;
const maxBytes = result.maxBytes || 0;
const simThreshold = result.threshold || 0.9;
const simAlgo = result.algo || 'sim';
setLoad(true);
isGUISensitive = true;
let nodeMap = new Map();
const largeFolders = [];
const startNodes = [];
const getRealLineage = (item) => {
if (item._lineage && item._lineage.length > 0) {
return item._lineage;
}
const cleanPath = S.path.filter(p => p.id !== 'analyze_root' && p.id !== 'virtual_search_root');
return [...cleanPath, { id: item.id, name: item.name }];
};
if (S.sel.size > 0) {
S.sel.forEach(id => {
const item = S.itemMap.get(id);
if (item && item.kind === 'drive#folder') {
const fullLineage = getRealLineage(item);
startNodes.push({
id: item.id,
name: item.name,
icon_link: item.icon_link,
starred: item.starred,
tags: item.tags,
lineage: fullLineage,
retryCount: 0,
_pathStr: fullLineage.map(x => x.name).join('/')
});
}
});
} else {
const subFolders = S.items.filter(it => it.kind === 'drive#folder');
if (subFolders.length > 0) {
subFolders.forEach(item => {
const fullLineage = getRealLineage(item);
startNodes.push({
id: item.id,
name: item.name,
icon_link: item.icon_link,
starred: item.starred,
tags: item.tags,
lineage: fullLineage,
retryCount: 0,
_pathStr: fullLineage.map(x => x.name).join('/')
});
});
} else {
const cur = S.path[S.path.length - 1];
const cleanPath = S.path.filter(p => p.id !== 'analyze_root' && p.id !== 'virtual_search_root');
if (cleanPath.length === 0) cleanPath.push({ id: '', name: L.btn_nav_home });
const rootName = cur.name || 'Root';
const actualCur = S.itemMap.get(cur.id);
startNodes.push({
id: cur.id || '',
name: rootName,
icon_link: cur.icon_link,
starred: actualCur ? actualCur.starred : false,
tags: actualCur ? actualCur.tags :[],
lineage: cleanPath,
retryCount: 0,
_pathStr: cleanPath.map(x => x.name).join('/')
});
}
}
if (startNodes.length === 0) {
setLoad(false); isGUISensitive = false;
showToast(L.msg_analyze_only_normal_dir);
return;
}
startNodes.forEach(n => {
nodeMap.set(n.id, {
id: n.id,
name: n.name,
icon_link: n.icon_link,
starred: n.starred,
tags: n.tags,
size: 0,
parentId: null,
marked: false,
_pathStr: n._pathStr,
lineage: n.lineage,
isRoot: true,
files:[]
});
});
const cacheKey = '__analyze_nodeMap_' + startNodes.map(n => n.id).join('_');
let useCache = false;
if (typeof globalCache !== 'undefined' && globalCache.has(cacheKey) && globalDirtyFolders.size === 0) {
const cachedArr = globalCache.get(cacheKey);
nodeMap = new Map(cachedArr);
useCache = true;
console.log("[Analyze] Using cached global nodeMap.");
}
const propagateSize = (parentId, addSize) => {
let curId = parentId;
while (curId !== null && nodeMap.has(curId)) {
const node = nodeMap.get(curId);
node.size += addSize;
if (!node.isRoot && node.size >= minBytes && !node.marked) {
node.marked = true;
largeFolders.push(node);
}
curId = node.parentId;
}
};
S.scanId = (S.scanId || 0) + 1;
const myScanId = S.scanId;
if (S.scanAbortController) S.scanAbortController.abort();
S.scanAbortController = new AbortController();
const signal = S.scanAbortController.signal;
let isRunning = true;
UI.stopBtn.onclick = () => {
isRunning = false;
S.scanning = false;
if (S.scanAbortController) S.scanAbortController.abort();
updateLoadTxt(L.str_stopping);
};
S.scanning = true;
let totalFilesScanned = 0;
let totalDirsScanned = 0;
try {
if (!useCache) {
await coreRecursiveEngine(startNodes, {
signal: signal,
onFolder: (folder, filesInFolder, nextSubFolders) => {
totalDirsScanned++;
nextSubFolders.forEach(sub => {
if (!nodeMap.has(sub.id)) {
const fullPathStr = sub.lineage.map(x => x.name).join('/');
nodeMap.set(sub.id, {
id: sub.id,
name: sub.name,
icon_link: sub.icon_link,
starred: sub.starred,
tags: sub.tags,
size: 0,
parentId: folder.id,
marked: false,
_pathStr: fullPathStr,
lineage: sub.lineage,
isRoot: false,
files:[]
});
}
});
},
onFile: (file, parent) => {
totalFilesScanned++;
const sz = Number(file.size || 0);
if (sz > 0) {
propagateSize(parent.id, sz);
}
if (nodeMap.has(parent.id)) {
const fingerprint = file.hash ? `${file.hash}_${sz}` : `${file.name}_${sz}`;
let curId = parent.id;
while (curId !== null && nodeMap.has(curId)) {
nodeMap.get(curId).files.push(fingerprint);
curId = nodeMap.get(curId).parentId;
}
}
},
onProgress: (st) => {
updateLoadTxt(`${L.str_scanning} ${st.folders} ${L.unit_folders} | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`);
}
});
if (!isRunning || signal.aborted || myScanId !== S.scanId) {
throw new Error('StoppedByUser');
}
if (typeof globalCache !== 'undefined') {
globalCache.set(cacheKey, Array.from(nodeMap.entries()));
}
} else {
Array.from(nodeMap.values()).forEach(node => {
if (!node.isRoot && node.size >= minBytes) {
largeFolders.push(node);
}
});
}
} catch (e) {
if (isRunning && e.message !== 'StoppedByUser' && e.name !== 'AbortError') {
showAlert(`${L.str_error}: ${e.message}`);
setLoad(false);
}
} finally {
isGUISensitive = false;
S.scanning = false;
S.scanAbortController = null;
if (!isRunning) {
setLoad(false);
return;
}
let viewItems = [];
if (isSimMode) {
updateLoadTxt(`${L.str_analyzing}...`);
Array.from(nodeMap.values()).forEach(node => {
node._ancestorSet = new Set(node.lineage ? node.lineage.map(p => p.id) : []);
if (node.parentId && nodeMap.has(node.parentId)) {
const parent = nodeMap.get(node.parentId);
if (parent.files.length === node.files.length) {
parent.isShell = true;
}
}
});
const isDescendant = (childId, parentId) => {
const childNode = nodeMap.get(childId);
return childNode ? childNode._ancestorSet.has(parentId) : false;
};
const folderArr = Array.from(nodeMap.values())
.filter(f => !f.isShell && (f.files.length >= 2 || f.size > 1024 * 1024))
.map(f => {
const counts = new Map();
f.files.forEach(h => counts.set(h, (counts.get(h) || 0) + 1));
return { ...f, fileCounts: counts, _keys: Array.from(counts.keys()), totalFiles: f.files.length };
})
.sort((a, b) => b.totalFiles - a.totalFiles);
const invertedIndex = new Map();
folderArr.forEach((f, i) => {
f.fileCounts.forEach((_, hash) => {
let arr = invertedIndex.get(hash);
if (!arr) { arr = []; invertedIndex.set(hash, arr); }
arr.push(i);
});
});
const totalDocs = folderArr.length;
const weightMap = new Map();
invertedIndex.forEach((arr, hash) => {
const df = arr.length;
let w = 1.0;
if (totalDocs >= 20 && (df / totalDocs) > 0.05) {
w = 0.05;
}
weightMap.set(hash, w);
});
folderArr.forEach(f => {
let wt = 0;
f.fileCounts.forEach((count, hash) => {
wt += count * weightMap.get(hash);
});
f.weightedTotal = wt;
});
let groups =[];
const assigned = new Set();
const total = folderArr.length;
const candidateSeen = new Uint32Array(total);
let lastYieldTime = performance.now();
updateLoadTxt(`${L.str_analyzing}... 0%`);
try {
if (simAlgo === 'name') {
const nameGroups = new Map();
const cleanFolderName = (oldName) => {
let cleanName = oldName.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim();
cleanName = cleanName.replace(/^【[^】]+】 *[-_.]? */, '');
cleanName = cleanName.replace(/^[a-z0-9-]+[.](?:com|net|org|cc|xyz|vip|top|la) +/i, '');
const adKw = "(?:[.]com|[.]net|[.]org|[.]cc|[.]xyz|[.]vip|[.]top|[.]la|2048|www[.])";
const atRegex = new RegExp('^.*?' + adKw + '.*?(?:@|--+|_\\s)', 'i');
cleanName = cleanName.replace(atRegex, '');
const hyphenRegex = new RegExp('^[a-z0-9.-]+' + adKw + '-', 'i');
cleanName = cleanName.replace(hyphenRegex, '');
cleanName = cleanName.replace(/^(?:精品加群|福利合集)[0-9]+[-_]+ */, '');
cleanName = cleanName.replace(/^[-_. ,,::;;\p{Extended_Pictographic}]+/u, '');
const pairs = [['【','】'], ['[',']'], ['《','》'],['<','>'], ['(',')'],['(',')'], ['{','}']];
pairs.forEach(([L_char, R_char]) => {
const idxR_Fix = cleanName.indexOf(R_char);
const idxL_Check = cleanName.indexOf(L_char);
if (idxR_Fix > 0 && idxR_Fix <= 10 && (idxL_Check === -1 || idxL_Check > idxR_Fix)) {
cleanName = L_char + cleanName;
}
const chars = cleanName.split('');
const stack = [];
const toRemove = new Set();
for (let i = 0; i < chars.length; i++) {
const c = chars[i];
if (c === L_char) stack.push(i);
else if (c === R_char) {
if (stack.length > 0) stack.pop();
else toRemove.add(i);
}
}
stack.forEach(i => toRemove.add(i));
if (toRemove.size > 0) cleanName = chars.filter((_, i) => !toRemove.has(i)).join('');
});
const quoteCount = (cleanName.match(/'/g) || []).length;
if (quoteCount % 2 !== 0) cleanName = cleanName.replace(/'/, '');
let finalResult = cleanName.toLowerCase().trim();
if (simThreshold <= 0.5) {
finalResult = finalResult.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '');
}
return finalResult || oldName.toLowerCase().trim();
};
folderArr.forEach(f => {
const k = cleanFolderName(f.name);
if (!nameGroups.has(k)) nameGroups.set(k,[]);
nameGroups.get(k).push(f);
});
const sizeRatioLimit = simThreshold >= 0.5 ? 0.05 : 0.10;
for (const[k, items] of nameGroups) {
if (items.length > 1) {
const sorted = [...items].sort((a,b) => Number(a.size) - Number(b.size));
let currentGroup = [sorted[0]];
for (let i = 1; i < sorted.length; i++) {
const target = sorted[i];
const root = currentGroup[0];
const rootSize = Number(root.size || 0);
const targetSize = Number(target.size || 0);
let isMatch = false;
if (rootSize === 0 && targetSize === 0) isMatch = true;
else {
const sizeDiff = Math.abs(targetSize - rootSize);
const maxBase = Math.max(targetSize, rootSize);
if (maxBase > 0 && (sizeDiff / maxBase) <= sizeRatioLimit) isMatch = true;
}
if (isMatch) currentGroup.push(target);
else {
if (currentGroup.length > 1) {
const gNodes = currentGroup;
let minS = Number.MAX_SAFE_INTEGER, maxS = 0;
gNodes.forEach(n => { const sz = Number(n.size||0); if(sz
maxS) maxS=sz; });
if (minS === Number.MAX_SAFE_INTEGER) minS = 0;
const range = (minS === maxS) ? fmtSize(minS) : `${fmtSize(minS)} ~ ${fmtSize(maxS)}`;
groups.push({ ids: gNodes.map(f => f.id), type: `${gNodes.length} ${L.str_items} | ${range}`, _sim: 1 });
gNodes.forEach(f => assigned.add(f.id));
}
currentGroup = [target];
}
}
if (currentGroup.length > 1) {
const gNodes = currentGroup;
let minS = Number.MAX_SAFE_INTEGER, maxS = 0;
gNodes.forEach(n => { const sz = Number(n.size||0); if(szmaxS) maxS=sz; });
if (minS === Number.MAX_SAFE_INTEGER) minS = 0;
const range = (minS === maxS) ? fmtSize(minS) : `${fmtSize(minS)} ~ ${fmtSize(maxS)}`;
groups.push({ ids: gNodes.map(f => f.id), type: `${gNodes.length} ${L.str_items} | ${range}`, _sim: 1 });
gNodes.forEach(f => assigned.add(f.id));
}
}
}
} else {
for (let i = 0; i < total; i++) {
if (i % 50 === 0 || performance.now() - lastYieldTime > 16) {
if (!isRunning) break;
updateLoadTxt(`${L.str_analyzing}\n${Math.round((i / total) * 100)}%`);
await sleep(0);
lastYieldTime = performance.now();
}
if (assigned.has(folderArr[i].id)) continue;
const f1 = folderArr[i];
const group = [f1];
let groupMinSim = 1.0;
const candidateIndices = [];
const marker = i + 1;
f1.fileCounts.forEach((_, hash) => {
const foldersWithHash = invertedIndex.get(hash);
if (foldersWithHash) {
for (let k = 0, len = foldersWithHash.length; k < len; k++) {
const idx = foldersWithHash[k];
if (idx > i && candidateSeen[idx] !== marker && !assigned.has(folderArr[idx].id)) {
candidateSeen[idx] = marker;
candidateIndices.push(idx);
}
}
}
});
for (let m = 0, cLen = candidateIndices.length; m < cLen; m++) {
const j = candidateIndices[m];
const f2 = folderArr[j];
if (simAlgo === 'sim' && (isDescendant(f2.id, f1.id) || isDescendant(f1.id, f2.id))) continue;
let total1 = f1.weightedTotal;
let total2 = f2.weightedTotal;
let intersect = 0;
if (isDescendant(f2.id, f1.id)) {
total1 -= total2;
if (total1 <= 0 || total2 <= 0) continue;
const maxS = total1 > total2 ? total1 : total2;
const minS = total1 < total2 ? total1 : total2;
if (simAlgo === 'sim' && minS / maxS < simThreshold) continue;
for (let k = 0, len = f2._keys.length; k < len; k++) {
const hash = f2._keys[k];
const c2 = f2.fileCounts.get(hash);
const c1 = f1.fileCounts.get(hash) || 0;
const diff = c1 - c2;
if (diff > 0) intersect += (diff < c2 ? diff : c2) * weightMap.get(hash);
}
} else if (isDescendant(f1.id, f2.id)) {
total2 -= total1;
if (total1 <= 0 || total2 <= 0) continue;
const maxS = total1 > total2 ? total1 : total2;
const minS = total1 < total2 ? total1 : total2;
if (simAlgo === 'sim' && minS / maxS < simThreshold) continue;
for (let k = 0, len = f1._keys.length; k < len; k++) {
const hash = f1._keys[k];
const c1 = f1.fileCounts.get(hash);
const c2 = f2.fileCounts.get(hash) || 0;
const diff = c2 - c1;
if (diff > 0) intersect += (diff < c1 ? diff : c1) * weightMap.get(hash);
}
} else {
if (total1 <= 0 || total2 <= 0) continue;
const maxS = total1 > total2 ? total1 : total2;
const minS = total1 < total2 ? total1 : total2;
if (simAlgo === 'sim' && minS / maxS < simThreshold) continue;
const fSmall = f1._keys.length < f2._keys.length ? f1 : f2;
const fLarge = f1._keys.length < f2._keys.length ? f2 : f1;
for (let k = 0, len = fSmall._keys.length; k < len; k++) {
const hash = fSmall._keys[k];
const cLarge = fLarge.fileCounts.get(hash);
if (cLarge !== undefined) {
const cSmall = fSmall.fileCounts.get(hash);
intersect += (cSmall < cLarge ? cSmall : cLarge) * weightMap.get(hash);
}
}
}
const minTotal = total1 < total2 ? total1 : total2;
const union = total1 + total2 - intersect;
const sim = simAlgo === 'contain' ? (minTotal > 0 ? (intersect / minTotal) : 0) : (union > 0 ? (intersect / union) : 0);
if (sim >= simThreshold) {
let isGroupQualified = true;
let currentMinSim = sim;
for (let gIdx = 1; gIdx < group.length; gIdx++) {
const gMember = group[gIdx];
if (simAlgo === 'sim' && (isDescendant(f2.id, gMember.id) || isDescendant(gMember.id, f2.id))) {
isGroupQualified = false;
break;
}
let tA = gMember.weightedTotal;
let tB = f2.weightedTotal;
let intS = 0;
if (isDescendant(f2.id, gMember.id)) {
tA -= tB;
} else if (isDescendant(gMember.id, f2.id)) {
tB -= tA;
}
if (tA <= 0 || tB <= 0) {
isGroupQualified = false;
break;
}
const maxS = tA > tB ? tA : tB;
const minS = tA < tB ? tA : tB;
if (simAlgo === 'sim' && minS / maxS < simThreshold) {
isGroupQualified = false;
break;
}
if (isDescendant(f2.id, gMember.id)) {
for (let k = 0, len = f2._keys.length; k < len; k++) {
const h = f2._keys[k];
const cB = f2.fileCounts.get(h);
const cA = gMember.fileCounts.get(h) || 0;
const diff = cA - cB;
if (diff > 0) intS += (diff < cB ? diff : cB) * weightMap.get(h);
}
} else if (isDescendant(gMember.id, f2.id)) {
for (let k = 0, len = gMember._keys.length; k < len; k++) {
const h = gMember._keys[k];
const cA = gMember.fileCounts.get(h);
const cB = f2.fileCounts.get(h) || 0;
const diff = cB - cA;
if (diff > 0) intS += (diff < cA ? diff : cA) * weightMap.get(h);
}
} else {
const fSmall = gMember._keys.length < f2._keys.length ? gMember : f2;
const fLarge = gMember._keys.length < f2._keys.length ? f2 : gMember;
for (let k = 0, len = fSmall._keys.length; k < len; k++) {
const h = fSmall._keys[k];
const cLarge = fLarge.fileCounts.get(h);
if (cLarge !== undefined) {
const cSmall = fSmall.fileCounts.get(h);
intS += (cSmall < cLarge ? cSmall : cLarge) * weightMap.get(h);
}
}
}
const minT = tA < tB ? tA : tB;
const un = tA + tB - intS;
const pairwiseSim = simAlgo === 'contain' ? (minT > 0 ? (intS / minT) : 0) : (un > 0 ? (intS / un) : 0);
if (pairwiseSim < simThreshold) {
isGroupQualified = false;
break;
}
if (pairwiseSim < currentMinSim) {
currentMinSim = pairwiseSim;
}
}
if (isGroupQualified) {
group.push(f2);
if (currentMinSim < groupMinSim) groupMinSim = currentMinSim;
}
}
}
if (group.length > 1) {
group.forEach(f => assigned.add(f.id));
groups.push({
ids: group.map(f => f.id),
type: `${group.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${Math.round(groupMinSim * 100)}%`,
_sim: groupMinSim
});
}
}
}
if (simAlgo !== 'name') {
const folderObjMap = new Map();
folderArr.forEach(f => folderObjMap.set(f.id, f));
const finalGroups =[];
groups.forEach(g => {
const nodes = g.ids.map(id => folderObjMap.get(id)).filter(Boolean);
const toRemove = new Set();
nodes.forEach(node => {
const descendants = nodes.filter(n => n.id !== node.id && isDescendant(n.id, node.id));
if (descendants.length === 0) return;
const hasExternal = nodes.some(n => n.id !== node.id && !isDescendant(n.id, node.id) && !isDescendant(node.id, n.id));
if (hasExternal) return;
const topDescendants = descendants.filter(d1 =>
!descendants.some(d2 => d1.id !== d2.id && isDescendant(d1.id, d2.id))
);
let isCovered = true;
let sumTotal = 0;
const sumCounts = new Map();
topDescendants.forEach(td => {
sumTotal += td.totalFiles;
td.fileCounts.forEach((count, hash) => {
sumCounts.set(hash, (sumCounts.get(hash) || 0) + count);
});
});
if (sumTotal !== node.totalFiles) {
isCovered = false;
} else {
for (const [hash, count] of node.fileCounts) {
if (sumCounts.get(hash) !== count) {
isCovered = false;
break;
}
}
}
if (isCovered) {
toRemove.add(node.id);
}
});
const finalIds = g.ids.filter(id => !toRemove.has(id));
if (finalIds.length >= 2) {
finalGroups.push({
ids: finalIds,
type: g.type,
_sim: g._sim
});
}
});
groups = finalGroups;
const partitionedGroups = [];
const varianceTolerance = 0.15;
groups.forEach(g => {
const nodes = g.ids.map(id => folderObjMap.get(id)).filter(Boolean);
if (nodes.length <= 2) {
const pct = Math.round(g._sim * 100);
g.type = `${nodes.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${pct}%`;
partitionedGroups.push(g);
return;
}
const simMatrix = [];
let maxSim = 0, minSim = 1;
for (let i = 0; i < nodes.length; i++) {
simMatrix[i] = [];
for (let j = 0; j < nodes.length; j++) {
if (i === j) { simMatrix[i][j] = 1; continue; }
if (j < i) { simMatrix[i][j] = simMatrix[j][i]; continue; }
const n1 = nodes[i], n2 = nodes[j];
if (simAlgo === 'sim' && (isDescendant(n2.id, n1.id) || isDescendant(n1.id, n2.id))) {
simMatrix[i][j] = 0;
minSim = 0;
continue;
}
let tA = n1.weightedTotal, tB = n2.weightedTotal, intS = 0;
if (isDescendant(n2.id, n1.id)) tA -= tB; else if (isDescendant(n1.id, n2.id)) tB -= tA;
if (tA > 0 && tB > 0) {
if (isDescendant(n2.id, n1.id)) {
for (let k = 0; k < n2._keys.length; k++) {
const h = n2._keys[k], cB = n2.fileCounts.get(h), cA = n1.fileCounts.get(h) || 0, diff = cA - cB;
if (diff > 0) intS += (diff < cB ? diff : cB) * weightMap.get(h);
}
} else if (isDescendant(n1.id, n2.id)) {
for (let k = 0; k < n1._keys.length; k++) {
const h = n1._keys[k], cA = n1.fileCounts.get(h), cB = n2.fileCounts.get(h) || 0, diff = cB - cA;
if (diff > 0) intS += (diff < cA ? diff : cA) * weightMap.get(h);
}
} else {
const fSmall = n1._keys.length < n2._keys.length ? n1 : n2, fLarge = n1._keys.length < n2._keys.length ? n2 : n1;
for (let k = 0; k < fSmall._keys.length; k++) {
const h = fSmall._keys[k], cLarge = fLarge.fileCounts.get(h);
if (cLarge !== undefined) intS += (fSmall.fileCounts.get(h) < cLarge ? fSmall.fileCounts.get(h) : cLarge) * weightMap.get(h);
}
}
const minT = tA < tB ? tA : tB;
const un = tA + tB - intS;
const s = simAlgo === 'contain' ? (minT > 0 ? (intS / minT) : 0) : (un > 0 ? (intS / un) : 0);
simMatrix[i][j] = s;
if (s > maxSim) maxSim = s;
if (s < minSim) minSim = s;
} else {
simMatrix[i][j] = 0;
minSim = 0;
}
}
}
if (maxSim - minSim <= varianceTolerance) {
const minPct = Math.round(minSim * 100);
const maxPct = Math.round(maxSim * 100);
const range = (minPct === maxPct) ? `${minPct}%` : `${minPct}% ~ ${maxPct}%`;
g.type = `${nodes.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${range}`;
partitionedGroups.push(g);
} else {
const unassigned = new Set(nodes.map((_, idx) => idx));
const pairs =[];
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) pairs.push({i, j, s: simMatrix[i][j]});
}
pairs.sort((a, b) => b.s - a.s);
pairs.forEach(pair => {
if (unassigned.has(pair.i) && unassigned.has(pair.j) && pair.s >= simThreshold) {
const sg = [pair.i, pair.j];
let sgMinSim = pair.s;
unassigned.delete(pair.i); unassigned.delete(pair.j);
for (const u of Array.from(unassigned)) {
let canAdd = true, localMin = 1;
for (const mem of sg) {
const s = simMatrix[u < mem ? u : mem][u > mem ? u : mem];
if (pair.s - s > varianceTolerance || s < simThreshold) { canAdd = false; break; }
if (s < localMin) localMin = s;
}
if (canAdd) { sg.push(u); unassigned.delete(u); if (localMin < sgMinSim) sgMinSim = localMin; }
}
if (sg.length >= 2) {
let subMin = 1, subMax = 0;
for(let x=0; x sg[y] ? sg[x] : sg[y];
const sVal = simMatrix[idx1][idx2];
if (sVal < subMin) subMin = sVal;
if (sVal > subMax) subMax = sVal;
}
}
const minPct = Math.round(subMin * 100);
const maxPct = Math.round(subMax * 100);
const range = (minPct === maxPct) ? `${minPct}%` : `${minPct}% ~ ${maxPct}%`;
partitionedGroups.push({
ids: sg.map(idx => nodes[idx].id),
type: `${sg.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${range}`,
_sim: subMin
});
}
}
});
}
});
groups = partitionedGroups;
}
} catch (e) {
console.error("[SimMode] Error:", e);
}
if (groups.length === 0) {
setLoad(false);
showToast(L.msg_dup_none);
return;
}
groups.sort((a, b) => (b._sim - a._sim) || (b.ids.length - a.ids.length));
S.analyzeSimGroups = groups;
viewItems = Array.from(assigned).map(id => {
const node = nodeMap.get(id);
return {
id: node.id,
kind: 'drive#folder',
name: node.name,
icon_link: node.icon_link,
starred: node.starred,
tags: node.tags,
size: node.size.toString(),
_pathStr: (node._pathStr && node._pathStr.includes('/')) ? node._pathStr.substring(0, node._pathStr.lastIndexOf('/')) : L.btn_nav_home,
_lineage: node.lineage,
modified_time: new Date(getServerNow()).toISOString(),
parent_id: node.parentId
};
});
} else {
updateLoadTxt(L.str_rendering);
const uniqueResults = Array.from(new Set(largeFolders));
const kw = gmGet('pk_analyze_last_keyword', '');
const kwList = kw ? kw.toLowerCase().split(/[,,]/).map(k => k.trim()).filter(k => k) : [];
let filteredResults = uniqueResults.filter(n => n.size >= minBytes && (maxBytes === 0 || n.size <= maxBytes));
if (kwList.length > 0) {
filteredResults = filteredResults.filter(node => !kwList.some(k => (node.name || "").toLowerCase().includes(k)));
}
filteredResults.sort((a, b) => b.size - a.size);
if (filteredResults.length === 0) {
setLoad(false);
let rangeStr = maxBytes > 0 ? `${fmtSize(minBytes)} - ${fmtSize(maxBytes)}` : `≥ ${fmtSize(minBytes)}`;
showAlert(L.msg_analyze_no_large_folders.replace('{s}', rangeStr));
return;
}
viewItems = filteredResults.map(node => ({
id: node.id,
kind: 'drive#folder',
name: node.name,
icon_link: node.icon_link,
starred: node.starred,
tags: node.tags,
size: node.size.toString(),
_pathStr: (node._pathStr && node._pathStr.includes('/')) ? node._pathStr.substring(0, node._pathStr.lastIndexOf('/')) : L.btn_nav_home,
_lineage: node.lineage,
modified_time: new Date(getServerNow()).toISOString(),
parent_id: node.parentId
}));
}
S.analyzeMode = true;
S.hasShownAnaWarn = false;
S.sort = 'size'; S.dir = 1;
S.isFlattened = false;
S.dupMode = false;
S.analyzeResultItems = [...viewItems];
S.analyzeMap = nodeMap;
S.path = [{ id: 'analyze_root', name: L.str_analyze_results }];
renderCrumb();
S.items = viewItems;
S.itemMap.clear();
S.items.forEach(i => S.itemMap.set(i.id, i));
S.sel.clear();
UI.scan.style.display = 'none';
if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none';
UI.btnExit.style.display = 'flex';
if (UI.dupTools) UI.dupTools.style.display = 'none';
if (UI.dupFilters) UI.dupFilters.style.display = 'none';
if (UI.lblGlobal) UI.lblGlobal.style.display = 'none';
if (UI.chkGlobal) UI.chkGlobal.checked = false;
refresh();
updateStat();
setTimeout(() => {
setLoad(false);
isGUISensitive = false;
}, 200);
}
};
}
function showAnalyzeResultModal(list, threshold) {
const L = getStrings();
const modalHtml = `
${L.title_analyze_result}
${L.msg_analyze_summary_fmt.replace('{n}', list.length).replace('{s}', threshold)}
${L.btn_close}
`;
const m = showModal(modalHtml);
const modalBox = m.querySelector('.pk-modal');
modalBox.style.width = "800px";
modalBox.style.height = "80vh";
const container = m.querySelector('#pk_analyze_list');
const fragment = document.createDocumentFragment();
list.forEach(item => {
const row = document.createElement('div');
row.style.cssText = "display:grid; grid-template-columns: 1fr 100px 80px; padding:10px 12px; border-bottom:1px solid var(--pk-bd); font-size:13px; align-items:center;";
const fullPath = item.path;
const name = item.name;
const parentPath = fullPath.substring(0, fullPath.lastIndexOf(name));
const sizeNum = Number(item.size);
row.innerHTML = `
${esc(name)}
${esc(parentPath || '/')}
${fmtSize(sizeNum)}
${L.btn_jump}
`;
row.querySelector('button').onclick = () => {
m.remove();
const wasAnalyzeMode = S.analyzeMode;
const cleanLineage = item.lineage.filter(x => x.id);
S.analyzeMode = false;
S.isFlattened = false;
S.dupMode = false;
if (UI.chkSearchPath) UI.chkSearchPath.checked = false;
S.path =[{ id: '', name: getStrings().btn_nav_home }, ...cleanLineage];
if (UI.lblGlobal) UI.lblGlobal.style.display = 'flex';
if (UI.chkGlobal && wasAnalyzeMode && typeof S.wasGlobalChecked !== 'undefined') {
UI.chkGlobal.checked = S.wasGlobalChecked;
}
if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex';
if (UI.scan) UI.scan.style.display = 'flex';
load();
};
fragment.appendChild(row);
});
container.appendChild(fragment);
m.querySelector('#pk_analyze_close').onclick = () => m.remove();
}
if (UI.btnExport) {
UI.btnExport.onclick = async () => {
if (S.trashMode) return;
const curFolderId = S.path[S.path.length - 1].id || '';
if (isPathBusy(curFolderId)) {
showAlert(L.msg_op_blocked_exporting);
return;
}
const format = await new Promise((resolve) => {
const m = showModal(`
${L.title_export_format}
${L.lbl_export_current}
${L.opt_tree_view}
Root\n├─ Folder 1\n│ ├─ Folder 1-1\n│ └─ Folder 1-2\n└─ Folder 2\n └─ Folder 2-1
${L.opt_list_view}
Root/Folder 1\nRoot/Folder 1/Folder 1-1\nRoot/Folder 1/Folder 1-2\nRoot/Folder 2\nRoot/Folder 2/Folder 2-1
${L.btn_cancel}
${L.btn_ok}
`);
const modalBox = m.querySelector('.pk-modal');
if (modalBox) {
modalBox.style.width = '520px';
modalBox.style.height = 'auto';
modalBox.style.minHeight = 'auto';
const closeBtn = m.querySelector('.pk-modal-close');
if (closeBtn) Object.assign(closeBtn.style, { top: '22px', right: '22px' });
}
let selectedFormat = 'tree';
m.querySelectorAll('.pk-exp-card').forEach(card => {
card.onclick = () => {
m.querySelectorAll('.pk-exp-card').forEach(c => {
c.style.borderColor = 'var(--pk-bd)';
c.querySelector('.pk-exp-title').style.color = 'var(--pk-fg)';
c.querySelector('.pk-exp-check').style.display = 'none';
c.classList.remove('act');
});
card.style.borderColor = 'var(--pk-pri)';
card.querySelector('.pk-exp-title').style.color = 'var(--pk-pri)';
card.querySelector('.pk-exp-check').style.display = 'flex';
card.classList.add('act');
selectedFormat = card.dataset.fmt;
};
});
m.querySelector('#exp_cancel').onclick = () => { m.remove(); resolve(null); };
m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(null); };
m.querySelector('#exp_confirm').onclick = () => { m.remove(); resolve(selectedFormat); };
m.tabIndex = 0;
setTimeout(() => m.focus(), 10);
m.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); e.stopPropagation();
m.querySelector('#exp_confirm').click();
}
});
});
if (!format) return;
setLoad(true);
S.scanning = true;
S.scanId = (S.scanId || 0) + 1;
const myScanId = S.scanId;
if (S.scanAbortController) S.scanAbortController.abort();
S.scanAbortController = new AbortController();
const signal = S.scanAbortController.signal;
let isRunning = true;
UI.stopBtn.onclick = () => {
isRunning = false;
S.scanning = false;
if (S.scanAbortController) S.scanAbortController.abort();
updateLoadTxt(L.str_stopping);
};
const rootNode = S.path[S.path.length - 1];
const startNodes =[{
id: rootNode.id || '',
name: rootNode.name || L.btn_nav_home,
lineage: [],
retryCount: 0
}];
const itemTree = new Map();
try {
await coreRecursiveEngine(startNodes, {
signal: signal,
onFolder: (folder, filesInFolder) => {
itemTree.set(folder.id, [...filesInFolder]);
if (typeof globalCache !== 'undefined' && !globalCache.has(folder.id)) {
globalCache.set(folder.id, [...filesInFolder]);
}
indexParents(folder.id, folder.name, filesInFolder);
},
onProgress: (st) => {
updateLoadTxt(`${L.msg_exporting}\n${L.str_folders}: ${st.folders} | ${L.str_files}: ${st.files}`);
}
});
if (!isRunning || signal.aborted || myScanId !== S.scanId) return;
updateLoadTxt(L.str_processing);
await sleep(50);
const rootName = startNodes[0].name;
let outputLines =[];
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
const sortItems = (items) => {
return items.sort((a, b) => {
if (a.kind !== b.kind) return a.kind === 'drive#folder' ? -1 : 1;
return collator.compare(a.name, b.name);
});
};
if (format === 'tree') {
outputLines.push(rootName);
const buildTree = (parentId, prefix) => {
const children = itemTree.get(parentId);
if (!children || children.length === 0) return;
const sorted = sortItems(children);
for (let i = 0; i < sorted.length; i++) {
const child = sorted[i];
const isLast = (i === sorted.length - 1);
const connector = isLast ? '└─ ' : '├─ ';
outputLines.push(prefix + connector + child.name);
if (child.kind === 'drive#folder') {
const childPrefix = prefix + (isLast ? ' ' : '│ ');
buildTree(child.id, childPrefix);
}
}
};
buildTree(startNodes[0].id, '');
} else {
const buildList = (parentId, currentPath) => {
const children = itemTree.get(parentId);
if (!children || children.length === 0) return;
const sorted = sortItems(children);
for (let i = 0; i < sorted.length; i++) {
const child = sorted[i];
const fullPath = currentPath ? currentPath + '/' + child.name : child.name;
outputLines.push(fullPath);
if (child.kind === 'drive#folder') {
buildList(child.id, fullPath);
}
}
};
buildList(startNodes[0].id, rootName);
}
const outputText = outputLines.join('\r\n');
const blob = new Blob([outputText], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const safeRootName = rootName.replace(/[\\/:*?"<>|]/g, '_');
const a = document.createElement('a');
a.href = url;
a.download = `${safeRootName}_${format}.txt`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (e) {
if (e.name !== 'AbortError' && myScanId === S.scanId) {
showAlert(`${L.str_error}: ${e.message}`);
}
} finally {
if (myScanId === S.scanId) {
setLoad(false);
S.scanning = false;
S.scanAbortController = null;
isGUISensitive = false;
}
}
};
}
UI.btnNewFolder.onclick = async () => {
const name = await showPrompt(L.msg_newfolder_prompt, '');
if (!name) return;
isGUISensitive = true;
const cur = S.path[S.path.length - 1];
const cacheKey = cur.id || 'root';
try {
await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: 'drive#folder', parent_id: cur.id || '', name: name }) });
if (cur.id) gmSet('pk_fmod_' + cur.id, new Date(getServerNow()).toISOString());
await sleep(300);
S.cache.delete(cacheKey);
if (typeof globalCache !== 'undefined') globalCache.delete(cacheKey);
if (typeof globalDirtyFolders !== 'undefined') {
globalDirtyFolders.add(cacheKey === 'root' ? '' : cacheKey);
}
if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true;
if (cacheKey !== 'root') {
scannedFolderIds.delete(cacheKey);
}
if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler();
load(false, true);
} catch (e) {
showAlert(`${L.str_error}: ${e.message}`);
} finally {
isGUISensitive = false;
}
};
UI.btnCopy.onclick = async () => {
if (S.sel.size === 0) return;
if (S.sel.size > 10000) {
showToast(L.str_copying);
await sleep(10);
}
const itemList = [];
let count = 0;
for (const id of S.sel) {
const item = S.itemMap.get(id);
if (item) itemList.push(item);
count++;
if (count % 10000 === 0) await sleep(0);
}
S.clipItems = itemList;
S.clipType = 'copy';
const curId = S.path[S.path.length - 1].id || '';
S.clipSourceParentId = S.isFlattened ? '__VIRTUAL__' : curId;
setLoad(false);
UI.btnPaste.disabled = false;
showToast(L.msg_copy_done);
};
UI.btnCut.onclick = async () => {
if (S.sel.size === 0) return;
const itemList = [];
let count = 0;
for (const id of S.sel) {
const item = S.itemMap.get(id);
if (!S.trashMode && isSystemItem(item)) {
continue;
}
itemList.push(item);
count++;
if (count % 10000 === 0) await sleep(0);
}
if (itemList.length === 0) return;
if (itemList.length > 10000) {
showToast(L.str_moving);
await sleep(10);
}
S.clipItems = itemList;
S.clipType = 'move';
const curId = S.path[S.path.length - 1].id || '';
S.clipSourceParentId = S.isFlattened ? '__VIRTUAL__' : curId;
setLoad(false);
UI.btnPaste.disabled = false;
showToast(L.msg_cut_done);
};
UI.btnPaste.onclick = async () => {
if (!S.clipItems || S.clipItems.length === 0) {
showToast(L.msg_paste_empty, 'error');
return;
}
if (S.movingIds && S.movingIds.size > 0) {
showAlert(L.msg_op_blocked_moving);
return;
}
const items = S.clipItems;
const type = S.clipType;
const srcId = S.clipSourceParentId;
const destId = S.path[S.path.length - 1].id || '';
const normalize = (id) => (!id || id === 'root') ? 'root' : id;
S.clipItems = [];
S.clipType = '';
updateStat();
const targetFolderName = S.path[S.path.length - 1].name || "Target";
await executeFileTransfer(items, type, normalize(srcId), normalize(destId), targetFolderName);
};
UI.btnRename.onclick = async () => {
if (S.sel.size !== 1) return;
const id = Array.from(S.sel)[0];
const item = S.itemMap.get(id);
if (!item || (!S.trashMode && isSystemItem(item))) return;
const newName = await showPrompt(L.msg_rename_prompt, item.name, L.modal_rename_title);
if (!newName || newName === item.name) return;
let progressTask = null;
try {
progressTask = FloatBarManager.create(L.str_renaming);
await apiAction(`/${id}`, { name: newName });
item.name = newName;
const nowIso = new Date(getServerNow()).toISOString();
item.modified_time = nowIso;
if (item.kind === 'drive#folder') gmSet('pk_fmod_' + item.id, nowIso);
const parentIdForFmod = item.parent_id || 'root';
gmSet('pk_fmod_' + parentIdForFmod, nowIso);
const row = UI.in.querySelector(`.pk-row[data-id="${id}"]`);
if (row && row.lastElementChild) {
row.lastElementChild.textContent = fmtDate(nowIso);
row.lastElementChild.style.transition = 'color 0.3s';
row.lastElementChild.style.color = 'var(--pk-pri)';
setTimeout(() => { if(row.lastElementChild) row.lastElementChild.style.color = ''; }, 2000);
}
const pid = item.parent_id || 'root';
if (typeof globalCache !== 'undefined') {
if (globalCache.has(pid)) {
const list = globalCache.get(pid);
const target = list.find(f => f.id === id);
if (target) target.name = newName;
}
globalDirtyFolders.add(pid === 'root' ? '' : pid);
if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler();
}
if (S.lastGlobalResults && S.lastGlobalResults.length > 0) {
const globalTarget = S.lastGlobalResults.find(f => f.id === id);
if (globalTarget) globalTarget.name = newName;
}
if (S.analyzeResultItems) {
const anaItem = S.analyzeResultItems.find(x => x.id === id);
if (anaItem) anaItem.name = newName;
}
if (S.analyzeMap && S.analyzeMap.has(id)) {
S.analyzeMap.get(id).name = newName;
}
if (S.dupMode) renderDupView(); else refresh();
} catch (e) {
showAlert(`${L.str_error}: ${e.message}`);
} finally {
if (progressTask) progressTask.destroy();
}
};
UI.btnBulkRename.onclick = () => {
if (S.sel.size < 2) return;
if (S.movingIds && S.movingIds.size > 0) {
const hasConflict = Array.from(S.sel).some(id => S.movingIds.has(id));
if (hasConflict) {
showAlert(L.msg_op_blocked_analyzing);
return;
}
}
const getSmartDisplayHTML = (fullStr, hlStr) => {
if (!hlStr || hlStr.indexOf('§§§MATCH_START§§§') === -1) {
return esc(fullStr);
}
const parts = hlStr.split(/(§§§MATCH_START§§§.*?§§§MATCH_END§§§)/g);
let result = "";
const PRE_CTX = 12;
const SUF_CTX = 80;
parts.forEach((part, index) => {
if (part.startsWith('§§§MATCH_START§§§')) {
const content = part.replace(/§§§MATCH_START§§§|§§§MATCH_END§§§/g, '');
result += `${esc(content)} `;
} else {
let text = part;
if (index === 0) {
if (text.length > PRE_CTX + 3) {
text = "..." + text.slice(-PRE_CTX);
}
}
else if (index === parts.length - 1) {
if (text.length > SUF_CTX) {
text = text.slice(0, SUF_CTX) + "...";
}
}
else {
if (text.length > 20) {
text = text.slice(0, 8) + ".." + text.slice(-8);
}
}
result += esc(text);
}
});
return result;
};
const extractKeyword = (fileName) => {
const fc2Regex = /(?:FC2|FC)(?:[-_. ]*PPV)?[-_. @]*(\d{5,8})(?:[-_. ]*(?:part|pt|cd)?[-_. ]?(\d{1,2}|[a-e]))?(?![a-z\d])/i;
const fc2Match = fileName.match(fc2Regex);
if (fc2Match) {
const id = fc2Match[1];
const part = fc2Match[2];
return (part && part.trim()) ? `FC2-PPV-${id}-${part.toUpperCase()}` : `FC2-PPV-${id}`;
}
return null;
};
const inputStyle = `
width:100%; height:44px; padding:0 15px;
border:2px solid var(--pk-bd); border-radius:8px;
background:var(--pk-bg); color:var(--pk-fg);
font-size:14px; font-weight:600; outline:none;
transition:border-color 0.2s; box-sizing:border-box;
`;
const labelStyle = `
position:absolute; top:0; transform:translateY(-50%); left:10px;
background:var(--pk-bg); padding:0 5px; line-height:1;
font-size:11px; color:var(--pk-pri);
font-weight:bold; pointer-events:none; z-index:1;
`;
const m = showModal(`
`);
const modalBox = m.querySelector('.pk-modal');
Object.assign(modalBox.style, { width: '800px', maxWidth: '95vw', padding: '30px', borderRadius: '12px' });
const closeBtn = m.querySelector('.pk-modal-close');
if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' });
const radios = m.querySelectorAll('input[name="rn_mode"]');
const groupsRaw = {
jav: m.querySelector('#group_jav'),
pattern: m.querySelector('#group_pattern'),
replace: m.querySelector('#group_replace')
};
const inpPattern = m.querySelector('#rn_pattern');
const inpFind = m.querySelector('#rn_find');
const inpRep = m.querySelector('#rn_rep');
const chkRegex = m.querySelector('#rn_regex');
const chkCase = m.querySelector('#rn_case_sense');
const chkIncludeExt = m.querySelector('#rn_include_ext');
const btnApply = m.querySelector('#rn_apply');
const rnVp = m.querySelector('#pk-rn-vp');
const rnIn = m.querySelector('#pk-rn-in');
const txtStatNum = m.querySelector('#rn_stat_num');
const bindFocus = (el) => {
el.onfocus = () => el.style.borderColor = 'var(--pk-pri)';
el.onblur = () => el.style.borderColor = 'var(--pk-bd)';
};
[inpPattern, inpFind, inpRep].forEach(bindFocus);
const updateInputs = () => {
const mode = m.querySelector('input[name="rn_mode"]:checked').value;
const groups = {
'replace': m.querySelector('#group_replace'),
'pattern': m.querySelector('#group_pattern'),
'jav': m.querySelector('#group_jav'),
'format': m.querySelector('#group_format'),
'ad_remove': m.querySelector('#group_ad_remove'),
'ext_fix': m.querySelector('#group_ext_fix')
};
Object.keys(groups).forEach(k => {
if (groups[k]) {
let displayStyle = 'block';
if (k === 'replace' || k === 'format') displayStyle = 'flex';
groups[k].style.display = (k === mode) ? displayStyle : 'none';
}
});
plannedChanges = [];
rnDisplay = [];
previewSelectedIds.clear();
const initialTip = (mode === 'jav') ? L.rn_tip_jav : L.rn_tip_wait;
rnIn.innerHTML = `${initialTip}
`;
txtStatNum.innerText = "0";
btnApply.disabled = true;
const cbWrapper = m.querySelector('#rn_cb_wrapper');
const cbAll = m.querySelector('#rn_cb_all');
if (cbWrapper) cbWrapper.style.visibility = 'hidden';
if (cbAll) { cbAll.checked = false; cbAll.indeterminate = false; }
generatePreview();
};
radios.forEach(r => r.onchange = updateInputs);
const setupInputHistory = (inputEl, storageKey) => {
const pop = document.createElement('div');
pop.className = 'pk-hist-pop';
pop.style.cssText = "position:absolute; top:calc(100% + 5px); left:0; right:0; z-index:9999; display:none; flex-direction:column;";
inputEl.parentNode.appendChild(pop);
const loadHist = () => { try { return JSON.parse(gmGet(storageKey, '[]')); } catch { return []; } };
const saveHist = (val) => {
if (val === null || val === undefined) return;
let list = loadHist();
list = list.filter(x => x !== val);
list.unshift(val);
if (list.length > 3) list = list.slice(0, 3);
gmSet(storageKey, JSON.stringify(list));
};
const render = () => {
const list = loadHist();
if (list.length === 0) { pop.style.display = 'none'; return; }
document.querySelectorAll('.pk-hist-pop').forEach(p => p.style.display = 'none');
let html = `${L.title_search_hist} ${L.btn_clear_hist}
`;
list.forEach(txt => {
const displayTxt = txt === "" ? "(Empty)" : (txt.replace(/\s/g, '') === "" ? `"${txt}"` : txt);
html += ``;
});
pop.innerHTML = html;
pop.style.display = 'flex';
pop.querySelector('#pk-hist-del').onclick = (e) => {
e.stopPropagation();
gmSet(storageKey, '[]');
pop.style.display = 'none';
};
pop.querySelectorAll('.pk-select-item').forEach(el => {
el.onclick = (e) => {
e.stopPropagation();
const val = el.getAttribute('data-raw');
inputEl.value = val;
pop.style.display = 'none';
generatePreview();
};
});
};
inputEl.addEventListener('focus', render);
inputEl.addEventListener('click', (e) => { e.stopPropagation(); render(); });
const closeHandler = (e) => {
if (!inputEl || !pop || !pop.parentNode) return;
if (!inputEl.contains(e.target) && !pop.contains(e.target)) pop.style.display = 'none';
};
setTimeout(() => document.addEventListener('click', closeHandler), 0);
return { save: saveHist };
};
const findHist = setupInputHistory(inpFind, 'pk_bn_find_hist');
const repHist = setupInputHistory(inpRep, 'pk_bn_rep_hist');
const RN_ROW_HEIGHT = 40;
const RN_BUFFER = 15;
let rnDisplay = [];
let plannedChanges = [];
let previewSelectedIds = new Set();
let isRnScrollScheduled = false;
let currentPreviewId = 0;
const VALID_EXTS_LIST = [
'mp4', 'mkv', 'avi', 'mov', 'wmv', 'm4v', 'flv', '3gp', 'webm', 'ts', 'm2ts', 'mts', 'vob', 'mpg', 'mpeg', 'rm', 'rmvb', 'asf', 'divx', 'f4v', 'ogv', 'm2v', 'mpe',
'mp3', 'aac', 'flac', 'wav', 'ogg', 'm4a', 'wma', 'opus', 'ape', 'alac', 'aiff', 'mid', 'midi', 'amr', 'mka', 'dts', 'ac3',
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tif', 'tiff', 'heic', 'raw', 'ico', 'psd', 'ai', 'eps', 'cdr',
'srt', 'ass', 'ssa', 'vtt', 'smi', 'sub', 'idx', 'sup', 'lrc',
'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso', 'img', 'dmg', 'pkg', 'apk', 'ipa', 'exe', 'msi', 'torrent', 'nzb',
'pdf', 'txt', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'epub', 'mobi', 'azw3', 'cbz', 'cbr', 'md', 'rtf', 'csv', 'log', 'ini', 'cfg',
'html', 'htm', 'css', 'js', 'json', 'xml', 'php', 'py', 'java', 'c', 'cpp'
];
const VALID_EXTS = new Set(VALID_EXTS_LIST);
const recalcPatternNames = () => {
const mode = m.querySelector('input[name="rn_mode"]:checked').value;
if (mode !== 'pattern') return;
const pattern = inpPattern.value;
let counter = 1;
rnDisplay.forEach(item => {
const originalItem = S.itemMap.get(item.id);
const isFolder = originalItem?.kind === 'drive#folder';
const isSelected = previewSelectedIds.has(item.id);
if (isSelected && !isFolder) {
let ext = '';
const oldName = item.old;
const lastDotIndex = oldName.lastIndexOf('.');
if (lastDotIndex > 0) {
const potentialExt = oldName.substring(lastDotIndex + 1).toLowerCase();
if (VALID_EXTS.has(potentialExt)) ext = '.' + potentialExt;
}
const newName = pattern.replace(/{n}/g, String(counter).padStart(2, '0')) + ext;
item.new = newName;
item.newNameHTML = `${esc(newName)} `;
counter++;
} else {
item.new = item.old;
item.newNameHTML = `${esc(item.old)} `;
}
});
};
const updateHeaderCheckbox = () => {
const cbAll = m.querySelector('#rn_cb_all');
if (!cbAll) return;
const total = plannedChanges.length;
const count = previewSelectedIds.size;
cbAll.checked = total > 0 && count === total;
cbAll.indeterminate = count > 0 && count < total;
const validCount = rnDisplay.filter(c => c.new !== c.old && previewSelectedIds.has(c.id)).length;
btnApply.disabled = validCount === 0;
txtStatNum.style.display = 'inline';
txtStatNum.innerHTML = `${validCount} / ${total}`;
};
const renderRNVisible = () => {
const top = rnVp.scrollTop;
const h = rnVp.clientHeight;
const totalHeight = rnDisplay.length * RN_ROW_HEIGHT;
rnIn.style.height = `${totalHeight}px`;
const start = Math.max(0, Math.floor(top / RN_ROW_HEIGHT) - RN_BUFFER);
const end = Math.min(rnDisplay.length, Math.ceil((top + h) / RN_ROW_HEIGHT) + RN_BUFFER);
rnIn.innerHTML = '';
const fragment = document.createDocumentFragment();
const rowStyle = `
display: flex; align-items: center;
height: ${RN_ROW_HEIGHT}px; padding-right: 20px;
border-bottom: 1px dashed #f0f0f0;
box-sizing: border-box; font-size: 13px;
color: var(--pk-fg);
`;
for (let i = start; i < end; i++) {
const c = rnDisplay[i];
if (!c) continue;
const row = document.createElement('div');
row.style.cssText = `position:absolute; top:${i * RN_ROW_HEIGHT}px; width:100%;` + rowStyle;
let finalDisplayHTML = esc(c.old);
if (c.hl_old) {
finalDisplayHTML = getSmartDisplayHTML(c.old, c.hl_old);
}
const isChecked = previewSelectedIds.has(c.id);
const it = S.itemMap.get(c.id);
let iconHtml = '';
let thumbAttr = '';
if (it) {
const isFolder = it.kind === 'drive#folder';
const mime = (it.mime_type || '').toLowerCase();
const isMedia = mime.startsWith('video/') || mime.startsWith('image/');
const hasCover = it.thumbnail_link && it.thumbnail_link !== it.icon_link;
const fallbackSvg = getIcon(it).replace(/width="\d+"/, 'width="24"').replace(/height="\d+"/, 'height="24"');
if (hasCover) {
thumbAttr = `data-pk-thumb="${it.thumbnail_link}"`;
}
if (!isFolder && isMedia && hasCover) {
iconHtml = ` `;
const secondFallback = it.icon_link
? `${fallbackSvg} `
: fallbackSvg;
iconHtml += `${secondFallback} `;
} else {
const iconSrc = it.icon_link;
iconHtml = iconSrc
? `${fallbackSvg} `
: fallbackSvg;
}
}
row.innerHTML = `
${iconHtml}
${finalDisplayHTML}
➜
${c.newNameHTML}
${c.conflict ? `
${L.str_name_conflict}
` : ''}
`;
const toggleRow = () => {
const targetId = c.id;
if (previewSelectedIds.has(targetId)) previewSelectedIds.delete(targetId);
else previewSelectedIds.add(targetId);
recalcPatternNames();
renderRNVisible();
updateHeaderCheckbox();
};
row.onclick = () => {
toggleRow();
};
fragment.appendChild(row);
}
rnIn.appendChild(fragment);
};
rnVp.onscroll = () => {
if (!isRnScrollScheduled) {
requestAnimationFrame(() => { renderRNVisible(); isRnScrollScheduled = false; });
isRnScrollScheduled = true;
}
};
const handlePreviewResults = (changes, error) => {
if (error) {
rnIn.innerHTML = `❌ ${L.err_worker}: ${esc(error)}
`;
return;
}
plannedChanges = changes;
previewSelectedIds = new Set(changes.map(c => c.id));
rnDisplay = changes.map(c => {
let newH = esc(c.new);
if (c.new !== c.old) {
newH = `${newH} `;
}
return {
id: c.id,
old: c.old,
new: c.new,
hl_old: c.hl_old,
newNameHTML: newH,
conflict: c.conflict
};
});
recalcPatternNames();
const cbWrapper = m.querySelector('#rn_cb_wrapper');
const cbAll = m.querySelector('#rn_cb_all');
if (rnDisplay.length === 0) {
rnIn.innerHTML = `${L.rn_tip_none}
`;
txtStatNum.innerText = "0";
btnApply.disabled = true;
if (cbWrapper) cbWrapper.style.visibility = 'hidden';
if (cbAll) { cbAll.checked = false; cbAll.disabled = true; }
} else {
rnVp.scrollTop = 0;
renderRNVisible();
updateHeaderCheckbox();
if (cbWrapper) cbWrapper.style.visibility = 'visible';
if (cbAll) cbAll.disabled = false;
btnApply.disabled = false;
}
};
const javState = {
isRunning: false,
runId: 0,
signature: '',
cache: new Map(),
completed: 0,
total: 0,
ui: null,
activePlannedMap: new Map(),
activeNames: new Set()
};
const generatePreview = async () => {
const myPreviewId = ++currentPreviewId;
const mode = m.querySelector('input[name="rn_mode"]:checked').value;
rnDisplay = [];
plannedChanges = [];
rnIn.innerHTML = '';
const pattern = inpPattern.value;
const findStr = inpFind.value;
const repStr = inpRep.value || '';
const useRegex = chkRegex.checked;
const useIncludeExt = chkIncludeExt.checked;
const caseMode = selectedCase;
const widthMode = selectedWidth;
const useCaseSense = m.querySelector('#rn_case_sense').checked;
let isRuleActive = true;
if (mode === 'replace') isRuleActive = !!findStr;
else if (mode === 'format') isRuleActive = !!(caseMode || widthMode);
else if (mode === 'pattern') isRuleActive = !!pattern;
if (!isRuleActive && mode !== 'jav' && mode !== 'ad_remove' && mode !== 'ext_fix') {
plannedChanges = [];
rnDisplay = [];
previewSelectedIds.clear();
rnIn.innerHTML = `${L.rn_tip_wait}
`;
txtStatNum.innerText = "0";
btnApply.disabled = true;
const cbWrapper = m.querySelector('#rn_cb_wrapper');
if (cbWrapper) cbWrapper.style.visibility = 'hidden';
return;
}
if (mode === 'replace') {
if (findStr) findHist.save(findStr);
if (repStr) repHist.save(repStr);
}
const cbWrapper = m.querySelector('#rn_cb_wrapper');
if (cbWrapper) cbWrapper.style.visibility = 'hidden';
rnIn.innerHTML = ``;
btnApply.disabled = true;
txtStatNum.innerText = "...";
plannedChanges = []; rnDisplay = []; previewSelectedIds = new Set();
const cbAll = m.querySelector('#rn_cb_all');
if(cbAll) { cbAll.checked = false; cbAll.indeterminate = false; }
const selectedIds = Array.from(S.sel);
const items = S.display.filter(i => !i.isHeader);
if (false) {
const targetIds =[];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const id = item.id;
if (!S.sel.has(id)) continue;
if (isSystemItem(item)) continue;
const isFolder = item.kind === 'drive#folder';
const mime = (item.mime_type || '').toLowerCase();
const duration = (item.params && item.params.duration) || 0;
const isVideo = !isFolder && (mime.startsWith('video/') || duration > 0);
if ((isFolder || isVideo) && extractKeyword(item.name)) {
targetIds.push(id);
}
}
if (targetIds.length === 0) {
rnIn.innerHTML = `${L.rn_tip_none}
`;
txtStatNum.innerText = "0";
btnApply.disabled = true;
javState.isRunning = false;
return;
}
const sigIds = targetIds.length > 200
? targetIds.slice(0, 100).concat(targetIds.slice(-100))
: targetIds;
const currentSignature = [...sigIds].sort().join('|') + `_len_${targetIds.length}`;
const isSameSelection = (currentSignature === javState.signature);
if (!isSameSelection) {
javState.runId++;
javState.signature = currentSignature;
javState.cache.clear();
javState.completed = 0;
javState.total = targetIds.length;
javState.isRunning = false;
}
const changes = targetIds.map(id => {
const item = S.itemMap.get(id);
const cached = javState.cache.get(id);
if (cached) {
return {
id: item.id, old: item.name, new: cached.new,
hl_old: cached.hlOld, conflict: cached.conflict, parent_id: item.parent_id
};
} else {
return {
id: item.id, old: item.name, new: item.name,
hl_old: null, conflict: false, parent_id: item.parent_id
};
}
});
handlePreviewResults(changes, null);
const plannedMap = new Map();
plannedChanges.forEach(c => plannedMap.set(c.id, c));
const displayMap = new Map();
javState.activePlannedMap.clear();
plannedChanges.forEach(c => javState.activePlannedMap.set(c.id, c));
javState.activeNames = new Set(items.map(i => i.name));
rnDisplay.forEach(d => {
displayMap.set(d.id, d);
const cached = javState.cache.get(d.id);
if (cached) {
if (cached.new === d.old) {
d.newNameHTML = `${L.str_jav_no_match} `;
} else {
d.newNameHTML = `${esc(cached.new)} `;
}
if (cached.hlOld) {
d.hl_old = cached.hlOld;
}
} else {
d.newNameHTML = ` ${L.str_jav_querying} `;
}
});
if (!document.getElementById('pk-spin-style')) {
const style = document.createElement('style');
style.id = 'pk-spin-style';
style.innerHTML = `@keyframes pk-spin { 100% { transform: rotate(360deg); } }`;
document.head.appendChild(style);
}
const updateProgress = () => {
if(!txtStatNum) return;
txtStatNum.style.display = 'inline-flex';
txtStatNum.style.alignItems = 'baseline';
txtStatNum.style.gap = '8px';
txtStatNum.innerHTML = `
${javState.completed}
/
${javState.total}
`;
};
javState.ui = {
displayMap: displayMap,
render: renderRNVisible,
updateProgress: updateProgress
};
renderRNVisible();
if (isSameSelection && (javState.isRunning || javState.completed === javState.total)) {
updateProgress();
if (javState.completed === javState.total) {
if (rnVp) rnVp.scrollTop = 0;
renderRNVisible();
btnApply.disabled = false;
updateHeaderCheckbox();
const validCount = plannedChanges.filter(c => c.new !== c.old && !c.conflict).length;
txtStatNum.innerHTML = `${validCount} / ${javState.total}`;
}
return;
}
javState.isRunning = true;
const currentRunId = javState.runId;
btnApply.disabled = true;
updateProgress();
const allNames = new Set(items.map(i => i.name));
const queue = targetIds.filter(id => !javState.cache.has(id));
if (queue.length === 0) {
javState.isRunning = false;
btnApply.disabled = false;
updateHeaderCheckbox();
return;
}
let lastRenderTime = 0;
const processItem = async (id) => {
if (currentRunId !== javState.runId || !document.body.contains(m)) return;
const item = S.itemMap.get(id);
if (!item) { javState.completed++; updateProgress(); return; }
const oldName = item.name;
let ext = '';
if (item.kind !== 'drive#folder') {
const extIndex = oldName.lastIndexOf('.');
if (extIndex > 0) {
const potentialExt = oldName.substring(extIndex + 1).toLowerCase();
if (VALID_EXTS.has(potentialExt)) {
ext = oldName.substring(extIndex);
}
}
}
const code = extractKeyword(oldName);
let newName = oldName;
let hlOld = null;
let displayHTML = `${L.str_jav_no_match} `;
if (code) {
try {
const parts = code.split(/[^a-zA-Z0-9]+/).filter(p => p.length > 0);
if (parts.length > 0) {
const escapedParts = parts.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const pattern = `(${escapedParts.join('|')})`;
const re = new RegExp(pattern, 'gi');
hlOld = oldName.replace(re, (m) => '§§§MATCH_START§§§' + m + '§§§MATCH_END§§§');
}
} catch(e) {}
newName = code + ext;
displayHTML = `${esc(newName)} `;
}
let isConflict = false;
if (newName !== oldName) {
if (javState.activeNames.has(newName)) isConflict = true;
else javState.activeNames.add(newName);
}
javState.cache.set(id, { new: newName, hlOld, conflict: isConflict });
javState.completed++;
const currentMode = m.querySelector('input[name="rn_mode"]:checked').value;
if (currentMode === 'jav' && currentRunId === javState.runId && javState.ui) {
const displayItem = javState.ui.displayMap.get(id);
const planItem = javState.activePlannedMap.get(id);
if (displayItem) {
displayItem.new = newName;
displayItem.hl_old = hlOld;
displayItem.newNameHTML = displayHTML;
if (isConflict) displayItem.conflict = true;
if (planItem) {
planItem.new = newName;
planItem.hl_old = hlOld;
planItem.conflict = isConflict;
}
javState.ui.updateProgress();
const now = Date.now();
if (now - lastRenderTime > 500) {
javState.ui.render();
updateHeaderCheckbox();
lastRenderTime = now;
}
}
}
};
const CONCURRENCY = 6;
await Promise.all(Array.from({ length: CONCURRENCY }).map(async () => {
while (queue.length > 0) {
if (currentRunId !== javState.runId) break;
const id = queue.shift();
if (id) await processItem(id);
}
}));
if (currentRunId === javState.runId) {
javState.isRunning = false;
const currentMode = m.querySelector('input[name="rn_mode"]:checked').value;
if (currentMode === 'jav') {
if (rnVp) rnVp.scrollTop = 0;
renderRNVisible();
btnApply.disabled = false;
updateHeaderCheckbox();
const validCount = plannedChanges.filter(c => c.new !== c.old && !c.conflict).length;
txtStatNum.style.display = 'inline';
txtStatNum.innerHTML = `${validCount} / ${javState.total}`;
}
}
return;
}
const workerFunction = function(e) {
try {
const { selectedIds, items, mode, pattern, findStr, repStr, useRegex, validExtsList, caseMode, widthMode, useCaseSense, useIncludeExt } = e.data;
const changes =[];
const idSet = new Set(selectedIds);
const allNames = new Set();
items.forEach(i => allNames.add(i.name));
const VALID_EXTS = new Set(validExtsList);
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const extractKeyword = (fileName) => {
const fc2Regex = /(?:FC2|FC)(?:[-_. ]*PPV)?[-_. @]*(\d{5,8})(?:[-_. ]*(?:part|pt|cd)?[-_. ]?(\d{1,2}|[a-e]))?(?![a-z\d])/i;
const fc2Match = fileName.match(fc2Regex);
if (fc2Match) {
const id = fc2Match[1];
const part = fc2Match[2];
return (part && part.trim()) ? `FC2-PPV-${id}-${part.toUpperCase()}` : `FC2-PPV-${id}`;
}
return null;
};
const toHalf = (str) => str.replace(/[!-~]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0)).replace(/ /g, ' ');
const toFull = (str) => str.replace(/[!-~]/g, c => String.fromCharCode(c.charCodeAt(0) + 0xFEE0)).replace(/ /g, ' ');
const toTitle = (str) => str.replace(/\b\w/g, c => c.toUpperCase());
let regex = null;
if (mode === 'replace' && findStr) {
try {
const flags = useCaseSense ? 'g' : 'gi';
const p = useRegex ? findStr : escapeRegExp(findStr);
regex = new RegExp(p, flags);
} catch (err) {}
}
let counter = 1;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (!idSet.has(item.id)) continue;
const oldName = item.name;
let newName = oldName, hlOld = null;
if (mode === 'pattern') {
if (item.kind === 'drive#folder') continue;
let ext = '';
const lastDotIndex = oldName.lastIndexOf('.');
if (lastDotIndex > 0) {
const potentialExt = oldName.substring(lastDotIndex + 1).toLowerCase();
if (VALID_EXTS.has(potentialExt)) ext = '.' + potentialExt;
}
newName = pattern.replace(/{n}/g, String(counter).padStart(2, '0')) + ext;
counter++;
}
else if (mode === 'replace') {
if (findStr && regex) {
let targetStr = oldName;
let extStr = "";
if (!useIncludeExt && item.kind !== 'drive#folder') {
const lastDot = oldName.lastIndexOf('.');
if (lastDot > 0) {
targetStr = oldName.substring(0, lastDot);
extStr = oldName.substring(lastDot);
}
}
if (regex.test(targetStr)) {
regex.lastIndex = 0;
const newBase = targetStr.replace(regex, repStr);
regex.lastIndex = 0;
const hlBase = targetStr.replace(regex, (m) => '§§§MATCH_START§§§' + m + '§§§MATCH_END§§§');
newName = newBase + extStr;
hlOld = hlBase + extStr;
}
}
}
else if (mode === 'format') {
let base = newName;
let ext = '';
if (item.kind !== 'drive#folder') {
const lastDot = newName.lastIndexOf('.');
if (lastDot > 0) {
base = newName.substring(0, lastDot);
ext = newName.substring(lastDot);
}
}
if (widthMode === 'half') base = toHalf(base);
else if (widthMode === 'full') base = toFull(base);
if (caseMode === 'upper') base = base.toUpperCase();
else if (caseMode === 'lower') base = base.toLowerCase();
else if (caseMode === 'title') base = toTitle(base);
newName = base + ext;
}
else if (mode === 'jav') {
const code = extractKeyword(oldName);
if (code) {
let ext = '', ext_old = '', base_old = oldName;
if (item.kind !== 'drive#folder') {
const lastDotIndex = oldName.lastIndexOf('.');
if (lastDotIndex > 0) {
const potentialExt = oldName.substring(lastDotIndex + 1).toLowerCase();
if (VALID_EXTS.has(potentialExt)) {
ext = oldName.substring(lastDotIndex);
ext_old = ext;
base_old = oldName.substring(0, lastDotIndex);
}
}
}
newName = code + ext;
try {
const parts = code.split(/[^a-zA-Z0-9]+/).filter(p => p.length > 0);
if (parts.length > 0) {
const escapedParts = parts.map(p => escapeRegExp(p));
const pat = `(${escapedParts.join('|')})`;
const re = new RegExp(pat, 'gi');
hlOld = base_old.replace(re, (m) => '§§§MATCH_START§§§' + m + '§§§MATCH_END§§§') + ext_old;
}
} catch(e) {}
}
}
else if (mode === 'ad_remove') {
let cleanName = oldName;
cleanName = cleanName.replace(/^【[^】]+】 *[-_.]? */, '');
cleanName = cleanName.replace(/^[a-z0-9-]+[.](?:com|net|org|cc|xyz|vip|top|la) +/i, '');
const adKw = "(?:[.]com|[.]net|[.]org|[.]cc|[.]xyz|[.]vip|[.]top|[.]la|2048|www[.])";
const atRegex = new RegExp('^.*?' + adKw + '.*?(?:@|--+|_\\s)', 'i');
cleanName = cleanName.replace(atRegex, '');
const hyphenRegex = new RegExp('^[a-z0-9.-]+' + adKw + '-', 'i');
cleanName = cleanName.replace(hyphenRegex, '');
cleanName = cleanName.replace(/^(?:精品加群|福利合集)[0-9]+[-_]+ */, '');
cleanName = cleanName.replace(/^[-_. ,,::;;\p{Extended_Pictographic}]+/u, '');
const idxChnR_Fix = cleanName.indexOf('】');
const idxChnL_Check = cleanName.indexOf('【');
if (idxChnR_Fix > 0 && idxChnR_Fix <= 10 && (idxChnL_Check === -1 || idxChnL_Check > idxChnR_Fix)) {
cleanName = '【' + cleanName;
}
const idxEngR_Fix = cleanName.indexOf(']');
const idxEngL_Check = cleanName.indexOf('[');
if (idxEngR_Fix > 0 && idxEngR_Fix <= 10 && (idxEngL_Check === -1 || idxEngL_Check > idxEngR_Fix)) {
cleanName = '[' + cleanName;
}
const idxBkR_Fix = cleanName.indexOf('》');
const idxBkL_Check = cleanName.indexOf('《');
if (idxBkR_Fix > 0 && idxBkR_Fix <= 10 && (idxBkL_Check === -1 || idxBkL_Check > idxBkR_Fix)) {
cleanName = '《' + cleanName;
}
const idxAngR_Fix = cleanName.indexOf('>');
const idxAngL_Check = cleanName.indexOf('<');
if (idxAngR_Fix > 0 && idxAngR_Fix <= 10 && (idxAngL_Check === -1 || idxAngL_Check > idxAngR_Fix)) {
cleanName = '<' + cleanName;
}
const idxChnParR_Fix = cleanName.indexOf(')');
const idxChnParL_Check = cleanName.indexOf('(');
if (idxChnParR_Fix > 0 && idxChnParR_Fix <= 10 && (idxChnParL_Check === -1 || idxChnParL_Check > idxChnParR_Fix)) {
cleanName = '(' + cleanName;
}
const idxEngParR_Fix = cleanName.indexOf(')');
const idxEngParL_Check = cleanName.indexOf('(');
if (idxEngParR_Fix > 0 && idxEngParR_Fix <= 10 && (idxEngParL_Check === -1 || idxEngParL_Check > idxEngParR_Fix)) {
cleanName = '(' + cleanName;
}
const idxCurR_Fix = cleanName.indexOf('}');
const idxCurL_Check = cleanName.indexOf('{');
if (idxCurR_Fix > 0 && idxCurR_Fix <= 10 && (idxCurL_Check === -1 || idxCurL_Check > idxCurR_Fix)) {
cleanName = '{' + cleanName;
}
const cleanStack = (L, R) => {
const chars = cleanName.split('');
const stack = [];
const toRemove = new Set();
for (let i = 0; i < chars.length; i++) {
const c = chars[i];
if (c === L) {
stack.push(i);
} else if (c === R) {
if (stack.length > 0) stack.pop();
else toRemove.add(i);
}
}
stack.forEach(i => toRemove.add(i));
if (toRemove.size > 0) {
cleanName = chars.filter((_, i) => !toRemove.has(i)).join('');
}
};
cleanStack('【', '】');
cleanStack('[', ']');
cleanStack('{', '}');
cleanStack('(', ')');
cleanStack('(', ')');
cleanStack('《', '》');
cleanStack('<', '>');
const quote2 = (cleanName.match(/'/g) || []).length;
if (quote2 % 2 !== 0) cleanName = cleanName.replace(/"/, '');
const result = cleanName.trim();
const lastDot = oldName.lastIndexOf('.');
if (lastDot !== -1) {
const ext = oldName.substring(lastDot);
const extNoDot = ext.substring(1);
if (!result || result === ext || result === extNoDot) {
newName = oldName;
} else {
newName = result;
}
} else {
if (!result) newName = oldName;
else newName = result;
}
}
else if (mode === 'ext_fix') {
const mimeMap = {
'video/mp4': ['.mp4', '.m4v', '.f4v', '.mp4v', '.mov', '.avi', '.m4a', '.m4b'],
'video/x-matroska': ['.mkv', '.mk3d', '.mka', '.mks'],
'video/x-msvideo': ['.avi'],
'video/quicktime': ['.mov', '.qt', '.mp4', '.m4v'],
'video/x-flv': ['.flv'],
'video/webm': ['.webm'],
'video/mpeg': ['.mpg', '.mpeg', '.mpe', '.vob'],
'video/3gpp': ['.3gp', '.3g2', '.mp4'],
'video/mp2t': ['.ts', '.m2ts', '.mts'],
'video/x-m4v': ['.m4v', '.mp4'],
'video/x-ms-wmv': ['.wmv'],
'video/x-ms-asf': ['.asf', '.wmv', '.wma'],
'audio/mpeg': ['.mp3', '.mp2'],
'audio/mp4': ['.m4a', '.m4b', '.mp4'],
'audio/x-wav': ['.wav'],
'audio/flac': ['.flac'],
'audio/aac': ['.aac'],
'audio/ogg': ['.ogg', '.opus'],
'audio/x-ms-wma': ['.wma'],
'audio/webm': ['.weba'],
'image/jpeg': ['.jpg', '.jpeg', '.jpe', '.jif', '.jfif'],
'image/png': ['.png'],
'image/gif': ['.gif'],
'image/webp': ['.webp'],
'image/bmp': ['.bmp'],
'image/svg+xml': ['.svg'],
'image/heif': ['.heic', '.heif'],
'image/vnd.adobe.photoshop': ['.psd', '.abr'],
'image/x-icon': ['.ico'],
'image/vnd.microsoft.icon': ['.ico'],
'image/tiff': ['.tif', '.tiff', '.cr2', '.cr3', '.nef', '.dng', '.arw', '.orf', '.rw2', '.pef', '.sr2', '.raf'],
'application/postscript': ['.ps', '.eps', '.ai'],
'application/dicom': ['.dcm'],
'application/zip': [
'.zip',
'.exe',
'.pt', '.pth',
'.apk', '.xapk', '.apks', '.obb', '.aar', '.aab', '.ipa', '.ipsw', '.wgt',
'.docx', '.docm', '.dotx', '.dotm',
'.xlsx', '.xlsm', '.xltx', '.xltm',
'.pptx', '.pptm', '.potx', '.potm',
'.vsdx', '.xmind', '.xlam', '.thmx',
'.odt', '.ods', '.odp', '.oxps', '.xps',
'.pages', '.numbers', '.key',
'.xd', '.idml', '.zxp', '.fig', '.sketch', '.brush', '.brushset', '.3mf', '.usdz', '.dwfx', '.ora', '.ufo',
'.h5p', '.apkg', '.colpkg', '.mcpack', '.mcworld', '.unitypackage', '.sb3', '.love', '.egg',
'.nsp', '.xci', '.cia',
'.alfredworkflow', '.sublime-package', '.otf', '.ttf', '.woff', '.woff2',
'.epub', '.kmz', '.cbz',
'.jar', '.war', '.ear', '.sar', '.whl', '.nupkg', '.wsz', '.crx', '.xpi', '.vsix', '.msix', '.appx', '.msixbundle', '.appxbundle', '.kra', '.appv',
'.jks', '.keystore', '.truststore'
],
'application/x-rar-compressed': ['.rar', '.cbr', '.exe'],
'application/x-rar': ['.rar', '.cbr', '.exe'],
'application/vnd.rar': ['.rar', '.cbr', '.exe'],
'application/x-7z-compressed': ['.7z', '.exe', '.cb7', '.wim', '.esd'],
'application/x-tar': ['.tar', '.cbt', '.ova', '.unitypackage', '.gem'],
'application/gzip': ['.gz', '.tgz', '.svgz', '.als', '.schematic', '.litematic', '.tgs', '.unitypackage', '.box'],
'application/x-lzh-compressed': ['.lzh', '.lha'],
'application/x-lha': ['.lzh', '.lha'],
'application/x-iso9660-image': ['.iso', '.img'],
'application/vnd.android.package-archive': ['.apk'],
'application/x-apple-diskimage': ['.dmg'],
'application/x-debian-package': ['.deb'],
'application/x-redhat-package-manager': ['.rpm'],
'application/pdf': ['.pdf', '.ai'],
'text/plain': [
'.txt', '.log', '.md', '.markdown', '.nfo', '.rtf', '.rst', '.adoc', '.org','.mhtml', '.mht',
'.dts', '.dtsi',
'.ofx', '.qif', '.gnucash',
'.tscn', '.tres', '.gd', '.godot',
'.out', '.err', '.pid',
'.asc', '.md5', '.sha1', '.sha256', '.sha512',
'.dockerfile', '.makefile', '.jenkinsfile', '.tf',
'.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte', '.astro', '.mdx',
'.css', '.scss', '.less', '.html', '.htm', '.pug', '.jade', '.coffee', '.wat', '.pac',
'.graphql', '.gql', '.prisma',
'.py', '.java', '.c', '.cpp', '.h', '.hh', '.hpp', '.cs', '.php', '.go', '.rs', '.rb', '.lua',
'.kt', '.swift', '.dart', '.pl', '.pm', '.scala', '.groovy', '.hs', '.asp', '.aspx', '.jsp',
'.m', '.mm', '.r', '.rmd', '.jl', '.nb',
'.ex', '.exs', '.erl', '.hrl', '.clj', '.lisp', '.ml',
'.v', '.sv', '.vhd', '.vhdl', '.sas', '.do',
'.sh', '.bat', '.cmd', '.ps1', '.psd1', '.psm1', '.vbs', '.reg', '.vmx',
'.lock', '.toml', '.hex', '.gradle', '.cmake', '.editorconfig',
'.ini', '.cfg', '.conf', '.rc', '.list', '.yaml', '.yml', '.json', '.xml', '.properties', '.env', '.gitignore', '.sql', '.drawio', '.dio',
'.htaccess', '.npmrc', '.eps', '.ps',
'.meta', '.asset',
'.fbx', '.step', '.stp', '.iges', '.igs', '.gcode', '.stl', '.ply',
'.fasta', '.fa',
'.eml', '.mbox', '.ics', '.ifb',
'.vcf', '.ovpn', '.glsl', '.hlsl', '.shader', '.cginc', '.unity',
'.pem', '.key', '.crt', '.csr', '.p7b', '.p7c',
'.tex', '.sty', '.cls', '.bib',
'.srt', '.ass', '.ssa', '.sub', '.vtt', '.smi', '.lrc', '.sup', '.idx', '.sbv',
'.m3u', '.m3u8', '.cue', '.torrent'
],
'text/html': ['.html', '.htm', '.mhtml', '.mht', '.vue', '.svelte', '.astro', '.txt'],
'text/xml': [
'.xml', '.ui', '.opml', '.kml', '.gpx', '.rss', '.nfo', '.txt', '.svg', '.plist',
'.mobileconfig', '.webloc', '.ttml', '.musicxml',
'.drawio', '.dio', '.csproj', '.vbproj', '.xaml', '.kdenlive', '.fb2', '.xmp', '.dae',
'.fods', '.fodt', '.fodp', '.mobileprovision',
'.nuspec', '.resx', '.vbox', '.osm',
'.application', '.manifest'
],
'application/json': [
'.json', '.txt', '.ipynb', '.gltf', '.geojson', '.map', '.har',
'.topojson', '.webmanifest', '.postman_collection', '.tfstate', '.webapp',
'.uproject', '.uplugin', '.glyphs'
],
'application/x-hdf': ['.h5', '.hdf5', '.keras'],
'application/x-hdf5': ['.h5', '.hdf5', '.keras'],
'text/calendar': ['.ics', '.ifb'],
'application/x-bittorrent': ['.torrent'],
'message/rfc822': ['.mhtml', '.mht', '.eml'],
'multipart/related': ['.mhtml', '.mht'],
'application/x-mobipocket-ebook': ['.mobi', '.azw3'],
'application/vnd.amazon.ebook': ['.azw3', '.mobi'],
'text/vcard': ['.vcf'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx', '.docm', '.dotx', '.dotm'],
'application/msword': ['.doc'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx', '.xlsm', '.xltx', '.xltm', '.csv'],
'application/vnd.ms-excel': ['.xls', '.csv'],
'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx', '.pptm', '.potx', '.potm'],
'application/vnd.ms-powerpoint': ['.ppt'],
'application/epub+zip': ['.epub', '.zip']
};
const exactNamesToKeep = new Set([
'thumbs.db', 'desktop.ini', '.ds_store',
'dockerfile', 'makefile', 'jenkinsfile', 'rakefile', 'gemfile', 'vagrantfile', 'procfile',
'license', 'readme', 'changelog', 'copying', 'authors', 'cmakelists.txt',
'contributors', 'patents', 'security', 'notice', 'version',
'cname', 'owners', 'robots.txt',
'go.mod', 'go.sum', 'podfile', 'podfile.lock', 'yarn.lock', 'package-lock.json'
]);
const binaryMimes = ['application/octet-stream', 'binary/octet-stream'];
const safeImgExts = ['.jpg', '.jpeg', '.png', '.bmp', '.heic'];
const safeVidExts = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.3gp', '.ts', '.mpg', '.mpeg', '.vob', '.rmvb', '.asf'];
const highRiskExts = [
'.rar', '.zip', '.7z', '.iso', '.img', '.dmg', '.apk', '.ipa',
'.mhtml', '.mht', '.html', '.htm', '.xml', '.json',
'.db', '.dat', '.tmp', '.dts', '.dtsi',
'.ts', '.3gp', '.mkv', '.avi', '.mp4', '.flv', '.mov', '.wmv'
];
if (item.mimeType === 'application/vnd.google-apps.folder') continue;
const pureMimeType = (item.mimeType || '').split(';')[0].trim().toLowerCase();
const lowerName = oldName.toLowerCase();
const lastDotIndex = oldName.lastIndexOf('.');
const currentExt = lastDotIndex !== -1 ? oldName.substring(lastDotIndex).toLowerCase() : '';
const isPartFile = /^\.\d+$/.test(currentExt) || /\.part\d+$/i.test(currentExt);
let newName = oldName;
let shouldFix = false;
if (binaryMimes.includes(pureMimeType) || exactNamesToKeep.has(lowerName) || oldName.startsWith('.') || isPartFile) {
shouldFix = false;
}
else {
const validExtensions = mimeMap[pureMimeType];
if (validExtensions && validExtensions.length > 0) {
const primaryExt = validExtensions[0];
if (validExtensions.includes(currentExt)) {
newName = oldName.substring(0, lastDotIndex) + currentExt;
shouldFix = true;
}
else if (
(safeImgExts.includes(currentExt) && safeImgExts.includes(primaryExt)) ||
(safeVidExts.includes(currentExt) && safeVidExts.includes(primaryExt))
) {
newName = oldName.substring(0, lastDotIndex) + currentExt;
shouldFix = true;
}
else if (
(pureMimeType === 'application/pdf' && ['.rar', '.zip', '.7z'].includes(currentExt)) ||
highRiskExts.includes(currentExt)
) {
shouldFix = false;
}
else {
shouldFix = true;
if (lastDotIndex === -1) {
const ambiguousTextExts = ['.svg', '.html', '.htm', '.xml', '.json'];
if (ambiguousTextExts.includes(primaryExt) && !oldName.includes(' ')) {
shouldFix = false;
} else {
if (ambiguousTextExts.includes(primaryExt)) newName = oldName + '.txt';
else newName = oldName + primaryExt;
}
}
else {
const isSourceText = ['.txt', '.log', '.md', '.ini', '.nfo'].includes(currentExt);
const ambiguousTextExts = ['.svg', '.html', '.htm', '.xml', '.json'];
if (isSourceText && ambiguousTextExts.includes(primaryExt)) {
newName = oldName.substring(0, lastDotIndex) + currentExt;
} else {
newName = oldName.substring(0, lastDotIndex) + primaryExt;
}
}
}
}
}
if (shouldFix && newName !== oldName) {
changes.push({
id: item.id, old: oldName, new: newName,
hl_old: null, conflict: false, parent_id: item.parent_id
});
}
}
if (item._isSystem) continue;
if (newName !== oldName || (mode === 'replace' && hlOld) || mode === 'pattern') {
let isConflict = false;
const cleanNewName = newName.trim();
if (!cleanNewName) { isConflict = true; newName = e.data.STR_EMPTY_FILENAME; }
else if (allNames.has(cleanNewName)) { isConflict = true; }
else { allNames.add(cleanNewName); }
changes.push({
id: item.id, old: oldName, new: newName,
hl_old: hlOld, conflict: isConflict, parent_id: item.parent_id
});
}
}
self.postMessage({ changes: changes, error: null });
} catch (e) { self.postMessage({ changes: [], error: e.toString() }); }
};
const workerCode = 'self.onmessage = ' + workerFunction.toString();
const itemsCopy = items.map(i => ({ id: i.id, name: i.name, kind: i.kind, mimeType: i.mime_type, parent_id: i.parent_id, _isSystem: isSystemItem(i) }));
const blob = new Blob([workerCode], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);
const previewWorker = new Worker(workerUrl);
previewWorker.onmessage = (e) => {
const { changes, error } = e.data;
previewWorker.terminate();
URL.revokeObjectURL(workerUrl);
if (myPreviewId !== currentPreviewId) return;
handlePreviewResults(changes, error);
};
previewWorker.onerror = (e) => {
console.error("Worker Error:", e);
rnIn.innerHTML = `❌ Worker Error
`;
previewWorker.terminate();
URL.revokeObjectURL(workerUrl);
};
previewWorker.postMessage({
selectedIds, items: itemsCopy, mode, pattern, findStr, repStr, useRegex,
STR_INVALID_REGEX: L.err_invalid_regex,
STR_EMPTY_FILENAME: L.str_empty_filename,
validExtsList: VALID_EXTS_LIST,
caseMode, widthMode, useCaseSense, useIncludeExt
});
};
m.querySelector('#rn_cancel').onclick = () => m.remove();
[inpPattern, inpFind, inpRep, chkRegex, chkCase, chkIncludeExt].forEach(el => el.onchange = generatePreview);
[inpPattern, inpFind, inpRep].forEach(el => el.onkeydown = (e) => { if(e.key==='Enter') generatePreview(); });
let selectedCase = "";
let selectedWidth = "";
const bindRNSelect = (id, onSelect) => {
const container = m.querySelector(`#${id}`);
if (!container) return;
const trigger = container.querySelector('.pk-select-trigger');
const menu = container.querySelector('.pk-select-menu');
const txt = container.querySelector('span');
const items = container.querySelectorAll('.pk-select-item');
trigger.onclick = (e) => {
e.stopPropagation();
const allMenus = m.querySelectorAll('.pk-select-menu');
const isOpen = menu.style.display === 'block';
allMenus.forEach(om => om.style.display = 'none');
menu.style.display = isOpen ? 'none' : 'block';
};
items.forEach(item => {
item.onclick = (e) => {
e.stopPropagation();
items.forEach(i => i.classList.remove('act'));
item.classList.add('act');
txt.textContent = item.textContent;
menu.style.display = 'none';
onSelect(item.dataset.val);
generatePreview();
};
});
};
bindRNSelect('cs_rn_case', (val) => { selectedCase = val; });
bindRNSelect('cs_rn_width', (val) => { selectedWidth = val; });
const closeDropdowns = () => m.querySelectorAll('.pk-select-menu').forEach(menu => menu.style.display = 'none');
setTimeout(() => document.addEventListener('click', closeDropdowns), 0);
const _orgRemove = m.remove.bind(m);
m.remove = () => {
document.removeEventListener('click', closeDropdowns);
_orgRemove();
};
const bindHeaderCheckbox = () => {
const cbAll = m.querySelector('#rn_cb_all');
if(cbAll) {
cbAll.onclick = (e) => {
const isChecked = e.target.checked;
if(isChecked) {
plannedChanges.forEach(c => previewSelectedIds.add(c.id));
} else {
previewSelectedIds.clear();
}
recalcPatternNames();
renderRNVisible();
updateHeaderCheckbox();
};
}
};
setTimeout(bindHeaderCheckbox, 0);
btnApply.onclick = async () => {
const validChanges = rnDisplay.filter(c => previewSelectedIds.has(c.id) && c.new !== c.old);
const skippedCount = plannedChanges.length - validChanges.length;
if (validChanges.length === 0) {
showAlert(L.msg_rn_all_skipped);
return;
}
let confirmMsg = L.rn_warn_confirm.replace('{n}', validChanges.length);
if (!await showConfirm(confirmMsg)) return;
const progressTask = FloatBarManager.create(L.str_renaming);
m.remove();
let isRunning = true;
UI.stopBtn.onclick = () => { isRunning = false; updateLoadTxt(L.str_stopping); };
const USER_LIMIT = parseInt(localStorage.getItem('pk_user_limit') || "200");
let currentLimit = 2; const MIN_LIMIT = 2;
const queue = [...validChanges];
const activeTasks = new Set();
const stats = { success: 0, fail: 0, lastUiTime: 0 };
const total = validChanges.length;
const runRenameTask = async (task) => {
try {
await apiAction(`/${task.id}`, { name: task.new });
stats.success++;
const item = S.itemMap.get(task.id);
if (item) {
item.name = task.new;
const nowIso = new Date(getServerNow()).toISOString();
item.modified_time = nowIso;
if (item.kind === 'drive#folder') gmSet('pk_fmod_' + item.id, nowIso);
const parentIdForFmod = item.parent_id || 'root';
gmSet('pk_fmod_' + parentIdForFmod, nowIso);
const row = UI.in.querySelector(`.pk-row[data-id="${task.id}"]`);
if (row && row.lastElementChild) row.lastElementChild.textContent = fmtDate(nowIso);
if (S.analyzeResultItems) {
const anaItem = S.analyzeResultItems.find(x => x.id === task.id);
if (anaItem) anaItem.name = task.new;
}
if (S.analyzeMap && S.analyzeMap.has(task.id)) {
S.analyzeMap.get(task.id).name = task.new;
}
if (S.lastGlobalResults && S.lastGlobalResults.length > 0) {
const gItem = S.lastGlobalResults.find(x => x.id === task.id);
if (gItem) gItem.name = task.new;
}
if (typeof globalDirtyFolders !== 'undefined') {
const pid = item.parent_id || '';
globalDirtyFolders.add(pid);
}
}
if (currentLimit < USER_LIMIT) currentLimit++;
} catch (e) {
if (!isRunning) return;
if ((e.message && e.message.includes('429')) || (e.message && e.message.includes('Network'))) {
currentLimit = Math.max(MIN_LIMIT, Math.floor(currentLimit / 2));
task.retryCount = (task.retryCount || 0) + 1;
await sleep(Math.min(task.retryCount * 1000, 10000));
queue.push(task);
} else {
stats.fail++;
}
}
};
try {
updateLoadTxt(L.str_init_rename);
while ((queue.length > 0 || activeTasks.size > 0) && isRunning) {
while (queue.length > 0 && activeTasks.size < currentLimit && isRunning) {
const task = queue.shift();
const p = runRenameTask(task).finally(() => activeTasks.delete(p));
activeTasks.add(p);
}
if (activeTasks.size > 0) await Promise.race(activeTasks);
const now = Date.now();
if (now - stats.lastUiTime > 150) {
progressTask.update(`${L.str_renaming} ${stats.success + stats.fail}/${total} | ${L.str_speed}: ${activeTasks.size} | ${L.str_success}: ${stats.success}`);
stats.lastUiTime = now;
}
}
if (!isRunning) throw new Error('StoppedByUser');
updateLoadTxt(L.str_refreshing_cache);
if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler();
if (S.dupMode) renderDupView(); else refresh();
let msgParts = [];
if (stats.success > 0) msgParts.push(L.msg_bulkrename_done.replace('{n}', stats.success));
if (stats.fail > 0) msgParts.push(L.msg_rn_fail_count.replace('{n}', stats.fail));
const finalMsg = msgParts.join('\n');
await sleep(300); showAlert(finalMsg);
} catch (e) {
if (e.message !== 'StoppedByUser') showAlert(`${L.str_error_crit}: ${e.message}`);
} finally {
if (progressTask) progressTask.destroy();
setLoad(false);
}
};
};
UI.btnPrune.onclick = async () => {
isGUISensitive = true;
ensureItemMap();
const selectedFolders = Array.from(S.sel)
.map(id => S.itemMap.get(id))
.filter(i => i && i.kind === 'drive#folder');
if (selectedFolders.length === 0) return;
const hasConflict = selectedFolders.some(f => isPathBusy(f.id));
if (hasConflict) {
showAlert(L.msg_prune_blocked_moving);
return;
}
if (!await showConfirm(L.msg_prune_confirm)) return;
setLoad(true);
S.scanning = true;
S.scanId = (S.scanId || 0) + 1;
const myScanId = S.scanId;
if (S.scanAbortController) S.scanAbortController.abort();
S.scanAbortController = new AbortController();
const signal = S.scanAbortController.signal;
UI.stopBtn.onclick = () => {
S.scanning = false;
if (S.scanAbortController) S.scanAbortController.abort();
updateLoadTxt(L.str_stopping);
setLoad(false);
isGUISensitive = false;
};
const folderMap = new Map();
updateLoadTxt(L.str_scanning_dir);
try {
await coreRecursiveEngine(selectedFolders, {
signal: signal,
onFolder: (folder, filesInFolder, subFolders) => {
const hasFiles = filesInFolder.some(f => f.kind !== 'drive#folder');
folderMap.set(folder.id, {
id: folder.id,
name: folder.name,
parent_id: folder.parent_id,
depth: folder.depth || 0,
hasFiles: hasFiles,
subFolderIds: subFolders.map(s => s.id)
});
},
onProgress: (st) => {
const folderText = `${L.str_scanning_dir} ${st.folders} ${L.unit_folders}`;
const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`;
updateLoadTxt(folderText + statusInfo);
}
});
if (!S.scanning || signal.aborted || myScanId !== S.scanId) return;
updateLoadTxt(L.str_analyzing);
await sleep(50);
const allScanned = Array.from(folderMap.values()).sort((a, b) => b.depth - a.depth);
const toDeleteList = [];
const toDeleteIds = new Set();
for (let i = 0; i < allScanned.length; i++) {
if (!S.scanning) return;
const folder = allScanned[i];
const isSystemProtected = isSystemItem({ ...folder, kind: 'drive#folder' });
if (isSystemProtected) continue;
if (!folder.hasFiles) {
const allSubsWillBeDeleted = folder.subFolderIds.every(subId => toDeleteIds.has(subId));
if (allSubsWillBeDeleted) {
toDeleteIds.add(folder.id);
toDeleteList.push(folder);
}
}
}
if (toDeleteList.length === 0) {
setLoad(false);
showAlert(L.msg_prune_none);
} else {
setLoad(false);
const cacheHitCount = Array.from(folderMap.keys()).filter(id => globalCache.has(id)).length;
let confirmMsg = L.msg_prune_found.replace('{n}', toDeleteList.length);
if (await showConfirm(confirmMsg)) {
const allIds = toDeleteList.map(f => f.id);
await executeBatchDelete(allIds, {
silent: true,
forceRefresh: false
});
if (myScanId === S.scanId) {
const deletedSet = new Set(allIds);
if (S.lastGlobalResults && S.lastGlobalResults.length > 0) {
S.lastGlobalResults = S.lastGlobalResults.filter(x => !deletedSet.has(x.id));
}
if (S.analyzeMode && S.analyzeResultItems) {
S.analyzeResultItems = S.analyzeResultItems.filter(x => !deletedSet.has(x.id));
}
updateLoadTxt(L.str_refreshing);
const affectedParentIds = new Set();
affectedParentIds.add(S.path[S.path.length - 1].id || 'root');
toDeleteList.forEach(folder => {
if (folder.parent_id) affectedParentIds.add(folder.parent_id);
else affectedParentIds.add('root');
});
affectedParentIds.forEach(pid => {
if (typeof globalCache !== 'undefined') globalCache.delete(pid);
S.cache.delete(pid);
});
await load(false, true);
showToast(L.str_cleanup_done);
}
}
}
} catch (e) {
if (e.name !== 'AbortError' && myScanId === S.scanId) {
setLoad(false);
showAlert(`${L.str_error}: ${e.message}`);
}
} finally {
if (myScanId === S.scanId) {
setLoad(false);
S.scanning = false;
S.scanAbortController = null;
isGUISensitive = false;
if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun();
}
}
};
if (UI.btnUpPause) UI.btnUpPause.onclick = () => {
const ids = Array.from(S.sel);
ids.forEach(id => {
const task = S.uploadTasks.find(t => t.id === id);
if (task && S.upMng) S.upMng.pause(task, true);
});
if (S.uploadMode) { refresh(); }
};
if (UI.btnUpStart) UI.btnUpStart.onclick = () => {
const ids = Array.from(S.sel);
ids.forEach(id => {
const task = S.uploadTasks.find(t => t.id === id);
if (task && S.upMng) S.upMng.resume(task, true);
});
if (S.uploadMode) { refresh(); }
};
if (UI.btnUpDel) UI.btnUpDel.onclick = () => {
if (S.sel.size === 0) return;
const count = S.sel.size;
const html = `
`;
const m = showModal(html);
const modalBox = m.querySelector('.pk-modal');
if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; }
const close = () => m.remove();
m.querySelector('#del_up_cancel').onclick = close;
const closeBtn = m.querySelector('.pk-modal-close');
if(closeBtn) closeBtn.onclick = close;
m.querySelector('#del_up_confirm').onclick = async () => {
const isDeleteFile = m.querySelector('#del_up_files').checked;
m.remove();
const ids = Array.from(S.sel);
const filesToDelete = [];
ids.forEach(id => {
const task = S.uploadTasks.find(t => t.id === id);
if (task) {
task._deleted = true;
task._deleteFileIntent = isDeleteFile;
if (isDeleteFile && task.file_id) {
filesToDelete.push(task.file_id);
}
if (S.upMng) S.upMng.pause(task, true);
}
});
const idSet = S.sel;
S.uploadTasks = S.uploadTasks.filter(t => !idSet.has(t.id));
S.clearSelection();
load(false, true);
if (filesToDelete.length > 0) {
try {
await executeBatchDelete(filesToDelete, {
silent: false,
hardDelete: false,
forceRefresh: false
});
} catch(e) {
console.error("Failed to delete uploaded files:", e);
showToast(L.msg_file_del_failed + e.message, "error");
}
} else {
showToast(L.msg_task_del_success_fmt.replace('{n}', ids.length));
}
};
m.tabIndex = 0;
setTimeout(() => m.focus(), 10);
m.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); e.stopPropagation();
m.querySelector('#del_up_confirm').click();
}
});
};
if (UI.btnUpClearAll) UI.btnUpClearAll.onclick = () => {
if (S.uploadTasks.length === 0) return;
const count = S.uploadTasks.length;
const html = `
`;
const m = showModal(html);
const modalBox = m.querySelector('.pk-modal');
if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; }
const close = () => m.remove();
m.querySelector('#clear_up_cancel').onclick = close;
const closeBtn = m.querySelector('.pk-modal-close');
if(closeBtn) closeBtn.onclick = close;
m.querySelector('#clear_up_confirm').onclick = async () => {
const isDeleteFile = m.querySelector('#clear_all_up_files').checked;
m.remove();
const filesToDelete =[];
S.uploadTasks.forEach(task => {
task._deleted = true;
task._deleteFileIntent = isDeleteFile;
if (S.upMng) S.upMng.pause(task, true);
if (isDeleteFile && task.file_id) {
filesToDelete.push(task.file_id);
}
});
S.uploadTasks = [];
S.clearSelection();
load(false, true);
if (filesToDelete.length > 0) {
try {
await executeBatchDelete(filesToDelete, {
silent: false,
hardDelete: false,
forceRefresh: false
});
} catch(e) {
console.error("Clear All: Cloud deletion failed", e);
}
} else {
showToast(L.msg_task_clear_success_fmt.replace('{n}', count));
}
};
m.tabIndex = 0;
setTimeout(() => m.focus(), 10);
m.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); e.stopPropagation();
m.querySelector('#clear_up_confirm').click();
}
});
};
UI.btnExt.onclick = async () => {
const id = Array.from(S.sel)[0];
const item = S.itemMap.get(id);
if (!item) return;
setLoad(true);
updateLoadTxt(L.loading_detail);
const targetApiId = ((S.offlineMode && item.kind === 'drive#task') || (S.uploadMode && item.file_id)) ? item.file_id : item.id;
let detail = item;
try {
if (!targetApiId) throw new Error("File ID not ready");
detail = await apiGet(targetApiId);
} catch (e) {
console.warn("Fetch detail failed, using cached info");
if (!detail.web_content_link && !detail.medias) {
setLoad(false);
showToast(L.msg_video_fail, 'error');
return;
}
}
setLoad(false);
const qualities = generateQualityList(detail);
const bestSource = getBestSource(detail);
let selectedUrl = bestSource.src;
let selectedResName = bestSource.name;
const savedPlayer = gmGet('pk_ext_player', 'potplayer');
let selectedPlayer = (savedPlayer === 'potplayer') ? 'potplayer' : 'other';
const initialBtnTxt = (selectedPlayer === 'potplayer') ? L.btn_start_play : L.btn_copy_link;
const m = showModal(`
${L.btn_ext}
${L.lbl_resolution}
${selectedResName}
${CONF.crumbIcons.down}
${L.lbl_player}
${selectedPlayer === 'potplayer' ? 'PotPlayer' : L.opt_player_other}
${CONF.crumbIcons.down}
${L.btn_cancel}
${initialBtnTxt}
`);
const modalBox = m.querySelector('.pk-modal');
if (modalBox) {
Object.assign(modalBox.style, { width: '480px', padding: '30px', height: 'auto', minHeight: 'auto', overflow: 'visible' });
const closeBtn = m.querySelector('.pk-modal-close');
if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' });
}
const bindSelect = (id, onSelect) => {
const container = m.querySelector(`#${id}`);
const trigger = container.querySelector('.pk-select-trigger');
const menu = container.querySelector('.pk-select-menu');
const txt = container.querySelector('span');
trigger.onclick = (e) => {
e.stopPropagation();
const allMenus = m.querySelectorAll('.pk-select-menu');
const isCurrentlyOpen = menu.style.display === 'block';
allMenus.forEach(om => om.style.display = 'none');
menu.style.display = isCurrentlyOpen ? 'none' : 'block';
};
container.querySelectorAll('.pk-select-item').forEach(item => {
item.onclick = (e) => {
e.stopPropagation();
container.querySelectorAll('.pk-select-item').forEach(i => i.classList.remove('act'));
item.classList.add('act');
txt.textContent = item.textContent;
menu.style.display = 'none';
onSelect(item.dataset.val);
};
});
};
const runBtn = m.querySelector('#ext_run');
bindSelect('cs_res', (val) => { selectedUrl = val; });
bindSelect('cs_player', (val) => {
selectedPlayer = val;
runBtn.textContent = (val === 'potplayer') ? L.btn_start_play : L.btn_copy_link;
});
const closeAllMenus = () => m.querySelectorAll('.pk-select-menu').forEach(om => om.style.display = 'none');
setTimeout(() => document.addEventListener('click', closeAllMenus), 0);
const _orgRemove = m.remove.bind(m);
m.remove = () => {
document.removeEventListener('click', closeAllMenus);
_orgRemove();
};
m.querySelector('#ext_cancel').onclick = () => m.remove();
m.tabIndex = 0;
setTimeout(() => m.focus(), 10);
m.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); e.stopPropagation();
runBtn.click();
}
});
runBtn.onclick = () => {
gmSet('pk_ext_player', selectedPlayer);
let cleanUrl = selectedUrl.replace('&ext=.m3u8', '');
if (cleanUrl.includes('ts_downloader') && cleanUrl.includes('url=')) {
const urlParam = new URL(cleanUrl).searchParams.get('url');
if (urlParam) cleanUrl = decodeURIComponent(urlParam);
}
if (selectedPlayer === 'potplayer') {
m.remove();
const ua = navigator.userAgent.replace(/"/g, '');
const cmd = `${cleanUrl} /user_agent="${ua}" /referer="https://mypikpak.com/"`;
window.location.href = `potplayer://${cmd}`;
} else {
GM_setClipboard(cleanUrl);
runBtn.textContent = L.msg_copy_success;
runBtn.style.background = "#52c41a";
runBtn.disabled = true;
setTimeout(() => m.remove(), 1000);
}
};
};
UI.win.querySelector('#pk-down').onclick = async () => {
const hasConflict = Array.from(S.sel).some(id => {
const item = S.itemMap.get(id);
if (item && item.kind === 'drive#folder') return isPathBusy(item.id);
return S.movingIds.has(id);
});
if (hasConflict) {
showAlert(L.msg_resource_locked_download);
return;
}
setLoad(true);
isGUISensitive = true;
const abortCtrl = new AbortController();
const { signal } = abortCtrl;
let isRunning = true;
UI.stopBtn.onclick = () => { isRunning = false; abortCtrl.abort(); updateLoadTxt(L.str_stopping); };
const allFiles = [];
const rootNodes = [];
const HYDRATE_LIMIT = 20;
S.sel.forEach(id => {
const item = S.itemMap.get(id);
if (item) {
if (item.kind === 'drive#folder') rootNodes.push({...item, lineage:[], retryCount: 0});
else allFiles.push(item);
}
});
let progressTask = null;
const fExtStr = gmGet('pk_dl_filter_ext', '').toLowerCase();
const fNameStr = gmGet('pk_dl_filter_name', '').toLowerCase();
const fExts = fExtStr.split(/[,,]/).map(s => s.trim().replace(/^\./, '')).filter(Boolean);
const fNames = fNameStr.split(/[,,]/).map(s => s.trim()).filter(Boolean);
try {
await coreRecursiveEngine(rootNodes, {
signal,
onFile: (f) => {
const lowName = f.name.toLowerCase();
const ext = lowName.split('.').pop();
const isBlocked = fExts.some(e => ext === e) || fNames.some(n => lowName.includes(n));
if (!isBlocked) allFiles.push(f);
},
onProgress: (st) => {
updateLoadTxt(`${L.msg_batch_scanning}\n${L.str_files}: ${allFiles.length} | ${L.str_speed}: ${st.currentConcurrency}`);
}
});
if (!isRunning) throw new Error('StoppedByUser');
if (allFiles.length === 0) { setLoad(false); showToast(L.msg_batch_no_files); return; }
setLoad(false);
if (allFiles.length > 10) {
if (!await showConfirm(L.msg_down_confirm_total.replace('{n}', allFiles.length))) return;
}
progressTask = FloatBarManager.create(L.msg_batch_hydrating);
const readyFiles = [];
const hydrateQueue = [...allFiles];
const activeTasks = new Set();
while ((hydrateQueue.length > 0 || activeTasks.size > 0) && isRunning) {
while (hydrateQueue.length > 0 && activeTasks.size < HYDRATE_LIMIT && isRunning) {
const file = hydrateQueue.pop();
const p = (async () => {
try {
const detail = (file.web_content_link) ? file : await apiGet(file.id);
if (detail?.web_content_link) readyFiles.push(detail);
} catch (e) {}
})().finally(() => activeTasks.delete(p));
activeTasks.add(p);
}
if (activeTasks.size > 0) await Promise.race(activeTasks);
if (progressTask) progressTask.update(`${L.msg_batch_hydrating} ${readyFiles.length} / ${allFiles.length}`);
}
for (let i = 0; i < readyFiles.length; i++) {
if (!isRunning) break;
if (progressTask) progressTask.update(`${L.msg_down_progress} ${i + 1} / ${readyFiles.length}`);
const link = document.createElement('a');
link.href = readyFiles[i].web_content_link;
link.setAttribute('download', readyFiles[i].name);
link.style.display = 'none';
document.body.appendChild(link);
link.click();
setTimeout(() => { if (link.parentNode) link.remove(); }, 10000);
if (i < readyFiles.length - 1) await sleep(2000);
}
if (isRunning && readyFiles.length > 0) {
showToast(L.msg_down_success.replace('{n}', readyFiles.length));
}
} catch (e) {
if (e.message !== 'StoppedByUser' && e.name !== 'AbortError') showAlert(`${L.str_error}: ${e.message}`);
} finally {
setLoad(false);
isGUISensitive = false;
if (progressTask) progressTask.destroy();
}
};
UI.win.querySelector('#pk-aria2').onclick = async () => {
const hasConflict = Array.from(S.sel).some(id => {
const item = S.itemMap.get(id);
if (item && item.kind === 'drive#folder') return isPathBusy(item.id);
return S.movingIds.has(id);
});
if (hasConflict) {
showAlert(L.msg_resource_locked_aria2);
return;
}
let ariaUrl = gmGet('pk_aria2_url', '');
let ariaToken = gmGet('pk_aria2_token', '');
if (!ariaUrl) {
const result = await new Promise((resolve) => {
const m = showModal(`
${CONF.icons.aria2.replace('width="16"', 'width="22"').replace('height="16"', 'width="22"')}
${L.btn_aria2} - ${L.btn_settings}
${L.msg_aria2_not_set}
${L.btn_cancel}
${L.btn_save} & ${L.btn_aria2}
`);
const modalBox = m.querySelector('.pk-modal');
if (modalBox) Object.assign(modalBox.style, { width: '480px', padding: '30px', height: 'auto', minHeight: 'auto', overflow: 'visible' });
const closeBtn = m.querySelector('.pk-modal-close');
if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' });
const inpU = m.querySelector('#pop_aria_url');
const inpT = m.querySelector('#pop_aria_token');
const dot = m.querySelector('#pop_aria_test_dot');
const txt = m.querySelector('#pop_aria_test_txt');
const boxRes = m.querySelector('#pop_aria_test_res');
let testTimer = null;
const runLiveTest = async () => {
const url = inpU.value.trim();
const token = inpT.value.trim();
const showTip = () => showAlert(L.tip_mixed_content, L.lbl_aria2_status);
if (!url) {
dot.className = 'pk-aria-dot';
txt.textContent = L.lbl_aria2_status;
boxRes.onclick = showTip;
boxRes.style.cursor = 'pointer';
return;
}
dot.className = 'pk-aria-dot wait';
txt.textContent = L.str_connecting;
boxRes.onclick = showTip;
boxRes.style.cursor = 'pointer';
const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_quick_test', params: [`token:${token}`] };
let testUrl = url.replace(/^ws/i, 'http');
if (!testUrl.includes('/jsonrpc') && !testUrl.includes('?')) {
testUrl = testUrl.endsWith('/') ? testUrl + 'jsonrpc' : testUrl + '/jsonrpc';
}
try {
await new Promise((resolveReq, rejectReq) => {
GM_xmlhttpRequest({
method: 'POST', url: testUrl, data: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' }, timeout: 3000,
onload: (r) => { if (r.status === 200) resolveReq(); else rejectReq(new Error(r.status)); },
onerror: (e) => rejectReq(e)
});
});
dot.className = 'pk-aria-dot ok';
txt.textContent = L.str_connected;
boxRes.onclick = null;
boxRes.style.cursor = 'default';
} catch (e) {
dot.className = 'pk-aria-dot err';
txt.textContent = L.str_conn_fail;
boxRes.onclick = showTip;
boxRes.style.cursor = 'pointer';
}
};
const triggerTest = () => { clearTimeout(testTimer); testTimer = setTimeout(runLiveTest, 600); };
inpU.oninput = (e) => {
e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)';
triggerTest();
};
inpT.oninput = (e) => {
e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)';
triggerTest();
};
m.querySelector('#btn_pop_aria_default').onclick = () => {
inpU.value = 'http://localhost:6800/jsonrpc';
inpU.style.borderColor = 'var(--pk-pri)';
triggerTest();
};
setTimeout(runLiveTest, 200);
m.querySelector('#pop_aria_cancel').onclick = () => { m.remove(); resolve(null); };
m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(null); };
m.querySelector('#pop_aria_save').onclick = async () => {
let u = inpU.value.trim();
let t = inpT.value.trim();
if (!u) { inpU.style.borderColor = '#d93025'; return; }
if (!/^https?:\/\/|^wss?:\/\//i.test(u)) u = 'http://' + u;
const saveBtn = m.querySelector('#pop_aria_save');
const originalTxt = saveBtn.textContent;
saveBtn.disabled = true;
saveBtn.textContent = L.str_saving_dots;
try {
const testUrl = u.replace(/^ws/i, 'http');
const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_quick_test', params: [`token:${t}`] };
await new Promise((resolveReq, rejectReq) => {
GM_xmlhttpRequest({
method: 'POST',
url: testUrl,
data: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
timeout: 5000,
onload: (r) => { if (r.status === 200) resolveReq(); else rejectReq(new Error('HTTP ' + r.status)); },
onerror: () => rejectReq(new Error('Network Error')),
ontimeout: () => rejectReq(new Error('Timeout'))
});
});
gmSet('pk_aria2_url', u); gmSet('pk_aria2_token', t);
m.remove();
resolve({ url: u, token: t });
} catch (err) {
const confirmed = await showConfirm(
L.msg_aria2_test_fail,
L.title_aria2_fail
);
if (confirmed) {
gmSet('pk_aria2_url', u); gmSet('pk_aria2_token', t);
m.remove();
resolve({ url: u, token: t });
} else {
saveBtn.disabled = false;
saveBtn.textContent = originalTxt;
}
}
};
m.tabIndex = 0;
setTimeout(() => m.focus(), 10);
m.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); e.stopPropagation();
m.querySelector('#pop_aria_save').click();
}
});
});
if (!result) return;
ariaUrl = result.url;
ariaToken = result.token;
}
setLoad(true);
isGUISensitive = true;
const abortCtrl = new AbortController();
const { signal } = abortCtrl;
let isRunning = true;
UI.stopBtn.onclick = () => { isRunning = false; abortCtrl.abort(); updateLoadTxt(L.str_stopping); };
if (ariaUrl.startsWith('ws')) ariaUrl = ariaUrl.replace(/^ws/, 'http');
if (!ariaUrl.includes('/jsonrpc') && !ariaUrl.includes('?')) {
ariaUrl = ariaUrl.endsWith('/') ? ariaUrl + 'jsonrpc' : ariaUrl + '/jsonrpc';
}
const allFiles = [];
const rootNodes = [];
const HYDRATE_LIMIT = 40;
const fExtStr = gmGet('pk_dl_filter_ext', '').toLowerCase();
const fNameStr = gmGet('pk_dl_filter_name', '').toLowerCase();
const fExts = fExtStr.split(/[,,]/).map(s => s.trim().replace(/^\./, '')).filter(Boolean);
const fNames = fNameStr.split(/[,,]/).map(s => s.trim()).filter(Boolean);
const stats = { hydratedCount: 0, lastUiTime: 0 };
S.sel.forEach(id => {
const item = S.itemMap.get(id);
if (item) {
if (item.kind === 'drive#folder') {
rootNodes.push({...item, lineage: [{ id: item.id, name: item.name }], retryCount: 0});
} else {
allFiles.push({ ...item, _lineage: [] });
}
}
});
let progressTask = null;
try {
await coreRecursiveEngine(rootNodes, {
signal,
onFile: (f, parent) => {
const lowName = f.name.toLowerCase();
const ext = lowName.split('.').pop();
const isBlocked = fExts.some(e => ext === e) || fNames.some(n => lowName.includes(n));
if (!isBlocked) {
f._lineage = parent.lineage || [];
allFiles.push(f);
}
},
onProgress: (st) => {
const now = Date.now();
if (now - stats.lastUiTime > 150) {
updateLoadTxt(`${L.msg_batch_scanning}\n${L.str_files}: ${allFiles.length} | ${L.str_speed}: ${st.currentConcurrency}`);
stats.lastUiTime = now;
}
}
});
if (!isRunning) throw new Error('StoppedByUser');
if (allFiles.length === 0) { setLoad(false); showAlert(L.msg_batch_no_files); return; }
setLoad(false);
progressTask = FloatBarManager.create(L.msg_batch_hydrating);
const readyFiles =[];
const failedFiles = [];
const hydrateQueue = [...allFiles];
const activeTasks = new Set();
const hydrateWithRetry = async (file, maxRetries = 3) => {
if (file.web_content_link) return file;
let lastErr = null;
for (let i = 0; i < maxRetries; i++) {
if (!isRunning) return null;
try {
const detail = await apiGet(file.id);
if (detail && detail.web_content_link) return detail;
throw new Error("Link Empty");
} catch (e) {
lastErr = e;
if (i < maxRetries - 1) await sleep(1000 * (i + 1));
}
}
throw lastErr;
};
while ((hydrateQueue.length > 0 || activeTasks.size > 0) && isRunning) {
while (hydrateQueue.length > 0 && activeTasks.size < HYDRATE_LIMIT && isRunning) {
const file = hydrateQueue.pop();
const p = (async () => {
try {
const detail = await hydrateWithRetry(file);
if (detail) {
detail._lineage = file._lineage;
readyFiles.push(detail);
}
} catch (e) {
console.error(`[Hydrate Failed] ${file.name}:`, e);
failedFiles.push(file.name + " " + L.str_aria2_fetch_err);
}
})().finally(() => {
activeTasks.delete(p);
stats.hydratedCount++;
if (progressTask) progressTask.update(`${L.msg_batch_hydrating} ${stats.hydratedCount} / ${allFiles.length}`);
});
activeTasks.add(p);
}
if (activeTasks.size > 0) await Promise.race(activeTasks);
}
if (!isRunning) throw new Error('StoppedByUser');
if (readyFiles.length > 0 && isRunning) {
const BATCH_SIZE = 50;
let successCount = 0;
let rpcFatalError = false;
for (let i = 0; i < readyFiles.length; i += BATCH_SIZE) {
if (!isRunning || rpcFatalError) break;
const chunk = readyFiles.slice(i, i + BATCH_SIZE);
const sanitize = (s) => s.replace(/[\\/:*?"<>|]/g, '_').trim();
const payload = chunk.map(f => {
let relativePrefix = '';
if (f._lineage && f._lineage.length > 0) {
relativePrefix = f._lineage.map(n => sanitize(n.name)).join('/') + '/';
}
const outPath = relativePrefix + sanitize(f.name);
return {
jsonrpc: '2.0', method: 'aria2.addUri',
id: `pk_${Date.now()}_${Math.random().toString(16).slice(2)}`,
params:[`token:${ariaToken}`, [f.web_content_link], {
out: outPath,
header:[`User-Agent: ${navigator.userAgent}`, `Referer: https://mypikpak.com/`]
}]
};
});
try {
await new Promise((resolveReq, rejectReq) => {
GM_xmlhttpRequest({
method: 'POST', url: ariaUrl, data: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
onload: (r) => {
if (r.status === 200) resolveReq();
else rejectReq(new Error(`RPC ${r.status}`));
},
onerror: () => rejectReq(new Error("Network Error")),
ontimeout: () => rejectReq(new Error("Timeout"))
});
});
successCount += chunk.length;
} catch (rpcErr) {
console.error("[RPC Batch Error]", rpcErr);
chunk.forEach(f => failedFiles.push(f.name + " " + L.str_aria2_rpc_err));
if (rpcErr.message === "Network Error" || rpcErr.message === "Timeout") {
console.warn("[RPC Circuit Breaker] Aria2 disconnected. Aborting remaining batches.");
rpcFatalError = true;
const remainingFiles = readyFiles.slice(i + BATCH_SIZE);
remainingFiles.forEach(f => failedFiles.push(f.name + " " + L.str_aria2_aborted));
}
}
if (progressTask) progressTask.update(`${L.msg_aria2_sending_batch} ${successCount} / ${readyFiles.length}`);
}
if (failedFiles.length > 0) {
let failListText = failedFiles.slice(0, 10).join('\n');
if (failedFiles.length > 10) {
failListText += '\n...';
failListText += L.msg_aria2_batch_fail_log;
}
await showAlert(`${L.str_failed}: ${failedFiles.length}\n\n${failListText}`, L.title_alert);
if (failedFiles.length > 10) {
try {
const blob = new Blob([failedFiles.join('\r\n')], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const now = new Date();
const dateStr = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
a.href = url;
a.download = `${L.str_aria2_fail_file_name}_${dateStr}.txt`;
document.body.appendChild(a);
a.click();
setTimeout(() => {
if (a.parentNode) document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 1000);
} catch (exportErr) {
console.error("[Export Error] Failed to generate failure log:", exportErr);
}
}
} else {
showToast(L.msg_aria2_sent.replace('{n}', successCount));
}
}
} catch (e) {
if (e.message !== 'StoppedByUser' && e.name !== 'AbortError') {
showAlert(`${L.msg_aria2_check_fail}\n(${e.message})`);
}
} finally {
setLoad(false);
isGUISensitive = false;
if (progressTask) progressTask.destroy();
}
};
const ensureItemMap = () => {
S.itemMap.clear();
const len = S.items.length;
for (let i = 0; i < len; i++) {
const item = S.items[i];
if (item && item.id) {
S.itemMap.set(item.id, item);
}
}
};
const executeBatchDelete = async (ids, options = {}) => {
const {
silent = false,
deleteFiles = false,
isTask = false,
forceRefresh = false,
hardDelete = false,
explicitItems = []
} = options;
if (!ids || ids.length === 0) return;
const tempItemLookup = new Map();
if (S.itemMap) S.itemMap.forEach((v, k) => tempItemLookup.set(k, v));
if (explicitItems && explicitItems.length > 0) {
explicitItems.forEach(item => {
if (item && item.id) tempItemLookup.set(item.id, item);
});
}
const BATCH_SIZE = hardDelete ? 1000 : 200;
const SKIP_VERIFY = hardDelete;
const progressTask = FloatBarManager.create(L.str_deleting);
const updateFloat = progressTask.update;
S.movingSourceId = S.path[S.path.length - 1].id || 'root';
S.movingDestId = 'trash';
const allLockedIdsArray =[];
ids.forEach(id => {
S.movingIds.add(id);
allLockedIdsArray.push(id);
if (isTask && deleteFiles) {
const taskItem = tempItemLookup.get(id);
if (taskItem && taskItem.file_id) {
S.movingIds.add(taskItem.file_id);
allLockedIdsArray.push(taskItem.file_id);
}
}
});
if (S.broadcast) S.broadcast.postMessage({
type: 'LOCK_ADD',
ids: allLockedIdsArray,
src: S.movingSourceId,
dst: S.movingDestId
});
if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS();
isGUISensitive = true;
S.sel.clear();
S.lastSelIdx = -1;
S.activeId = null;
try {
const BATCH_SIZE = 200;
const totalToDelete = ids.length;
let deletedCount = 0;
const affectedParentIds = new Set();
const deletedSet = new Set();
affectedParentIds.add(S.path[S.path.length - 1].id || 'root');
for (let i = 0; i < totalToDelete; i += BATCH_SIZE) {
const chunk = ids.slice(i, i + BATCH_SIZE);
let retry = 0;
const maxRetries = 3;
let success = false;
while (retry < maxRetries && !success) {
try {
if (isTask) {
await apiCancelTask(chunk, deleteFiles);
} else {
const action = hardDelete ? 'files:batchDelete' : 'files:batchTrash';
const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/${action}`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ ids: chunk })
});
if (!res.ok) throw new Error(`API ${res.status}`);
}
success = true;
} catch (err) {
retry++;
console.warn(`[Delete] Retry ${retry}/${maxRetries}`);
updateFloat(`${L.str_deleting} (Retry ${retry})...`);
await sleep(1000 * retry);
if (retry >= maxRetries) throw err;
}
}
if (!isTask && chunk.length > 0 && !SKIP_VERIFY) {
const lastId = chunk[chunk.length - 1];
let verifyRetries = 0;
while (verifyRetries < 20) {
try {
const meta = await apiGet(lastId);
if (meta.trashed) break;
} catch (e) {
break;
}
updateFloat(`${L.str_deleting} ${deletedCount}/${totalToDelete}`);
verifyRetries++;
await sleep(500);
}
}
const chunkSet = new Set(chunk);
const chunkLockedIds =[];
chunk.forEach(id => {
S.movingIds.delete(id);
chunkLockedIds.push(id);
const it = tempItemLookup.get(id);
let physicalFolderId = null;
if (it) {
if (it.kind === 'drive#folder') {
physicalFolderId = it.id;
} else if (isTask && deleteFiles && it.file_id) {
const isFolderTask = (it.mime_type && (it.mime_type.includes('folder') || it.mime_type.includes('directory'))) ||
(it.icon_link && it.icon_link.includes('folder')) ||
(typeof globalCache !== 'undefined' && globalCache.has(it.file_id));
if (isFolderTask) {
physicalFolderId = it.file_id;
}
}
}
if (physicalFolderId && typeof globalCache !== 'undefined') {
const purgeDescendants = (fid) => {
const data = globalCache.get(fid) || (S.cache ? S.cache.get(fid) : null);
if (data) {
globalTombstoneCache.set(fid, Array.isArray(data) ?[...data] : {...data});
const list = Array.isArray(data) ? data : (data.items ||[]);
list.forEach(child => {
deletedSet.add(child.id);
S.itemMap.delete(child.id);
if (child.kind === 'drive#folder') {
purgeDescendants(child.id);
}
});
globalCache.delete(fid);
if (S.cache) S.cache.delete(fid);
}
};
purgeDescendants(physicalFolderId);
}
if (isTask && deleteFiles && it && it.file_id) {
S.movingIds.delete(it.file_id);
chunkLockedIds.push(it.file_id);
deletedSet.add(it.file_id);
if (typeof globalParentIndex !== 'undefined' && globalParentIndex.has(it.file_id)) {
const parentInfo = globalParentIndex.get(it.file_id);
if (parentInfo && parentInfo.id) {
affectedParentIds.add(parentInfo.id === 'root' ? '' : parentInfo.id);
}
}
affectedParentIds.add('root');
affectedParentIds.add('');
}
if (it && S.analyzeMap) {
const analyzeTargetId = physicalFolderId || id;
const analyzeIdsToRemove = new Set([analyzeTargetId, id]);
const queue = [analyzeTargetId, id];
while (queue.length > 0) {
const currId = queue.shift();
S.analyzeMap.forEach((node, nId) => {
if (node.parentId === currId && !analyzeIdsToRemove.has(nId)) {
analyzeIdsToRemove.add(nId);
queue.push(nId);
}
});
}
analyzeIdsToRemove.forEach(remId => {
if (S.analyzeMap.has(remId)) S.analyzeMap.delete(remId);
});
const lostSize = parseInt(it.size || 0);
if (lostSize > 0) {
let currPid = it.parent_id;
if (!currPid && isTask && typeof globalParentIndex !== 'undefined') {
const pInfo = globalParentIndex.get(it.file_id);
if (pInfo) currPid = pInfo.id;
}
let safety = 50;
while (currPid && S.analyzeMap.has(currPid) && safety > 0) {
const pNode = S.analyzeMap.get(currPid);
pNode.size = Math.max(0, pNode.size - lostSize);
currPid = pNode.parentId;
safety--;
}
}
if (S.analyzeSimGroups) {
S.analyzeSimGroups.forEach(group => {
group.ids = group.ids.filter(gid => !analyzeIdsToRemove.has(gid));
});
S.analyzeSimGroups = S.analyzeSimGroups.filter(group => group.ids.length >= 2);
if (S.analyzeSimGroups.length === 0) {
setTimeout(() => { if (UI.btnExit) UI.btnExit.click(); }, 500);
}
}
if (S.analyzeResultItems) {
S.analyzeResultItems = S.analyzeResultItems.filter(x => !analyzeIdsToRemove.has(x.id));
S.analyzeResultItems.forEach(resItem => {
if (S.analyzeMap.has(resItem.id)) {
resItem.size = S.analyzeMap.get(resItem.id).size.toString();
}
});
}
}
if (it && it.parent_id) affectedParentIds.add(it.parent_id);
S.itemMap.delete(id);
deletedSet.add(id);
});
if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: chunkLockedIds });
if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS();
S.items = S.items.filter(x => !deletedSet.has(x.id));
if (typeof pkState !== 'undefined' && pkState && pkState.lastGlobalResults) {
pkState.lastGlobalResults = pkState.lastGlobalResults.filter(x => !deletedSet.has(x.id));
}
if (typeof globalCache !== 'undefined') {
const cleanListChunk = (raw) => {
if (Array.isArray(raw)) return raw.filter(f => !deletedSet.has(f.id));
if (raw && Array.isArray(raw.items)) {
raw.items = raw.items.filter(f => !deletedSet.has(f.id));
return raw;
}
return raw;
};
for (const key of globalCache.keys()) {
globalCache.set(key, cleanListChunk(globalCache.get(key)));
}
for (const key of S.cache.keys()) {
S.cache.set(key, cleanListChunk(S.cache.get(key)));
}
}
if (!forceRefresh) {
if (S.dupMode) renderDupView();
else refresh();
}
await new Promise(r => requestAnimationFrame(r));
deletedCount += chunk.length;
updateFloat(`${L.str_deleting} ${Math.min(deletedCount, totalToDelete)} / ${totalToDelete}`);
if (deletedCount < totalToDelete) await sleep(50);
}
const cur = S.path[S.path.length - 1];
if (cur && cur.id) gmSet('pk_fmod_' + cur.id, new Date(getServerNow()).toISOString());
if (typeof globalCache !== 'undefined') {
const cleanList = (raw) => {
if (Array.isArray(raw)) return raw.filter(f => !deletedSet.has(f.id));
if (raw && Array.isArray(raw.items)) {
raw.items = raw.items.filter(f => !deletedSet.has(f.id));
return raw;
}
return raw;
};
for (const key of globalCache.keys()) {
globalCache.set(key, cleanList(globalCache.get(key)));
}
for (const key of S.cache.keys()) {
S.cache.set(key, cleanList(S.cache.get(key)));
}
affectedParentIds.forEach(pid => {
const keysToCheck = (pid === 'root' || pid === '') ? ['root', ''] : [pid];
keysToCheck.forEach(key => {
if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.add(key);
});
});
globalCache.delete('root_trashed');
S.cache.delete('root_trashed');
if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler();
}
if (window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger();
if (forceRefresh) {
await load(false, true);
} else {
updateStat();
setTimeout(() => updateQuotaUI(), 2000);
}
if (!silent && !S.dupMode) {
showToast(isTask ? L.msg_task_deleted : L.msg_del_items_done.replace('{n}', deletedCount));
}
} catch (e) {
console.error(e);
showAlert(`${L.str_error}: ${e.message}`);
} finally {
if (typeof allLockedIdsArray !== 'undefined' && allLockedIdsArray.length > 0) {
allLockedIdsArray.forEach(id => S.movingIds.delete(id));
if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: allLockedIdsArray });
} else if (ids && ids.length > 0) {
ids.forEach(id => S.movingIds.delete(id));
if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: ids });
}
if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS();
if (S.movingIds.size === 0) {
isGUISensitive = false;
}
if (progressTask) progressTask.destroy();
}
};
UI.btnDel.onclick = async () => {
if (!S.sel.size) return;
if (S.historyMode) {
const count = S.sel.size;
if (!await showConfirm(L.warn_clear_history.replace('{n}', count))) return;
const selIds = new Set(S.sel);
selIds.forEach(id => {
gmSet('pk_progress_' + id, null);
S.itemMap.delete(id);
});
S.items = S.items.filter(it => !selIds.has(it.id));
S.clearSelection();
refresh();
showToast(L.msg_clear_history_done);
return;
}
if (S.offlineMode) {
const count = S.sel.size;
const html = `
`;
if (typeof m !== 'undefined' && m.remove) m.remove();
const taskModal = showModal(html);
const modalBox = taskModal.querySelector('.pk-modal');
if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; }
const close = () => taskModal.remove();
taskModal.querySelector('#del_task_cancel').onclick = close;
const closeBtn = taskModal.querySelector('.pk-modal-close');
if(closeBtn) closeBtn.onclick = close;
taskModal.querySelector('#del_task_confirm').onclick = async () => {
const isDeleteFile = taskModal.querySelector('#del_task_files').checked;
taskModal.remove();
await executeBatchDelete(Array.from(S.sel), { isTask: true, deleteFiles: isDeleteFile, forceRefresh: true });
};
taskModal.tabIndex = 0;
setTimeout(() => taskModal.focus(), 10);
taskModal.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); e.stopPropagation();
taskModal.querySelector('#del_task_confirm').click();
}
});
return;
}
ensureItemMap();
setLoad(true);
const totalCount = S.sel.size;
updateLoadTxt(`${L.str_checking_bl}\n0 / ${totalCount}`);
await sleep(16);
const blSet = S.blSet;
const blFolderSet = S.blFolderSet;
const toDeleteIds = [];
let blacklistedCount = 0;
let processed = 0;
let lastYieldTime = performance.now();
let protectedCount = 0;
for (let id of S.sel) {
const item = S.itemMap.get(id);
if (!item) { processed++; continue; }
if (!S.trashMode && isSystemItem(item)) {
protectedCount++;
processed++;
continue;
}
const lowerName = item.name.toLowerCase().trim();
const isFolder = item.kind === 'drive#folder';
const isProtectedMode = gmGet('pk_skip_bl_on_del', true);
if (isProtectedMode && (isFolder ? blFolderSet.has(lowerName) : blSet.has(lowerName))) {
blacklistedCount++;
} else {
toDeleteIds.push(id);
}
processed++;
if ((processed % 500) === 0) {
const now = performance.now();
if (now - lastYieldTime > 12) {
updateLoadTxt(`${L.str_checking_bl}\n${processed} / ${totalCount}`);
await sleep(0);
lastYieldTime = performance.now();
}
}
}
setLoad(false);
if (blacklistedCount > 0 && toDeleteIds.length === 0) {
showAlert(L.msg_del_protected.replace('{n}', blacklistedCount));
S.sel.clear();
refresh();
updateStat();
return;
}
if (toDeleteIds.length === 0) {
showAlert(L.msg_del_none);
return;
}
if (!await showConfirm(L.warn_del.replace('{n}', toDeleteIds.length))) return;
await executeBatchDelete(toDeleteIds);
};
UI.btnDeselect.onclick = () => {
S.clearSelection();
refresh();
};
const processBlacklistAction = async (action) => {
const totalSelected = S.sel.size;
if (totalSelected === 0) return;
const isRemove = action === 'remove';
const progressTask = FloatBarManager.create(L.str_init_op);
const updateFloat = progressTask.update;
let isRunning = true;
UI.stopBtn.onclick = () => { isRunning = false; updateFloat(L.str_stopping); };
await sleep(16);
const getCleanKey = (str) => str ? str.toLowerCase().trim() : "";
const parseList = (str) => str ? str.split(/[\r\n]+/).map(s => s.trim()).filter(s => s) : [];
const fileListStr = gmGet('pk_blacklist', '');
const folderListStr = gmGet('pk_blacklist_folders', '');
let currentFiles = parseList(fileListStr);
let currentFolders = parseList(folderListStr);
const targetFileKeys = new Set();
const targetFolderKeys = new Set();
const toAddFiles = [];
const toAddFolders = [];
let processedCount = 0;
let lastYieldTime = performance.now();
const len = S.items.length;
for (let i = 0; i < len; i++) {
if (!isRunning) break;
const item = S.items[i];
if (S.sel.has(item.id)) {
const name = item.name.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim();
const key = getCleanKey(name);
if (item.kind === 'drive#folder') {
targetFolderKeys.add(key);
if (!isRemove) toAddFolders.push(name);
} else {
targetFileKeys.add(key);
if (!isRemove) toAddFiles.push(name);
}
processedCount++;
if ((processedCount & 63) === 0) {
const now = performance.now();
if (now - lastYieldTime > 12) {
updateFloat(`${L.str_analyzing} ${processedCount} / ${totalSelected}`);
await sleep(0); lastYieldTime = performance.now();
}
}
}
}
if (!isRunning) { progressTask.destroy(); showAlert(L.msg_bl_stop); return; }
updateFloat(L.str_processing);
await sleep(10);
let finalCount = 0;
let dataChanged = false;
if (isRemove) {
const oldFileCount = currentFiles.length;
const oldFolderCount = currentFolders.length;
currentFiles = currentFiles.filter(name => !targetFileKeys.has(getCleanKey(name)));
currentFolders = currentFolders.filter(name => !targetFolderKeys.has(getCleanKey(name)));
if (oldFileCount !== currentFiles.length || oldFolderCount !== currentFolders.length) {
dataChanged = true;
finalCount = (oldFileCount - currentFiles.length) + (oldFolderCount - currentFolders.length);
}
} else {
const existingFileKeys = new Set(currentFiles.map(s => getCleanKey(s)));
const existingFolderKeys = new Set(currentFolders.map(s => getCleanKey(s)));
let addedCount = 0;
for (const name of toAddFiles) {
const key = getCleanKey(name);
if (!existingFileKeys.has(key)) { currentFiles.push(name); existingFileKeys.add(key); addedCount++; dataChanged = true; }
}
for (const name of toAddFolders) {
const key = getCleanKey(name);
if (!existingFolderKeys.has(key)) { currentFolders.push(name); existingFolderKeys.add(key); addedCount++; dataChanged = true; }
}
finalCount = addedCount;
}
if (dataChanged) {
updateFloat(L.str_saving);
await sleep(10);
gmSet('pk_blacklist', currentFiles.join('\n'));
gmSet('pk_blacklist_folders', currentFolders.join('\n'));
S.updateBlCache();
renderVisible();
}
progressTask.destroy();
const msgTemplate = isRemove ? L.msg_bl_remove_done : L.msg_bl_add_done;
showToast(msgTemplate.replace('{n}', finalCount));
};
UI.btnBlacklistManager.onclick = showBlacklistModal;
if (UI.btnTrashBlacklistManager) UI.btnTrashBlacklistManager.onclick = showBlacklistModal;
if (UI.uploadWrap && UI.btnUpload) {
UI.btnUpload.onclick = (e) => {
e.stopPropagation();
const menu = UI.uploadWrap.querySelector('.pk-dropdown-menu');
const isActive = menu.style.display === 'flex';
document.querySelectorAll('.pk-dropdown-menu, .pk-select-menu').forEach(m => m.style.display = 'none');
document.querySelectorAll('.pk-dropdown-wrap').forEach(w => w.classList.remove('active'));
if (!isActive) {
menu.style.display = 'flex';
UI.uploadWrap.classList.add('active');
}
};
UI.actUpFile.onclick = (e) => {
e.stopPropagation();
UI.uploadWrap.querySelector('.pk-dropdown-menu').style.display = 'none';
UI.uploadWrap.classList.remove('active');
UI.inpFile.click();
};
UI.actUpFolder.onclick = (e) => {
e.stopPropagation();
UI.uploadWrap.querySelector('.pk-dropdown-menu').style.display = 'none';
UI.uploadWrap.classList.remove('active');
UI.inpFolder.click();
};
const updateRowUI = (task) => {
const row = document.querySelector(`.pk-row[data-id="${task.id}"]`);
if (row) {
const progBar = row.querySelector('.pk-up-prog-bar');
const progTxt = row.querySelector('.pk-up-prog-txt');
const spdTxt = row.querySelector('.pk-up-spd');
const statusCol = row.children[4];
const msgSpan = statusCol ? statusCol.querySelector('span:first-child') : null;
if (progBar) progBar.style.width = task.progress + '%';
if (progTxt) progTxt.textContent = Math.floor(task.progress) + '%';
if (msgSpan) {
msgSpan.textContent = task.message;
const activeStatus = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING'];
if (activeStatus.includes(task.status)) {
msgSpan.style.color = 'var(--pk-pri)';
if (progBar) progBar.style.backgroundColor = 'var(--pk-pri)';
}
}
if (spdTxt) spdTxt.innerHTML = task.status === 'DONE' ? '${L.lbl_done_check} ' : S.upMng.fmtSpeed(task.speed);
}
};
const resolveTask = (task) => { if (S.uploadMode) updateRowUI(task); };
S.upMng = {
limit: 3,
running: 0,
fmtSpeed: (bytesPerSec) => {
if (bytesPerSec === 0) return '0 B/s';
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
let i = 0;
while (bytesPerSec >= 1024 && i < units.length - 1) { bytesPerSec /= 1024; i++; }
return bytesPerSec.toFixed(2) + ' ' + units[i];
},
createTask: (file, parentId) => ({
id: 'up_' + Date.now() + '_' + Math.random().toString(36).substr(2),
kind: 'pk#upload',
file: file,
name: file.name,
size: file.size,
parentId: parentId,
status: 'WAITING',
progress: 0,
speed: 0,
message: L.msg_task_waiting,
_xhr: null,
_lastCalcTime: 0,
_lastCalcLoaded: 0,
_lastUiTime: 0
}),
scheduler: () => {
if (S.upMng.running >= S.upMng.limit) return;
const waiting = S.uploadTasks.find(t => t.status === 'WAITING');
if (waiting) S.upMng.start(waiting);
},
start: async (task) => {
if (S.quota && S.quota.limitRaw > 0) {
const remaining = S.quota.limitRaw - S.quota.usedRaw;
if (task.size > remaining) {
task.status = 'ERROR';
task.message = L.err_quota_exceeded ;
if (S.uploadMode) updateRowUI(task);
S.upMng.scheduler();
return;
}
}
S.upMng.running++;
task.status = 'HASHING';
task.message = L.msg_task_hashing;
task._totalUploadedBytes = 0;
let _lastPollTime = Date.now();
let _lastPollBytes = 0;
const speedTimer = setInterval(() => {
if (task.status === 'UPLOADING') {
const now = Date.now();
const timeDiff = now - _lastPollTime;
const bytesDiff = task._totalUploadedBytes - _lastPollBytes;
if (timeDiff > 0) {
task.speed = Math.max(0, (bytesDiff / timeDiff) * 1000);
}
_lastPollTime = now;
_lastPollBytes = task._totalUploadedBytes;
if (S.uploadMode) updateRowUI(task);
}
}, 1500);
const cryptoSign = async (secret, stringToSign) => {
const enc = new TextEncoder();
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
const signature = await crypto.subtle.sign("HMAC", key, enc.encode(stringToSign));
return btoa(String.fromCharCode(...new Uint8Array(signature)));
};
try {
let finalParentId = task.parentId;
if (finalParentId === 'root' || finalParentId === 'upload_root' || finalParentId === '') {
const UPLOAD_FOLDER_NAME = 'My Upload';
const cacheKey = `lock_root_${UPLOAD_FOLDER_NAME}`;
if (!S.upMng._syncLocks) S.upMng._syncLocks = new Map();
if (!S.upMng._syncLocks.has(cacheKey)) {
const createAction = (async () => {
const checkExisting = async (targetName) => {
try {
const list = await apiList('', 1000);
const found = list.find(f => f.kind === 'drive#folder' && f.name === targetName);
return found ? found.id : null;
} catch (e) { return null; }
};
let existingId = await checkExisting(UPLOAD_FOLDER_NAME);
if (existingId) return existingId;
let retry = 0;
while (retry < 3) {
try {
const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', {
method: 'POST', headers: getHeaders(),
body: JSON.stringify({ kind: "drive#folder", parent_id: "", name: UPLOAD_FOLDER_NAME })
});
if (res.ok) {
const data = await res.json();
if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.add('');
if (typeof globalCache !== 'undefined') globalCache.delete('');
if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler();
return data.file.id;
}
if (res.status === 400 || res.status === 429) {
await new Promise(r => setTimeout(r, 1000));
existingId = await checkExisting(UPLOAD_FOLDER_NAME);
if (existingId) return existingId;
}
} catch (e) {}
retry++;
}
existingId = await checkExisting(UPLOAD_FOLDER_NAME);
if (existingId) return existingId;
throw new Error("My Upload folder creation failed");
})();
S.upMng._syncLocks.set(cacheKey, createAction);
}
try {
finalParentId = await S.upMng._syncLocks.get(cacheKey);
task.parentId = finalParentId;
} catch (e) {
S.upMng._syncLocks.delete(cacheKey);
throw e;
}
}
if (task.relativeFolder) {
const folderNames = task.relativeFolder.split('/');
let currentPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || '');
if (!S.upMng._syncLocks) S.upMng._syncLocks = new Map();
for (const name of folderNames) {
const cacheKey = `lock_${currentPid}_${name}`;
if (!S.upMng._syncLocks.has(cacheKey)) {
const createAction = (async () => {
const checkExisting = async (pid, targetName) => {
try {
const list = await apiList(pid, 1000);
const found = list.find(f => f.kind === 'drive#folder' && f.name === targetName);
return found ? found.id : null;
} catch (e) { return null; }
};
let existingId = await checkExisting(currentPid, name);
if (existingId) return existingId;
let retry = 0;
while (retry < 3) {
try {
const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', {
method: 'POST', headers: getHeaders(),
body: JSON.stringify({ kind: "drive#folder", parent_id: currentPid, name: name })
});
if (res.ok) {
const data = await res.json();
return data.file.id;
}
if (res.status === 400 || res.status === 429) {
await new Promise(r => setTimeout(r, 1000));
existingId = await checkExisting(currentPid, name);
if (existingId) return existingId;
}
} catch (e) {}
retry++;
}
existingId = await checkExisting(currentPid, name);
if (existingId) return existingId;
throw new Error("Folder creation failed");
})();
S.upMng._syncLocks.set(cacheKey, createAction);
}
try {
currentPid = await S.upMng._syncLocks.get(cacheKey);
} catch (e) {
S.upMng._syncLocks.delete(cacheKey);
throw e;
}
}
finalParentId = currentPid;
}
if (task._deleted) throw new Error("Aborted");
const hash = await calcSha1(task.file);
if (task._deleted) throw new Error("Aborted");
task.status = 'UPLOADING';
task.message = L.msg_task_init_upload;
_lastPollTime = Date.now();
_lastPollBytes = 0;
const safePid = (finalParentId === 'root' || finalParentId === 'upload_root') ? '' : (finalParentId || '');
let res = null;
let createRetry = 0;
const maxCreateRetries = 5;
while (createRetry < maxCreateRetries) {
try {
res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', {
method: 'POST', headers: getHeaders(), body: JSON.stringify({
kind: "drive#file", parent_id: safePid, name: task.name, size: task.size, hash: hash, upload_type: "UPLOAD_TYPE_RESUMABLE"
})
});
if (res.status === 429) {
const waitMs = 2000 + Math.random() * 2000 * (createRetry + 1);
console.warn(`[Upload] Rate limited (429). Retrying in ${Math.round(waitMs)}ms...`);
await new Promise(r => setTimeout(r, waitMs));
createRetry++;
continue;
}
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
const errMsg = errData.error_description || `HTTP ${res.status}`;
const isQuotaExceeded = res.status === 400 && (errData.error_code === 12 || errMsg.toLowerCase().includes('quota'));
if (isQuotaExceeded) {
const quotaErr = new Error(L.err_quota_exceeded);
quotaErr.isFatal = true;
throw quotaErr;
}
if (res.status === 404 || errMsg.toLowerCase().includes('not found') || errMsg.toLowerCase().includes('invalid parent')) {
if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear();
throw new Error(L.err_parent_not_found);
}
throw new Error(errMsg);
}
break;
} catch (e) {
console.warn(`[Upload] Init request failed (${createRetry + 1}/${maxCreateRetries}):`, e.message);
if (e.isFatal) throw e;
createRetry++;
if (createRetry >= maxCreateRetries) throw e;
await new Promise(r => setTimeout(r, 1500));
}
}
const data = await res.json();
let newlyCreatedFileId = null;
if (data.file) {
if (data.file.id) {
task.file_id = data.file.id;
newlyCreatedFileId = data.file.id;
}
if (data.file.name) task.name = data.file.name;
if (data.file.thumbnail_link) task.thumbnail_link = data.file.thumbnail_link;
if (data.file.icon_link) task.icon_link = data.file.icon_link;
} else if (data.task && data.task.file_id) {
task.file_id = data.task.file_id;
newlyCreatedFileId = data.task.file_id;
if (data.task.name) task.name = data.task.name;
} else if (data.id) {
task.file_id = data.id;
newlyCreatedFileId = data.id;
if (data.name) task.name = data.name;
}
if (task._deleted) {
console.warn(`[Upload] Task ${task.id} was deleted by user during initialization. Triggering self-destruct.`);
if (newlyCreatedFileId && task._deleteFileIntent) {
try {
await fetch('https://api-drive.mypikpak.com/drive/v1/files:batchTrash', {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ ids: [newlyCreatedFileId] })
});
console.log(`[Upload] Ghost file ${newlyCreatedFileId} cleaned up.`);
} catch (err) {
console.error(`[Upload] Failed to cleanup ghost file ${newlyCreatedFileId}`, err);
}
}
throw new Error("Aborted");
}
if (data.upload_type === "UPLOAD_TYPE_URL" || data.phase === "PHASE_TYPE_COMPLETE" || (data.file && data.file.phase === "PHASE_TYPE_COMPLETE")) {
task.status = 'DONE'; task.progress = 100; task.speed = 0; task.message = L.msg_task_fast_success;
if (S.uploadMode) {
updateRowUI(task);
requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); });
}
setTimeout(() => updateQuotaUI(), 1000);
if (typeof globalCache !== 'undefined') {
const targetPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || '');
if (globalCache.has(targetPid)) {
const cacheEntry = globalCache.get(targetPid);
const list = Array.isArray(cacheEntry) ? cacheEntry : (cacheEntry.items || []);
const newFileStub = {
id: task.file_id, kind: 'drive#file', name: task.name, size: task.size,
parent_id: targetPid, mime_type: task.mime_type || '',
thumbnail_link: task.thumbnail_link || task.icon_link, icon_link: task.icon_link,
modified_time: new Date().toISOString(), hash: hash
};
if (!list.some(f => f.id === newFileStub.id)) list.push(newFileStub);
}
}
if (typeof globalDirtyFolders !== 'undefined') {
const targetPid = task.parentId === 'root' ? '' : (task.parentId || '');
globalDirtyFolders.add(targetPid);
if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler();
}
if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true;
}
else if (data.resumable && data.resumable.params) {
const p = data.resumable.params;
const ossUA = 'aliyun-sdk-js/6.23.0 Microsoft Edge 144.0.0.0 on Windows 10 64-bit';
const objectName = p.key;
const host = `https://${p.bucket}.${p.endpoint}`;
const totalSize = task.file.size;
let PART_SIZE = 5 * 1024 * 1024;
if (totalSize > 4 * 1024 * 1024 * 1024) {
PART_SIZE = 20 * 1024 * 1024;
} else if (totalSize > 1 * 1024 * 1024 * 1024) {
PART_SIZE = 10 * 1024 * 1024;
}
const partCount = Math.ceil(totalSize / PART_SIZE);
const getAuth = async (method, subResource = '', contentType = '') => {
const dateStr = new Date().toUTCString();
const headersToSign = {
'x-oss-date': dateStr,
'x-oss-security-token': p.security_token,
'x-oss-user-agent': ossUA
};
let canonicalResource = `/${p.bucket}/${objectName}`;
if (subResource) canonicalResource += `?${subResource}`;
const canonicalHeaders = Object.keys(headersToSign).sort().map(k => `${k}:${headersToSign[k]}`).join('\n') + '\n';
const stringToSign = [method, "", contentType, dateStr, canonicalHeaders + canonicalResource].join("\n");
const signature = await cryptoSign(p.access_key_secret, stringToSign);
return { date: dateStr, auth: `OSS ${p.access_key_id}:${signature}` };
};
const ossRequest = (method, query, body, contentType, onProgress) => {
return new Promise(async (resolve, reject) => {
try {
const creds = await getAuth(method, query, contentType);
const url = `${host}/${objectName.split('/').map(encodeURIComponent).join('/')}${query ? '?' + query : ''}`;
const req = GM_xmlhttpRequest({
method: method, url: url, data: body,
headers: {
'Authorization': creds.auth,
'x-oss-date': creds.date,
'x-oss-security-token': p.security_token,
'x-oss-user-agent': ossUA,
'Content-Type': contentType || ''
},
upload: { onprogress: onProgress },
onload: (res) => {
if (res.status >= 200 && res.status < 300) resolve(res);
else reject(new Error(`OSS ${method} Error: ${res.status} ${res.statusText}`));
},
onerror: (err) => reject(new Error("Network Error")),
onabort: () => reject(new Error("Aborted"))
});
if (onProgress) task._xhr = { abort: () => req.abort() };
} catch (e) { reject(e); }
});
};
task.message = L.msg_task_uploading;
if (partCount <= 1) {
await ossRequest('PUT', '', task.file, 'application/octet-stream', (pe) => {
task._totalUploadedBytes = pe.loaded;
task.progress = (pe.loaded / totalSize) * 100;
const now = Date.now();
if (now - task._lastUiTime > 100) {
if (S.uploadMode) updateRowUI(task);
task._lastUiTime = now;
}
});
task.progress = 100;
} else {
task.message = L.msg_task_init_part;
if (S.uploadMode) updateRowUI(task);
const initRes = await ossRequest('POST', 'uploads', null, '');
const initXml = new DOMParser().parseFromString(initRes.responseText, "text/xml");
const uploadId = initXml.querySelector('UploadId')?.textContent || initXml.getElementsByTagName('UploadId')[0]?.textContent;
if (!uploadId) throw new Error("Failed to get UploadId");
const parts = new Array(partCount);
const CONCURRENCY = 3;
let completedBytes = 0;
const activeParts = new Map();
const updateProgress = () => {
let activeTotal = 0;
for (const bytes of activeParts.values()) activeTotal += bytes;
const currentTotal = Math.min(totalSize, completedBytes + activeTotal);
task._totalUploadedBytes = currentTotal;
task.progress = (currentTotal / totalSize) * 100;
const now = Date.now();
if (now - task._lastUiTime > 100) {
if (S.uploadMode) updateRowUI(task);
task._lastUiTime = now;
}
};
const pool = Array.from({length: partCount}, (_, k) => k + 1);
const worker = async () => {
while (pool.length > 0) {
if (task.status === 'PAUSED' || !document.body.contains(el)) throw new Error("Aborted");
const i = pool.shift();
const startByte = (i - 1) * PART_SIZE;
const endByte = Math.min(i * PART_SIZE, totalSize);
const chunk = task.file.slice(startByte, endByte);
activeParts.set(i, 0);
const query = `partNumber=${i}&uploadId=${uploadId}`;
const partRes = await ossRequest('PUT', query, chunk, 'application/octet-stream', (pe) => {
activeParts.set(i, pe.loaded);
updateProgress();
});
const finalizedSize = chunk.size;
completedBytes += finalizedSize;
activeParts.delete(i);
updateProgress();
const etagHeader = partRes.responseHeaders.match(/etag:\s*"?([^"\r\n]+)"?/i);
const etag = etagHeader ? etagHeader[1] : null;
if (!etag) throw new Error(`Part ${i} missing ETag`);
parts[i - 1] = { partNumber: i, etag: etag };
}
};
task.message = L.msg_task_uploading_2;
if (S.uploadMode) updateRowUI(task);
const workers = Array(Math.min(partCount, CONCURRENCY)).fill(0).map(worker);
await Promise.all(workers);
if (task._deleted) throw new Error("Aborted");
const xmlBody = `${parts.map(p => `${p.partNumber} ${p.etag} `).join('')} `;
await ossRequest('POST', `uploadId=${uploadId}`, xmlBody, 'application/xml');
if (task._deleted && task.file_id && task._deleteFileIntent) {
fetch('https://api-drive.mypikpak.com/drive/v1/files:batchTrash', {
method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [task.file_id] })
}).catch(()=>{});
throw new Error("Aborted");
}
task.progress = 100;
task._totalUploadedBytes = totalSize;
setTimeout(() => updateQuotaUI(), 1500);
const tryFetchMeta = async (isRetry = false) => {
if (task._deleted) return true;
try {
const meta = await apiGet(task.file_id);
if (meta) {
if (meta.icon_link) task.icon_link = meta.icon_link;
if (meta.mime_type) task.mime_type = meta.mime_type;
if (meta.medias) task.medias = meta.medias;
const isValidThumb = meta.thumbnail_link && meta.thumbnail_link !== meta.icon_link;
if (isValidThumb) {
const testUrl = meta.thumbnail_link + (meta.thumbnail_link.includes('?') ? '&' : '?') + '_t=' + Date.now();
const isImageReady = await new Promise(resolve => {
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
img.src = testUrl;
});
if (isImageReady) {
task.thumbnail_link = testUrl;
if (typeof globalCache !== 'undefined') {
const pid = task.parentId === 'root' ? '' : (task.parentId || '');
if (globalCache.has(pid)) {
const list = globalCache.get(pid);
const target = Array.isArray(list) ? list.find(f => f.id === task.file_id) :
(list.items ? list.items.find(f => f.id === task.file_id) : null);
if (target) target.thumbnail_link = testUrl;
}
}
if (isRetry && S.uploadMode) {
requestAnimationFrame(() => {
if (typeof renderVisible === 'function') renderVisible();
});
console.log(`[Upload] Cover synced globally: ${task.name}`);
}
return true;
} else {
console.log(`[Upload] Cover CDN not ready (404). Retrying later...`);
return false;
}
}
}
} catch(e) { console.warn("[Upload] Meta fetch warning", e); }
return false;
};
(async () => {
if (await tryFetchMeta(false)) return;
for (let i = 0; i < 8; i++) {
await sleep(3500);
if (await tryFetchMeta(true)) return;
}
for (let i = 0; i < 20; i++) {
await sleep(15000);
if (await tryFetchMeta(true)) return;
}
console.log(`[Upload] Entering infinite polling mode for: ${task.name}`);
while (true) {
await sleep(60000);
if (await tryFetchMeta(true)) return;
}
})();
}
task.status = 'DONE'; task.progress = 100; task.speed = 0; task.message = L.msg_task_upload_done;
if (S.uploadMode) {
updateRowUI(task);
requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); });
}
if (typeof globalCache !== 'undefined') {
const targetPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || '');
if (globalCache.has(targetPid)) {
const cacheEntry = globalCache.get(targetPid);
const list = Array.isArray(cacheEntry) ? cacheEntry : (cacheEntry.items || []);
const newFileStub = {
id: task.file_id, kind: 'drive#file', name: task.name, size: task.size,
parent_id: targetPid, mime_type: task.mime_type || '',
thumbnail_link: task.thumbnail_link || task.icon_link, icon_link: task.icon_link,
modified_time: new Date().toISOString(), hash: hash
};
if (!list.some(f => f.id === newFileStub.id)) list.push(newFileStub);
}
}
if (typeof globalDirtyFolders !== 'undefined') {
const targetPid = task.parentId === 'root' ? '' : (task.parentId || '');
globalDirtyFolders.add(targetPid);
if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler();
}
if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true;
}
} catch (e) {
const isManualAbort = e.message === 'Aborted';
task.status = isManualAbort ? 'PAUSED' : 'ERROR';
task.message = isManualAbort ? L.msg_task_paused : (e.message || L.err_unknown);
if (S.uploadMode) updateRowUI(task);
} finally {
clearInterval(speedTimer);
task._xhr = null; S.upMng.running--;
if (S.uploadMode) { refresh(); }
S.upMng.scheduler();
}
},
pause: (task, skipRender = false) => {
if (task.status === 'UPLOADING' && task._xhr) {
task._xhr.abort();
} else if (task.status === 'WAITING') {
task.status = 'PAUSED';
task.message = L.msg_task_paused;
if (S.uploadMode && !skipRender) { refresh(); }
}
},
resume: (task, skipRender = false) => {
if (task.status === 'PAUSED' || task.status === 'ERROR') {
task.status = 'WAITING';
task.message = L.msg_task_waiting;
S.upMng.scheduler();
if (S.uploadMode && !skipRender) { refresh(); }
}
}
};
const handleUploadInput = async (files) => {
if (!files || files.length === 0) return;
if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear();
const curPath = S.path[S.path.length - 1];
const isVirtual = curPath.id.startsWith('virtual_') || curPath.id.includes('_root') || curPath.id === 'upload_root';
const safeParentId = (curPath.id && !isVirtual) ? curPath.id : '';
let addedCount = 0;
const processEntry = async (entry, parentId) => {
if (entry.isFile) {
return new Promise(resolve => {
entry.file(file => {
if (file.name.startsWith('.')) return resolve();
if (S.upMng) {
const task = S.upMng.createTask(file, parentId);
S.uploadTasks.push(task);
addedCount++;
}
resolve();
});
});
} else if (entry.isDirectory) {
}
};
const fileList = Array.from(files);
for (const file of fileList) {
if (file.name.startsWith('.')) continue;
let relativeFolder = "";
if (file.webkitRelativePath) {
const parts = file.webkitRelativePath.split('/');
if (parts.length > 1) {
parts.pop();
relativeFolder = parts.join('/');
}
}
if (S.upMng) {
const task = S.upMng.createTask(file, safeParentId);
task.relativeFolder = relativeFolder;
S.uploadTasks.unshift(task);
addedCount++;
}
}
showToast(L.msg_task_added.replace('{n}', addedCount));
if (!S.uploadMode) {
switchTab('upload');
} else {
renderVisible();
updateStat();
}
if (S.upMng) S.upMng.scheduler();
UI.inpFile.value = '';
UI.inpFolder.value = '';
};
UI.inpFile.onchange = (e) => handleUploadInput(e.target.files);
UI.inpFolder.onchange = (e) => handleUploadInput(e.target.files);
const dragMask = document.createElement('div');
dragMask.className = 'pk-drag-mask';
UI.win.appendChild(dragMask);
const parseEntries = async (entries, relPath = "") => {
for (const entry of entries) {
if (entry.isFile) {
const file = await new Promise(res => entry.file(res));
const curPath = S.path[S.path.length - 1];
const safeParentId = (curPath.id && !curPath.id.includes('_root')) ? curPath.id : '';
const task = S.upMng.createTask(file, safeParentId);
task.relativeFolder = relPath;
S.uploadTasks.unshift(task);
} else if (entry.isDirectory) {
const reader = entry.createReader();
const subEntries = await new Promise(res => reader.readEntries(res));
await parseEntries(subEntries, (relPath ? relPath + "/" : "") + entry.name);
}
}
};
const canDragUpload = () => {
return !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode &&
!S.recentMode && !S.historyMode && !S.isFlattened &&
!S.dupMode && !S.analyzeMode && !S.uploadMode;
};
let dragCounter = 0;
const handleDragGuard = (e) => {
e.preventDefault();
e.stopPropagation();
if (canDragUpload()) {
e.dataTransfer.dropEffect = 'copy';
} else {
e.dataTransfer.dropEffect = 'none';
}
};
el.addEventListener('dragenter', (e) => {
handleDragGuard(e);
if (!canDragUpload()) return;
dragCounter++;
const curPath = S.path[S.path.length - 1];
const isRoot = S.path.length === 1 && (curPath.id === '' || curPath.id === 'root');
let destHtml = "";
if (isRoot) {
const homeIcon = CONF.icons.home.replace('width="24"', 'width="16"').replace('height="24"', 'height="16"').replace('