add clean browsers

This commit is contained in:
Julian Freeman
2026-03-03 17:02:40 -04:00
parent 39e2f41aab
commit 33a1dfa27b
3 changed files with 441 additions and 57 deletions

View File

@@ -365,3 +365,118 @@ fn clean_directory_contents(path: &Path, filter_days: Option<u64>) -> (u64, u32,
}
(freed, success, fail)
}
// --- 浏览器清理逻辑 ---
#[derive(Serialize, Clone)]
pub struct BrowserProfile {
pub name: String,
pub path_name: String,
pub cache_size: u64,
pub cache_size_str: String,
}
#[derive(Serialize)]
pub struct BrowserScanResult {
pub profiles: Vec<BrowserProfile>,
pub total_size: String,
}
pub enum BrowserType {
Chrome,
Edge,
}
impl BrowserType {
fn get_user_data_path(&self) -> Result<std::path::PathBuf, String> {
let local_app_data = std::env::var("LOCALAPPDATA").map_err(|_| "无法获取 LocalAppData 路径")?;
let base = std::path::Path::new(&local_app_data);
match self {
BrowserType::Chrome => Ok(base.join("Google\\Chrome\\User Data")),
BrowserType::Edge => Ok(base.join("Microsoft\\Edge\\User Data")),
}
}
}
pub async fn run_browser_scan(browser: BrowserType) -> Result<BrowserScanResult, String> {
let user_data_path = browser.get_user_data_path()?;
let local_state_path = user_data_path.join("Local State");
let mut profiles = Vec::new();
let mut total_bytes = 0;
if local_state_path.exists() {
let content = fs::read_to_string(local_state_path).map_err(|e| e.to_string())?;
let v: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
if let Some(info_cache) = v.get("profile").and_then(|p| p.get("info_cache")).and_then(|i| i.as_object()) {
for (dir_name, info) in info_cache {
let profile_display_name = info.get("name").and_then(|n| n.as_str()).unwrap_or(dir_name);
let profile_path = user_data_path.join(dir_name);
if profile_path.exists() {
let mut size = 0;
// 扫描常见的缓存目录
let cache_dirs = ["Cache", "Code Cache", "GPUCache", "Media Cache"];
for sub in cache_dirs {
let target = profile_path.join(sub);
if target.exists() {
size += get_dir_size_simple(&target);
}
}
total_bytes += size;
profiles.push(BrowserProfile {
name: profile_display_name.to_string(),
path_name: dir_name.clone(),
cache_size: size,
cache_size_str: format_size(size),
});
}
}
}
}
Ok(BrowserScanResult {
profiles,
total_size: format_size(total_bytes),
})
}
fn get_dir_size_simple(path: &std::path::Path) -> u64 {
walkdir::WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.map(|e| e.metadata().map(|m| m.len()).unwrap_or(0))
.sum()
}
pub async fn run_browser_clean(browser: BrowserType, profile_paths: Vec<String>) -> Result<CleanResult, String> {
let user_data_path = browser.get_user_data_path()?;
let mut total_freed = 0;
let mut success_count = 0;
let mut fail_count = 0;
for profile_dir in profile_paths {
let profile_path = user_data_path.join(&profile_dir);
if profile_path.exists() {
let cache_dirs = ["Cache", "Code Cache", "GPUCache", "Media Cache"];
for sub in cache_dirs {
let target = profile_path.join(sub);
if target.exists() {
let (f, s, fl) = clean_directory_contents(&target, None);
total_freed += f;
success_count += s;
fail_count += fl;
}
}
}
}
Ok(CleanResult {
total_freed: format_size(total_freed),
success_count,
fail_count,
})
}

View File

@@ -46,6 +46,20 @@ async fn disable_hibernation() -> Result<String, String> {
cleaner::disable_hibernation().await
}
// --- 浏览器清理命令 ---
#[tauri::command]
async fn start_browser_scan(browser: String) -> Result<cleaner::BrowserScanResult, String> {
let b_type = if browser == "chrome" { cleaner::BrowserType::Chrome } else { cleaner::BrowserType::Edge };
cleaner::run_browser_scan(b_type).await
}
#[tauri::command]
async fn start_browser_clean(browser: String, profiles: Vec<String>) -> Result<cleaner::CleanResult, String> {
let b_type = if browser == "chrome" { cleaner::BrowserType::Chrome } else { cleaner::BrowserType::Edge };
cleaner::run_browser_clean(b_type, profiles).await
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@@ -62,7 +76,9 @@ pub fn run() {
open_in_explorer,
clean_system_components,
clean_thumbnails,
disable_hibernation
disable_hibernation,
start_browser_scan,
start_browser_clean
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -1,19 +1,22 @@
<script setup lang="ts">
import { ref, onUnmounted } from "vue";
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import pkg from "../package.json";
// --- 导航状态 ---
type Tab = 'clean-c-fast' | 'clean-c-advanced' | 'clean-c-deep' | 'clean-browser' | 'clean-memory';
type Tab = 'clean-c-fast' | 'clean-c-advanced' | 'clean-c-deep' | 'clean-browser-chrome' | 'clean-browser-edge' | 'clean-memory';
const activeTab = ref<Tab>('clean-c-fast');
const isCMenuOpen = ref(true);
const isBrowserMenuOpen = ref(false);
// --- 数据结构 ---
interface ScanItem { name: string; path: string; size: number; count: number; enabled: boolean; }
interface FastScanResult { items: ScanItem[]; total_size: string; total_count: number; }
interface CleanResult { total_freed: string; success_count: number; fail_count: number; }
interface BrowserProfile { name: string; path_name: string; cache_size: number; cache_size_str: string; enabled: boolean; }
interface BrowserScanResult { profiles: BrowserProfile[]; total_size: string; }
interface FileNode {
name: string; path: string; is_dir: boolean; size: number; size_str: string;
percent: number; has_children: boolean;
@@ -21,22 +24,42 @@ interface FileNode {
}
// --- 状态管理 ---
const isScanning = ref(false);
const isCleaning = ref(false);
const isCleanDone = ref(false);
const fastState = ref({
isScanning: false,
isCleaning: false,
isDone: false,
progress: 0,
scanResult: null as FastScanResult | null,
cleanResult: null as CleanResult | null,
});
const chromeState = ref({
isScanning: false,
isCleaning: false,
isDone: false,
scanResult: null as BrowserScanResult | null,
cleanResult: null as CleanResult | null,
});
const edgeState = ref({
isScanning: false,
isCleaning: false,
isDone: false,
scanResult: null as BrowserScanResult | null,
cleanResult: null as CleanResult | null,
});
const isFullScanning = ref(false);
const scanProgress = ref(0);
const fullScanProgress = ref({ fileCount: 0, currentPath: "" });
const fastScanResult = ref<FastScanResult | null>(null);
const cleanResult = ref<CleanResult | null>(null);
const treeData = ref<FileNode[]>([]);
// --- 动态汇总计算 ---
import { computed } from "vue";
const selectedStats = computed(() => {
if (!fastScanResult.value) return { sizeStr: "0 B", count: 0, hasSelection: false };
const enabledItems = fastScanResult.value.items.filter(i => i.enabled);
const s = fastState.value;
if (!s.scanResult) return { sizeStr: "0 B", count: 0, hasSelection: false };
const enabledItems = s.scanResult.items.filter(i => i.enabled);
const totalBytes = enabledItems.reduce((acc, i) => acc + i.size, 0);
const totalCount = enabledItems.reduce((acc, i) => acc + i.count, 0);
return {
@@ -46,6 +69,20 @@ const selectedStats = computed(() => {
};
});
function getBrowserSelectedStats(state: any) {
if (!state.scanResult) return { sizeStr: "0 B", count: 0, hasSelection: false };
const enabledProfiles = state.scanResult.profiles.filter((p: any) => p.enabled);
const totalBytes = enabledProfiles.reduce((acc: number, p: any) => acc + p.cache_size, 0);
return {
sizeStr: formatItemSize(totalBytes),
count: enabledProfiles.length,
hasSelection: enabledProfiles.length > 0
};
}
const chromeSelectedStats = computed(() => getBrowserSelectedStats(chromeState.value));
const edgeSelectedStats = computed(() => getBrowserSelectedStats(edgeState.value));
// --- 弹窗状态 ---
const showModal = ref(false);
const modalTitle = ref("");
@@ -114,26 +151,28 @@ const advLoading = ref<Record<string, boolean>>({});
// --- 快速模式逻辑 ---
async function startFastScan() {
isScanning.value = true;
isCleanDone.value = false;
scanProgress.value = 0;
fastScanResult.value = null;
const interval = setInterval(() => { if (scanProgress.value < 95) scanProgress.value += Math.floor(Math.random() * 5); }, 100);
const s = fastState.value;
s.isScanning = true;
s.isDone = false;
s.progress = 0;
s.scanResult = null;
const interval = setInterval(() => { if (s.progress < 95) s.progress += Math.floor(Math.random() * 5); }, 100);
try {
const result = await invoke<FastScanResult>("start_fast_scan");
scanProgress.value = 100;
fastScanResult.value = result;
s.progress = 100;
s.scanResult = result;
} catch (err) {
showAlert("扫描失败", "请尝试以管理员身份运行程序。", 'error');
} finally {
clearInterval(interval);
isScanning.value = false;
s.isScanning = false;
}
}
async function startFastClean() {
if (isCleaning.value || !fastScanResult.value) return;
const selectedPaths = fastScanResult.value.items
const s = fastState.value;
if (s.isCleaning || !s.scanResult) return;
const selectedPaths = s.scanResult.items
.filter(item => item.enabled)
.map(item => item.path);
@@ -142,15 +181,15 @@ async function startFastClean() {
return;
}
isCleaning.value = true;
s.isCleaning = true;
try {
const res = await invoke<CleanResult>("start_fast_clean", { selectedPaths });
cleanResult.value = res;
isCleanDone.value = true;
fastScanResult.value = null;
s.cleanResult = res;
s.isDone = true;
s.scanResult = null;
} catch (err) {
showAlert("清理失败", String(err), 'error');
} finally { isCleaning.value = false; }
} finally { s.isCleaning = false; }
}
// --- 高级模式逻辑 ---
@@ -172,6 +211,51 @@ async function runAdvancedTask(task: string) {
}
}
// --- 浏览器清理逻辑 ---
async function startBrowserScan(browser: 'chrome' | 'edge') {
const s = browser === 'chrome' ? chromeState.value : edgeState.value;
s.isScanning = true;
s.isDone = false;
s.scanResult = null;
s.cleanResult = null;
try {
const res = await invoke<BrowserScanResult>("start_browser_scan", { browser });
s.scanResult = {
...res,
profiles: res.profiles.map(p => ({ ...p, enabled: true }))
};
} catch (err) {
showAlert("扫描失败", String(err), 'error');
} finally {
s.isScanning = false;
}
}
async function startBrowserClean(browser: 'chrome' | 'edge') {
const s = browser === 'chrome' ? chromeState.value : edgeState.value;
if (!s.scanResult || s.isCleaning) return;
const selectedProfiles = s.scanResult.profiles
.filter(p => p.enabled)
.map(p => p.path_name);
if (selectedProfiles.length === 0) {
showAlert("未选择", "请选择至少一个用户资料进行清理。", 'info');
return;
}
s.isCleaning = true;
try {
const res = await invoke<CleanResult>("start_browser_clean", { browser, profiles: selectedProfiles });
s.cleanResult = res;
s.isDone = true;
s.scanResult = null;
} catch (err) {
showAlert("清理失败", String(err), 'error');
} finally {
s.isCleaning = false;
}
}
// --- 深度分析 (TreeSize) 逻辑 ---
async function startFullDiskScan() {
isFullScanning.value = true;
@@ -220,11 +304,14 @@ async function toggleNode(index: number) {
}
}
function resetAll() {
isCleanDone.value = false;
fastScanResult.value = null;
cleanResult.value = null;
treeData.value = [];
function resetPageState() {
if (activeTab.value === 'clean-c-fast') {
fastState.value = { isScanning: false, isCleaning: false, isDone: false, progress: 0, scanResult: null, cleanResult: null };
} else if (activeTab.value === 'clean-browser-chrome') {
chromeState.value = { isScanning: false, isCleaning: false, isDone: false, scanResult: null, cleanResult: null };
} else if (activeTab.value === 'clean-browser-edge') {
edgeState.value = { isScanning: false, isCleaning: false, isDone: false, scanResult: null, cleanResult: null };
}
}
function formatItemSize(bytes: number): string {
@@ -286,15 +373,32 @@ function splitSize(sizeStr: string | number) {
</div>
</div>
<!-- 其它项 -->
<div
class="nav-item"
:class="{ active: activeTab === 'clean-browser' }"
@click="activeTab = 'clean-browser'"
>
<!-- 清理浏览器组 -->
<div class="nav-group">
<div class="nav-item-header" @click="isBrowserMenuOpen = !isBrowserMenuOpen">
<span class="icon">🌐</span>
<span class="label">清理浏览器</span>
<span class="arrow" :class="{ open: isBrowserMenuOpen }"></span>
</div>
<div class="nav-sub-items" v-show="isBrowserMenuOpen">
<div
class="nav-sub-item"
:class="{ active: activeTab === 'clean-browser-chrome' }"
@click="activeTab = 'clean-browser-chrome'"
>
谷歌浏览器
</div>
<div
class="nav-sub-item"
:class="{ active: activeTab === 'clean-browser-edge' }"
@click="activeTab = 'clean-browser-edge'"
>
微软浏览器
</div>
</div>
</div>
<!-- 其它项 -->
<div
class="nav-item"
:class="{ active: activeTab === 'clean-memory' }"
@@ -323,17 +427,17 @@ function splitSize(sizeStr: string | number) {
<div class="main-action">
<!-- 扫描前/ -->
<div class="scan-circle-container" v-if="!fastScanResult && !isCleanDone">
<div class="scan-circle" :class="{ scanning: isScanning }">
<div class="scan-inner" @click="!isScanning && startFastScan()">
<span v-if="!isScanning" class="scan-btn-text">开始扫描</span>
<span v-else class="scan-percent">{{ scanProgress }}%</span>
<div class="scan-circle-container" v-if="!fastState.scanResult && !fastState.isDone">
<div class="scan-circle" :class="{ scanning: fastState.isScanning }">
<div class="scan-inner" @click="!fastState.isScanning && startFastScan()">
<span v-if="!fastState.isScanning" class="scan-btn-text">开始扫描</span>
<span v-else class="scan-percent">{{ fastState.progress }}%</span>
</div>
</div>
</div>
<!-- 扫描完成 -->
<div class="result-card" v-else-if="fastScanResult && !isCleanDone">
<div class="result-card" v-else-if="fastState.scanResult && !fastState.isDone">
<div class="result-header">
<span class="result-icon">📋</span>
<h2>扫描完成</h2>
@@ -356,14 +460,14 @@ function splitSize(sizeStr: string | number) {
<button
class="btn-primary main-btn"
@click="startFastClean"
:disabled="isCleaning || !selectedStats.hasSelection"
:disabled="fastState.isCleaning || !selectedStats.hasSelection"
>
{{ isCleaning ? '正在清理...' : '立即清理' }}
{{ fastState.isCleaning ? '正在清理...' : '立即清理' }}
</button>
</div>
<!-- 清理完成报告 -->
<div class="result-card done-card" v-else-if="isCleanDone && cleanResult">
<div class="result-card done-card" v-else-if="fastState.isDone && fastState.cleanResult">
<div class="result-header">
<span class="result-icon success">🎉</span>
<h2>清理完成</h2>
@@ -372,31 +476,31 @@ function splitSize(sizeStr: string | number) {
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(cleanResult.total_freed).unit }}</span>
{{ splitSize(fastState.cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(fastState.cleanResult.total_freed).unit }}</span>
</span>
<span class="stat-label">释放空间</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ cleanResult.success_count }}</span>
<span class="stat-value">{{ fastState.cleanResult.success_count }}</span>
<span class="stat-label">成功清理</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value highlight-gray">{{ cleanResult.fail_count }}</span>
<span class="stat-value highlight-gray">{{ fastState.cleanResult.fail_count }}</span>
<span class="stat-label">跳过/失败</span>
</div>
</div>
<button class="btn-secondary" @click="resetAll">返回首页</button>
<button class="btn-secondary" @click="resetPageState">返回</button>
</div>
</div>
<div class="detail-list" v-if="(isScanning || fastScanResult) && !isCleanDone">
<div class="detail-list" v-if="(fastState.isScanning || fastState.scanResult) && !fastState.isDone">
<h3>清理项详情</h3>
<div
class="detail-item"
v-for="item in fastScanResult?.items || []"
v-for="item in fastState.scanResult?.items || []"
:key="item.path"
@click="item.enabled = !item.enabled"
:class="{ disabled: !item.enabled }"
@@ -410,7 +514,7 @@ function splitSize(sizeStr: string | number) {
</div>
<span class="item-size">{{ formatItemSize(item.size) }}</span>
</div>
<div v-if="isScanning" class="scanning-placeholder">正在深度扫描文件系统...</div>
<div v-if="fastState.isScanning" class="scanning-placeholder">正在深度扫描文件系统...</div>
</div>
</section>
@@ -504,6 +608,155 @@ function splitSize(sizeStr: string | number) {
</div>
</section>
<!-- 2.5 浏览器清理页面 -->
<section v-else-if="activeTab === 'clean-browser-chrome' || activeTab === 'clean-browser-edge'" class="page-container">
<div class="page-header">
<div class="header-info">
<h1>清理{{ activeTab === 'clean-browser-chrome' ? '谷歌浏览器' : '微软浏览器' }}</h1>
<p>安全清理浏览器缓存临时文件释放磁盘空间</p>
</div>
</div>
<div class="main-action">
<!-- 谷歌浏览器视图 -->
<template v-if="activeTab === 'clean-browser-chrome'">
<!-- 扫描前 -->
<div class="scan-circle-container" v-if="!chromeState.scanResult && !chromeState.isDone">
<div class="scan-circle" :class="{ scanning: chromeState.isScanning }">
<div class="scan-inner" @click="!chromeState.isScanning && startBrowserScan('chrome')">
<span v-if="!chromeState.isScanning" class="scan-btn-text">开始扫描</span>
<span v-else class="spinner"></span>
</div>
</div>
</div>
<!-- 扫描完成 -->
<div class="result-card" v-else-if="chromeState.scanResult && !chromeState.isDone">
<div class="result-header">
<span class="result-icon">🌐</span>
<h2>扫描完成</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(chromeSelectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(chromeSelectedStats.sizeStr).unit }}</span>
</span>
<span class="stat-label">预计释放</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ chromeSelectedStats.count }}</span>
<span class="stat-label">用户资料数量</span>
</div>
</div>
<button class="btn-primary main-btn" @click="startBrowserClean('chrome')" :disabled="chromeState.isCleaning || !chromeSelectedStats.hasSelection">
{{ chromeState.isCleaning ? '正在清理...' : '立即清理' }}
</button>
</div>
<!-- 清理完成 -->
<div class="result-card done-card" v-else-if="chromeState.isDone && chromeState.cleanResult">
<div class="result-header">
<span class="result-icon success">🎉</span>
<h2>清理完成</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(chromeState.cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(chromeState.cleanResult.total_freed).unit }}</span>
</span>
<span class="stat-label">释放空间</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ chromeState.cleanResult.success_count }}</span>
<span class="stat-label">成功清理</span>
</div>
</div>
<button class="btn-secondary" @click="resetPageState">返回</button>
</div>
</template>
<!-- 微软浏览器视图 -->
<template v-else-if="activeTab === 'clean-browser-edge'">
<div class="scan-circle-container" v-if="!edgeState.scanResult && !edgeState.isDone">
<div class="scan-circle" :class="{ scanning: edgeState.isScanning }">
<div class="scan-inner" @click="!edgeState.isScanning && startBrowserScan('edge')">
<span v-if="!edgeState.isScanning" class="scan-btn-text">开始扫描</span>
<span v-else class="spinner"></span>
</div>
</div>
</div>
<div class="result-card" v-else-if="edgeState.scanResult && !edgeState.isDone">
<div class="result-header">
<span class="result-icon">🌐</span>
<h2>扫描完成</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(edgeSelectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(edgeSelectedStats.sizeStr).unit }}</span>
</span>
<span class="stat-label">预计释放</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ edgeSelectedStats.count }}</span>
<span class="stat-label">用户资料数量</span>
</div>
</div>
<button class="btn-primary main-btn" @click="startBrowserClean('edge')" :disabled="edgeState.isCleaning || !edgeSelectedStats.hasSelection">
{{ edgeState.isCleaning ? '正在清理...' : '立即清理' }}
</button>
</div>
<div class="result-card done-card" v-else-if="edgeState.isDone && edgeState.cleanResult">
<div class="result-header">
<span class="result-icon success">🎉</span>
<h2>清理完成</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(edgeState.cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(edgeState.cleanResult.total_freed).unit }}</span>
</span>
<span class="stat-label">释放空间</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ edgeState.cleanResult.success_count }}</span>
<span class="stat-label">成功清理</span>
</div>
</div>
<button class="btn-secondary" @click="resetPageState">返回</button>
</div>
</template>
</div>
<!-- 详情列表 -->
<div class="detail-list" v-if="activeTab === 'clean-browser-chrome' ? (chromeState.isScanning || chromeState.scanResult) && !chromeState.isDone : (edgeState.isScanning || edgeState.scanResult) && !edgeState.isDone">
<h3>用户资料列表</h3>
<div
class="detail-item"
v-for="profile in (activeTab === 'clean-browser-chrome' ? chromeState.scanResult?.profiles : edgeState.scanResult?.profiles) || []"
:key="profile.path_name"
@click="profile.enabled = !profile.enabled"
:class="{ disabled: !profile.enabled }"
>
<div class="item-info">
<label class="checkbox-container" @click.stop>
<input type="checkbox" v-model="profile.enabled">
<span class="checkmark"></span>
</label>
<span>{{ profile.name }}</span>
</div>
<span class="item-size">{{ profile.cache_size_str }}</span>
</div>
<div v-if="activeTab === 'clean-browser-chrome' ? chromeState.isScanning : edgeState.isScanning" class="scanning-placeholder">正在定位并分析浏览器用户资料...</div>
</div>
</section>
<!-- 3. 深度分析页面 -->
<section v-else-if="activeTab === 'clean-c-deep'" class="page-container full-width">
<div class="page-header">