Files
cat-catch/js/preview.js
ChuXun 53f9554f38 1
2025-10-19 20:55:27 +08:00

1035 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class FilePreview {
MAX_CONCURRENT = 5; // 最大并行生成预览数
MAX_LIST_SIZE = 128; // 最大文件列表长度
constructor() {
this.fileItems = []; // 文件列表
this.originalItems = []; // 原始文件列表
this.regexFilters = null; // 正则过滤
this.pushDebounce = null; // 添加文件防抖
this.alertTimer = null; // 提示信息定时器
this.isDragging = false; // 是否正在拖动
this.previewHLS = null; // 全屏预览视频HLS工具
this.catDownloadIsProcessing = false; // 猫抓下载器是否正在处理
this.showTitle = false; // 是否显示标题
this.deleteDuplicateFilenames = false; // 是否删除重复文件名
this._tabId = -1; // 当前标签ID
this.currentRange = null; // 当前显示范围
this.currentPage = 1; // 当前页码
// 初始化
this.init();
}
/**
* 初始化
*/
async init() {
this.parseParams(); // url解析参数
this.tab = await chrome.tabs.getCurrent(); // 获取当前标签
this.setupEventListeners(); // 设置事件监听
await this.loadFileItems(); // 载入数据
this.setupFilters(); // 设置 后缀/类型 筛选
this.setOptions(); // 设置选项
this.updateFileList(); // 渲染文件列表
this.startPreviewGeneration(); // 开始预览生成
this.setupSelectionBox(); // 框选
this.srciptList(); // 脚本列表
this.updateSrciptButton(); // 更新按钮状态
this.checkVersion(); // 检查版本
}
/**
* 解析参数
*/
parseParams() {
// 获取tabId
const params = new URL(location.href).searchParams;
this._tabId = parseInt(params.get("tabId"));
if (isNaN(this._tabId)) {
this.alert(i18n.noData, 1500);
return;
}
// 显示范围
this.currentRange = params.get("range")?.split("-").map(Number);
if (this.currentRange) {
this.currentRange = { start: this.currentRange[0], end: this.currentRange[1] || undefined };
}
// 分页
this.currentPage = params.get("page");
this.currentPage = this.currentPage ? parseInt(this.currentPage) : 1;
}
/**
* 设置按钮、键盘 、等事件监听
*/
setupEventListeners() {
// 全选
document.querySelector('#select-all').addEventListener('click', () => this.toggleSelection('all'));
// 反选
document.querySelector('#select-reverse').addEventListener('click', () => this.toggleSelection('reverse'));
// 下载选中
document.querySelector('#download-selected').addEventListener('click', () => this.downloadSelected());
// 合并下载
document.querySelector('#merge-download').addEventListener('click', () => this.mergeDownload());
// 点击非视频区域 关闭视频
document.querySelectorAll('.preview-container').forEach(container => {
container.addEventListener('click', (event) => {
if (event.target.closest('video, img')) { return; }
this.closePreview()
});
});
// 按键盘ESC关闭视频
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
this.closePreview();
return;
}
// ctrl + a
if ((event.ctrlKey || event.metaKey) && event.key === 'a' && event.target.tagName != "INPUT") {
this.toggleSelection('all');
event.preventDefault();
}
});
// 排序按钮
document.querySelectorAll('.sort-options input').forEach(input => {
input.addEventListener('change', () => this.updateFileList());
});
// 正则过滤 监听回车
document.querySelector('#regular').addEventListener('keypress', (e) => {
if (e.keyCode == 13) {
const value = e.target.value.trim();
try {
this.regexFilters = value ? new RegExp(value) : null;
} catch (error) {
this.regexFilters = null;
this.alert(i18n.noMatch);
}
this.updateFileList();
}
});
// 复制
document.querySelector('#copy-selected').addEventListener('click', () => this.copy());
// 清理数据
document.querySelector('#clear').addEventListener('click', () => this.clearData());
// 删除
document.querySelector('#delete-selected').addEventListener('click', () => this.deleteItem());
// debug
document.querySelector('#debug').addEventListener('click', () => console.dir(this.fileItems));
// 显示标题
document.querySelector('#showTitle').addEventListener('change', (e) => {
(chrome.storage.session ?? chrome.storage.local).set({ previewShowTitle: e.target.checked });
this.showTitle = e.target.checked;
this.fileItems.forEach(item => {
item.html.querySelector('.file-title').classList.toggle('hide', !e.target.checked);
});
this.updateFileList();
});
document.querySelector('#deleteDuplicateFilenames').addEventListener('change', (e) => {
(chrome.storage.session ?? chrome.storage.local).set({ previewDeleteDuplicateFilenames: e.target.checked });
this.deleteDuplicateFilenames = e.target.checked;
this.updateFileList();
});
// aria2
if (G.enableAria2Rpc) {
const aria2 = document.querySelector("#aria2-selected");
aria2.classList.remove("hide");
aria2.addEventListener('click', () => {
this.getSelectedItems().forEach(item => this.aria2(item));
});
}
// 发送
if (G.send2localManual) {
const send = document.querySelector("#send-selected");
send.classList.remove("hide");
send.addEventListener('click', () => {
this.getSelectedItems().forEach(item => this.send(item));
});
}
// 默认弹出模式
document.querySelector('#defaultPopup').addEventListener('change', (e) => {
chrome.storage.sync.set({ popup: e.target.checked });
});
}
// 全选/反选
toggleSelection(type) {
this.fileItems.forEach(item => {
item.selected = type === 'all' ? true :
type === 'reverse' ? !item.selected : false;
});
this.updateButtonStatus();
}
/**
* 获取选中元素 转为对象
*/
getSelectedItems() {
return this.fileItems.filter(item => item.selected);
}
/**
* 更新按钮状态
*/
updateButtonStatus() {
const selectedItems = this.getSelectedItems();
const hasItems = selectedItems.length > 0;
const canMerge = selectedItems.length === 2 && (
selectedItems.every(item => (item.size ?? 0) <= G.chromeLimitSize && isMedia(item)) ||
selectedItems.every(isM3U8)
);
document.querySelector('#delete-selected').disabled = !hasItems;
document.querySelector('#merge-download').disabled = !canMerge;
document.querySelector('#copy-selected').disabled = !hasItems;
document.querySelector('#download-selected').disabled = !hasItems;
document.querySelector('#aria2-selected').disabled = !hasItems;
document.querySelector('#send-selected').disabled = !hasItems;
}
/**
* 合并下载
*/
mergeDownload() {
chrome.runtime.sendMessage({
Message: "catCatchFFmpeg",
action: "openFFmpeg",
extra: i18n.waitingForMedia
});
const checkedData = this.getSelectedItems();
// 都是m3u8 自动合并并发送到ffmpeg
if (checkedData.every(data => isM3U8(data))) {
const taskId = Date.parse(new Date());
checkedData.forEach((data) => {
this.openM3U8(data, { ffmpeg: "merge", quantity: checkedData.length, taskId: taskId, autoDown: true, autoClose: true });
});
return;
}
this.catDownload(checkedData, { ffmpeg: "merge" });
}
/**
* 下载文件
* @param {Object} data 下载数据
*/
downloadItem(data) {
if (G.m3u8dl && isM3U8(data)) {
if (!data.url.startsWith("blob:")) {
const m3u8dlArg = data.m3u8dlArg ?? templates(G.m3u8dlArg, data);
const url = 'm3u8dl:' + (G.m3u8dl == 1 ? Base64.encode(m3u8dlArg) : m3u8dlArg);
if (url.length >= 2046) {
navigator.clipboard.writeText(m3u8dlArg);
alert(i18n.M3U8DLparameterLong);
return;
}
if (G.isFirefox) {
window.location.href = url;
return;
}
chrome.tabs.update({ url: url });
return;
}
}
if (G.m3u8AutoDown && isM3U8(data)) {
this.openM3U8(data, { taskId: Date.parse(new Date()), autoDown: true, autoClose: true });
return;
}
this.catDownload(data);
}
/**
* 删除文件
* @param {Object|null} data
*/
deleteItem(data = null) {
data = data ? [data] : this.getSelectedItems();
data.forEach(item => {
const index = this.originalItems.findIndex(originalItem => originalItem.requestId === item.requestId);
if (index !== -1) {
this.originalItems.splice(index, 1);
}
});
this.updateFileList();
}
/**
* 复制文件链接
* @param {Object|null} item
*/
copy(data = null) {
data = data ? [data] : this.getSelectedItems();
const url = [];
data.forEach(function (item) {
url.push(copyLink(item));
});
navigator.clipboard.writeText(url.join("\n"));
this.alert(i18n.copiedToClipboard);
}
mqtt(data) {
this.alert(i18n.sending2MQTT);
sendToMQTT(data, { alert: this.alert }).then((success) => {
// 5. 已发送消息到 MQTT 服务器
this.alert(i18n.messageSentToMQTT || "Message sent to MQTT server", 2000);
}).catch((error) => {
// 失败时显示详细错误信息
const errorMsg = error ? error.toString() : (i18n.sendFailed || "Send failed");
this.alert(errorMsg, 10000);
console.error("MQTT send error:", error);
});
}
/**
* 下载选中
*/
downloadSelected() {
const data = this.getSelectedItems();
data.length && this.catDownload(data);
}
/**
* 发送到aria2
* @param {Object} data 文件对象
*/
aria2(data) {
aria2AddUri(data, (success) => {
this.alert(success, 1000);
}, (msg) => {
this.alert(msg, 1500);
});
}
/**
* 调用第三方工具
* @param {Object} data 文件对象
*/
invoke(data) {
const url = templates(G.invokeText, data);
if (G.isFirefox) {
window.location.href = url;
} else {
chrome.tabs.update({ url: url });
}
}
/**
* 发送到远程或本地地址
* @param {Object} data 文件对象
*/
send(data) {
send2local("catch", data, this._tabId).then((success) => {
success && success?.ok && this.alert(i18n.hasSent, 1000);
}).catch((error) => {
error ? this.alert(error, 1500) : this.alert(i18n.sendFailed, 1500);
});
}
/**
* 更新文件列表
*/
updateFileList() {
this.fileItems = [...this.originalItems];
// 删除重复的文件名
if (this.deleteDuplicateFilenames) {
const uniqueNames = new Set();
this.fileItems = this.fileItems.filter(item => {
if (uniqueNames.has(item.name)) {
return false;
}
uniqueNames.add(item.name);
return true;
});
}
// 获取勾选扩展
const selectedExts = Array.from(document.querySelectorAll('input[name="ext"]:checked'))
.map(checkbox => checkbox.value);
//勾选类型
const selectedTyps = Array.from(document.querySelectorAll('input[name="type"]:checked'))
.map(checkbox => checkbox.value);
// 应用 正则 and 扩展过滤
this.fileItems = this.fileItems.filter(item =>
selectedExts.includes(item.ext) && selectedTyps.includes(item.type) &&
(!this.regexFilters || this.regexFilters.test(item.url))
);
// 排序
const order = document.querySelector('input[name="sortOrder"]:checked').value === 'asc' ? 1 : -1;
const field = document.querySelector('input[name="sortField"]:checked').value;
if (field === 'name') {
this.fileItems.sort((a, b) => order * a[field].localeCompare(b[field]));
} else if (field == 'duration') {
this.fileItems.sort((a, b) => {
// If both have invalid duration (-1), maintain original order
if (a[field] === -1 && b[field] === -1) return 0;
// Items with duration -1 always go to the end
if (a[field] === -1) return 1;
if (b[field] === -1) return -1;
// Normal comparison for valid durations
return order * (a[field] - b[field]);
});
} else {
this.fileItems.sort((a, b) => order * (a[field] - b[field]));
}
// 更新显示
this.renderFileItems();
this.updateButtonStatus();
}
/**
* 创建文件元素
* @param {Object} item 数据
* @param {Number} index 索引
*/
createFileElement(item, index) {
if (item.html) { return item.html; }
item.html = document.createElement('div');
item.html.setAttribute('data-index', index);
item.html.className = 'file-item';
item.html.innerHTML = `
<div class="file-title ${this.showTitle ? "" : "hide"}">${item.title}</div>
<div class="file-name">${item.name}</div>
<div class="preview-container">
<img src="${item.favIconUrl || 'img/icon.png'}" class="preview-image icon">
</div>
<div class="bottom-row">
<img src="img/regex.png" class="${item.isRegex ? "" : "hide"}" title="${i18n.regexTitle}" style="width: 23px;">
<div class="file-info">${item.ext}</div>
</div>
<div class="actions">
<img src="img/copy.png" class="icon copy" title="${i18n.copy}">
<img src="img/delete.svg" class="icon delete" title="${i18n.delete}">
<img src="img/download.svg" class="icon download" title="${i18n.download}">
<img src="img/mqtt.svg" class="icon mqtt ${G.mqttEnable ? "" : "hide"}" title="${i18n.send2MQTT}">
</div>`;
// 添加文件信息
if (item.size && item.size >= 1024) {
item.html.querySelector('.file-info').textContent += ` / ${byteToSize(item.size)}`;
}
item.html.addEventListener('click', (event) => {
if (event.target.closest('.icon') || this.isDragging) { return; }
item.selected = !item.selected;
this.updateButtonStatus();
});
// 复制图标
item.html.querySelector('.copy').addEventListener('click', () => this.copy(item));
// 删除图标
item.html.querySelector('.delete').addEventListener('click', () => this.deleteItem(item));
// 下载图标
item.html.querySelector('.download').addEventListener('click', () => this.downloadItem(item));
// MQTT图标
item.html.querySelector('.mqtt').addEventListener('click', () => this.mqtt(item));
// 选中状态 添加对应class
item._selected = false;
Object.defineProperty(item, "selected", {
get: () => item._selected,
set(newValue) {
item._selected = newValue;
item.html.classList.toggle('selected', newValue);
}
});
// 图片预览
if (isPicture(item)) {
const previewImage = item.html.querySelector('.preview-image');
previewImage.onload = () => {
item.html.querySelector('.file-info').textContent += ` / ${previewImage.naturalWidth}*${previewImage.naturalHeight}`;
};
previewImage.src = item.url;
// 点击预览图片
previewImage.addEventListener('click', (event) => {
event.stopPropagation();
const container = document.querySelector('.image-container');
container.querySelector('img').src = item.url;
container.classList.remove('hide');
});
}
// 添加一些图标 和 事件
const actions = item.html.querySelector('.actions');
if (isM3U8(item)) {
const m3u8 = document.createElement('img');
m3u8.src = 'img/parsing.png';
m3u8.className = 'icon m3u8';
m3u8.title = i18n.parser;
m3u8.addEventListener('click', () => this.openM3U8(item));
actions.appendChild(m3u8);
}
// 发送到aria2
if (G.enableAria2Rpc) {
const aria2 = document.createElement('img');
aria2.src = 'img/aria2.png';
aria2.className = 'icon aria2';
aria2.title = "aria2";
aria2.addEventListener('click', () => this.aria2(item));
actions.appendChild(aria2);
}
// 调用第三方工具
if (G.invoke) {
const invoke = document.createElement('img');
invoke.src = 'img/invoke.svg';
invoke.className = 'icon invoke';
invoke.title = i18n.invoke;
invoke.addEventListener('click', () => this.invoke(item));
actions.appendChild(invoke);
}
// 发送到远程或本地地址
if (G.send2localManual) {
const send = document.createElement('img');
send.src = 'img/send.svg';
send.className = 'icon send';
send.title = i18n.send2local;
send.addEventListener('click', () => this.send(item));
actions.appendChild(send);
}
return item.html;
}
/**
* 设置复选框
* @param {String} filterId 过滤器的DOM ID
* @param {Array} items 数据项
* @param {String} property 数据项的属性
*/
setupFilters(filterId, property) {
if (arguments.length === 0) {
this.setupFilters('extensionFilters', 'ext');
this.setupFilters('typeFilters', 'type');
return;
}
const uniqueValues = [...new Set(this.originalItems.map(item => item[property]))];
const filterContainer = document.querySelector(`#${filterId}`);
uniqueValues.forEach(value => {
if (filterContainer.querySelector(`input[value="${value}"]`)) return;
const label = document.createElement('label');
label.innerHTML = `<input type="checkbox" name="${property}" value="${value}" checked>${value == 'Unknown' ? value : value.toLowerCase()}`;
label.querySelector('input').addEventListener('click', () => this.updateFileList());
filterContainer.appendChild(label);
});
}
setOptions() {
if (G.previewShowTitle) {
document.querySelector('#showTitle').checked = true;
this.showTitle = true;
}
if (G.previewDeleteDuplicateFilenames) {
document.querySelector('#deleteDuplicateFilenames').checked = true;
this.deleteDuplicateFilenames = true;
}
}
/**
* 渲染文件列表
*/
renderFileItems() {
const fragment = document.createDocumentFragment();
this.fileItems.forEach((item, index) => {
fragment.appendChild(this.createFileElement(item, index));
});
const container = document.querySelector('#file-container');
container.innerHTML = '';
container.appendChild(fragment);
}
/**
* 修整数据
* @param {Object} data 数据
*/
trimData(data) {
data._title = data.title;
data.title = stringModify(data.title);
data.name = isEmpty(data.name) ? data.title + '.' + data.ext : decodeURIComponent(stringModify(data.name));
data.downFileName = G.TitleName ? templates(G.downFileName, data) : data.name;
data.downFileName = filterFileName(data.downFileName);
if (isEmpty(data.downFileName)) {
data.downFileName = data.name;
}
data.ext = data.ext ? data.ext : 'Unknown';
data.type = data.type ? data.type : 'Unknown';
data.duration = data.duration ? data.duration : -1;
return data;
}
/**
* 载入数据
*/
async loadFileItems() {
this.originalItems = await chrome.runtime.sendMessage(chrome.runtime.id, { Message: "getData", tabId: this._tabId }) || [];
if (this.originalItems.length == 0) {
this.alert(i18n.noData, 1500);
return;
}
// 设置分页
if (this.originalItems.length > this.MAX_LIST_SIZE) {
this.setupPage(this.originalItems.length);
this.originalItems = this.originalItems.slice((this.currentPage - 1) * this.MAX_LIST_SIZE, this.currentPage * this.MAX_LIST_SIZE);
}
// 显示范围
if (this.currentRange) {
this.originalItems = this.originalItems.slice(this.currentRange.start, this.currentRange.end ?? this.originalItems.length);
}
this.originalItems = this.originalItems.map(data => this.trimData(data));
this.fileItems = [...this.originalItems];
setHeaders(this.fileItems, null, this.tab.id);
}
/**
* 关闭预览视频
*/
closePreview() {
document.querySelector('.play-container').classList.add('hide');
const video = document.querySelector('#video-player');
video.pause();
video.src = '';
this.previewHLS && this.previewHLS.destroy();
const imageContainer = document.querySelector('.image-container');
imageContainer.classList.add('hide');
}
/**
* 播放文件
* @param {Object} item
*/
playItem(item) {
const video = document.querySelector('#video-player');
const container = document.querySelector('.play-container');
if (isM3U8(item)) {
this.previewHLS = new Hls({ enableWorker: false });
this.previewHLS.loadSource(item.url);
this.previewHLS.attachMedia(video);
this.previewHLS.on(Hls.Events.ERROR, (event, data) => {
this.previewHLS.stopLoad();
this.previewHLS.destroy();
});
this.previewHLS.on(Hls.Events.MEDIA_ATTACHED, () => {
container.classList.remove('hide');
video.play();
});
} else {
video.src = item.url;
container.classList.remove('hide');
video.play();
}
}
/**
* 生成预览video标签
* @param {Object} item 数据
*/
async generatePreview(item) {
return new Promise((resolve, reject) => {
const getVideoInfo = (video) => {
video.pause();
videoInfo.height = video.videoHeight;
videoInfo.width = video.videoWidth;
if (video.duration && video.duration != Infinity) {
videoInfo._duration = video.duration;
videoInfo.duration = secToTime(video.duration);
}
// 判断是否为音频文件
if (item.type?.startsWith('audio/') || ['mp3', 'wav', 'm4a', 'aac', 'ogg'].includes(item.ext)) {
videoInfo.type = 'audio';
videoInfo.video = null;
videoInfo.height = 0;
videoInfo.width = 0;
}
resolve(videoInfo);
};
const video = document.createElement('video');
video.muted = true;
video.playsInline = true;
video.loop = true;
video.preload = 'metadata';
video.addEventListener('loadedmetadata', () => {
video.currentTime = 0.5;
if (video.videoHeight && video.videoWidth) {
getVideoInfo(video);
} else {
setTimeout(getVideoInfo, 500, video);
}
});
let hls = null;
const cleanup = () => {
if (hls) hls.destroy();
video.remove();
};
const videoInfo = { video: video, height: 0, width: 0, duration: 0, type: 'video' };
// 处理HLS视频
if (isM3U8(item)) {
if (!Hls.isSupported()) {
return reject(new Error('HLS is not supported'));
}
hls = new Hls({ enableWorker: false });
hls.loadSource(item.url);
hls.attachMedia(video);
videoInfo.type = 'hlsVideo';
hls.on(Hls.Events.ERROR, (event, data) => {
cleanup();
reject(data);
});
}
// 处理普通视频
else {
video.src = item.url;
video.addEventListener('error', () => {
cleanup();
reject(new Error('Video load failed'));
});
}
});
}
/**
* 设置预览video标签到对应位置 以及添加鼠标悬停事件
* @param {Object} item data
*/
setPerviewVideo(item) {
// 视频放入预览容器 增加class
const container = item.html.querySelector('.preview-container');
container.classList.add('video-preview');
if (item.previewVideo.type == 'audio' || (item.previewVideo.width == 0 && item.previewVideo.height == 0)) {
// 如果是音频文件,使用音乐图标
container.innerHTML = '<img src="img/music.svg" class="preview-music icon" />';
} else {
if (container.querySelector('video')) return;
// 如果是视频文件,使用视频预览
container.appendChild(item.previewVideo.video);
// 鼠标悬停事件
item.html.addEventListener('mouseenter', () => {
item.previewVideo.video.play();
});
item.html.addEventListener('mouseleave', () => {
item.previewVideo.video.pause();
});
// 填写视频信息
item.html.querySelector('.file-info').textContent += ` / ${item.previewVideo.width}*${item.previewVideo.height}`;
}
// 点击视频 全屏播放 阻止冒泡 以免选中
container.querySelectorAll("video, .preview-music").forEach((element) => {
element.addEventListener('click', (event) => {
event.stopPropagation();
this.playItem(item);
});
});
// 填写时长
if (item.previewVideo.duration) {
item.duration = item.previewVideo._duration;
item.html.querySelector('.file-info').textContent += ` / ${item.previewVideo.duration}`;
}
// 删除 preview-image
item.html.querySelector('.preview-image')?.remove();
}
/**
* 多线程 开始生成预览video标签
*/
async startPreviewGeneration() {
const pendingItems = this.fileItems.filter(item =>
!item.previewVideo &&
!item.previewVideoError &&
(item.type?.startsWith('video/') ||
item.type?.startsWith('audio/') ||
isMediaExt(item.ext) ||
isM3U8(item))
);
const processItem = async () => {
while (pendingItems.length) {
const item = pendingItems.shift();
if (!item?.url) continue;
try {
item.previewVideo = await this.generatePreview(item);
this.setPerviewVideo(item);
// console.log('Preview generated for:', item.url);
} catch (e) {
item.previewVideoError = true;
// console.warn('Failed to generate preview for:', item.url, e);
}
}
};
await Promise.all(Array(this.MAX_CONCURRENT).fill().map(processItem));
}
/**
* 猫抓下载器
* @param {Object} data
* @param {Object} extra
*/
catDownload(data, extra = {}) {
// 防止连续多次提交
if (this.catDownloadIsProcessing) {
setTimeout(() => {
catDownload(data, extra);
}, 233);
return;
}
this.catDownloadIsProcessing = true;
if (!Array.isArray(data)) { data = [data]; }
// 储存数据到临时变量 提高检索速度
localStorage.setItem('downloadData', JSON.stringify(data));
// 如果大于2G 询问是否使用流式下载
if (!extra.ffmpeg && !G.downStream && Math.max(...data.map(item => item._size)) > G.chromeLimitSize && confirm(i18n("fileTooLargeStream", ["2G"]))) {
extra.downStream = 1;
}
// 发送消息给下载器
chrome.runtime.sendMessage(chrome.runtime.id, { Message: "catDownload", data: data }, (message) => {
// 不存在下载器或者下载器出错 新建一个下载器
if (chrome.runtime.lastError || !message || message.message != "OK") {
chrome.tabs.create({
url: `/downloader.html?${new URLSearchParams({
requestId: data.map(item => item.requestId).join(","),
...extra
})}`,
index: this.tab.index + 1,
active: !G.downActive
}, (tab) => {
const listener = (tabId, info) => {
if (tab && tabId === tab.id && info.status === "complete") {
chrome.tabs.onUpdated.removeListener(listener);
this.catDownloadIsProcessing = false;
}
};
chrome.tabs.onUpdated.addListener(listener);
});
return;
}
this.catDownloadIsProcessing = false;
});
}
/**
* 设置框选
*/
setupSelectionBox() {
const selectionBox = document.getElementById('selection-box');
const container = document.querySelector('body');
// let isDragging = false;
let isSelecting = false;
const startPoint = { x: 0, y: 0 };
container.addEventListener('mousedown', (e) => {
if (e.button == 2) return;
// 限定起始位范围
if (e.target.closest('.icon, .preview-image, video, button, input')) return;
isSelecting = true;
startPoint.x = e.pageX;
startPoint.y = e.pageY;
});
document.addEventListener('mousemove', (e) => {
if (!isSelecting) return;
const currentPoint = {
x: e.pageX,
y: e.pageY
};
// 计算移动距离,只有真正拖动时才显示选择框
const moveDistance = Math.sqrt(
Math.pow(currentPoint.x - startPoint.x, 2) +
Math.pow(currentPoint.y - startPoint.y, 2)
);
// 如果移动距离大于5像素认为是拖动而不是点击
if (!this.isDragging && moveDistance > 5) {
this.isDragging = true;
selectionBox.style.display = 'block';
}
if (!this.isDragging) return;
// 计算选择框的位置和大小
const left = Math.min(startPoint.x, currentPoint.x);
const top = Math.min(startPoint.y, currentPoint.y);
const width = Math.abs(currentPoint.x - startPoint.x);
const height = Math.abs(currentPoint.y - startPoint.y);
selectionBox.style.left = `${left}px`;
selectionBox.style.top = `${top}px`;
selectionBox.style.width = `${width}px`;
selectionBox.style.height = `${height}px`;
// 检查每个file-item是否在选择框内
this.fileItems.forEach(item => {
const rect = item.html.getBoundingClientRect();
if (rect.left + window.scrollX < left + width &&
rect.left + rect.width + window.scrollX > left &&
rect.top + window.scrollY < top + height &&
rect.top + rect.height + window.scrollY > top) {
item.selected = true;
} else {
item.selected = false;
}
});
});
document.addEventListener('mouseup', (e) => {
if (e.button == 2 || !isSelecting) return;
isSelecting = false;
setTimeout(() => { this.isDragging = false; }, 10);
selectionBox.style.display = 'none';
selectionBox.style.width = '0';
selectionBox.style.height = '0';
this.updateButtonStatus();
});
}
/**
* 打开m3u8解析器
* @param {Object} data
* @param {Object} options
*/
openM3U8(data, options = {}) {
const url = `/m3u8.html?${new URLSearchParams({
url: data.url,
title: data.title,
filename: data.downFileName,
tabid: data.tabId == -1 ? this._tabId : data.tabId,
initiator: data.initiator,
requestHeaders: data.requestHeaders ? JSON.stringify(data.requestHeaders) : undefined,
...Object.fromEntries(Object.entries(options).map(([key, value]) => [key, typeof value === 'boolean' ? 1 : value])),
})}`
chrome.tabs.create({ url: url, index: this.tab.index + 1, active: !options.autoDown });
}
/**
* 提示信息
* @param {String} message 提示信息
* @param {Number} sec 显示时间
*/
alert = (message, sec = 1000) => {
let toast = document.querySelector('.alert-box');
if (!toast) {
toast = document.createElement('div');
toast.className = 'alert-box';
document.body.appendChild(toast);
}
// 显示期间新消息顶替
clearTimeout(this.alertTimer);
toast.classList.remove('active');
toast.textContent = message;
toast.classList.add('active');
this.alertTimer = setTimeout(() => {
toast.classList.remove('active');
}, sec);
}
/**
* 添加文件
* @param {Object} data
*/
push(data) {
if (this.originalItems.length >= this.MAX_LIST_SIZE) {
return;
}
setHeaders(data, null, this.tab.id);
this.originalItems.push(this.trimData(data));
// this.startPreviewGeneration(); 防抖
clearTimeout(this.pushDebounce);
this.pushDebounce = setTimeout(() => {
this.setupFilters();
this.updateFileList();
this.startPreviewGeneration();
}, 1000);
}
/**
* 设置分页
* @param {Number} fileLength 文件数
*/
setupPage(fileLength) {
const url = new URL(location.href);
document.querySelector('.pagination').classList.remove('hide'); // 显示页面组件
const maxPage = Math.ceil(fileLength / this.MAX_LIST_SIZE); // 最大页数
// 设置页码
document.querySelector('.page-numbers').textContent = `${this.currentPage} / ${maxPage}`;
// 上一页按钮
if (this.currentPage != 1) {
const prev = document.querySelector('#prev-page');
prev.disabled = false;
prev.addEventListener('click', () => {
url.searchParams.set('page', this.currentPage - 1);
chrome.tabs.update({ url: url.toString() });
});
}
// 下一页按钮
if (this.currentPage != maxPage) {
const next = document.querySelector('#next-page');
next.disabled = false;
next.addEventListener('click', () => {
url.searchParams.set('page', this.currentPage + 1);
chrome.tabs.update({ url: url.toString() });
});
}
}
// 清理数据
clearData() {
chrome.runtime.sendMessage({ Message: "clearData", type: true, tabId: this._tabId });
chrome.runtime.sendMessage({ Message: "ClearIcon", type: true, tabId: this._tabId });
this.originalItems = [];
document.querySelector('#extensionFilters').innerHTML = '';
this.updateFileList();
}
// 脚本
srciptList() {
document.querySelectorAll("[type='script']").forEach((script) => {
script.addEventListener('click', (e) => {
chrome.runtime.sendMessage({ Message: "script", tabId: this._tabId, script: e.target.id + ".js" }, () => {
G.autoClearMode > 0 && this.clearData();
this.updateSrciptButton();
});
});
});
}
// 更新脚本按钮状态
updateSrciptButton() {
chrome.runtime.sendMessage({ Message: "getButtonState", tabId: this._tabId }, (state) => {
Object.entries(state).forEach(([key, value]) => {
const element = document.getElementById(key);
if (!element) return;
const script = G.scriptList.get(`${key}.js`);
if (script) {
element.textContent = value ? script.off : script.name;
}
});
});
document.querySelector('#defaultPopup').checked = G.popup;
}
// 版本检测
checkVersion() {
if (G.version < 102 || (G.isFirefox && G.version < 128)) {
document.querySelectorAll("[type='script']").forEach(script => script.style.display = 'none');
}
}
}
awaitG(() => {
loadCSS();
// 实例化 FilePreview
const filePreview = new FilePreview();
// 监听新数据
chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {
if (!Message.Message || !Message.data || !filePreview || Message.data.tabId != filePreview._tabId) { return; }
// 添加资源
if (Message.Message == "popupAddData") {
filePreview.push(Message.data);
return;
}
});
});