support clean orogress

This commit is contained in:
Julian Freeman
2026-04-17 12:25:40 -04:00
parent 54b8701644
commit 4e40fa9f80
11 changed files with 218 additions and 38 deletions

View File

@@ -2,8 +2,11 @@ use std::fs;
use std::path::{Path, PathBuf};
use crate::backend::fast_clean::clean_directory_contents;
use crate::backend::models::{BrowserProfile, BrowserScanResult, BrowserType, CleanResult};
use crate::backend::models::{
BrowserProfile, BrowserScanResult, BrowserType, CleanResult, ProjectCleanProgress,
};
use crate::backend::utils::{format_size, get_dir_size_simple};
use tauri::Emitter;
const BROWSER_CACHE_DIRS: &[&str] = &[
"Cache",
@@ -80,18 +83,23 @@ pub async fn run_browser_scan(browser: BrowserType) -> Result<BrowserScanResult,
pub async fn run_browser_clean(
browser: BrowserType,
profile_paths: Vec<String>,
app_handle: tauri::AppHandle,
) -> 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;
let mut approx_completed_bytes = 0;
let total_items = profile_paths.len() as u32;
for profile_dir in profile_paths {
for (index, profile_dir) in profile_paths.into_iter().enumerate() {
let profile_path = user_data_path.join(&profile_dir);
let mut profile_estimated_size = 0;
if profile_path.exists() {
for sub_dir in BROWSER_CACHE_DIRS {
let target = profile_path.join(sub_dir);
if target.exists() {
profile_estimated_size += get_dir_size_simple(&target);
let (freed, success, fail) = clean_directory_contents(&target, None);
total_freed += freed;
success_count += success;
@@ -99,6 +107,17 @@ pub async fn run_browser_clean(
}
}
}
approx_completed_bytes += profile_estimated_size;
let _ = app_handle.emit(
"browser-clean-progress",
ProjectCleanProgress {
completed_items: (index + 1) as u32,
total_items,
current_item: profile_dir,
approx_completed_bytes,
},
);
}
Ok(CleanResult {

View File

@@ -2,7 +2,9 @@ use std::fs;
use std::path::Path;
use std::time::{Duration, SystemTime};
use crate::backend::models::{CleanResult, CleaningConfig, FastScanResult, ScanItem};
use tauri::Emitter;
use crate::backend::models::{CleanResult, CleaningConfig, FastScanResult, ProjectCleanProgress, ScanItem};
use crate::backend::utils::format_size;
fn get_fast_cleaning_configs() -> Vec<CleaningConfig> {
@@ -89,22 +91,42 @@ fn get_dir_stats(path: &Path, filter_days: Option<u64>) -> (u64, u32) {
(size, count)
}
pub async fn run_fast_clean(selected_paths: Vec<String>) -> Result<CleanResult, String> {
let configs = get_fast_cleaning_configs();
pub async fn run_fast_clean(
selected_paths: Vec<String>,
app_handle: tauri::AppHandle,
) -> Result<CleanResult, String> {
let selected_configs: Vec<CleaningConfig> = get_fast_cleaning_configs()
.into_iter()
.filter(|config| selected_paths.contains(&config.path))
.collect();
let mut success_count = 0;
let mut fail_count = 0;
let mut total_freed = 0;
let mut approx_completed_bytes = 0;
let total_items = selected_configs.len() as u32;
for config in configs {
if selected_paths.contains(&config.path) {
let path = Path::new(&config.path);
if path.exists() {
let (freed, success, fail) = clean_directory_contents(path, config.filter_days);
total_freed += freed;
success_count += success;
fail_count += fail;
}
for (index, config) in selected_configs.into_iter().enumerate() {
let path = Path::new(&config.path);
let item_size = get_dir_stats(path, config.filter_days).0;
if path.exists() {
let (freed, success, fail) = clean_directory_contents(path, config.filter_days);
total_freed += freed;
success_count += success;
fail_count += fail;
}
approx_completed_bytes += item_size;
let _ = app_handle.emit(
"fast-clean-progress",
ProjectCleanProgress {
completed_items: (index + 1) as u32,
total_items,
current_item: config.name,
approx_completed_bytes,
},
);
}
Ok(CleanResult {

View File

@@ -18,6 +18,14 @@ pub struct ScanProgress {
pub current_path: String,
}
#[derive(Serialize, Clone)]
pub struct ProjectCleanProgress {
pub completed_items: u32,
pub total_items: u32,
pub current_item: String,
pub approx_completed_bytes: u64,
}
#[derive(Clone)]
pub struct CleaningConfig {
pub name: String,

View File

@@ -10,8 +10,11 @@ async fn start_fast_scan() -> backend::models::FastScanResult {
}
#[tauri::command]
async fn start_fast_clean(selected_paths: Vec<String>) -> Result<backend::models::CleanResult, String> {
backend::fast_clean::run_fast_clean(selected_paths).await
async fn start_fast_clean(
selected_paths: Vec<String>,
app_handle: tauri::AppHandle,
) -> Result<backend::models::CleanResult, String> {
backend::fast_clean::run_fast_clean(selected_paths, app_handle).await
}
#[tauri::command]
@@ -66,6 +69,7 @@ async fn start_browser_scan(browser: String) -> Result<backend::models::BrowserS
async fn start_browser_clean(
browser: String,
profiles: Vec<String>,
app_handle: tauri::AppHandle,
) -> Result<backend::models::CleanResult, String> {
let browser_type = if browser == "chrome" {
backend::models::BrowserType::Chrome
@@ -73,7 +77,7 @@ async fn start_browser_clean(
backend::models::BrowserType::Edge
};
backend::browser_clean::run_browser_clean(browser_type, profiles).await
backend::browser_clean::run_browser_clean(browser_type, profiles, app_handle).await
}
#[tauri::command]

View File

@@ -2,12 +2,14 @@ import { computed, ref } from "vue";
import {
startBrowserClean as runBrowserCleanCommand,
startBrowserScan as runBrowserScanCommand,
subscribeBrowserCleanProgress,
} from "../services/tauri/cleaner";
import type {
AlertOptions,
BrowserScanResult,
CleanResult,
ConfirmOptions,
ProjectCleanProgressPayload,
} from "../types/cleaner";
import { formatItemSize } from "../utils/format";
@@ -19,6 +21,13 @@ interface BrowserState {
cleanResult: CleanResult | null;
}
interface BrowserCleanProgressState {
completedItems: number;
totalItems: number;
currentItem: string;
approxCompletedBytes: number;
}
export function useBrowserClean(
browser: "chrome" | "edge",
showAlert: (options: AlertOptions) => void,
@@ -31,20 +40,50 @@ export function useBrowserClean(
scanResult: null,
cleanResult: null,
});
const cleanProgress = ref<BrowserCleanProgressState>({
completedItems: 0,
totalItems: 0,
currentItem: "",
approxCompletedBytes: 0,
});
const selectedStats = computed(() => {
const scanResult = state.value.scanResult;
if (!scanResult) return { sizeStr: "0 B", count: 0, hasSelection: false };
if (!scanResult) return { totalBytes: 0, sizeStr: "0 B", count: 0, hasSelection: false };
const enabledProfiles = scanResult.profiles.filter((profile) => profile.enabled);
const totalBytes = enabledProfiles.reduce((acc, profile) => acc + profile.cache_size, 0);
return {
totalBytes,
sizeStr: formatItemSize(totalBytes),
count: enabledProfiles.length,
hasSelection: enabledProfiles.length > 0,
};
});
const cleanProgressSizeStr = computed(() => formatSizeValue(cleanProgress.value.approxCompletedBytes));
function formatSizeValue(bytes: number) {
return formatItemSize(bytes);
}
function resetCleanProgress() {
cleanProgress.value = {
completedItems: 0,
totalItems: 0,
currentItem: "",
approxCompletedBytes: 0,
};
}
function handleCleanProgress(payload: ProjectCleanProgressPayload) {
cleanProgress.value = {
completedItems: payload.completed_items,
totalItems: payload.total_items,
currentItem: payload.current_item,
approxCompletedBytes: payload.approx_completed_bytes,
};
}
async function startScan() {
const current = state.value;
@@ -102,7 +141,9 @@ export function useBrowserClean(
return;
}
resetCleanProgress();
current.isCleaning = true;
const unlisten = await subscribeBrowserCleanProgress(handleCleanProgress);
try {
current.cleanResult = await runBrowserCleanCommand(browser, selectedProfiles);
current.isDone = true;
@@ -114,6 +155,7 @@ export function useBrowserClean(
type: "error",
});
} finally {
unlisten();
current.isCleaning = false;
}
}
@@ -138,11 +180,14 @@ export function useBrowserClean(
scanResult: null,
cleanResult: null,
};
resetCleanProgress();
}
return {
state,
selectedStats,
cleanProgress,
cleanProgressSizeStr,
startScan,
startClean,
toggleAllProfiles,

View File

@@ -1,10 +1,15 @@
import { computed, ref } from "vue";
import { startFastClean as runFastCleanCommand, startFastScan as runFastScanCommand } from "../services/tauri/cleaner";
import {
startFastClean as runFastCleanCommand,
startFastScan as runFastScanCommand,
subscribeFastCleanProgress,
} from "../services/tauri/cleaner";
import type {
AlertOptions,
CleanResult,
ConfirmOptions,
FastScanResult,
ProjectCleanProgressPayload,
} from "../types/cleaner";
import { formatItemSize } from "../utils/format";
@@ -17,6 +22,13 @@ interface FastState {
cleanResult: CleanResult | null;
}
interface FastCleanProgressState {
completedItems: number;
totalItems: number;
currentItem: string;
approxCompletedBytes: number;
}
export function useFastClean(
showAlert: (options: AlertOptions) => void,
requestConfirm: (options: ConfirmOptions) => Promise<boolean>,
@@ -29,21 +41,47 @@ export function useFastClean(
scanResult: null,
cleanResult: null,
});
const cleanProgress = ref<FastCleanProgressState>({
completedItems: 0,
totalItems: 0,
currentItem: "",
approxCompletedBytes: 0,
});
const selectedStats = computed(() => {
const scanResult = state.value.scanResult;
if (!scanResult) return { sizeStr: "0 B", count: 0, hasSelection: false };
if (!scanResult) return { totalBytes: 0, sizeStr: "0 B", count: 0, hasSelection: false };
const enabledItems = scanResult.items.filter((item) => item.enabled);
const totalBytes = enabledItems.reduce((acc, item) => acc + item.size, 0);
const totalCount = enabledItems.reduce((acc, item) => acc + item.count, 0);
return {
totalBytes,
sizeStr: formatItemSize(totalBytes),
count: totalCount,
hasSelection: enabledItems.length > 0,
};
});
const cleanProgressSizeStr = computed(() => formatItemSize(cleanProgress.value.approxCompletedBytes));
function resetCleanProgress() {
cleanProgress.value = {
completedItems: 0,
totalItems: 0,
currentItem: "",
approxCompletedBytes: 0,
};
}
function handleCleanProgress(payload: ProjectCleanProgressPayload) {
cleanProgress.value = {
completedItems: payload.completed_items,
totalItems: payload.total_items,
currentItem: payload.current_item,
approxCompletedBytes: payload.approx_completed_bytes,
};
}
async function startScan() {
const current = state.value;
@@ -104,7 +142,9 @@ export function useFastClean(
}
}
resetCleanProgress();
current.isCleaning = true;
const unlisten = await subscribeFastCleanProgress(handleCleanProgress);
try {
current.cleanResult = await runFastCleanCommand(selectedPaths);
current.isDone = true;
@@ -116,6 +156,7 @@ export function useFastClean(
type: "error",
});
} finally {
unlisten();
current.isCleaning = false;
}
}
@@ -129,11 +170,14 @@ export function useFastClean(
scanResult: null,
cleanResult: null,
};
resetCleanProgress();
}
return {
state,
selectedStats,
cleanProgress,
cleanProgressSizeStr,
startScan,
startClean,
reset,

View File

@@ -9,7 +9,7 @@ const props = defineProps<{
requestConfirm: (options: ConfirmOptions) => Promise<boolean>;
}>();
const { state, selectedStats, startScan, startClean, toggleAllProfiles, invertProfiles, reset } =
const { state, selectedStats, cleanProgress, cleanProgressSizeStr, startScan, startClean, toggleAllProfiles, invertProfiles, reset } =
useBrowserClean(props.browser, props.showAlert, props.requestConfirm);
const browserName = props.browser === "chrome" ? "谷歌浏览器" : "微软浏览器";
@@ -36,23 +36,26 @@ const browserName = props.browser === "chrome" ? "谷歌浏览器" : "微软浏
<div v-else-if="state.scanResult && !state.isDone" class="result-card">
<div class="result-header">
<span class="result-icon">🌍</span>
<h2>扫描完成</h2>
<span class="result-icon">{{ state.isCleaning ? "🧹" : "🌍" }}</span>
<h2>{{ state.isCleaning ? "正在清理" : "扫描完成" }}</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(selectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(selectedStats.sizeStr).unit }}</span>
{{ splitSize(state.isCleaning ? cleanProgressSizeStr : selectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(state.isCleaning ? cleanProgressSizeStr : selectedStats.sizeStr).unit }}</span>
</span>
<span class="stat-label">预计释放</span>
<span class="stat-label">{{ state.isCleaning ? "已处理约" : "预计释放" }}</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ selectedStats.count }}</span>
<span class="stat-label">用户资料数量</span>
<span class="stat-value">{{ state.isCleaning ? `${cleanProgress.completedItems}/${cleanProgress.totalItems}` : selectedStats.count }}</span>
<span class="stat-label">{{ state.isCleaning ? "已完成资料" : "用户资料数量" }}</span>
</div>
</div>
<p v-if="state.isCleaning" class="cleaning-note">
正在清理{{ cleanProgress.currentItem || "准备开始..." }}建议保持浏览器关闭以减少文件占用
</p>
<button class="btn-primary main-btn" :disabled="state.isCleaning || !selectedStats.hasSelection" @click="startClean">
{{ state.isCleaning ? "正在清理..." : "立即清理" }}
</button>
@@ -86,7 +89,7 @@ const browserName = props.browser === "chrome" ? "谷歌浏览器" : "微软浏
</div>
</div>
<div v-if="(state.isScanning || state.scanResult) && !state.isDone" class="detail-list">
<div v-if="(state.isScanning || state.scanResult) && !state.isDone && !state.isCleaning" class="detail-list">
<div class="list-header">
<h3>用户资料列表</h3>
<div class="list-actions">

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
requestConfirm: (options: ConfirmOptions) => Promise<boolean>;
}>();
const { state, selectedStats, startScan, startClean, reset } = useFastClean(
const { state, selectedStats, cleanProgress, cleanProgressSizeStr, startScan, startClean, reset } = useFastClean(
props.showAlert,
props.requestConfirm,
);
@@ -35,24 +35,28 @@ const { state, selectedStats, startScan, startClean, reset } = useFastClean(
<div v-else-if="state.scanResult && !state.isDone" class="result-card">
<div class="result-header">
<span class="result-icon">📋</span>
<h2>扫描完成</h2>
<span class="result-icon">{{ state.isCleaning ? "🧹" : "📋" }}</span>
<h2>{{ state.isCleaning ? "正在清理" : "扫描完成" }}</h2>
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">
{{ splitSize(selectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(selectedStats.sizeStr).unit }}</span>
{{ splitSize(state.isCleaning ? cleanProgressSizeStr : selectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(state.isCleaning ? cleanProgressSizeStr : selectedStats.sizeStr).unit }}</span>
</span>
<span class="stat-label">预计释放</span>
<span class="stat-label">{{ state.isCleaning ? "已处理约" : "预计释放" }}</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ selectedStats.count }}</span>
<span class="stat-label">文件数量</span>
<span class="stat-value">{{ state.isCleaning ? `${cleanProgress.completedItems}/${cleanProgress.totalItems}` : selectedStats.count }}</span>
<span class="stat-label">{{ state.isCleaning ? "已完成项目" : "文件数量" }}</span>
</div>
</div>
<p v-if="state.isCleaning" class="cleaning-note">
正在清理{{ cleanProgress.currentItem || "准备开始..." }}请稍候不要关闭程序
</p>
<button class="btn-primary main-btn" :disabled="state.isCleaning || !selectedStats.hasSelection" @click="startClean">
{{ state.isCleaning ? "正在清理..." : "立即清理" }}
</button>
@@ -87,7 +91,7 @@ const { state, selectedStats, startScan, startClean, reset } = useFastClean(
</div>
</div>
<div v-if="(state.isScanning || state.scanResult) && !state.isDone" class="detail-list">
<div v-if="(state.isScanning || state.scanResult) && !state.isDone && !state.isCleaning" class="detail-list">
<h3>清理项详情</h3>
<div
v-for="item in state.scanResult?.items || []"

View File

@@ -7,6 +7,7 @@ import type {
FastScanResult,
FileNode,
MemoryStats,
ProjectCleanProgressPayload,
ScanProgressPayload,
} from "../../types/cleaner";
@@ -54,6 +55,22 @@ export function subscribeScanProgress(
});
}
export function subscribeFastCleanProgress(
handler: (payload: ProjectCleanProgressPayload) => void,
) {
return listen<ProjectCleanProgressPayload>("fast-clean-progress", (event) => {
handler(event.payload);
});
}
export function subscribeBrowserCleanProgress(
handler: (payload: ProjectCleanProgressPayload) => void,
) {
return listen<ProjectCleanProgressPayload>("browser-clean-progress", (event) => {
handler(event.payload);
});
}
export function openInExplorer(path: string) {
return invoke("open_in_explorer", { path });
}

View File

@@ -136,6 +136,13 @@
width: 180px;
}
.cleaning-note {
margin: -8px 0 24px;
font-size: 13px;
line-height: 1.6;
color: var(--text-sec);
}
.scan-circle-container {
width: 200px;
height: 200px;

View File

@@ -64,6 +64,13 @@ export interface ScanProgressPayload {
current_path: string;
}
export interface ProjectCleanProgressPayload {
completed_items: number;
total_items: number;
current_item: string;
approx_completed_bytes: number;
}
export type ModalType = "info" | "success" | "error";
export type ModalMode = "alert" | "confirm";