diff --git a/Ink Canvas/Properties/Strings.en-US.resx b/Ink Canvas/Properties/Strings.en-US.resx
index d719c19b..5bf4f832 100644
--- a/Ink Canvas/Properties/Strings.en-US.resx
+++ b/Ink Canvas/Properties/Strings.en-US.resx
@@ -2658,4 +2658,130 @@ Hide
When enabled, the whiteboard toolbar Booth button launches Seewo Video Showcase (must be installed). When disabled, the built-in booth is used.
+
+ Storage
+
+
+ ICC CE total usage
+
+
+ Disk percentage
+
+
+ Calculating…
+
+
+ Calculation failed
+
+
+ Refresh
+
+
+ Open app folder
+
+
+ Category details
+
+
+ Clean
+
+
+ Core
+
+
+ Logs
+
+
+ Ink
+
+
+ Backups
+
+
+ Custom
+
+
+ Plugins
+
+
+ Update
+
+
+ Other
+
+
+ Core files
+
+
+ Includes .json configs, .enc usage stats, .exe / .dll executables and .dat data files.
+
+
+ Core files cannot be cleaned to keep the application functional.
+
+
+ Logs
+
+
+ .txt logs and crash reports under Logs / Crashs. Cleanable.
+
+
+ Ink
+
+
+ .icstk / .xml / screenshot .png files under Saves.
+
+
+ Backups
+
+
+ Settings / config backups (.json / .zip) under Backups.
+
+
+ Custom files
+
+
+ Custom icons and roll-call backgrounds (icons / backgrounds, .png).
+
+
+ Custom files are added by the user and won't be auto-cleaned. Manage them in their dedicated UI.
+
+
+ Plugins
+
+
+ .iccpp plugin packages under the plugins directory.
+
+
+ Use the plugins page to uninstall plugins.
+
+
+ Auto update
+
+
+ Installer packages and download cache under AutoUpdate. Refreshed each release.
+
+
+ Other
+
+
+ Files not classified into the categories above.
+
+
+ Confirm cleanup
+
+
+ All files in the "{0}" category will be permanently deleted. Continue?
+
+
+ Confirm again
+
+
+ Confirm again: are you sure you want to delete "{0}"?
+
+
+ Cleanup failed: {0}
+
+
+ View and clean storage used by ICC CE
+
\ No newline at end of file
diff --git a/Ink Canvas/Properties/Strings.resx b/Ink Canvas/Properties/Strings.resx
index 3a9e27e6..35004b2e 100644
--- a/Ink Canvas/Properties/Strings.resx
+++ b/Ink Canvas/Properties/Strings.resx
@@ -2707,4 +2707,130 @@
开启后,点击白板工具栏「展台」将打开希沃视频展台(需已安装);关闭则使用内置展台。
+
+ 存储管理
+
+
+ ICC CE 总占用
+
+
+ 占磁盘比例
+
+
+ 计算中…
+
+
+ 计算失败
+
+
+ 刷新
+
+
+ 打开应用目录
+
+
+ 分类详情
+
+
+ 清理
+
+
+ 核心文件
+
+
+ 日志
+
+
+ 墨迹
+
+
+ 备份
+
+
+ 自定义
+
+
+ 插件
+
+
+ 自动更新
+
+
+ 其他
+
+
+ 核心文件
+
+
+ 包含 .json 配置文件、.enc 使用统计、.exe 主程序、.dll 文件、.dat 数据文件等
+
+
+ 核心文件不可清理,以免影响应用正常运行。
+
+
+ 日志
+
+
+ 位于 Logs / Crashs 目录下的 .txt 日志与崩溃报告,可清理
+
+
+ 墨迹
+
+
+ 位于 Saves 目录下的 .icstk / .xml / 截图 .png 等墨迹文件
+
+
+ 备份
+
+
+ 位于 Backups 目录的设置 / 配置备份(.json / .zip)
+
+
+ 自定义文件
+
+
+ 自定义图标与点名背景图(icons / backgrounds 等 .png)
+
+
+ 自定义文件由用户手动添加,不会自动清理,如需删除请前往对应管理界面。
+
+
+ 插件
+
+
+ 位于 plugins 目录的 .iccpp 插件包
+
+
+ 插件请前往插件管理页面进行卸载。
+
+
+ 自动更新
+
+
+ AutoUpdate 目录中的安装包与下载缓存,每次新版本发布或更新时会被刷新
+
+
+ 其他
+
+
+ 未归类至上述项目的其他文件
+
+
+ 清理确认
+
+
+ 将永久删除「{0}」分类下的所有文件,操作不可恢复。\n是否继续?
+
+
+ 二次确认
+
+
+ 再次确认:确定要删除「{0}」吗?
+
+
+ 清理失败:{0}
+
+
+ 查看与清理 ICC CE 占用的存储空间
+
\ No newline at end of file
diff --git a/Ink Canvas/Windows/SettingsViews/Pages/HomePage.xaml b/Ink Canvas/Windows/SettingsViews/Pages/HomePage.xaml
index d860c397..d5a3d047 100644
--- a/Ink Canvas/Windows/SettingsViews/Pages/HomePage.xaml
+++ b/Ink Canvas/Windows/SettingsViews/Pages/HomePage.xaml
@@ -132,6 +132,13 @@
+
+
+
+
+
+
diff --git a/Ink Canvas/Windows/SettingsViews/Pages/StoragePage.xaml b/Ink Canvas/Windows/SettingsViews/Pages/StoragePage.xaml
new file mode 100644
index 00000000..bf5ba90e
--- /dev/null
+++ b/Ink Canvas/Windows/SettingsViews/Pages/StoragePage.xaml
@@ -0,0 +1,240 @@
+
+
+
+
+
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Ink Canvas/Windows/SettingsViews/Pages/StoragePage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/StoragePage.xaml.cs
new file mode 100644
index 00000000..f9096378
--- /dev/null
+++ b/Ink Canvas/Windows/SettingsViews/Pages/StoragePage.xaml.cs
@@ -0,0 +1,330 @@
+using Ink_Canvas.Helpers;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using Page = iNKORE.UI.WPF.Modern.Controls.Page;
+using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
+
+namespace Ink_Canvas.Windows.SettingsViews.Pages
+{
+ public partial class StoragePage : Page
+ {
+ // 已知子目录定义。其余顶层文件归入“核心文件”或“其他”。
+ private static readonly string[] LogDirs = { "Logs", "Crashs" };
+ private static readonly string[] InkDirs = { "Saves" };
+ private static readonly string[] BackupDirs = { "Backups" };
+ private static readonly string[] CustomDirs = { "icons", "backgrounds" };
+ private static readonly string[] PluginDirs = { "plugins", "Plugins" };
+ private static readonly string[] UpdateDirs = { "AutoUpdate" };
+ private static readonly string[] ConfigDirs = { "Configs" };
+
+ // 视为核心文件的扩展名(位于应用根目录下)
+ private static readonly string[] CoreFileExtensions =
+ {
+ ".exe", ".dll", ".json", ".enc", ".dat", ".config", ".pdb",
+ ".xml", ".manifest", ".runtimeconfig.json", ".deps.json"
+ };
+
+ private long _coreSize, _logsSize, _inkSize, _backupsSize,
+ _customSize, _pluginsSize, _updateSize, _otherSize;
+
+ public StoragePage()
+ {
+ InitializeComponent();
+ Loaded += StoragePage_Loaded;
+ }
+
+ private async void StoragePage_Loaded(object sender, RoutedEventArgs e)
+ {
+ await RefreshAsync();
+ }
+
+ private void BtnRefresh_Click(object sender, RoutedEventArgs e)
+ {
+ _ = RefreshAsync();
+ }
+
+ private void BtnOpenAppFolder_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = App.RootPath,
+ UseShellExecute = true
+ });
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"打开应用目录失败: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ private async Task RefreshAsync()
+ {
+ SetAllSizesText(LocalizationHelper.GetString("Storage_Calculating"));
+ try
+ {
+ await Task.Run(() => CalculateSizes());
+ UpdateUI();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"计算存储占用失败: {ex.Message}", LogHelper.LogType.Error);
+ SetAllSizesText(LocalizationHelper.GetString("Storage_CalculateFailed"));
+ }
+ }
+
+ private void CalculateSizes()
+ {
+ _coreSize = _logsSize = _inkSize = _backupsSize =
+ _customSize = _pluginsSize = _updateSize = _otherSize = 0;
+
+ string root = App.RootPath;
+ if (string.IsNullOrEmpty(root) || !Directory.Exists(root)) return;
+
+ // 顶层目录归类
+ foreach (var dir in SafeEnumerateDirectories(root))
+ {
+ string name = Path.GetFileName(dir);
+ long size = GetDirectorySize(dir);
+
+ if (LogDirs.Any(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)))
+ _logsSize += size;
+ else if (InkDirs.Any(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)))
+ _inkSize += size;
+ else if (BackupDirs.Any(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)))
+ _backupsSize += size;
+ else if (CustomDirs.Any(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)))
+ _customSize += size;
+ else if (PluginDirs.Any(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)))
+ _pluginsSize += size;
+ else if (UpdateDirs.Any(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)))
+ _updateSize += size;
+ else if (ConfigDirs.Any(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase)))
+ _coreSize += size;
+ else
+ _otherSize += size;
+ }
+
+ // 顶层文件按扩展名归类
+ foreach (var file in SafeEnumerateFiles(root))
+ {
+ long size = SafeFileLength(file);
+ string ext = Path.GetExtension(file).ToLowerInvariant();
+ if (CoreFileExtensions.Contains(ext))
+ _coreSize += size;
+ else
+ _otherSize += size;
+ }
+ }
+
+ private void UpdateUI()
+ {
+ long total = _coreSize + _logsSize + _inkSize + _backupsSize
+ + _customSize + _pluginsSize + _updateSize + _otherSize;
+
+ TotalSizeTextBlock.Text = FormatSize(total);
+
+ CoreSizeText.Text = FormatSize(_coreSize);
+ LogsSizeText.Text = FormatSize(_logsSize);
+ InkSizeText.Text = FormatSize(_inkSize);
+ BackupsSizeText.Text = FormatSize(_backupsSize);
+ CustomSizeText.Text = FormatSize(_customSize);
+ PluginsSizeText.Text = FormatSize(_pluginsSize);
+ UpdateSizeText.Text = FormatSize(_updateSize);
+ OtherSizeText.Text = FormatSize(_otherSize);
+
+ // 更新柱状图列宽
+ if (total > 0)
+ {
+ BarCoreCol.Width = new GridLength(_coreSize, GridUnitType.Star);
+ BarLogsCol.Width = new GridLength(_logsSize, GridUnitType.Star);
+ BarInkCol.Width = new GridLength(_inkSize, GridUnitType.Star);
+ BarBackupsCol.Width = new GridLength(_backupsSize, GridUnitType.Star);
+ BarCustomCol.Width = new GridLength(_customSize, GridUnitType.Star);
+ BarPluginsCol.Width = new GridLength(_pluginsSize, GridUnitType.Star);
+ BarUpdateCol.Width = new GridLength(_updateSize, GridUnitType.Star);
+ BarOtherCol.Width = new GridLength(_otherSize, GridUnitType.Star);
+ }
+ else
+ {
+ BarCoreCol.Width = BarLogsCol.Width = BarInkCol.Width =
+ BarBackupsCol.Width = BarCustomCol.Width =
+ BarPluginsCol.Width = BarUpdateCol.Width =
+ BarOtherCol.Width = new GridLength(0, GridUnitType.Star);
+ }
+
+ // 占磁盘比例
+ try
+ {
+ var driveLetter = Path.GetPathRoot(App.RootPath);
+ if (!string.IsNullOrEmpty(driveLetter))
+ {
+ var drive = new DriveInfo(driveLetter);
+ if (drive.IsReady && drive.TotalSize > 0)
+ {
+ double pct = (double)total / drive.TotalSize * 100.0;
+ DiskPercentTextBlock.Text = pct < 0.01 ? "<0.01 %" : $"{pct:0.##} %";
+ return;
+ }
+ }
+ }
+ catch { }
+ DiskPercentTextBlock.Text = "—";
+ }
+
+ private void SetAllSizesText(string text)
+ {
+ TotalSizeTextBlock.Text = text;
+ CoreSizeText.Text = text;
+ LogsSizeText.Text = text;
+ InkSizeText.Text = text;
+ BackupsSizeText.Text = text;
+ CustomSizeText.Text = text;
+ PluginsSizeText.Text = text;
+ UpdateSizeText.Text = text;
+ OtherSizeText.Text = text;
+ }
+
+ #region 清理操作
+
+ private void BtnCleanLogs_Click(object sender, RoutedEventArgs e)
+ {
+ CleanWithConfirm(LocalizationHelper.GetString("Storage_Logs_Header"), LogDirs, keepRoot: true);
+ }
+
+ private void BtnCleanInk_Click(object sender, RoutedEventArgs e)
+ {
+ CleanWithConfirm(LocalizationHelper.GetString("Storage_Ink_Header"), InkDirs, keepRoot: true);
+ }
+
+ private void BtnCleanBackups_Click(object sender, RoutedEventArgs e)
+ {
+ CleanWithConfirm(LocalizationHelper.GetString("Storage_Backups_Header"), BackupDirs, keepRoot: true);
+ }
+
+ private void BtnCleanUpdate_Click(object sender, RoutedEventArgs e)
+ {
+ CleanWithConfirm(LocalizationHelper.GetString("Storage_Update_Header"), UpdateDirs, keepRoot: true);
+ }
+
+ private async void CleanWithConfirm(string displayName, string[] subDirs, bool keepRoot)
+ {
+ var result = MessageBox.Show(
+ string.Format(LocalizationHelper.GetString("Storage_Confirm_Body"), displayName).Replace("\\n", "\n"),
+ LocalizationHelper.GetString("Storage_Confirm_Title"),
+ MessageBoxButton.OKCancel,
+ MessageBoxImage.Warning);
+ if (result != MessageBoxResult.OK) return;
+
+ var second = MessageBox.Show(
+ string.Format(LocalizationHelper.GetString("Storage_Confirm_Second_Body"), displayName),
+ LocalizationHelper.GetString("Storage_Confirm_Second_Title"),
+ MessageBoxButton.OKCancel,
+ MessageBoxImage.Warning);
+ if (second != MessageBoxResult.OK) return;
+
+ try
+ {
+ await Task.Run(() =>
+ {
+ foreach (var sub in subDirs)
+ {
+ string path = Path.Combine(App.RootPath, sub);
+ if (!Directory.Exists(path)) continue;
+
+ if (keepRoot)
+ {
+ foreach (var file in SafeEnumerateFiles(path, recursive: true))
+ TryDeleteFile(file);
+ foreach (var dir in SafeEnumerateDirectories(path).Reverse())
+ TryDeleteDirectory(dir);
+ }
+ else
+ {
+ TryDeleteDirectory(path);
+ }
+ }
+ });
+ LogHelper.WriteLogToFile($"已清理「{displayName}」分类");
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"清理「{displayName}」失败: {ex.Message}", LogHelper.LogType.Error);
+ MessageBox.Show(
+ string.Format(LocalizationHelper.GetString("Storage_CleanFailed"), ex.Message),
+ LocalizationHelper.GetString("Storage_Confirm_Title"),
+ MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+
+ await RefreshAsync();
+ }
+
+ #endregion
+
+ #region 工具方法
+
+ private static long GetDirectorySize(string path)
+ {
+ long total = 0;
+ try
+ {
+ foreach (var file in SafeEnumerateFiles(path, recursive: true))
+ total += SafeFileLength(file);
+ }
+ catch { }
+ return total;
+ }
+
+ private static System.Collections.Generic.IEnumerable SafeEnumerateDirectories(string path)
+ {
+ try { return Directory.EnumerateDirectories(path); }
+ catch { return System.Linq.Enumerable.Empty(); }
+ }
+
+ private static System.Collections.Generic.IEnumerable SafeEnumerateFiles(string path, bool recursive = false)
+ {
+ try
+ {
+ return Directory.EnumerateFiles(path, "*",
+ recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
+ }
+ catch { return System.Linq.Enumerable.Empty(); }
+ }
+
+ private static long SafeFileLength(string path)
+ {
+ try { return new FileInfo(path).Length; }
+ catch { return 0; }
+ }
+
+ private static void TryDeleteFile(string path)
+ {
+ try { File.Delete(path); } catch { }
+ }
+
+ private static void TryDeleteDirectory(string path)
+ {
+ try { Directory.Delete(path, true); } catch { }
+ }
+
+ private static string FormatSize(long bytes)
+ {
+ if (bytes <= 0) return "0 B";
+ string[] units = { "B", "KB", "MB", "GB", "TB" };
+ double size = bytes;
+ int u = 0;
+ while (size >= 1024 && u < units.Length - 1)
+ {
+ size /= 1024;
+ u++;
+ }
+ return u == 0 ? $"{(long)size} {units[u]}" : $"{size:0.##} {units[u]}";
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml
index 61ca0bbe..97a8db66 100644
--- a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml
+++ b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml
@@ -6,6 +6,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
+ xmlns:i18n="clr-namespace:Ink_Canvas.MarkupExtensions"
Title="InkCanvasForClass 设置"
Width="1138" Height="750"
MinWidth="270" MinHeight="220"
@@ -261,6 +262,17 @@
+
+
+
+
+
+
+