Compare commits

...

11 Commits

Author SHA1 Message Date
Julian Freeman
616483b2a8 dynamic calc 2026-03-03 13:43:10 -04:00
Julian Freeman
4cbf6379ba support fast clean options 2026-03-03 13:35:44 -04:00
Julian Freeman
dbec37cd3d support context menu 2026-03-03 13:19:18 -04:00
Julian Freeman
e7030f0be1 modify file tree 2026-03-03 11:50:54 -04:00
Julian Freeman
add9227fa1 fix ui 2026-03-03 11:36:15 -04:00
Julian Freeman
ba7aa8cc22 change ui 2026-03-03 11:23:26 -04:00
Julian Freeman
39e36fb4e1 change ui color 2026-03-03 11:04:26 -04:00
Julian Freeman
588f7740a1 support uac 2026-03-03 10:51:50 -04:00
Julian Freeman
6ffb80f265 change icon 2026-03-03 10:36:43 -04:00
Julian Freeman
9fe3f606f4 upgrade fast clean 2026-03-03 10:16:00 -04:00
Julian Freeman
b63135a0e0 fix size result 2026-03-03 09:59:28 -04:00
63 changed files with 586 additions and 143 deletions

BIN
app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/app-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Windows 清理工具</title>
</head>

42
public/app-icon.svg Normal file
View File

@@ -0,0 +1,42 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg_gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00A4EF"/>
<stop offset="100%" stop-color="#0078D4"/>
</linearGradient>
<filter id="soft_glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="5" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<!-- 背景圆角矩形 -->
<rect width="512" height="512" rx="112" fill="url(#bg_gradient)"/>
<!-- 扫帚主体 (倾斜 30度) -->
<g transform="translate(256, 256) rotate(-30) translate(-256, -256)">
<!-- 扫帚手柄 -->
<rect x="236" y="80" width="40" height="200" rx="20" fill="white"/>
<!-- 扫帚头/刷柄连接处 -->
<path d="M160 280H352L362 320H150L160 280Z" fill="#F2F2F2"/>
<!-- 刷毛细节 -->
<path d="M150 320L130 420H382L362 320H150Z" fill="white"/>
<!-- 刷毛间的缝隙感 -->
<line x1="180" y1="330" x2="170" y2="410" stroke="#0078D4" stroke-width="4" stroke-linecap="round" opacity="0.2"/>
<line x1="220" y1="330" x2="215" y2="410" stroke="#0078D4" stroke-width="4" stroke-linecap="round" opacity="0.2"/>
<line x1="260" y1="330" x2="260" y2="410" stroke="#0078D4" stroke-width="4" stroke-linecap="round" opacity="0.2"/>
<line x1="300" y1="330" x2="305" y2="410" stroke="#0078D4" stroke-width="4" stroke-linecap="round" opacity="0.2"/>
<line x1="340" y1="330" x2="350" y2="410" stroke="#0078D4" stroke-width="4" stroke-linecap="round" opacity="0.2"/>
</g>
<!-- 清理后的闪烁火花 -->
<g filter="url(#soft_glow)">
<path d="M400 120L410 150L440 160L410 170L400 200L390 170L360 160L390 150L400 120Z" fill="white"/>
<path d="M440 220L445 235L460 240L445 245L440 260L435 245L420 240L435 235L440 220Z" fill="white" opacity="0.8"/>
</g>
<!-- 扫过的动态弧线 -->
<path d="M100 400C150 450 350 450 420 380" stroke="white" stroke-width="12" stroke-linecap="round" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,6 +0,0 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="WinCleaner"
type="win32"
/>
<description>Windows Cleaner</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 & 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
</application>
</compatibility>
<!-- 启用长路径支持 -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>

View File

@@ -1,3 +1,8 @@
fn main() {
tauri_build::build()
let mut windows = tauri_build::WindowsAttributes::new();
windows = windows.app_manifest(include_str!("admin.exe.manifest"));
tauri_build::try_build(
tauri_build::Attributes::new().windows_attributes(windows)
).expect("failed to run build script");
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -10,7 +10,7 @@ use std::os::windows::process::CommandExt;
// 存储全盘扫描后的结果
pub struct DiskState {
pub dir_sizes: Mutex<HashMap<String, u64>>,
pub file_info: Mutex<HashMap<String, (u64, u32)>>,
// pub file_info: Mutex<HashMap<String, (u64, u32)>>,
}
#[derive(Serialize, Clone)]
@@ -101,16 +101,11 @@ pub async fn disable_hibernation() -> Result<String, String> {
pub async fn run_full_scan(root_path: String, state: &DiskState) {
use jwalk::WalkDir;
let mut dir_sizes = HashMap::new();
let mut file_info = HashMap::new();
let root = Path::new(&root_path);
for entry in WalkDir::new(root).skip_hidden(false).into_iter().filter_map(|e| e.ok()) {
if entry.file_type.is_file() {
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
if let Some(parent) = entry.parent_path().to_str() {
let info = file_info.entry(parent.to_string()).or_insert((0, 0));
info.0 += size; info.1 += 1;
}
let mut current_path = entry.parent_path().to_path_buf();
while current_path.starts_with(root) {
let path_str = current_path.to_string_lossy().to_string();
@@ -121,81 +116,153 @@ pub async fn run_full_scan(root_path: String, state: &DiskState) {
}
}
let mut state_dirs = state.dir_sizes.lock().unwrap();
let mut state_files = state.file_info.lock().unwrap();
*state_dirs = dir_sizes; *state_files = file_info;
*state_dirs = dir_sizes;
}
pub fn get_children(parent_path: String, state: &DiskState) -> Vec<FileTreeNode> {
let dir_sizes = state.dir_sizes.lock().unwrap();
let file_info = state.file_info.lock().unwrap();
let mut results = Vec::new();
let parent_size = *dir_sizes.get(&parent_path).unwrap_or(&1);
if let Ok(entries) = fs::read_dir(Path::new(&parent_path)) {
for entry in entries.filter_map(|e| e.ok()) {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let path_str = entry.path().to_string_lossy().to_string();
if let Some(&size) = dir_sizes.get(&path_str) {
results.push(FileTreeNode {
name: entry.file_name().to_string_lossy().to_string(),
path: path_str, is_dir: true, size, size_str: format_size(size),
percent: (size as f64 / parent_size as f64 * 100.0) as f32,
file_count: 0, has_children: true,
});
}
let path = entry.path();
let path_str = path.to_string_lossy().to_string();
let is_dir = path.is_dir();
let name = entry.file_name().to_string_lossy().to_string();
let size = if is_dir {
*dir_sizes.get(&path_str).unwrap_or(&0)
} else {
entry.metadata().map(|m| m.len()).unwrap_or(0)
};
if size > 0 || !is_dir {
results.push(FileTreeNode {
name,
path: path_str,
is_dir,
size,
size_str: format_size(size),
percent: (size as f64 / parent_size as f64 * 100.0) as f32,
file_count: 0,
has_children: is_dir,
});
}
}
}
if let Some(&(size, count)) = file_info.get(&parent_path) {
if count > 0 {
results.push(FileTreeNode {
name: format!("[{} 个文件]", count),
path: format!("{}\\__files__", parent_path),
is_dir: false, size, size_str: format_size(size),
percent: (size as f64 / parent_size as f64 * 100.0) as f32,
file_count: count, has_children: false,
});
}
}
results.sort_by(|a, b| b.size.cmp(&a.size));
results
}
pub async fn open_explorer(path: String) -> Result<(), String> {
const CREATE_NO_WINDOW: u32 = 0x08000000;
// 使用 /select, 参数可以在打开目录的同时选中目标
Command::new("explorer.exe")
.arg("/select,")
.arg(&path)
.creation_flags(CREATE_NO_WINDOW)
.spawn()
.map_err(|e| e.to_string())?;
Ok(())
}
// --- 快速模式配置与逻辑 ---
#[derive(Clone)]
pub struct CleaningConfig {
pub name: String,
pub path: String,
pub filter_days: Option<u64>,
pub default_enabled: bool,
}
impl CleaningConfig {
fn new(name: &str, path: &str, filter_days: Option<u64>, default_enabled: bool) -> Self {
Self { name: name.into(), path: path.into(), filter_days, default_enabled }
}
}
/// 获取当前所有快速清理项的配置
fn get_fast_cleaning_configs() -> Vec<CleaningConfig> {
let mut configs = Vec::new();
// 1. 用户临时文件
if let Ok(t) = std::env::var("TEMP") {
configs.push(CleaningConfig::new("用户临时文件", &t, None, true));
}
// 2. 系统临时文件
configs.push(CleaningConfig::new("系统临时文件", "C:\\Windows\\Temp", None, true));
// 3. Windows 更新残留 (通常建议清理 10 天前的)
configs.push(CleaningConfig::new("Windows 更新残留", "C:\\Windows\\SoftwareDistribution\\Download", Some(10), true));
// 4. 内核转储文件
configs.push(CleaningConfig::new("内核转储文件", "C:\\Windows\\LiveKernelReports", None, false));
configs
}
#[derive(Serialize, Clone)]
pub struct ScanItem { pub name: String, pub path: String, pub size: u64, pub count: u32 }
pub struct ScanItem {
pub name: String,
pub path: String,
pub size: u64,
pub count: u32,
pub enabled: bool,
}
#[derive(Serialize)]
pub struct FastScanResult { pub items: Vec<ScanItem>, pub total_size: String, pub total_count: u32 }
pub struct FastScanResult { pub items: Vec<ScanItem>, total_size: String, total_count: u32 }
pub async fn run_fast_scan() -> FastScanResult {
let configs = get_fast_cleaning_configs();
let mut items = Vec::new();
let mut total_bytes = 0;
let mut total_count = 0;
let mut add_item = |name: &str, path: &str, filter: Option<u64>| {
let (size, count) = get_dir_stats(Path::new(path), filter);
items.push(ScanItem { name: name.into(), path: path.into(), size, count });
total_bytes += size; total_count += count;
};
if let Ok(t) = std::env::var("TEMP") { add_item("用户临时文件", &t, None); }
add_item("系统临时文件", "C:\\Windows\\Temp", None);
add_item("Windows 更新残留", "C:\\Windows\\SoftwareDistribution\\Download", Some(10));
add_item("传递优化缓存", "C:\\Windows\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Windows\\DeliveryOptimization", None);
FastScanResult { items, total_size: format_size(total_bytes), total_count }
for config in configs {
let (size, count) = get_dir_stats(Path::new(&config.path), config.filter_days);
items.push(ScanItem {
name: config.name,
path: config.path,
size,
count,
enabled: config.default_enabled,
});
total_bytes += size;
total_count += count;
}
FastScanResult {
items,
total_size: format_size(total_bytes),
total_count
}
}
fn get_dir_stats(path: &Path, filter_days: Option<u64>) -> (u64, u32) {
if !path.exists() { return (0, 0); }
let mut size = 0; let mut count = 0;
let mut size = 0;
let mut count = 0;
let now = SystemTime::now();
let dur = filter_days.map(|d| Duration::from_secs(d * 24 * 3600));
for entry in walkdir::WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let mut ok = true;
if let (Some(d), Ok(m)) = (dur, entry.metadata()) {
if let Ok(mod_t) = m.modified() {
if let Ok(el) = now.duration_since(mod_t) { if el < d { ok = false; } }
if let Ok(el) = now.duration_since(mod_t) {
if el < d { ok = false; }
}
}
}
if ok { size += entry.metadata().map(|m| m.len()).unwrap_or(0); count += 1; }
if ok {
size += entry.metadata().map(|m| m.len()).unwrap_or(0);
count += 1;
}
}
}
(size, count)
@@ -208,32 +275,21 @@ pub struct CleanResult {
pub fail_count: u32,
}
pub async fn run_fast_clean(is_simulation: bool) -> Result<CleanResult, String> {
if is_simulation {
return Ok(CleanResult {
total_freed: "0 B".into(),
success_count: 0,
fail_count: 0,
});
}
pub async fn run_fast_clean(selected_paths: Vec<String>) -> Result<CleanResult, String> {
let configs = get_fast_cleaning_configs();
let mut success_count = 0;
let mut fail_count = 0;
let mut total_freed: u64 = 0;
let mut target_paths = Vec::new();
if let Ok(t) = std::env::var("TEMP") { target_paths.push(t); }
target_paths.push("C:\\Windows\\Temp".into());
target_paths.push("C:\\Windows\\SoftwareDistribution\\Download".into());
target_paths.push("C:\\Windows\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Windows\\DeliveryOptimization".into());
for path_str in target_paths {
let path = Path::new(&path_str);
if path.exists() {
let (freed, s, f) = clean_directory_contents(path);
total_freed += freed;
success_count += s;
fail_count += f;
for config in configs {
if selected_paths.contains(&config.path) {
let path = Path::new(&config.path);
if path.exists() {
let (freed, s, f) = clean_directory_contents(path, config.filter_days);
total_freed += freed;
success_count += s;
fail_count += f;
}
}
}
@@ -244,10 +300,12 @@ pub async fn run_fast_clean(is_simulation: bool) -> Result<CleanResult, String>
})
}
fn clean_directory_contents(path: &Path) -> (u64, u32, u32) {
fn clean_directory_contents(path: &Path, filter_days: Option<u64>) -> (u64, u32, u32) {
let mut freed = 0;
let mut success = 0;
let mut fail = 0;
let now = SystemTime::now();
let dur = filter_days.map(|d| Duration::from_secs(d * 24 * 3600));
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.filter_map(|e| e.ok()) {
@@ -255,6 +313,15 @@ fn clean_directory_contents(path: &Path) -> (u64, u32, u32) {
let metadata = entry.metadata();
let size = metadata.as_ref().map(|m| m.len()).unwrap_or(0);
// 检查过滤逻辑 (如果设置了天数)
if let (Some(d), Ok(m)) = (dur, &metadata) {
if let Ok(mod_t) = m.modified() {
if let Ok(el) = now.duration_since(mod_t) {
if el < d { continue; }
}
}
}
if entry_path.is_file() {
if fs::remove_file(&entry_path).is_ok() {
freed += size;
@@ -264,15 +331,15 @@ fn clean_directory_contents(path: &Path) -> (u64, u32, u32) {
}
} else if entry_path.is_dir() {
// 递归清理子目录
let (f, s, fl) = clean_directory_contents(&entry_path);
let (f, s, fl) = clean_directory_contents(&entry_path, filter_days);
freed += f;
success += s;
fail += fl;
// 尝试删除已清空的目录
// 尝试删除已清空的目录 (如果它本身不是根清理目录且已过期)
if fs::remove_dir(&entry_path).is_ok() {
success += 1;
} else {
fail += 1;
// 目录可能因为包含未过期的文件而无法删除,这是正常的
}
}
}

View File

@@ -9,8 +9,8 @@ async fn start_fast_scan() -> cleaner::FastScanResult {
}
#[tauri::command]
async fn start_fast_clean(is_simulation: bool) -> Result<cleaner::CleanResult, String> {
cleaner::run_fast_clean(is_simulation).await
async fn start_fast_clean(selected_paths: Vec<String>) -> Result<cleaner::CleanResult, String> {
cleaner::run_fast_clean(selected_paths).await
}
#[tauri::command]
@@ -24,6 +24,11 @@ async fn get_tree_children(path: String, state: State<'_, cleaner::DiskState>) -
Ok(cleaner::get_children(path, &state))
}
#[tauri::command]
async fn open_in_explorer(path: String) -> Result<(), String> {
cleaner::open_explorer(path).await
}
// --- 高级清理命令 ---
#[tauri::command]
@@ -47,13 +52,14 @@ pub fn run() {
.plugin(tauri_plugin_opener::init())
.manage(cleaner::DiskState {
dir_sizes: Mutex::new(HashMap::new()),
file_info: Mutex::new(HashMap::new()),
// file_info: Mutex::new(HashMap::new()),
})
.invoke_handler(tauri::generate_handler![
start_fast_scan,
start_fast_clean,
start_full_disk_scan,
get_tree_children,
open_in_explorer,
clean_system_components,
clean_thumbnails,
disable_hibernation

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { openUrl } from "@tauri-apps/plugin-opener";
import pkg from "../package.json";
// --- 导航状态 ---
@@ -9,7 +10,7 @@ const activeTab = ref<Tab>('clean-c-fast');
const isCMenuOpen = ref(true);
// --- 数据结构 ---
interface ScanItem { name: string; path: string; size: number; count: number; }
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 FileNode {
@@ -28,6 +29,21 @@ 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 totalBytes = enabledItems.reduce((acc, i) => acc + i.size, 0);
const totalCount = enabledItems.reduce((acc, i) => acc + i.count, 0);
return {
sizeStr: formatItemSize(totalBytes),
count: totalCount,
hasSelection: enabledItems.length > 0
};
});
// --- 弹窗状态 ---
const showModal = ref(false);
const modalTitle = ref("");
@@ -41,6 +57,55 @@ function showAlert(title: string, message: string, type: 'info' | 'success' | 'e
showModal.value = true;
}
// --- 右键菜单状态 ---
const contextMenu = ref({
show: false,
x: 0,
y: 0,
node: null as FileNode | null
});
function handleContextMenu(e: MouseEvent, node: FileNode) {
e.preventDefault();
contextMenu.value = {
show: true,
x: e.clientX,
y: e.clientY,
node: node
};
// 监听一次性点击以关闭菜单
const close = () => {
contextMenu.value.show = false;
window.removeEventListener('click', close);
};
window.addEventListener('click', close);
}
async function openNodeInExplorer() {
if (contextMenu.value.node) {
try {
await invoke("open_in_explorer", { path: contextMenu.value.node.path });
} catch (err) {
console.error(err);
}
}
}
async function searchNode(type: 'google' | 'perplexity') {
if (contextMenu.value.node) {
const name = contextMenu.value.node.name;
const query = encodeURIComponent(`Windows 文件或目录 ${name} 是做什么用的,我可以删除吗`);
const url = type === 'google'
? `https://www.google.com/search?q=${query}`
: `https://www.perplexity.ai/?q=${query}`;
try {
await openUrl(url);
} catch (err) {
console.error(err);
}
}
}
// 高级模式特有
const expandedAdvanced = ref<string | null>(null);
const advLoading = ref<Record<string, boolean>>({});
@@ -65,10 +130,19 @@ async function startFastScan() {
}
async function startFastClean() {
if (isCleaning.value) return;
if (isCleaning.value || !fastScanResult.value) return;
const selectedPaths = fastScanResult.value.items
.filter(item => item.enabled)
.map(item => item.path);
if (selectedPaths.length === 0) {
showAlert("未选择任何项", "请至少勾选一个需要清理的项目。", 'info');
return;
}
isCleaning.value = true;
try {
const res = await invoke<CleanResult>("start_fast_clean", { isSimulation: false });
const res = await invoke<CleanResult>("start_fast_clean", { selectedPaths });
cleanResult.value = res;
isCleanDone.value = true;
fastScanResult.value = null;
@@ -149,6 +223,15 @@ function formatItemSize(bytes: number): string {
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function splitSize(sizeStr: string | number) {
const str = String(sizeStr);
const parts = str.split(' ');
if (parts.length === 2) {
return { value: parts[0], unit: parts[1] };
}
return { value: str, unit: '' };
}
</script>
<template>
@@ -244,17 +327,24 @@ function formatItemSize(bytes: number): string {
</div>
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">{{ fastScanResult.total_size }}</span>
<span class="stat-value">
{{ splitSize(selectedStats.sizeStr).value }}
<span class="unit">{{ splitSize(selectedStats.sizeStr).unit }}</span>
</span>
<span class="stat-label">预计释放</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ fastScanResult.total_count }}</span>
<span class="stat-value">{{ selectedStats.count }}</span>
<span class="stat-label">文件数量</span>
</div>
</div>
<button class="btn-primary main-btn" @click="startFastClean" :disabled="isCleaning">
<button
class="btn-primary main-btn"
@click="startFastClean"
:disabled="isCleaning || !selectedStats.hasSelection"
>
{{ isCleaning ? '正在清理...' : '立即清理' }}
</button>
</div>
@@ -268,7 +358,10 @@ function formatItemSize(bytes: number): string {
<div class="result-stats">
<div class="stat-item">
<span class="stat-value">{{ cleanResult.total_freed }}</span>
<span class="stat-value">
{{ splitSize(cleanResult.total_freed).value }}
<span class="unit">{{ splitSize(cleanResult.total_freed).unit }}</span>
</span>
<span class="stat-label">释放空间</span>
</div>
<div class="stat-divider"></div>
@@ -288,8 +381,20 @@ function formatItemSize(bytes: number): string {
<div class="detail-list" v-if="(isScanning || fastScanResult) && !isCleanDone">
<h3>清理项详情</h3>
<div class="detail-item" v-for="item in fastScanResult?.items || []" :key="item.path">
<span>{{ item.name }}</span>
<div
class="detail-item"
v-for="item in fastScanResult?.items || []"
:key="item.path"
@click="item.enabled = !item.enabled"
:class="{ disabled: !item.enabled }"
>
<div class="item-info">
<label class="checkbox-container" @click.stop>
<input type="checkbox" v-model="item.enabled">
<span class="checkmark"></span>
</label>
<span>{{ item.name }}</span>
</div>
<span class="item-size">{{ formatItemSize(item.size) }}</span>
</div>
<div v-if="isScanning" class="scanning-placeholder">正在深度扫描文件系统...</div>
@@ -403,7 +508,7 @@ function formatItemSize(bytes: number): string {
<p>正在分析数百万个文件请稍候...</p>
</div>
<div v-else>
<div v-else class="tree-content-wrapper">
<div class="tree-header">
<span class="col-name">文件/文件夹名称</span>
<span class="col-size">大小</span>
@@ -416,6 +521,7 @@ function formatItemSize(bytes: number): string {
class="tree-row"
:class="{ 'is-file': !node.is_dir }"
:style="{ paddingLeft: (node.level * 20 + 16) + 'px' }"
@contextmenu="handleContextMenu($event, node)"
>
<div class="col-name" @click="toggleNode(index)">
<span v-if="node.is_dir" class="node-toggle">
@@ -447,6 +553,28 @@ function formatItemSize(bytes: number): string {
</section>
</main>
<!-- 右键菜单 -->
<div
v-if="contextMenu.show"
class="context-menu shadow-card"
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
@click.stop
>
<div class="menu-item" @click="openNodeInExplorer">
<span class="menu-icon">📂</span>
<span>在文件夹中打开</span>
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="searchNode('google')">
<span class="menu-icon">🌐</span>
<span> Google 搜索</span>
</div>
<div class="menu-item" @click="searchNode('perplexity')">
<span class="menu-icon">🤖</span>
<span>询问 Perplexity</span>
</div>
</div>
<!-- 自定义弹窗 -->
<div class="modal-overlay" v-if="showModal" @click.self="showModal = false">
<div class="modal-card" :class="modalType">
@@ -501,74 +629,88 @@ body {
height: 100%;
}
/* --- 侧边栏优化 --- */
/* --- 侧边栏优化 (清爽浅蓝重构) --- */
.sidebar {
width: 250px;
background-color: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
width: 240px;
background-color: #F8FAFD;
border-right: 1px solid #E9EFF6;
display: flex;
flex-direction: column;
padding: 32px 0;
padding: 40px 0 32px;
z-index: 10;
}
.sidebar-header { padding: 0 28px 36px; }
.brand { font-size: 20px; font-weight: 700; color: var(--text-main); letter-spacing: -0.3px; }
.sidebar-nav { flex: 1; }
.nav-item, .nav-item-header {
padding: 10px 24px;
margin: 2px 12px;
padding: 12px 20px;
margin: 4px 12px;
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
color: #424245;
color: #4A5568;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
border-radius: 12px;
}
.nav-item:hover, .nav-item-header:hover {
background-color: #F5F5F7;
background-color: #EDF2F7;
color: #2D3748;
}
.nav-item.active {
background-color: #F0F7FF;
color: var(--primary-color);
background-color: #EBF4FF;
color: #04448a;
font-weight: 600;
}
.icon { margin-right: 10px; font-size: 16px; width: 20px; text-align: center; }
.icon { margin-right: 12px; font-size: 18px; width: 24px; text-align: center; }
.arrow {
margin-left: auto;
transition: transform 0.3s;
font-size: 10px;
color: #C1C1C1;
color: #A0AEC0;
}
.arrow.open { transform: rotate(180deg); }
.arrow.open { transform: rotate(180deg); color: #4A5568; }
.nav-sub-items { margin-bottom: 8px; }
.nav-sub-item {
padding: 8px 24px 8px 54px;
margin: 1px 12px;
padding: 10px 20px 10px 52px;
margin: 2px 12px;
cursor: pointer;
font-size: 13px;
color: #6E6E73;
color: #718096;
transition: all 0.2s;
border-radius: 6px;
}
.nav-sub-item:hover { background-color: #F5F5F7; color: var(--text-main); }
.nav-sub-item.active {
color: var(--primary-color);
font-weight: 600;
background-color: #F0F7FF;
border-radius: 10px;
}
.sidebar-footer { padding: 16px 28px; border-top: 1px solid var(--border-color); }
.version { font-size: 11px; color: var(--text-sec); font-weight: 500; }
.nav-sub-item:hover {
background-color: #EDF2F7;
color: #2D3748;
}
.nav-sub-item.active {
color: #007AFF;
font-weight: 600;
background-color: #EBF4FF;
}
.sidebar-footer {
padding: 20px 24px;
border-top: 1px solid #E9EFF6;
}
.version {
font-size: 11px;
color: #A0AEC0;
font-weight: 600;
letter-spacing: 0.5px;
}
/* --- 内容区 --- */
.content {
@@ -577,10 +719,19 @@ body {
overflow-y: auto;
height: 100%;
}
/* 当处于全屏模式(深度分析)时,内容区本身不滚动,让内部树形滚动 */
.content:has(.page-container.full-width) {
overflow-y: hidden;
}
.page-container { max-width: 800px; margin: 0 auto; padding-bottom: 60px; transition: max-width 0.4s ease; }
.page-container.full-width { max-width: 1400px; }
.page-header { margin-bottom: 40px; text-align: center; }
.page-container { max-width: 800px; margin: 0 auto; padding-bottom: 10px; transition: max-width 0.4s ease; }
.page-container.full-width {
max-width: 1400px;
height: calc(100vh - 96px);
display: flex;
flex-direction: column;
}
.page-header { margin-bottom: 40px; text-align: center; flex-shrink: 0; }
.page-header h1 { font-size: 28px; font-weight: 700; margin-bottom: 8px; color: var(--text-main); }
.page-header p { color: var(--text-sec); font-size: 15px; }
@@ -596,6 +747,7 @@ body {
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--btn-shadow);
flex-shrink: 0;
}
.btn-primary:hover { background-color: var(--primary-hover); transform: translateY(-1.5px); box-shadow: 0 6px 16px rgba(0, 122, 255, 0.35); }
.btn-primary:active { transform: translateY(0); }
@@ -634,28 +786,46 @@ body {
.result-stats {
display: flex;
justify-content: center;
align-items: baseline;
align-items: center;
margin-bottom: 40px;
}
.stat-item { flex: 1; }
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
display: block;
font-size: 48px;
display: flex;
align-items: baseline;
justify-content: center;
font-size: 44px;
font-weight: 800;
color: var(--primary-color);
letter-spacing: -1px;
line-height: 1.1;
letter-spacing: -1.5px;
line-height: 1;
margin-bottom: 8px;
white-space: nowrap;
}
.stat-label { font-size: 15px; color: var(--text-sec); font-weight: 500; }
.stat-value .unit {
font-size: 16px;
font-weight: 700;
margin-left: 4px;
letter-spacing: 0;
opacity: 0.9;
}
.stat-label { font-size: 14px; color: var(--text-sec); font-weight: 500; }
.stat-divider {
width: 1px;
height: 60px;
height: 48px;
background-color: #F2F2F7;
margin: 0 40px;
align-self: center;
margin: 0 20px;
flex-shrink: 0;
}
.main-btn { width: 220px; }
@@ -745,16 +915,25 @@ body {
.detail-content li { margin-bottom: 8px; line-height: 1.4; }
/* --- 磁盘树样式 --- */
.advanced-actions { display: flex; justify-content: center; margin-bottom: 32px; }
.advanced-actions { display: flex; justify-content: center; margin-bottom: 32px; flex-shrink: 0; }
.tree-table-container {
background: #fff;
border-radius: 24px;
overflow: hidden;
margin-top: 32px;
min-height: 400px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
box-shadow: var(--card-shadow);
border: 1px solid rgba(0,0,0,0.02);
}
.tree-content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.tree-header {
display: flex;
background: #F9FAFB;
@@ -765,8 +944,12 @@ body {
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.tree-body {
flex: 1;
overflow-y: auto;
}
.tree-body { max-height: 550px; overflow-y: auto; }
.tree-row {
display: flex;
align-items: center;
@@ -782,13 +965,17 @@ body {
flex: 2;
display: flex;
align-items: center;
font-weight: 500;
min-width: 0;
}
.node-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
text-overflow: ellipsis;
flex: 1;
}
.col-size { width: 100px; text-align: right; font-weight: 600; color: var(--text-main); }
.col-graph { width: 180px; display: flex; align-items: center; gap: 12px; padding-left: 32px; }
.col-size { width: 100px; text-align: right; font-weight: 600; color: var(--text-main); flex-shrink: 0; }
.col-graph { width: 180px; display: flex; align-items: center; gap: 12px; padding-left: 32px; flex-shrink: 0; }
.mini-bar-bg { flex: 1; height: 6px; background: #F0F0F2; border-radius: 3px; overflow: hidden; }
.mini-bar-fill {
@@ -825,14 +1012,63 @@ body {
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #F5F5F7;
font-size: 14px;
color: #424245;
transition: transform 0.2s ease;
transition: all 0.2s ease;
cursor: pointer;
}
.detail-item:hover { transform: translateX(4px); }
.detail-item:hover { background-color: #FAFAFB; padding-left: 8px; padding-right: 8px; border-radius: 8px; }
.detail-item.disabled { opacity: 0.5; }
.detail-item:last-child { border-bottom: none; }
.item-info { display: flex; align-items: center; gap: 12px; }
/* --- 自定义复选框 --- */
.checkbox-container {
display: block;
position: relative;
width: 20px;
height: 20px;
cursor: pointer;
user-select: none;
}
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0; width: 0;
}
.checkmark {
position: absolute;
top: 0; left: 0;
height: 20px; width: 20px;
background-color: #F2F2F7;
border-radius: 6px;
transition: all 0.2s;
border: 1px solid #E5E5E7;
}
.checkbox-container:hover input ~ .checkmark { background-color: #E5E5E7; }
.checkbox-container input:checked ~ .checkmark { background-color: var(--primary-color); border-color: var(--primary-color); }
.checkmark:after {
content: "";
position: absolute;
display: none;
left: 6px; top: 1px;
width: 5px; height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-container input:checked ~ .checkmark:after { display: block; }
.item-size { font-weight: 600; color: var(--primary-color); }
.placeholder-page { padding-top: 120px; text-align: center; color: var(--text-sec); }
@@ -878,4 +1114,41 @@ body {
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes modalIn { from { opacity: 0; transform: scale(0.9) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } }
/* --- 右键菜单样式 --- */
.context-menu {
position: fixed;
background: white;
min-width: 180px;
border-radius: 12px;
padding: 6px;
z-index: 2000;
border: 1px solid rgba(0,0,0,0.08);
animation: fadeIn 0.1s ease;
}
.menu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
font-size: 13px;
color: var(--text-main);
cursor: pointer;
border-radius: 8px;
transition: background 0.15s;
}
.menu-item:hover {
background-color: #F2F2F7;
color: var(--primary-color);
}
.menu-icon { font-size: 16px; }
.menu-divider {
height: 1px;
background: #F5F5F7;
margin: 4px 0;
}
</style>

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B