Files
PYAS/Interface/static/js/main.js
T
2026-05-04 23:27:50 +08:00

1175 lines
56 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
let currentLang = "english_switch";
let isScanning = false;
const { dict, langMap } = window.AppI18n;
const themes = {
"white_switch": { "--bg-window": "245, 245, 247", "--bg-nav": "255, 255, 255", "--bg-panel": "255, 255, 255", "--bg-hover": "rgba(0,0,0,0.05)", "--text-primary": "29, 29, 31", "--text-secondary": "134, 134, 139", "--border-color":"210, 210, 215", "--accent-color":"0, 122, 255", "--accent-alpha":"rgba(0, 122, 255, 0.1)", "--accent-shadow":"rgba(0, 122, 255, 0.2)", "--accent-hover":"rgba(0, 122, 255, 0.3)" },
"black_switch": { "--bg-window": "0, 0, 0", "--bg-nav": "28, 28, 30", "--bg-panel": "28, 28, 30", "--bg-hover": "rgba(255,255,255,0.1)", "--text-primary": "245, 245, 247", "--text-secondary": "134, 134, 139", "--border-color":"44, 44, 46", "--accent-color":"10, 132, 255", "--accent-alpha":"rgba(10, 132, 255, 0.1)", "--accent-shadow":"rgba(10, 132, 255, 0.2)", "--accent-hover":"rgba(10, 132, 255, 0.3)" },
"red_switch": { "--bg-window": "253, 246, 246", "--bg-nav": "255, 240, 240", "--bg-panel": "255, 255, 255", "--bg-hover": "rgba(255,59,48,0.05)", "--text-primary": "29, 29, 31", "--text-secondary": "134, 134, 139", "--border-color":"250, 220, 220", "--accent-color":"255, 59, 48", "--accent-alpha":"rgba(255, 59, 48, 0.1)", "--accent-shadow":"rgba(255, 59, 48, 0.2)", "--accent-hover":"rgba(255, 59, 48, 0.3)" },
"yellow_switch": { "--bg-window": "254, 251, 243", "--bg-nav": "255, 248, 230", "--bg-panel": "255, 255, 255", "--bg-hover": "rgba(255,149,0,0.05)", "--text-primary": "29, 29, 31", "--text-secondary": "134, 134, 139", "--border-color":"250, 240, 215", "--accent-color":"255, 149, 0", "--accent-alpha":"rgba(255, 149, 0, 0.1)", "--accent-shadow":"rgba(255, 149, 0, 0.2)", "--accent-hover":"rgba(255, 149, 0, 0.3)" },
"green_switch": { "--bg-window": "246, 253, 248", "--bg-nav": "235, 250, 240", "--bg-panel": "255, 255, 255", "--bg-hover": "rgba(52,199,89,0.05)", "--text-primary": "29, 29, 31", "--text-secondary": "134, 134, 139", "--border-color":"220, 240, 225", "--accent-color":"52, 199, 89", "--accent-alpha":"rgba(52, 199, 89, 0.1)", "--accent-shadow":"rgba(52, 199, 89, 0.2)", "--accent-hover":"rgba(52, 199, 89, 0.3)" },
"blue_switch": { "--bg-window": "246, 249, 253", "--bg-nav": "235, 244, 255", "--bg-panel": "255, 255, 255", "--bg-hover": "rgba(0,122,255,0.05)", "--text-primary": "29, 29, 31", "--text-secondary": "134, 134, 139", "--border-color":"220, 230, 250", "--accent-color":"0, 122, 255", "--accent-alpha":"rgba(0, 122, 255, 0.1)", "--accent-shadow":"rgba(0, 122, 255, 0.2)", "--accent-hover":"rgba(0, 122, 255, 0.3)" }
};
const cols = {
log: [
{key: 'time_str', label: '日期', flex: 1.5},
{key: 'level', label: '類型', flex: 0.8},
{key: 'action', label: '功能', flex: 1.2},
{key: 'source', label: '路徑', flex: 3}
],
process: [
{key: 'name', label: '名稱', flex: 1.5},
{key: 'pid', label: 'PID', flex: 0.5},
{key: 'path', label: '路徑', flex: 3}
],
virus: [
{key: 'label', label: '類型', flex: 1},
{key: 'path', label: '路徑', flex: 3}
],
pathOnly: [
{key: 'path', label: '路徑', flex: 1}
],
junk: [
{key: 'path', label: '路徑', flex: 3},
{key: 'sizeStr', label: '大小', flex: 1}
],
popup: [
{key: 'exe', label: '程式', flex: 1},
{key: 'class', label: '類別', flex: 1},
{key: 'title', label: '標題', flex: 2}
],
repair: [
{key: 'display', label: '修復項目', flex: 1}
]
};
const getMsg = (key) => (dict[currentLang] || dict["english_switch"])[key] || key;
function initCustomSelects() {
document.querySelectorAll('select.modern-select').forEach(select => {
select.style.display = 'none';
const wrapper = document.createElement('div');
wrapper.className = 'custom-select-wrapper';
const trigger = document.createElement('div');
trigger.className = 'custom-select-trigger';
const triggerText = document.createElement('span');
triggerText.className = 'custom-select-text';
const selectedOpt = select.options[select.selectedIndex];
if(selectedOpt) {
triggerText.textContent = selectedOpt.textContent;
if(selectedOpt.hasAttribute('data-i18n')) triggerText.setAttribute('data-i18n', '');
if(selectedOpt.hasAttribute('data-origin-text')) triggerText.dataset.originText = selectedOpt.dataset.originText;
}
trigger.appendChild(triggerText);
const icon = document.createElement('div');
icon.className = 'custom-select-icon';
icon.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`;
trigger.appendChild(icon);
const optionsContainer = document.createElement('ul');
optionsContainer.className = 'custom-select-options';
const phantom = document.createElement('div');
phantom.className = 'custom-select-phantom';
phantom.setAttribute('aria-hidden', 'true');
Array.from(select.options).forEach(option => {
if(option.disabled) return;
const li = document.createElement('li');
li.className = 'custom-select-option';
li.textContent = option.textContent;
li.dataset.value = option.value;
if(option.hasAttribute('data-i18n')) li.setAttribute('data-i18n', '');
if(option.hasAttribute('data-origin-text')) li.dataset.originText = option.dataset.originText;
if(option.selected) li.classList.add('selected');
li.addEventListener('click', (e) => {
e.stopPropagation();
select.value = option.value;
select.dispatchEvent(new Event('change'));
triggerText.textContent = li.textContent;
if(li.hasAttribute('data-origin-text')) {
triggerText.dataset.originText = li.dataset.originText;
}
wrapper.classList.remove('open');
optionsContainer.querySelectorAll('.custom-select-option').forEach(el => el.classList.remove('selected'));
li.classList.add('selected');
});
optionsContainer.appendChild(li);
const phantomSpan = document.createElement('span');
phantomSpan.textContent = option.textContent;
if(option.hasAttribute('data-i18n')) phantomSpan.setAttribute('data-i18n', '');
if(option.hasAttribute('data-origin-text')) phantomSpan.dataset.originText = option.dataset.originText;
phantom.appendChild(phantomSpan);
});
wrapper.appendChild(phantom);
wrapper.appendChild(trigger);
wrapper.appendChild(optionsContainer);
select.parentNode.insertBefore(wrapper, select.nextSibling);
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = wrapper.classList.contains('open');
document.querySelectorAll('.custom-select-wrapper').forEach(w => w.classList.remove('open'));
if (!isOpen) wrapper.classList.add('open');
});
});
document.addEventListener('click', () => {
document.querySelectorAll('.custom-select-wrapper').forEach(w => w.classList.remove('open'));
});
}
initCustomSelects();
document.querySelectorAll("[data-i18n]").forEach(el => { el.dataset.originText = el.textContent.trim(); });
document.querySelectorAll("[data-i18n-placeholder]").forEach(el => { el.dataset.originPlaceholder = el.getAttribute('placeholder'); });
document.querySelectorAll("option[data-i18n]").forEach(el => { el.dataset.originText = el.textContent.trim(); });
const translateText = (lang) => {
currentLang = lang;
const currentDict = dict[lang] || dict["english_switch"];
document.querySelectorAll("[data-i18n]").forEach(el => {
const originText = el.dataset.originText;
el.textContent = currentDict[originText] || originText;
});
document.querySelectorAll("[data-i18n-placeholder]").forEach(el => {
const originText = el.dataset.originPlaceholder;
el.setAttribute('placeholder', currentDict[originText] || originText);
});
document.querySelectorAll('.manage-widget, .virus-widget').forEach(widget => {
if (widget.renderData) widget.renderData();
});
document.documentElement.lang = langMap[currentLang] || "en";
const progressText = document.getElementById('progress_text');
if (progressText && progressText.dataset.dynamicMsg) {
try {
const msgs = JSON.parse(progressText.dataset.dynamicMsg);
if (msgs[lang]) {
progressText.textContent = msgs[lang];
}
} catch (e) {}
}
};
const applyTheme = (theme) => {
const themeVars = themes[theme];
if (themeVars) {
for (const [key, value] of Object.entries(themeVars)) {
document.documentElement.style.setProperty(key, value.startsWith("rgba") ? value : `rgb(${value})`);
}
}
};
function updateCustomSelectUI(selectId, val) {
const select = document.getElementById(selectId);
if (!select) return;
select.value = val;
const wrapper = select.nextElementSibling;
if (wrapper && wrapper.classList.contains('custom-select-wrapper')) {
const triggerText = wrapper.querySelector('.custom-select-text');
const selectedOpt = select.options[select.selectedIndex];
if(selectedOpt) {
triggerText.textContent = selectedOpt.textContent;
if(selectedOpt.hasAttribute('data-origin-text')) {
triggerText.dataset.originText = selectedOpt.dataset.originText;
}
}
wrapper.querySelectorAll('.custom-select-option').forEach(el => {
el.classList.toggle('selected', el.dataset.value === val);
});
}
}
function renderDataGrid(containerSelector, dataList, columns, valKey, defaultChecked = false) {
const widget = document.querySelector(`${containerSelector} .manage-widget`) || document.querySelector(`${containerSelector} .virus-widget`);
const listUl = document.querySelector(`${containerSelector} .manage-list`) || document.querySelector(`${containerSelector} .virus-list`);
const searchInput = document.querySelector(`${containerSelector} .search-box input`);
if (!widget || !listUl) return;
if (!widget.gridState) {
let containerWidth = widget.clientWidth || 800;
let availableWidth = Math.max(containerWidth - 60 - (columns.length * 16), 400);
let totalFlex = columns.reduce((sum, c) => sum + (c.flex || 1), 0);
widget.gridState = {
sortKey: columns[0]?.key || '',
sortAsc: true,
filterText: "",
colWidths: columns.map(c => Math.max(((c.flex || 1) / totalFlex) * availableWidth, 60))
};
widget.gridState.colWidths.forEach((w, idx) => {
widget.style.setProperty(`--col-${idx}`, `${w}px`);
});
listUl.addEventListener('click', (e) => {
const li = e.target.closest('.manage-list-item');
if (!li) return;
const cb = li.querySelector('input[type="checkbox"]');
if (e.target.tagName !== 'INPUT') {
cb.checked = !cb.checked;
}
if (containerSelector === '#scan_window') {
virusState.set(cb.value, cb.checked);
if (typeof checkVirusListEmpty === 'function') checkVirusListEmpty();
}
if (typeof widget.updateSelectAllState === 'function') {
widget.updateSelectAllState();
}
});
}
const state = widget.gridState;
widget.renderData = (filterText = state.filterText) => {
state.filterText = filterText;
let header = widget.querySelector('.manage-list-header');
if (!header) {
header = document.createElement('div');
header.className = 'manage-list-header';
widget.insertBefore(header, widget.firstChild);
}
let headerColsHtml = columns.map((col, idx) =>
`<div class="col-header" data-key="${col.key}" data-idx="${idx}" style="width: var(--col-${idx}); flex: 0 0 auto;">
<span class="header-text">${getMsg(col.label)}</span>
<span class="sort-icon">${state.sortKey === col.key ? (state.sortAsc ? '▲' : '▼') : ''}</span>
<div class="col-resizer"></div>
</div>`
).join('');
header.innerHTML = `
<input type="checkbox" class="select-all-cb" title="${getMsg('全選')}">
<div class="header-cols-container">${headerColsHtml}</div>
`;
header.querySelectorAll('.col-header').forEach(el => {
el.addEventListener('click', () => {
const key = el.dataset.key;
if (state.sortKey === key) {
state.sortAsc = !state.sortAsc;
} else {
state.sortKey = key;
state.sortAsc = true;
}
widget.renderData();
});
});
header.querySelectorAll('.col-resizer').forEach(resizer => {
resizer.addEventListener('mousedown', (e) => {
e.stopPropagation();
const headerCell = resizer.parentElement;
const idx = parseInt(headerCell.dataset.idx);
const startX = e.clientX;
const startWidth = state.colWidths[idx];
const onMouseMove = (moveEvent) => {
const newWidth = Math.max(startWidth + (moveEvent.clientX - startX), 40);
state.colWidths[idx] = newWidth;
widget.style.setProperty(`--col-${idx}`, `${newWidth}px`);
};
const onMouseUp = () => {
document.body.style.userSelect = '';
document.body.style.cursor = '';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
resizer.addEventListener('click', (e) => {
e.stopPropagation();
});
});
const selectAllCb = header.querySelector('.select-all-cb');
selectAllCb.addEventListener('change', (e) => {
const isChecked = e.target.checked;
listUl.querySelectorAll('.manage-list-item input[type="checkbox"]').forEach(cb => {
cb.checked = isChecked;
if (containerSelector === '#scan_window') {
virusState.set(cb.value, isChecked);
}
});
if (containerSelector === '#scan_window' && typeof checkVirusListEmpty === 'function') {
checkVirusListEmpty();
}
});
let displayData = dataList.filter(item => {
if (!filterText) return true;
const ft = filterText.toLowerCase();
return columns.some(col => String(item[col.key] || '').toLowerCase().includes(ft));
});
displayData.sort((a, b) => {
let valA = a[state.sortKey];
let valB = b[state.sortKey];
if (valA === undefined || valA === null) valA = '';
if (valB === undefined || valB === null) valB = '';
let numA = parseFloat(valA);
let numB = parseFloat(valB);
if (!isNaN(numA) && !isNaN(numB) && String(valA).trim() !== '' && String(valB).trim() !== '') {
return state.sortAsc ? numA - numB : numB - numA;
}
return state.sortAsc ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(valA));
});
const existingCheckboxes = listUl.querySelectorAll('input[type="checkbox"]');
const hadPreviousData = existingCheckboxes.length > 0;
const currentChecked = new Set();
if (hadPreviousData) {
existingCheckboxes.forEach(cb => {
if (cb.checked) currentChecked.add(cb.value);
});
}
widget.updateSelectAllState = () => {
const total = listUl.querySelectorAll('input[type="checkbox"]').length;
const checked = listUl.querySelectorAll('input[type="checkbox"]:checked').length;
selectAllCb.checked = (total > 0 && total === checked);
selectAllCb.indeterminate = (checked > 0 && checked < total);
};
let htmlContent = '';
displayData.forEach(item => {
let isChecked = defaultChecked;
if (containerSelector === '#scan_window') {
isChecked = virusState.get(item[valKey]) ?? true;
virusState.set(item[valKey], isChecked);
} else if (hadPreviousData) {
isChecked = currentChecked.has(String(item[valKey]));
}
const checkedAttr = isChecked ? 'checked' : '';
let rowColsHtml = columns.map((col, idx) =>
`<div class="row-col" style="width: var(--col-${idx}); flex: 0 0 auto;" title="${String(item[col.key] || '').replace(/"/g, '&quot;')}">${item[col.key] || ''}</div>`
).join('');
htmlContent += `
<li class="manage-list-item">
<input type="checkbox" value="${item[valKey]}" ${checkedAttr} data-path="${item.path || ''}">
<div class="manage-list-item-content">${rowColsHtml}</div>
</li>
`;
});
listUl.innerHTML = htmlContent;
widget.updateSelectAllState();
};
widget.renderData();
if (searchInput) {
searchInput.oninput = (e) => widget.renderData(e.target.value);
}
}
function getCheckedValues(containerSelector) {
const checkboxes = document.querySelectorAll(`${containerSelector} .manage-list-item input[type="checkbox"]:checked`);
return Array.from(checkboxes).map(cb => cb.value);
}
let processInterval = null;
let currentJunkList = [];
window.virusResults = [];
renderDataGrid('#scan_window', window.virusResults, cols.virus, 'path', true);
renderDataGrid('#taskmgr_window', [], cols.process, 'pid', false);
renderDataGrid('#junk_window', [], cols.junk, 'path', true);
renderDataGrid('#repair_window', [], cols.repair, 'value', true);
renderDataGrid('#whitelist_window', [], cols.pathOnly, 'path', false);
renderDataGrid('#quarantine_window', [], cols.pathOnly, 'path', false);
renderDataGrid('#custom_protect_window', [], cols.pathOnly, 'path', false);
renderDataGrid('#popup_window', [], cols.popup, 'value', false);
renderDataGrid('#log_export_window', [], cols.log, 'id', true);
const switchPage = (targetId) => {
const oldActive = document.querySelector('.page.active');
if (oldActive && oldActive.id !== targetId) {
if (oldActive.id === 'junk_window') {
currentJunkList = [];
renderDataGrid('#junk_window', [], cols.junk, 'path', true);
const searchInput = document.querySelector('#junk_window .search-box input');
if (searchInput) searchInput.value = '';
} else if (oldActive.id === 'repair_window') {
renderDataGrid('#repair_window', [], cols.repair, 'value', true);
}
}
document.querySelectorAll('.page').forEach(page => page.classList.remove('active'));
document.querySelectorAll('.nav-btn').forEach(btn => btn.classList.remove('active'));
const targetPage = document.getElementById(targetId);
if (targetPage) targetPage.classList.add('active');
const activeNav = document.querySelector(`aside .nav-btn[data-target="${targetId}"]`);
if (activeNav) activeNav.classList.add('active');
if (targetId === 'taskmgr_window') {
const fetchProcs = () => {
if (window.pywebview && document.querySelectorAll('#taskmgr_window .manage-list-item input[type="checkbox"]:checked').length === 0) {
window.pywebview.api.get_process_list().then(updateProcessList);
}
};
fetchProcs();
if (!processInterval) processInterval = setInterval(fetchProcs, 2000);
} else {
if (processInterval) {
clearInterval(processInterval);
processInterval = null;
}
}
if (['whitelist_window', 'quarantine_window', 'popup_window', 'custom_protect_window'].includes(targetId)) {
refreshConfigLists();
}
if (targetId === 'log_export_window' && window.pywebview) {
window.pywebview.api.get_logs().then(logs => {
const logList = logs.reverse().map(log => ({
time_str: `[${log.time_str}]`,
level: log.level,
action: log.action,
source: log.source ? log.source : '-',
id: log.id
}));
renderDataGrid('#log_export_window', logList, cols.log, 'id', true);
});
}
};
document.querySelectorAll('[data-target]').forEach(btn => {
btn.addEventListener('click', (e) => {
const targetId = e.currentTarget.getAttribute('data-target');
if (targetId) switchPage(targetId);
});
});
document.getElementById('quick_scan_btn')?.addEventListener('click', () => {
switchPage('scan_window');
if (window.pywebview) triggerScan('smart');
});
const scanMethodSelect = document.getElementById('scan_method_select');
const stopBtn = document.getElementById('stop_btn');
const progressText = document.getElementById('progress_text');
const progressTitle = document.querySelector('#scan_window .section-title');
const virusState = new Map();
function rebuildCustomSelect(selectId) {
const select = document.getElementById(selectId);
if (!select) return;
const existingWrapper = select.nextElementSibling;
if (existingWrapper && existingWrapper.classList.contains('custom-select-wrapper')) {
existingWrapper.remove();
}
select.style.display = 'none';
const wrapper = document.createElement('div');
wrapper.className = 'custom-select-wrapper';
const trigger = document.createElement('div');
trigger.className = 'custom-select-trigger';
const triggerText = document.createElement('span');
triggerText.className = 'custom-select-text';
const selectedOpt = select.options[select.selectedIndex];
if(selectedOpt) {
triggerText.textContent = selectedOpt.textContent;
if(selectedOpt.hasAttribute('data-i18n')) triggerText.setAttribute('data-i18n', '');
if(selectedOpt.hasAttribute('data-origin-text')) triggerText.dataset.originText = selectedOpt.dataset.originText;
}
trigger.appendChild(triggerText);
const icon = document.createElement('div');
icon.className = 'custom-select-icon';
icon.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`;
trigger.appendChild(icon);
const optionsContainer = document.createElement('ul');
optionsContainer.className = 'custom-select-options';
const phantom = document.createElement('div');
phantom.className = 'custom-select-phantom';
phantom.setAttribute('aria-hidden', 'true');
Array.from(select.options).forEach(option => {
if(option.disabled) return;
const li = document.createElement('li');
li.className = 'custom-select-option';
li.textContent = option.textContent;
li.dataset.value = option.value;
if(option.hasAttribute('data-i18n')) li.setAttribute('data-i18n', '');
if(option.hasAttribute('data-origin-text')) li.dataset.originText = option.dataset.originText;
if(option.selected) li.classList.add('selected');
li.addEventListener('click', (e) => {
e.stopPropagation();
select.value = option.value;
select.dispatchEvent(new Event('change'));
triggerText.textContent = li.textContent;
if(li.hasAttribute('data-origin-text')) {
triggerText.dataset.originText = li.dataset.originText;
}
wrapper.classList.remove('open');
optionsContainer.querySelectorAll('.custom-select-option').forEach(el => el.classList.remove('selected'));
li.classList.add('selected');
});
optionsContainer.appendChild(li);
const phantomSpan = document.createElement('span');
phantomSpan.textContent = option.textContent;
if(option.hasAttribute('data-i18n')) phantomSpan.setAttribute('data-i18n', '');
if(option.hasAttribute('data-origin-text')) phantomSpan.dataset.originText = option.dataset.originText;
phantom.appendChild(phantomSpan);
});
wrapper.appendChild(phantom);
wrapper.appendChild(trigger);
wrapper.appendChild(optionsContainer);
select.parentNode.insertBefore(wrapper, select.nextSibling);
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = wrapper.classList.contains('open');
document.querySelectorAll('.custom-select-wrapper').forEach(w => w.classList.remove('open'));
if (!isOpen) wrapper.classList.add('open');
});
}
function changeScanSelectMode(mode) {
if (!scanMethodSelect) return;
scanMethodSelect.innerHTML = '';
if (mode === 'scan') {
scanMethodSelect.innerHTML = `
<option value="none" selected disabled data-i18n>選擇</option>
<option value="smart" data-i18n>智能掃描</option>
<option value="file" data-i18n>檔案掃描</option>
<option value="path" data-i18n>路徑掃描</option>
<option value="full" data-i18n>全盤掃描</option>
`;
} else if (mode === 'action') {
scanMethodSelect.innerHTML = `
<option value="none" selected disabled data-i18n>選擇</option>
<option value="delete" data-i18n>刪除項目</option>
<option value="ignore" data-i18n>忽略項目</option>
<option value="quarantine" data-i18n>加入隔離區</option>
<option value="whitelist" data-i18n>加入白名單</option>
`;
}
scanMethodSelect.querySelectorAll('option[data-i18n]').forEach(el => {
el.dataset.originText = el.textContent.trim();
el.textContent = getMsg(el.dataset.originText);
});
rebuildCustomSelect('scan_method_select');
}
function handleVirusActions(action) {
if (!window.pywebview) return;
const paths = Array.from(virusState.entries()).filter(([_, isChecked]) => isChecked).map(([path, _]) => path);
if (paths.length === 0) {
updateCustomSelectUI('scan_method_select', 'none');
return;
}
const wrapper = scanMethodSelect.nextElementSibling;
if (wrapper) wrapper.style.pointerEvents = 'none';
const finalizeAction = () => {
if (wrapper) wrapper.style.pointerEvents = '';
checkVirusListEmpty();
};
const removeListItems = (removedPaths) => {
const removedSet = new Set(removedPaths);
removedPaths.forEach(p => virusState.delete(p));
window.virusResults = window.virusResults.filter(v => !removedSet.has(v.path));
const listUl = document.querySelector('#scan_window .virus-list');
if (listUl) {
listUl.querySelectorAll('.manage-list-item').forEach(li => {
const cb = li.querySelector('input[type="checkbox"]');
if (cb && removedSet.has(cb.value)) {
li.remove();
}
});
}
const widget = document.querySelector('#scan_window .virus-widget');
if (widget && typeof widget.updateSelectAllState === 'function') {
widget.updateSelectAllState();
}
};
if (action === 'delete') {
window.pywebview.api.solve_scan(paths).then((deletedPaths) => {
removeListItems(deletedPaths);
finalizeAction();
});
} else if (action === 'ignore') {
removeListItems(paths);
finalizeAction();
} else if (action === 'quarantine' || action === 'whitelist') {
const listKey = action === 'quarantine' ? 'quarantine' : 'white_list';
window.pywebview.api.manage_named_list(listKey, paths, 'add').then(() => {
window.pywebview.api.remove_virus_result(paths).then(() => {
removeListItems(paths);
finalizeAction();
});
});
}
}
function checkVirusListEmpty() {
updateCustomSelectUI('scan_method_select', 'none');
if (virusState.size === 0) {
changeScanSelectMode('scan');
if (progressTitle) {
progressTitle.setAttribute('data-i18n', '');
progressTitle.dataset.originText = "病毒掃描";
progressTitle.textContent = getMsg("病毒掃描");
}
if (progressText) {
progressText.setAttribute('data-i18n', '');
progressText.removeAttribute('data-dynamic-msg');
progressText.dataset.originText = "此選項可以選擇進階掃描方式";
progressText.textContent = getMsg("此選項可以選擇進階掃描方式");
}
}
}
window.addVirusResult = (label, path) => {
if (!virusState.has(path)) {
const item = { label: label, path: path };
window.virusResults.push(item);
virusState.set(path, true);
const listUl = document.querySelector('#scan_window .virus-list');
const widget = document.querySelector('#scan_window .virus-widget');
if (listUl && widget && widget.gridState) {
const rowColsHtml = cols.virus.map((col, idx) =>
`<div class="row-col" style="width: var(--col-${idx}); flex: 0 0 auto;" title="${String(item[col.key] || '').replace(/"/g, '&quot;')}">${item[col.key] || ''}</div>`
).join('');
const liHtml = `
<li class="manage-list-item">
<input type="checkbox" value="${path}" checked data-path="${path}">
<div class="manage-list-item-content">${rowColsHtml}</div>
</li>
`;
listUl.insertAdjacentHTML('beforeend', liHtml);
if (typeof widget.updateSelectAllState === 'function') {
widget.updateSelectAllState();
}
} else {
renderDataGrid('#scan_window', window.virusResults, cols.virus, 'path', true);
}
}
};
window.updateScanProgress = (path) => {
if (progressTitle) {
progressTitle.setAttribute('data-i18n', '');
progressTitle.dataset.originText = "正在掃描";
progressTitle.textContent = getMsg("正在掃描");
}
if (progressText) {
progressText.removeAttribute('data-i18n');
progressText.removeAttribute('data-dynamic-msg');
progressText.textContent = path;
}
};
window.updateDeleteProgress = (path) => {
if (progressTitle) {
progressTitle.setAttribute('data-i18n', '');
progressTitle.dataset.originText = "正在刪除";
progressTitle.textContent = getMsg("正在刪除");
}
if (progressText) {
progressText.removeAttribute('data-i18n');
progressText.removeAttribute('data-dynamic-msg');
progressText.textContent = path;
}
};
window.finishScan = (msgData, count) => {
isScanning = false;
if (progressTitle) {
progressTitle.setAttribute('data-i18n', '');
progressTitle.dataset.originText = "病毒掃描";
progressTitle.textContent = getMsg("病毒掃描");
}
if (progressText) {
progressText.removeAttribute('data-i18n');
if (typeof msgData === 'object' && msgData !== null) {
progressText.dataset.dynamicMsg = JSON.stringify(msgData);
progressText.textContent = msgData[currentLang] || msgData["english_switch"];
} else {
progressText.removeAttribute('data-dynamic-msg');
progressText.textContent = msgData;
}
}
stopBtn.classList.add('hidden');
scanMethodSelect.classList.remove('hidden');
if (count > 0) {
changeScanSelectMode('action');
} else {
changeScanSelectMode('scan');
}
updateCustomSelectUI('scan_method_select', 'none');
};
function triggerScan(method) {
if (!window.pywebview) return;
isScanning = true;
virusState.clear();
window.virusResults = [];
renderDataGrid('#scan_window', window.virusResults, cols.virus, 'path', true);
changeScanSelectMode('scan');
scanMethodSelect.classList.add('hidden');
stopBtn.classList.remove('hidden');
stopBtn.disabled = false;
if (progressTitle) {
progressTitle.setAttribute('data-i18n', '');
progressTitle.dataset.originText = "正在掃描";
progressTitle.textContent = getMsg("正在掃描");
}
if (progressText) {
progressText.setAttribute('data-i18n', '');
progressText.removeAttribute('data-dynamic-msg');
progressText.dataset.originText = "正在初始化中";
progressText.textContent = getMsg("正在初始化中");
}
window.pywebview.api.trigger_scan(method);
}
scanMethodSelect?.addEventListener('change', () => {
const val = scanMethodSelect.value;
if (val === 'none') return;
if (!isScanning) {
if (['smart', 'file', 'path', 'full'].includes(val)) {
triggerScan(val);
} else if (['delete', 'ignore', 'quarantine', 'whitelist'].includes(val)) {
handleVirusActions(val);
}
}
});
stopBtn?.addEventListener('click', () => {
if (window.pywebview) window.pywebview.api.stop_scan();
stopBtn.disabled = true;
});
document.getElementById('theme_select').addEventListener('change', (e) => {
applyTheme(e.target.value);
if (window.pywebview) window.pywebview.api.update_config('theme', e.target.value);
});
document.getElementById('lang_select').addEventListener('change', (e) => {
translateText(e.target.value);
if (window.pywebview) window.pywebview.api.update_config('language', e.target.value);
});
window.revertSwitch = (switchKey) => {
const switchMap = ["process_switch", "document_switch", "system_switch", "driver_switch", "network_switch", "sensitive_switch", "extension_switch", "cloud_switch", "context_switch"];
const index = switchMap.indexOf(switchKey);
if (index !== -1) {
const toggle = document.querySelectorAll('.toggle-switch input')[index];
if (toggle) toggle.checked = false;
}
};
document.querySelectorAll('.toggle-switch input').forEach((toggle, index) => {
toggle.addEventListener('change', async (e) => {
const switchMap = ["process_switch", "document_switch", "system_switch", "driver_switch", "network_switch", "sensitive_switch", "extension_switch", "cloud_switch", "context_switch"];
const key = switchMap[index];
if (window.pywebview && key) {
toggle.disabled = true;
try {
const result = await window.pywebview.api.update_config(key, e.target.checked);
if (result !== undefined && result !== null) {
toggle.checked = result;
}
} catch (err) {
toggle.checked = !e.target.checked;
} finally {
toggle.disabled = false;
}
}
});
});
window.triggerContextScan = (path) => {
if (isScanning) return;
switchPage('scan_window');
if (window.pywebview) {
isScanning = true;
virusState.clear();
window.virusResults = [];
renderDataGrid('#scan_window', window.virusResults, cols.virus, 'path', true);
scanMethodSelect.classList.add('hidden');
stopBtn.classList.remove('hidden');
changeScanSelectMode('scan');
if (progressTitle) {
progressTitle.setAttribute('data-i18n', '');
progressTitle.dataset.originText = "正在掃描";
progressTitle.textContent = getMsg("正在掃描");
}
if (progressText) {
progressText.setAttribute('data-i18n', '');
progressText.removeAttribute('data-dynamic-msg');
progressText.dataset.originText = "正在初始化中";
progressText.textContent = getMsg("正在初始化中");
}
window.pywebview.api.start_scan([path]);
}
};
window.addEventListener('pywebviewready', () => {
window.pywebview.api.get_config().then(cfg => {
const theme = cfg.theme || "white_switch";
const lang = cfg.language || "english_switch";
applyTheme(theme);
translateText(lang);
updateCustomSelectUI('theme_select', theme);
updateCustomSelectUI('lang_select', lang);
const switchMap = ["process_switch", "document_switch", "system_switch", "driver_switch", "network_switch", "sensitive_switch", "extension_switch", "cloud_switch", "context_switch"];
document.querySelectorAll('.toggle-switch input').forEach((toggle, index) => {
if (switchMap[index]) {
toggle.checked = !!cfg[switchMap[index]];
}
});
});
window.pywebview.api.init_ui_ready();
});
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
document.querySelector('#junk_window .primary-btn')?.addEventListener('click', () => {
if (!window.pywebview) return;
const btn = document.querySelector('#junk_window .primary-btn');
btn.textContent = getMsg("正在初始化中");
window.pywebview.api.scan_system_junk().then(list => {
currentJunkList = list.map(item => ({
path: item.path,
sizeStr: formatSize(item.size),
size: item.size
}));
renderDataGrid('#junk_window', currentJunkList, cols.junk, 'path', true);
btn.textContent = getMsg("掃描");
});
});
document.querySelector('#junk_window .danger-btn')?.addEventListener('click', () => {
if (!window.pywebview) return;
const checkedPaths = getCheckedValues('#junk_window');
if (checkedPaths.length === 0) return;
window.pywebview.api.clean_system_junk(checkedPaths).then(deletedSize => {
const msg = getMsg("已清理 ");
window.pywebview.api.show_alert(getMsg("提示"), `${msg}${formatSize(deletedSize)}`, "info");
currentJunkList = currentJunkList.filter(item => !checkedPaths.includes(item.path));
renderDataGrid('#junk_window', currentJunkList, cols.junk, 'path', true);
});
});
function refreshConfigLists() {
if (!window.pywebview) return;
window.pywebview.api.get_config().then(cfg => {
renderDataGrid('#whitelist_window', (cfg.white_list || []).map(i => ({path: i.file || i})), cols.pathOnly, 'path');
renderDataGrid('#quarantine_window', (cfg.quarantine || []).map(i => ({path: i.file || i})), cols.pathOnly, 'path');
renderDataGrid('#custom_protect_window', (cfg.custom_rule || []).map(i => ({path: i.file || i})), cols.pathOnly, 'path');
const popupRules = (cfg.block_list || []).map(item => ({
exe: item.exe || '*',
class: item.class || '*',
title: item.title || '*',
value: item.exe || item.title
}));
renderDataGrid('#popup_window', popupRules, cols.popup, 'value');
});
}
document.querySelector('#whitelist_window .modern-btn:not(.primary-btn)')?.addEventListener('click', () => {
if (!window.pywebview) return;
const checked = getCheckedValues('#whitelist_window');
if (checked.length > 0) window.pywebview.api.remove_list_items('white_list', checked).then(refreshConfigLists);
});
document.querySelector('#quarantine_window .modern-btn:not(.primary-btn)')?.addEventListener('click', () => {
if (!window.pywebview) return;
const checked = getCheckedValues('#quarantine_window');
if (checked.length > 0) window.pywebview.api.remove_list_items('quarantine', checked).then(refreshConfigLists);
});
document.querySelector('#popup_window .modern-btn:not(.primary-btn)')?.addEventListener('click', () => {
if (!window.pywebview) return;
const checked = getCheckedValues('#popup_window');
if (checked.length > 0) window.pywebview.api.remove_list_items('block_list', checked).then(refreshConfigLists);
});
document.querySelector('#custom_protect_window .modern-btn:not(.primary-btn)')?.addEventListener('click', () => {
if (!window.pywebview) return;
const checked = getCheckedValues('#custom_protect_window');
if (checked.length > 0) window.pywebview.api.remove_list_items('custom_rule', checked).then(refreshConfigLists);
});
document.querySelector('#whitelist_window .primary-btn')?.addEventListener('click', () => {
window.pywebview?.api.select_files().then(files => {
if (files && files.length > 0) window.pywebview.api.manage_named_list('white_list', files, 'add').then(refreshConfigLists);
});
});
document.querySelector('#quarantine_window .primary-btn')?.addEventListener('click', () => {
window.pywebview?.api.select_files().then(files => {
if (files && files.length > 0) window.pywebview.api.manage_named_list('quarantine', files, 'add').then(refreshConfigLists);
});
});
document.querySelector('#custom_protect_window .primary-btn')?.addEventListener('click', () => {
window.pywebview?.api.select_files().then(files => {
if (files && files.length > 0) window.pywebview.api.manage_named_list('custom_rule', files, 'add').then(refreshConfigLists);
});
});
document.querySelector('#repair_window .primary-btn')?.addEventListener('click', () => {
if (!window.pywebview) return;
window.pywebview.api.scan_system_repair().then(list => {
const repairData = list.map(item => ({ display: getMsg(item.display), value: item.value }));
renderDataGrid('#repair_window', repairData, cols.repair, 'value', true);
});
});
document.querySelector('#repair_window .danger-btn')?.addEventListener('click', () => {
if (!window.pywebview) return;
const checkedPaths = getCheckedValues('#repair_window');
if (checkedPaths.length === 0) return;
window.pywebview.api.execute_system_repair(checkedPaths).then(() => {
const msg = getMsg("已修復選取項目");
window.pywebview.api.show_alert(getMsg("提示"), msg, "info");
renderDataGrid('#repair_window', [], cols.repair, 'value');
});
});
document.getElementById('minimize_button')?.addEventListener('click', () => window.pywebview?.api.minimize());
document.getElementById('close_button')?.addEventListener('click', () => window.pywebview?.api.hide_window());
window.updateLogs = (entry) => {
const logWidget = document.querySelector('.log-text');
if (logWidget) {
let parts = [`[${entry.time_str}]`, entry.level, entry.action];
if (entry.source) parts.push(`Src: ${entry.source}`);
if (entry.target) parts.push(`Tgt: ${entry.target}`);
if (entry.code) parts.push(`Code: ${entry.code}`);
if (entry.pid) parts.push(`PID: ${entry.pid}`);
if (entry.hash) parts.push(`Hash: ${entry.hash}`);
if (entry.detail) parts.push(`Detail: ${entry.detail}`);
if (entry.operate !== null) parts.push(`Op: ${entry.operate}`);
parts.push(`Success: ${entry.success}`);
logWidget.value += parts.join(' | ') + '\n';
logWidget.scrollTop = logWidget.scrollHeight;
}
const exportWindow = document.getElementById('log_export_window');
if (exportWindow && exportWindow.classList.contains('active')) {
if (window.pywebview) {
window.pywebview.api.get_logs().then(logs => {
const logList = logs.reverse().map(log => ({
time_str: `[${log.time_str}]`,
level: log.level,
action: log.action,
source: log.source ? log.source : '-',
id: log.id
}));
renderDataGrid('#log_export_window', logList, cols.log, 'id', true);
});
}
}
if ((entry.action === 'System' && entry.detail === 'Engine Initialization Complete') ||
(entry.level === 'WARN' && entry.action === 'init_engine_thread')) {
const overlay = document.getElementById('loading_overlay');
const appContainer = document.querySelector('.app-container');
if (overlay && !overlay.classList.contains('fade-out')) {
overlay.classList.add('fade-out');
setTimeout(() => {
if (appContainer) appContainer.classList.add('fade-in');
}, 400);
}
}
};
const websiteBtn = Array.from(document.querySelectorAll('#about_window .list-item')).find(el => el.textContent.includes('官方網站') || el.textContent.includes('Website'));
if (websiteBtn) {
websiteBtn.querySelector('button').addEventListener('click', () => {
if (window.pywebview) window.pywebview.api.open_website();
});
}
const updateBtn = Array.from(document.querySelectorAll('#about_window .list-item')).find(el => el.textContent.includes('檢查更新') || el.textContent.includes('Update'));
if (updateBtn) {
updateBtn.querySelector('button').addEventListener('click', () => {
if (window.pywebview) {
window.pywebview.api.check_update().then(res => {
if (res.error) {
window.pywebview.api.show_alert(getMsg("錯誤"), getMsg("檢查更新失敗"), "error");
} else if (res.has_update) {
const msg = getMsg("發現新版本");
window.pywebview.api.show_confirm(getMsg("提示"), `${msg} ${res.latest}\n(${res.current} -> ${res.latest})`).then(confirmRes => {
if (confirmRes) window.pywebview.api.open_url(res.url);
});
} else {
const msg = getMsg("當前已是最新版本");
window.pywebview.api.show_alert(getMsg("提示"), `${msg} ${res.current}`, "info");
}
});
}
});
}
const resetBtn = Array.from(document.querySelectorAll('#setting_window .list-item')).find(el => el.textContent.includes('重置選項') || el.textContent.includes('Reset'));
if (resetBtn) {
resetBtn.querySelector('button').addEventListener('click', () => {
const confirmMsg = getMsg("此選項可以重置所有設定");
if (window.pywebview) {
window.pywebview.api.show_confirm(getMsg("提示"), confirmMsg).then(res => {
if (res) {
window.pywebview.api.reset_config().then(() => {
window.pywebview.api.show_alert(getMsg("提示"), getMsg("設定已重置,請重新啟動程式以套用預設值。"), "info");
});
}
});
}
});
}
document.querySelector('#log_export_window .primary-btn')?.addEventListener('click', () => {
if (window.pywebview) {
const checkedIds = getCheckedValues('#log_export_window');
if (checkedIds.length === 0) {
window.pywebview.api.show_alert(getMsg("提示"), getMsg("請選擇要導出的日誌"), "warning");
return;
}
window.pywebview.api.export_logs(checkedIds).then(res => {
if (res) window.pywebview.api.show_alert(getMsg("提示"), getMsg("導出成功"), "info");
});
}
});
document.querySelector('#log_export_window .danger-btn')?.addEventListener('click', () => {
const checkedIds = getCheckedValues('#log_export_window');
if (checkedIds.length === 0) return;
const confirmMsg = getMsg("確定刪除選取的日誌記錄嗎?");
if (window.pywebview) {
window.pywebview.api.show_confirm(getMsg("提示"), confirmMsg).then(res => {
if (res) {
window.pywebview.api.clear_logs(checkedIds).then(() => {
window.pywebview.api.get_logs().then(logs => {
const logList = logs.reverse().map(log => ({
time_str: `[${log.time_str}]`,
level: log.level,
action: log.action,
source: log.source ? log.source : '-',
id: log.id
}));
renderDataGrid('#log_export_window', logList, cols.log, 'id', true);
});
const successMsg = getMsg("選取的日誌已刪除。");
window.pywebview.api.show_alert(getMsg("提示"), successMsg, "info");
});
}
});
}
});
let currentProcessList = [];
function updateProcessList(procs) {
currentProcessList = procs.map(p => ({
name: p.name,
pid: p.pid,
path: p.path && p.path !== "None" ? p.path : getMsg("未知路徑")
}));
renderDataGrid('#taskmgr_window', currentProcessList, cols.process, 'pid', false);
}
document.querySelector('#taskmgr_window .primary-btn')?.addEventListener('click', () => {
const cbs = document.querySelectorAll('#taskmgr_window .manage-list-item input[type="checkbox"]:checked');
if (cbs.length > 0 && window.pywebview) {
cbs.forEach(cb => {
const path = cb.dataset.path;
if (path && path !== "Unknown" && path !== "None") {
window.pywebview.api.open_file_location(path);
}
});
}
});
document.querySelector('#popup_window .primary-btn')?.addEventListener('click', () => {
if (!window.pywebview) return;
const btn = document.querySelector('#popup_window .primary-btn');
const originalText = btn.textContent;
const waitText = getMsg("請在5秒內點擊目標視窗...");
btn.textContent = waitText;
btn.disabled = true;
window.pywebview.api.capture_popup_window().then(rule => {
btn.textContent = originalText;
btn.disabled = false;
if (rule) {
window.pywebview.api.add_popup_rule(rule).then(success => {
if (success) refreshConfigLists();
});
}
});
});
document.querySelector('#taskmgr_window .danger-btn')?.addEventListener('click', () => {
const cbs = document.querySelectorAll('#taskmgr_window .manage-list-item input[type="checkbox"]:checked');
const confirmMsg = getMsg("確定要結束選取的進程嗎?");
if (cbs.length > 0 && window.pywebview) {
window.pywebview.api.show_confirm(getMsg("提示"), confirmMsg).then(res => {
if (res) {
const promises = Array.from(cbs).map(cb => window.pywebview.api.kill_process(parseInt(cb.value)));
Promise.all(promises).then(() => {
window.pywebview.api.get_process_list().then(updateProcessList);
});
}
});
}
});
});