77using Microsoft . UI . Xaml . Controls ;
88using Microsoft . UI . Xaml . Markup ;
99using Microsoft . UI . Xaml . XamlTypeInfo ;
10+ using DialogResult = System . Windows . Forms . DialogResult ;
11+ using OpenFileDialog = System . Windows . Forms . OpenFileDialog ;
12+ using SaveFileDialog = System . Windows . Forms . SaveFileDialog ;
1013using Windows . UI . ViewManagement ;
1114
1215namespace Inputor . WinUI ;
@@ -44,7 +47,11 @@ public App()
4447 StatsStore = new StatsStore ( _dataDirectory ) ;
4548 StatsStore . SetStatus ( StatusText . StatisticsSourceFallbackToDefault ( ) , StatsStore . CurrentAppName , StatsStore . IsCurrentTargetSupported , StatsStore . CurrentProcessName ) ;
4649 }
50+
51+ MigrateLegacyStatisticsSourceToDefault ( ) ;
52+
4753 Exporter = new CsvExportService ( AppVariant . GetExportDirectory ( ) ) ;
54+ BackupArchives = new BackupArchiveService ( ) ;
4855 AutoStartService = new AutoStartService ( AppVariant . AutoStartEntryName ) ;
4956 AutoStartService . Apply ( Settings . StartWithWindows ) ;
5057 MonitoringService = new MonitoringService ( StatsStore , Settings ) ;
@@ -66,6 +73,8 @@ public App()
6673
6774 public CsvExportService Exporter { get ; }
6875
76+ public BackupArchiveService BackupArchives { get ; }
77+
6978 public AutoStartService AutoStartService { get ; }
7079
7180 public MonitoringService MonitoringService { get ; }
@@ -229,6 +238,106 @@ public void ClearIconCache()
229238 }
230239 }
231240
241+ public void OpenDataDirectory ( )
242+ {
243+ try
244+ {
245+ var path = AppVariant . GetDataDirectory ( ) ;
246+ Directory . CreateDirectory ( path ) ;
247+ Process . Start ( new ProcessStartInfo
248+ {
249+ FileName = path ,
250+ UseShellExecute = true
251+ } ) ;
252+ StatsStore . SetStatus ( StatusText . DataDirectoryOpened ( path ) , StatsStore . CurrentAppName , StatsStore . IsCurrentTargetSupported , StatsStore . CurrentProcessName ) ;
253+ }
254+ catch ( Exception exception )
255+ {
256+ StartupDiagnostics . Log ( $ "OpenDataDirectory failed: { exception } ") ;
257+ StatsStore . SetStatus ( StatusText . DataDirectoryOpenFailed ( exception . Message ) , StatsStore . CurrentAppName , StatsStore . IsCurrentTargetSupported , StatsStore . CurrentProcessName ) ;
258+ }
259+ }
260+
261+ public void ExportBackupArchive ( )
262+ {
263+ try
264+ {
265+ Directory . CreateDirectory ( AppVariant . GetBackupDirectory ( ) ) ;
266+ using var dialog = new SaveFileDialog
267+ {
268+ AddExtension = true ,
269+ DefaultExt = "zip" ,
270+ Filter = "ZIP archives (*.zip)|*.zip" ,
271+ InitialDirectory = AppVariant . GetBackupDirectory ( ) ,
272+ FileName = $ "inputor-backup-{ DateTime . Now : yyyyMMdd-HHmmss} .zip",
273+ OverwritePrompt = true ,
274+ RestoreDirectory = true ,
275+ Title = AppStrings . Get ( "Settings.Dialog.ExportBackupArchiveTitle" )
276+ } ;
277+
278+ if ( dialog . ShowDialog ( ) != DialogResult . OK || string . IsNullOrWhiteSpace ( dialog . FileName ) )
279+ {
280+ return ;
281+ }
282+
283+ var archivePath = BackupArchives . Export ( dialog . FileName , Settings , StatsStore . CurrentSourcePath ) ;
284+ StatsStore . SetStatus ( StatusText . BackupArchiveExported ( archivePath ) , StatsStore . CurrentAppName , StatsStore . IsCurrentTargetSupported , StatsStore . CurrentProcessName ) ;
285+ }
286+ catch ( Exception exception )
287+ {
288+ StartupDiagnostics . Log ( $ "ExportBackupArchive failed: { exception } ") ;
289+ StatsStore . SetStatus ( StatusText . BackupArchiveExportFailed ( exception . Message ) , StatsStore . CurrentAppName , StatsStore . IsCurrentTargetSupported , StatsStore . CurrentProcessName ) ;
290+ }
291+ }
292+
293+ public void RestoreBackupArchive ( )
294+ {
295+ try
296+ {
297+ Directory . CreateDirectory ( AppVariant . GetBackupDirectory ( ) ) ;
298+ using var dialog = new OpenFileDialog
299+ {
300+ CheckFileExists = true ,
301+ Filter = "ZIP archives (*.zip)|*.zip" ,
302+ InitialDirectory = AppVariant . GetBackupDirectory ( ) ,
303+ RestoreDirectory = true ,
304+ Title = AppStrings . Get ( "Settings.Dialog.RestoreBackupArchiveTitle" )
305+ } ;
306+
307+ if ( dialog . ShowDialog ( ) != DialogResult . OK || string . IsNullOrWhiteSpace ( dialog . FileName ) )
308+ {
309+ return ;
310+ }
311+
312+ var previousSettings = CloneSettings ( Settings ) ;
313+ var previousStatsSourcePath = StatsStore . CurrentSourcePath ;
314+ var previousStatsJson = File . Exists ( previousStatsSourcePath )
315+ ? File . ReadAllText ( previousStatsSourcePath )
316+ : string . Empty ;
317+ var payload = BackupArchives . Load ( dialog . FileName ) ;
318+ StatsStore . ValidateSourceJson ( payload . StatsJson ) ;
319+
320+ try
321+ {
322+ ApplySettingsSnapshot ( payload . Settings , forceDefaultStatisticsSource : true ) ;
323+ StatsStore . RestoreSource ( string . Empty , payload . StatsJson ) ;
324+ MonitoringService . ResetTrackingState ( ) ;
325+ StatsStore . SetDebugCaptureEnabled ( Settings . DebugCaptureEnabled ) ;
326+ StatsStore . SetStatus ( StatusText . BackupArchiveRestored ( dialog . FileName ) , StatsStore . CurrentAppName , StatsStore . IsCurrentTargetSupported , StatsStore . CurrentProcessName ) ;
327+ }
328+ catch
329+ {
330+ RollbackRestoredBackup ( previousSettings , previousStatsSourcePath , previousStatsJson ) ;
331+ throw ;
332+ }
333+ }
334+ catch ( Exception exception )
335+ {
336+ StartupDiagnostics . Log ( $ "RestoreBackupArchive failed: { exception } ") ;
337+ StatsStore . SetStatus ( StatusText . BackupArchiveRestoreFailed ( exception . Message ) , StatsStore . CurrentAppName , StatsStore . IsCurrentTargetSupported , StatsStore . CurrentProcessName ) ;
338+ }
339+ }
340+
232341 public void SwitchStatisticsSource ( string ? sourcePath )
233342 {
234343 var previousSourcePath = StatsStore . CurrentSourcePath ;
@@ -346,6 +455,93 @@ private void ApplyThemeMode()
346455 MainWindow ? . ApplyThemeMode ( AppStrings . ResolveThemeMode ( Settings . ThemeMode ) ) ;
347456 }
348457
458+ private void MigrateLegacyStatisticsSourceToDefault ( )
459+ {
460+ if ( string . IsNullOrWhiteSpace ( Settings . StatisticsSourcePath ) )
461+ {
462+ return ;
463+ }
464+
465+ var currentSourcePath = StatsStore . CurrentSourcePath ;
466+ var defaultSourcePath = StatsStore . DefaultSourcePath ;
467+ try
468+ {
469+ if ( ! string . Equals ( Path . GetFullPath ( currentSourcePath ) , Path . GetFullPath ( defaultSourcePath ) , StringComparison . OrdinalIgnoreCase ) )
470+ {
471+ var legacyStatsJson = File . Exists ( currentSourcePath )
472+ ? File . ReadAllText ( currentSourcePath )
473+ : string . Empty ;
474+ StatsStore . ValidateSourceJson ( legacyStatsJson ) ;
475+ StatsStore . RestoreSource ( string . Empty , legacyStatsJson ) ;
476+ }
477+
478+ Settings . StatisticsSourcePath = string . Empty ;
479+ SettingsService . Save ( Settings ) ;
480+ StartupDiagnostics . Log ( $ "Migrated legacy statistics source to default local storage: { currentSourcePath } -> { defaultSourcePath } ") ;
481+ StatsStore . SetStatus ( StatusText . LegacyStatisticsSourceMigratedToDefault ( defaultSourcePath ) , StatsStore . CurrentAppName , StatsStore . IsCurrentTargetSupported , StatsStore . CurrentProcessName ) ;
482+ }
483+ catch ( Exception exception )
484+ {
485+ StartupDiagnostics . Log ( $ "Legacy statistics source migration failed: { exception } ") ;
486+ StatsStore . SetStatus ( StatusText . LegacyStatisticsSourceMigrationFailed ( exception . Message ) , StatsStore . CurrentAppName , StatsStore . IsCurrentTargetSupported , StatsStore . CurrentProcessName ) ;
487+ }
488+ }
489+
490+ private void ApplySettingsSnapshot ( AppSettings settingsSnapshot , bool forceDefaultStatisticsSource )
491+ {
492+ Settings . StartWithWindows = settingsSnapshot . StartWithWindows ;
493+ Settings . PrivacyMode = settingsSnapshot . PrivacyMode ;
494+ Settings . DebugCaptureEnabled = settingsSnapshot . DebugCaptureEnabled ;
495+ Settings . ThemeMode = settingsSnapshot . ThemeMode ;
496+ Settings . StatisticsSourcePath = forceDefaultStatisticsSource ? string . Empty : settingsSnapshot . StatisticsSourcePath ;
497+ Settings . Language = settingsSnapshot . Language ;
498+ Settings . ExcludedApps = settingsSnapshot . ExcludedApps ;
499+ Settings . AppTagMappings = settingsSnapshot . GetNormalizedTagMappings ( )
500+ . Select ( mapping => new AppTagMapping
501+ {
502+ AppName = mapping . AppName ,
503+ Tags = mapping . Tags . ToList ( )
504+ } )
505+ . ToList ( ) ;
506+ SaveSettings ( ) ;
507+ }
508+
509+ private void RollbackRestoredBackup ( AppSettings previousSettings , string previousStatsSourcePath , string previousStatsJson )
510+ {
511+ try
512+ {
513+ ApplySettingsSnapshot ( previousSettings , forceDefaultStatisticsSource : false ) ;
514+ StatsStore . RestoreSource ( previousStatsSourcePath , previousStatsJson ) ;
515+ MonitoringService . ResetTrackingState ( ) ;
516+ StatsStore . SetDebugCaptureEnabled ( Settings . DebugCaptureEnabled ) ;
517+ }
518+ catch ( Exception rollbackException )
519+ {
520+ StartupDiagnostics . Log ( $ "RestoreBackupArchive rollback failed: { rollbackException } ") ;
521+ }
522+ }
523+
524+ private static AppSettings CloneSettings ( AppSettings source )
525+ {
526+ return new AppSettings
527+ {
528+ StartWithWindows = source . StartWithWindows ,
529+ PrivacyMode = source . PrivacyMode ,
530+ DebugCaptureEnabled = source . DebugCaptureEnabled ,
531+ ThemeMode = source . ThemeMode ,
532+ StatisticsSourcePath = source . StatisticsSourcePath ,
533+ Language = source . Language ,
534+ ExcludedApps = source . ExcludedApps ,
535+ AppTagMappings = source . GetNormalizedTagMappings ( )
536+ . Select ( mapping => new AppTagMapping
537+ {
538+ AppName = mapping . AppName ,
539+ Tags = mapping . Tags . ToList ( )
540+ } )
541+ . ToList ( )
542+ } ;
543+ }
544+
349545 private void UiSettings_ColorValuesChanged ( UISettings sender , object args )
350546 {
351547 if ( AppStrings . ResolveThemeMode ( Settings . ThemeMode ) is not AppThemeMode . FollowSystem )
0 commit comments