985 lines
34 KiB
C#
985 lines
34 KiB
C#
using System.Diagnostics;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using Avalonia.Platform.Storage;
|
|
using CodexProviderSync.Core;
|
|
|
|
namespace CodexProviderSync.Mac;
|
|
|
|
public sealed class MainWindow : Window
|
|
{
|
|
private readonly CodexSyncService _syncService = new();
|
|
private readonly SettingsService _settingsService = new();
|
|
|
|
private readonly TextBox _codexHomeText = new();
|
|
private readonly ComboBox _recentHomes = new();
|
|
private readonly TextBlock _languageLabel = new();
|
|
private readonly ComboBox _languageCombo = new();
|
|
private readonly Button _browseButton = new();
|
|
private readonly Button _refreshButton = new();
|
|
private readonly TextBlock _busyText = new();
|
|
private readonly TextBox _statusText = new();
|
|
private readonly ListBox _providerList = new();
|
|
private readonly TextBox _manualProviderText = new();
|
|
private readonly Button _addProviderButton = new();
|
|
private readonly Button _removeProviderButton = new();
|
|
private readonly TextBlock _selectedProviderText = new();
|
|
private readonly CheckBox _switchConfigCheck = new();
|
|
private readonly NumericUpDown _backupRetentionInput = new();
|
|
private readonly Button _executeButton = new();
|
|
private readonly CheckBox _restoreConfigCheck = new();
|
|
private readonly CheckBox _restoreDatabaseCheck = new();
|
|
private readonly CheckBox _restoreSessionsCheck = new();
|
|
private readonly Button _restoreButton = new();
|
|
private readonly Button _openBackupButton = new();
|
|
private readonly Button _pruneBackupsButton = new();
|
|
private readonly TextBox _logText = new();
|
|
private readonly TextBlock _codexHomeLabel = new();
|
|
private readonly TextBlock _statusTitle = new();
|
|
private readonly TextBlock _providersTitle = new();
|
|
private readonly TextBlock _actionsTitle = new();
|
|
private readonly TextBlock _logTitle = new();
|
|
private readonly TextBlock _targetProviderLabel = new();
|
|
private readonly TextBlock _restoreContentsLabel = new();
|
|
private readonly TextBlock _keepBackupsLabel = new();
|
|
private readonly TextBlock _writeWarningText = new();
|
|
|
|
private AppSettings _settings = new();
|
|
private StatusSnapshot? _currentStatus;
|
|
private bool _loadingSettings;
|
|
private string _language = MacUiText.English;
|
|
|
|
public MainWindow()
|
|
{
|
|
Title = "Codex Provider Sync";
|
|
MinWidth = 1120;
|
|
MinHeight = 760;
|
|
Width = 1240;
|
|
Height = 820;
|
|
Background = Brush.Parse("#f5f5f7");
|
|
|
|
Content = BuildLayout();
|
|
WireEvents();
|
|
}
|
|
|
|
protected override async void OnOpened(EventArgs e)
|
|
{
|
|
base.OnOpened(e);
|
|
await LoadStateAsync();
|
|
}
|
|
|
|
protected override void OnClosing(WindowClosingEventArgs e)
|
|
{
|
|
PersistUiState();
|
|
base.OnClosing(e);
|
|
}
|
|
|
|
private Control BuildLayout()
|
|
{
|
|
Grid root = new()
|
|
{
|
|
RowDefinitions = new RowDefinitions("Auto,*,220"),
|
|
Margin = new Thickness(18),
|
|
RowSpacing = 14
|
|
};
|
|
|
|
root.Children.Add(BuildCodexHomePanel());
|
|
root.Children.Add(BuildMainPanel());
|
|
root.Children.Add(BuildLogPanel());
|
|
Grid.SetRow(root.Children[1], 1);
|
|
Grid.SetRow(root.Children[2], 2);
|
|
return root;
|
|
}
|
|
|
|
private Control BuildCodexHomePanel()
|
|
{
|
|
Grid panel = new()
|
|
{
|
|
ColumnDefinitions = new ColumnDefinitions("Auto,*,170,Auto,Auto,Auto,132"),
|
|
ColumnSpacing = 10
|
|
};
|
|
|
|
ConfigureLabel(_codexHomeLabel, "Codex Home");
|
|
ConfigureLabel(_languageLabel, "Language");
|
|
_codexHomeText.PlaceholderText = AppConstants.DefaultCodexHome();
|
|
_codexHomeText.MinHeight = 34;
|
|
_recentHomes.MinHeight = 34;
|
|
_recentHomes.PlaceholderText = "Recent";
|
|
_languageCombo.MinHeight = 34;
|
|
_languageCombo.Items.Add("English");
|
|
_languageCombo.Items.Add("中文");
|
|
_languageCombo.SelectedIndex = 0;
|
|
_browseButton.Content = "Browse";
|
|
_browseButton.MinHeight = 34;
|
|
_refreshButton.Content = "Refresh";
|
|
_refreshButton.MinHeight = 34;
|
|
|
|
panel.Children.Add(_codexHomeLabel);
|
|
panel.Children.Add(_codexHomeText);
|
|
panel.Children.Add(_recentHomes);
|
|
panel.Children.Add(_browseButton);
|
|
panel.Children.Add(_refreshButton);
|
|
panel.Children.Add(_languageLabel);
|
|
panel.Children.Add(_languageCombo);
|
|
Grid.SetColumn(_codexHomeText, 1);
|
|
Grid.SetColumn(_recentHomes, 2);
|
|
Grid.SetColumn(_browseButton, 3);
|
|
Grid.SetColumn(_refreshButton, 4);
|
|
Grid.SetColumn(_languageLabel, 5);
|
|
Grid.SetColumn(_languageCombo, 6);
|
|
return Card(panel);
|
|
}
|
|
|
|
private Control BuildMainPanel()
|
|
{
|
|
Grid main = new()
|
|
{
|
|
ColumnDefinitions = new ColumnDefinitions("1.15*,1*,340"),
|
|
ColumnSpacing = 14
|
|
};
|
|
|
|
main.Children.Add(BuildStatusPanel());
|
|
main.Children.Add(BuildProviderPanel());
|
|
main.Children.Add(BuildActionsPanel());
|
|
Grid.SetColumn(main.Children[1], 1);
|
|
Grid.SetColumn(main.Children[2], 2);
|
|
return main;
|
|
}
|
|
|
|
private Control BuildStatusPanel()
|
|
{
|
|
_statusText.IsReadOnly = true;
|
|
_statusText.AcceptsReturn = true;
|
|
_statusText.TextWrapping = TextWrapping.NoWrap;
|
|
_statusText.FontFamily = FontFamily.Parse("Menlo, Consolas, monospace");
|
|
_statusText.FontSize = 12;
|
|
_statusText.Background = Brushes.White;
|
|
|
|
return Section(_statusTitle, _statusText);
|
|
}
|
|
|
|
private Control BuildProviderPanel()
|
|
{
|
|
Grid panel = new()
|
|
{
|
|
RowDefinitions = new RowDefinitions("*,Auto,Auto"),
|
|
RowSpacing = 10
|
|
};
|
|
|
|
_providerList.SelectionMode = SelectionMode.Single;
|
|
_providerList.Background = Brushes.White;
|
|
|
|
Grid addPanel = new()
|
|
{
|
|
ColumnDefinitions = new ColumnDefinitions("*,Auto"),
|
|
ColumnSpacing = 8
|
|
};
|
|
_manualProviderText.PlaceholderText = "Add provider id";
|
|
_manualProviderText.MinHeight = 34;
|
|
_addProviderButton.Content = "Add";
|
|
_addProviderButton.MinHeight = 34;
|
|
addPanel.Children.Add(_manualProviderText);
|
|
addPanel.Children.Add(_addProviderButton);
|
|
Grid.SetColumn(_addProviderButton, 1);
|
|
|
|
_removeProviderButton.Content = "Remove Manual";
|
|
_removeProviderButton.HorizontalAlignment = HorizontalAlignment.Left;
|
|
|
|
panel.Children.Add(_providerList);
|
|
panel.Children.Add(addPanel);
|
|
panel.Children.Add(_removeProviderButton);
|
|
Grid.SetRow(addPanel, 1);
|
|
Grid.SetRow(_removeProviderButton, 2);
|
|
|
|
return Section(_providersTitle, panel);
|
|
}
|
|
|
|
private Control BuildActionsPanel()
|
|
{
|
|
StackPanel panel = new()
|
|
{
|
|
Spacing = 12
|
|
};
|
|
|
|
_selectedProviderText.Text = "No provider selected";
|
|
_selectedProviderText.FontSize = 18;
|
|
_selectedProviderText.FontWeight = FontWeight.SemiBold;
|
|
_selectedProviderText.TextWrapping = TextWrapping.Wrap;
|
|
|
|
_switchConfigCheck.Content = "Switch config.toml and sync";
|
|
|
|
_backupRetentionInput.Minimum = 1;
|
|
_backupRetentionInput.Maximum = 100000;
|
|
_backupRetentionInput.Value = AppConstants.DefaultBackupRetentionCount;
|
|
_backupRetentionInput.Width = 112;
|
|
|
|
_executeButton.Content = "Sync Metadata Only";
|
|
_executeButton.MinHeight = 40;
|
|
_executeButton.HorizontalAlignment = HorizontalAlignment.Stretch;
|
|
|
|
_restoreConfigCheck.Content = "Restore config.toml";
|
|
_restoreDatabaseCheck.Content = "Restore SQLite";
|
|
_restoreSessionsCheck.Content = "Restore rollout metadata";
|
|
_restoreConfigCheck.IsChecked = false;
|
|
_restoreDatabaseCheck.IsChecked = true;
|
|
_restoreSessionsCheck.IsChecked = true;
|
|
|
|
_restoreButton.Content = "Restore Backup";
|
|
_restoreButton.MinHeight = 38;
|
|
_openBackupButton.Content = "Open Backup Folder";
|
|
_openBackupButton.MinHeight = 38;
|
|
_pruneBackupsButton.Content = "Clean Old Backups";
|
|
_pruneBackupsButton.MinHeight = 38;
|
|
|
|
_busyText.Text = "Ready";
|
|
_busyText.Foreground = Brush.Parse("#166534");
|
|
|
|
ConfigureMutedLabel(_targetProviderLabel, "Target provider");
|
|
ConfigureMutedLabel(_restoreContentsLabel, "Restore contents");
|
|
|
|
panel.Children.Add(_targetProviderLabel);
|
|
panel.Children.Add(_selectedProviderText);
|
|
panel.Children.Add(_switchConfigCheck);
|
|
panel.Children.Add(BuildRetentionPanel());
|
|
panel.Children.Add(WarningBlock(_writeWarningText));
|
|
panel.Children.Add(_executeButton);
|
|
panel.Children.Add(Separator());
|
|
panel.Children.Add(_restoreContentsLabel);
|
|
panel.Children.Add(_restoreConfigCheck);
|
|
panel.Children.Add(_restoreDatabaseCheck);
|
|
panel.Children.Add(_restoreSessionsCheck);
|
|
panel.Children.Add(_restoreButton);
|
|
panel.Children.Add(_openBackupButton);
|
|
panel.Children.Add(_pruneBackupsButton);
|
|
panel.Children.Add(_busyText);
|
|
|
|
return Section(_actionsTitle, panel);
|
|
}
|
|
|
|
private Control BuildRetentionPanel()
|
|
{
|
|
StackPanel panel = new()
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
Spacing = 8,
|
|
VerticalAlignment = VerticalAlignment.Center
|
|
};
|
|
ConfigureMutedLabel(_keepBackupsLabel, "Keep backups");
|
|
panel.Children.Add(_keepBackupsLabel);
|
|
panel.Children.Add(_backupRetentionInput);
|
|
return panel;
|
|
}
|
|
|
|
private Control BuildLogPanel()
|
|
{
|
|
_logText.IsReadOnly = true;
|
|
_logText.AcceptsReturn = true;
|
|
_logText.FontFamily = FontFamily.Parse("Menlo, Consolas, monospace");
|
|
_logText.FontSize = 12;
|
|
_logText.Background = Brushes.White;
|
|
return Section(_logTitle, _logText);
|
|
}
|
|
|
|
private void WireEvents()
|
|
{
|
|
_browseButton.Click += async (_, _) => await BrowseCodexHomeAsync();
|
|
_refreshButton.Click += async (_, _) => await RefreshStatusAsync();
|
|
_languageCombo.SelectionChanged += async (_, _) => await ChangeLanguageAsync();
|
|
_recentHomes.SelectionChanged += (_, _) =>
|
|
{
|
|
if (_recentHomes.SelectedItem is string home)
|
|
{
|
|
_codexHomeText.Text = home;
|
|
}
|
|
};
|
|
_addProviderButton.Click += async (_, _) => await AddManualProviderAsync();
|
|
_removeProviderButton.Click += async (_, _) => await RemoveManualProviderAsync();
|
|
_providerList.SelectionChanged += (_, _) => UpdateSelectionLabel();
|
|
_switchConfigCheck.PropertyChanged += (_, args) =>
|
|
{
|
|
if (args.Property.Name == nameof(CheckBox.IsChecked))
|
|
{
|
|
UpdateExecuteButtonText();
|
|
}
|
|
};
|
|
_backupRetentionInput.ValueChanged += async (_, _) => await PersistBackupRetentionAsync();
|
|
_executeButton.Click += async (_, _) => await ExecuteSyncOrSwitchAsync();
|
|
_restoreButton.Click += async (_, _) => await RestoreBackupAsync();
|
|
_openBackupButton.Click += async (_, _) => await OpenBackupFolderAsync();
|
|
_pruneBackupsButton.Click += async (_, _) => await PruneBackupsAsync();
|
|
_codexHomeText.LostFocus += async (_, _) => await PersistHomeSelectionAsync();
|
|
_manualProviderText.KeyDown += async (_, args) =>
|
|
{
|
|
if (args.Key == Key.Enter)
|
|
{
|
|
args.Handled = true;
|
|
await AddManualProviderAsync();
|
|
}
|
|
};
|
|
}
|
|
|
|
private async Task LoadStateAsync()
|
|
{
|
|
_loadingSettings = true;
|
|
_settings = await _settingsService.LoadAsync();
|
|
_language = MacUiText.NormalizeLanguage(_settings.UiLanguage);
|
|
_languageCombo.SelectedIndex = MacUiText.IsChinese(_language) ? 1 : 0;
|
|
ApplyLanguage();
|
|
ApplyWindowBounds(_settings.WindowBounds);
|
|
ReloadRecentHomes();
|
|
_codexHomeText.Text = _settings.LastCodexHome ?? AppConstants.DefaultCodexHome();
|
|
_backupRetentionInput.Value = Math.Max(1, _settings.BackupRetentionCount);
|
|
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {T("loadedSettings")}: {_settingsService.SettingsPath}");
|
|
_loadingSettings = false;
|
|
await RefreshStatusAsync();
|
|
}
|
|
|
|
private async Task RefreshStatusAsync()
|
|
{
|
|
string codexHome = CurrentCodexHome();
|
|
await RunBusyAsync(T("refreshing"), () => RefreshStatusCoreAsync(codexHome));
|
|
}
|
|
|
|
private async Task ChangeLanguageAsync()
|
|
{
|
|
if (_loadingSettings)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_language = _languageCombo.SelectedIndex == 1 ? MacUiText.Chinese : MacUiText.English;
|
|
_settings = _settingsService.UpdateUiLanguage(_settings, _language);
|
|
await _settingsService.SaveAsync(_settings);
|
|
ApplyLanguage();
|
|
if (_currentStatus is not null)
|
|
{
|
|
_statusText.Text = MacDisplayFormatter.FormatStatus(_currentStatus, _language);
|
|
}
|
|
ReloadProviderList();
|
|
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {T("languageChanged")}: {(_language == MacUiText.Chinese ? "中文" : "English")}");
|
|
}
|
|
|
|
private async Task BrowseCodexHomeAsync()
|
|
{
|
|
IStorageFolder? start = null;
|
|
string current = CurrentCodexHome();
|
|
if (Directory.Exists(current))
|
|
{
|
|
start = await StorageProvider.TryGetFolderFromPathAsync(current);
|
|
}
|
|
|
|
IReadOnlyList<IStorageFolder> folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
|
{
|
|
Title = T("chooseCodexFolder"),
|
|
AllowMultiple = false,
|
|
SuggestedStartLocation = start
|
|
});
|
|
|
|
if (folders.Count == 0 || folders[0].Path.LocalPath is not { Length: > 0 } selected)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_codexHomeText.Text = selected;
|
|
await PersistHomeSelectionAsync();
|
|
await RefreshStatusAsync();
|
|
}
|
|
|
|
private async Task PersistHomeSelectionAsync()
|
|
{
|
|
string codexHome = CurrentCodexHome();
|
|
if (string.IsNullOrWhiteSpace(codexHome))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_settings = _settingsService.RecordCodexHome(_settings, codexHome);
|
|
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), _settings.LastBackupDirectory, CaptureWindowBounds(), CurrentBackupRetentionCount());
|
|
await _settingsService.SaveAsync(_settings);
|
|
ReloadRecentHomes();
|
|
}
|
|
|
|
private async Task PersistBackupRetentionAsync()
|
|
{
|
|
if (_loadingSettings)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_settings = _settingsService.UpdateState(
|
|
_settings,
|
|
SelectedProvider(),
|
|
_settings.LastBackupDirectory,
|
|
CaptureWindowBounds(),
|
|
CurrentBackupRetentionCount());
|
|
await _settingsService.SaveAsync(_settings);
|
|
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Backup retention set to {CurrentBackupRetentionCount()}");
|
|
}
|
|
|
|
private async Task AddManualProviderAsync()
|
|
{
|
|
string provider = (_manualProviderText.Text ?? string.Empty).Trim();
|
|
if (string.IsNullOrWhiteSpace(provider))
|
|
{
|
|
await ShowInfoAsync(T("enterProvider"));
|
|
return;
|
|
}
|
|
|
|
_settings = _settingsService.AddManualProvider(_settings, provider);
|
|
await _settingsService.SaveAsync(_settings);
|
|
_manualProviderText.Clear();
|
|
ReloadProviderList();
|
|
SelectProvider(provider);
|
|
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {T("addedManualProvider")}: {provider}");
|
|
}
|
|
|
|
private async Task RemoveManualProviderAsync()
|
|
{
|
|
string? provider = SelectedProvider();
|
|
if (string.IsNullOrWhiteSpace(provider))
|
|
{
|
|
await ShowInfoAsync(T("selectProvider"));
|
|
return;
|
|
}
|
|
|
|
_settings = _settingsService.RemoveManualProvider(_settings, provider);
|
|
await _settingsService.SaveAsync(_settings);
|
|
ReloadProviderList();
|
|
SelectProvider(_currentStatus?.CurrentProvider.Provider);
|
|
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {T("removedManualProvider")}: {provider}");
|
|
}
|
|
|
|
private async Task ExecuteSyncOrSwitchAsync()
|
|
{
|
|
string? provider = SelectedProvider();
|
|
if (string.IsNullOrWhiteSpace(provider))
|
|
{
|
|
await ShowInfoAsync(T("selectTargetProvider"));
|
|
return;
|
|
}
|
|
|
|
string mode = IsSwitchMode()
|
|
? T("confirmSwitchMode")
|
|
: T("confirmSyncMode");
|
|
|
|
if (!await ConfirmAsync(
|
|
T("confirmWriteTitle"),
|
|
string.Format(T("confirmWriteMessage"), mode, provider)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await RunBusyAsync(T("executing"), async () =>
|
|
{
|
|
string codexHome = CurrentCodexHome();
|
|
int backupRetentionCount = CurrentBackupRetentionCount();
|
|
SyncResult result = IsSwitchMode()
|
|
? await Task.Run(async () => await _syncService.RunSwitchAsync(codexHome, provider, backupRetentionCount))
|
|
: await Task.Run(async () => await _syncService.RunSyncAsync(codexHome, provider: provider, keepCount: backupRetentionCount));
|
|
|
|
_settings = _settingsService.UpdateState(_settings, provider, result.BackupDir, CaptureWindowBounds(), backupRetentionCount);
|
|
await _settingsService.SaveAsync(_settings);
|
|
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {T("executionFinished")}");
|
|
AppendLog(MacDisplayFormatter.FormatSyncResult(result, IsSwitchMode() ? T("switchedAndSynced") : T("synced"), _language));
|
|
AppendLog(string.Empty);
|
|
await RefreshStatusCoreAsync(codexHome);
|
|
SelectProvider(provider);
|
|
});
|
|
}
|
|
|
|
private async Task RestoreBackupAsync()
|
|
{
|
|
string backupRoot = _currentStatus?.BackupRoot ?? AppConstants.DefaultBackupRoot(CurrentCodexHome());
|
|
string initialBackupDir = Directory.Exists(_settings.LastBackupDirectory)
|
|
? _settings.LastBackupDirectory!
|
|
: backupRoot;
|
|
|
|
IStorageFolder? start = Directory.Exists(initialBackupDir)
|
|
? await StorageProvider.TryGetFolderFromPathAsync(initialBackupDir)
|
|
: null;
|
|
|
|
IReadOnlyList<IStorageFolder> folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
|
{
|
|
Title = T("chooseBackupFolder"),
|
|
AllowMultiple = false,
|
|
SuggestedStartLocation = start
|
|
});
|
|
|
|
if (folders.Count == 0 || folders[0].Path.LocalPath is not { Length: > 0 } backupDir)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool restoreConfig = _restoreConfigCheck.IsChecked == true;
|
|
bool restoreDatabase = _restoreDatabaseCheck.IsChecked == true;
|
|
bool restoreSessions = _restoreSessionsCheck.IsChecked == true;
|
|
if (!restoreConfig && !restoreDatabase && !restoreSessions)
|
|
{
|
|
await ShowInfoAsync(T("chooseRestoreTarget"));
|
|
return;
|
|
}
|
|
|
|
string restoreTargets = string.Join(", ", new[]
|
|
{
|
|
restoreConfig ? "config.toml" : null,
|
|
restoreDatabase ? "SQLite" : null,
|
|
restoreSessions ? "rollout metadata" : null
|
|
}.Where(static value => value is not null));
|
|
|
|
if (!await ConfirmAsync(
|
|
T("restoreTitle"),
|
|
string.Format(T("restoreMessage"), backupDir, restoreTargets)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await RunBusyAsync(T("restoring"), async () =>
|
|
{
|
|
string codexHome = CurrentCodexHome();
|
|
RestoreResult result = await Task.Run(async () => await _syncService.RunRestoreAsync(
|
|
codexHome,
|
|
backupDir,
|
|
new RestoreBackupOptions
|
|
{
|
|
RestoreConfig = restoreConfig,
|
|
RestoreDatabase = restoreDatabase,
|
|
RestoreSessions = restoreSessions
|
|
}));
|
|
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), backupDir, CaptureWindowBounds(), CurrentBackupRetentionCount());
|
|
await _settingsService.SaveAsync(_settings);
|
|
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {T("restoreFinished")}");
|
|
AppendLog(MacDisplayFormatter.FormatRestoreResult(result, _language));
|
|
AppendLog(string.Empty);
|
|
await RefreshStatusCoreAsync(codexHome);
|
|
});
|
|
}
|
|
|
|
private async Task OpenBackupFolderAsync()
|
|
{
|
|
string path = _currentStatus?.BackupRoot ?? AppConstants.DefaultBackupRoot(CurrentCodexHome());
|
|
Directory.CreateDirectory(path);
|
|
try
|
|
{
|
|
Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = OperatingSystem.IsMacOS() ? "open" : path,
|
|
Arguments = OperatingSystem.IsMacOS() ? QuoteShellArgument(path) : string.Empty,
|
|
UseShellExecute = !OperatingSystem.IsMacOS()
|
|
});
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
await ShowErrorAsync(error);
|
|
}
|
|
}
|
|
|
|
private async Task PruneBackupsAsync()
|
|
{
|
|
if (!await ConfirmAsync(
|
|
T("cleanTitle"),
|
|
string.Format(T("cleanMessage"), CurrentBackupRetentionCount())))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await RunBusyAsync(T("cleaning"), async () =>
|
|
{
|
|
string codexHome = CurrentCodexHome();
|
|
BackupPruneResult result = await Task.Run(async () => await _syncService.RunPruneBackupsAsync(codexHome, CurrentBackupRetentionCount()));
|
|
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {T("backupCleanupFinished")}");
|
|
AppendLog(MacDisplayFormatter.FormatBackupPruneResult(result, _language));
|
|
AppendLog(string.Empty);
|
|
await RefreshStatusCoreAsync(codexHome);
|
|
});
|
|
}
|
|
|
|
private async Task RefreshStatusCoreAsync(string codexHome)
|
|
{
|
|
_currentStatus = await Task.Run(async () => await _syncService.GetStatusAsync(codexHome));
|
|
_settings = _settingsService.RecordCodexHome(_settings, _currentStatus.CodexHome);
|
|
_settings = _settingsService.MergeDetectedProviders(_settings, _syncService.ExtractDetectedProviderIds(_currentStatus));
|
|
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), _settings.LastBackupDirectory, CaptureWindowBounds(), CurrentBackupRetentionCount());
|
|
await _settingsService.SaveAsync(_settings);
|
|
|
|
_statusText.Text = MacDisplayFormatter.FormatStatus(_currentStatus, _language);
|
|
ReloadRecentHomes();
|
|
ReloadProviderList();
|
|
_codexHomeText.Text = _currentStatus.CodexHome;
|
|
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {T("refreshed")}: {_currentStatus.CodexHome}");
|
|
}
|
|
|
|
private void ReloadRecentHomes()
|
|
{
|
|
string? selected = _codexHomeText.Text;
|
|
_recentHomes.Items.Clear();
|
|
foreach (string home in _settings.RecentCodexHomes)
|
|
{
|
|
_recentHomes.Items.Add(home);
|
|
}
|
|
|
|
_codexHomeText.Text = selected;
|
|
}
|
|
|
|
private void ReloadProviderList()
|
|
{
|
|
_providerList.Items.Clear();
|
|
if (_currentStatus is not null)
|
|
{
|
|
foreach (ProviderOption option in _syncService.BuildProviderOptions(_currentStatus, _settings))
|
|
{
|
|
ProviderListItem item = new()
|
|
{
|
|
Option = option,
|
|
SourcesText = MacDisplayFormatter.FormatProviderSources(option, _language),
|
|
DetailText = ProviderDetailText(option)
|
|
};
|
|
_providerList.Items.Add(BuildProviderRow(item));
|
|
}
|
|
}
|
|
|
|
SelectProvider(_settings.LastSelectedProvider ?? _currentStatus?.CurrentProvider.Provider);
|
|
UpdateSelectionLabel();
|
|
}
|
|
|
|
private ListBoxItem BuildProviderRow(ProviderListItem provider)
|
|
{
|
|
Grid row = new()
|
|
{
|
|
ColumnDefinitions = new ColumnDefinitions("*,Auto"),
|
|
RowDefinitions = new RowDefinitions("Auto,Auto"),
|
|
Margin = new Thickness(8, 7)
|
|
};
|
|
|
|
TextBlock title = new()
|
|
{
|
|
Text = provider.Option.Id,
|
|
FontWeight = provider.Option.IsCurrentProvider ? FontWeight.SemiBold : FontWeight.Normal,
|
|
TextTrimming = TextTrimming.CharacterEllipsis
|
|
};
|
|
TextBlock sources = new()
|
|
{
|
|
Text = provider.SourcesText,
|
|
Foreground = Brush.Parse("#52525b"),
|
|
FontSize = 12,
|
|
TextWrapping = TextWrapping.Wrap
|
|
};
|
|
TextBlock detail = new()
|
|
{
|
|
Text = provider.DetailText,
|
|
Foreground = Brush.Parse("#71717a"),
|
|
FontSize = 12
|
|
};
|
|
|
|
row.Children.Add(title);
|
|
row.Children.Add(detail);
|
|
row.Children.Add(sources);
|
|
Grid.SetColumn(detail, 1);
|
|
Grid.SetRow(sources, 1);
|
|
Grid.SetColumnSpan(sources, 2);
|
|
|
|
return new ListBoxItem
|
|
{
|
|
Tag = provider.Option.Id,
|
|
Content = row
|
|
};
|
|
}
|
|
|
|
private string ProviderDetailText(ProviderOption option)
|
|
{
|
|
List<string> details = [];
|
|
if (option.IsCurrentProvider)
|
|
{
|
|
details.Add(T("current"));
|
|
}
|
|
if (option.IsManual)
|
|
{
|
|
details.Add(T("manual"));
|
|
}
|
|
if (option.IsSaved)
|
|
{
|
|
details.Add(T("saved"));
|
|
}
|
|
|
|
return details.Count == 0 ? string.Empty : string.Join(" · ", details);
|
|
}
|
|
|
|
private void SelectProvider(string? provider)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(provider))
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (object? item in _providerList.Items)
|
|
{
|
|
if (item is ListBoxItem row && string.Equals(row.Tag as string, provider, StringComparison.Ordinal))
|
|
{
|
|
_providerList.SelectedItem = row;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateSelectionLabel()
|
|
{
|
|
string? provider = SelectedProvider();
|
|
_selectedProviderText.Text = string.IsNullOrWhiteSpace(provider) ? T("noProvider") : provider;
|
|
}
|
|
|
|
private void UpdateExecuteButtonText()
|
|
{
|
|
_executeButton.Content = IsSwitchMode()
|
|
? T("switchSync")
|
|
: T("syncMetadata");
|
|
}
|
|
|
|
private string? SelectedProvider()
|
|
{
|
|
return _providerList.SelectedItem is ListBoxItem row ? row.Tag as string : null;
|
|
}
|
|
|
|
private string CurrentCodexHome()
|
|
{
|
|
string text = (_codexHomeText.Text ?? string.Empty).Trim();
|
|
return string.IsNullOrWhiteSpace(text) ? AppConstants.DefaultCodexHome() : text;
|
|
}
|
|
|
|
private int CurrentBackupRetentionCount()
|
|
{
|
|
decimal value = _backupRetentionInput.Value ?? AppConstants.DefaultBackupRetentionCount;
|
|
return Decimal.ToInt32(Math.Max(1, value));
|
|
}
|
|
|
|
private bool IsSwitchMode()
|
|
{
|
|
return _switchConfigCheck.IsChecked == true;
|
|
}
|
|
|
|
private void PersistUiState()
|
|
{
|
|
try
|
|
{
|
|
_settings = _settingsService.RecordCodexHome(_settings, CurrentCodexHome());
|
|
_settings = _settingsService.UpdateUiLanguage(_settings, _language);
|
|
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), _settings.LastBackupDirectory, CaptureWindowBounds(), CurrentBackupRetentionCount());
|
|
_settingsService.Save(_settings);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore shutdown persistence failures.
|
|
}
|
|
}
|
|
|
|
private WindowBoundsState CaptureWindowBounds()
|
|
{
|
|
return new WindowBoundsState
|
|
{
|
|
X = Position.X,
|
|
Y = Position.Y,
|
|
Width = Decimal.ToInt32((decimal)Math.Max(Width, MinWidth)),
|
|
Height = Decimal.ToInt32((decimal)Math.Max(Height, MinHeight)),
|
|
Maximized = WindowState == WindowState.Maximized
|
|
};
|
|
}
|
|
|
|
private void ApplyWindowBounds(WindowBoundsState? bounds)
|
|
{
|
|
if (bounds is null || bounds.Width < 800 || bounds.Height < 600)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Position = new PixelPoint(bounds.X, bounds.Y);
|
|
Width = bounds.Width;
|
|
Height = bounds.Height;
|
|
if (bounds.Maximized)
|
|
{
|
|
WindowState = WindowState.Maximized;
|
|
}
|
|
}
|
|
|
|
private async Task RunBusyAsync(string stateText, Func<Task> action)
|
|
{
|
|
SetBusy(true, stateText);
|
|
try
|
|
{
|
|
await action();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {T("error")}: {error}");
|
|
await ShowErrorAsync(error);
|
|
}
|
|
finally
|
|
{
|
|
SetBusy(false, T("ready"));
|
|
}
|
|
}
|
|
|
|
private void SetBusy(bool busy, string stateText)
|
|
{
|
|
Cursor = busy ? new Cursor(StandardCursorType.Wait) : Cursor.Default;
|
|
_busyText.Text = stateText;
|
|
_busyText.Foreground = busy ? Brush.Parse("#b45309") : Brush.Parse("#166534");
|
|
|
|
_browseButton.IsEnabled = !busy;
|
|
_refreshButton.IsEnabled = !busy;
|
|
_addProviderButton.IsEnabled = !busy;
|
|
_removeProviderButton.IsEnabled = !busy;
|
|
_switchConfigCheck.IsEnabled = !busy;
|
|
_backupRetentionInput.IsEnabled = !busy;
|
|
_restoreConfigCheck.IsEnabled = !busy;
|
|
_restoreDatabaseCheck.IsEnabled = !busy;
|
|
_restoreSessionsCheck.IsEnabled = !busy;
|
|
_executeButton.IsEnabled = !busy;
|
|
_restoreButton.IsEnabled = !busy;
|
|
_openBackupButton.IsEnabled = !busy;
|
|
_pruneBackupsButton.IsEnabled = !busy;
|
|
_providerList.IsEnabled = !busy;
|
|
_manualProviderText.IsEnabled = !busy;
|
|
_codexHomeText.IsEnabled = !busy;
|
|
_recentHomes.IsEnabled = !busy;
|
|
_languageCombo.IsEnabled = !busy;
|
|
}
|
|
|
|
private void AppendLog(string message)
|
|
{
|
|
_logText.Text = string.IsNullOrEmpty(_logText.Text)
|
|
? message
|
|
: $"{_logText.Text}{Environment.NewLine}{message}";
|
|
_logText.CaretIndex = _logText.Text?.Length ?? 0;
|
|
}
|
|
|
|
private async Task<bool> ConfirmAsync(string title, string message)
|
|
{
|
|
ConfirmationDialog dialog = new(title, message, T("continue"), T("cancel"));
|
|
return await dialog.ShowDialog<bool>(this);
|
|
}
|
|
|
|
private async Task ShowInfoAsync(string message)
|
|
{
|
|
ConfirmationDialog dialog = new("Codex Provider Sync", message, T("ok"), null);
|
|
await dialog.ShowDialog<bool>(this);
|
|
}
|
|
|
|
private async Task ShowErrorAsync(Exception error)
|
|
{
|
|
ConfirmationDialog dialog = new(T("error"), error.Message, T("ok"), null);
|
|
await dialog.ShowDialog<bool>(this);
|
|
}
|
|
|
|
private void ApplyLanguage()
|
|
{
|
|
_languageLabel.Text = T("language");
|
|
_recentHomes.PlaceholderText = T("recent");
|
|
_browseButton.Content = T("browse");
|
|
_refreshButton.Content = T("refresh");
|
|
_statusTitle.Text = T("status");
|
|
_providersTitle.Text = T("providers");
|
|
_actionsTitle.Text = T("actions");
|
|
_logTitle.Text = T("executionLog");
|
|
_manualProviderText.PlaceholderText = T("addProvider");
|
|
_addProviderButton.Content = T("add");
|
|
_removeProviderButton.Content = T("removeManual");
|
|
_switchConfigCheck.Content = T("switchConfig");
|
|
_restoreConfigCheck.Content = T("restoreConfig");
|
|
_restoreDatabaseCheck.Content = T("restoreSqlite");
|
|
_restoreSessionsCheck.Content = T("restoreRollout");
|
|
_restoreButton.Content = T("restoreBackup");
|
|
_openBackupButton.Content = T("openBackupFolder");
|
|
_pruneBackupsButton.Content = T("cleanBackups");
|
|
_targetProviderLabel.Text = T("targetProvider");
|
|
_restoreContentsLabel.Text = T("restoreContents");
|
|
_keepBackupsLabel.Text = T("keepBackups");
|
|
_writeWarningText.Text = T("writeWarning");
|
|
_busyText.Text = T("ready");
|
|
UpdateExecuteButtonText();
|
|
UpdateSelectionLabel();
|
|
}
|
|
|
|
private string T(string key)
|
|
{
|
|
return MacUiText.Get(_language, key);
|
|
}
|
|
|
|
private static Border Card(Control content)
|
|
{
|
|
return new Border
|
|
{
|
|
Background = Brushes.White,
|
|
BorderBrush = Brush.Parse("#d4d4d8"),
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(12),
|
|
Child = content
|
|
};
|
|
}
|
|
|
|
private static Control Section(TextBlock title, Control content)
|
|
{
|
|
Grid panel = new()
|
|
{
|
|
RowDefinitions = new RowDefinitions("Auto,*"),
|
|
RowSpacing = 8
|
|
};
|
|
ConfigureSectionTitle(title);
|
|
panel.Children.Add(title);
|
|
panel.Children.Add(content);
|
|
Grid.SetRow(content, 1);
|
|
return Card(panel);
|
|
}
|
|
|
|
private static void ConfigureLabel(TextBlock label, string text)
|
|
{
|
|
label.Text = text;
|
|
label.VerticalAlignment = VerticalAlignment.Center;
|
|
label.FontWeight = FontWeight.SemiBold;
|
|
label.Foreground = Brush.Parse("#18181b");
|
|
}
|
|
|
|
private static void ConfigureSectionTitle(TextBlock title)
|
|
{
|
|
title.FontSize = 13;
|
|
title.FontWeight = FontWeight.SemiBold;
|
|
title.Foreground = Brush.Parse("#18181b");
|
|
}
|
|
|
|
private static void ConfigureMutedLabel(TextBlock label, string text)
|
|
{
|
|
label.Text = text;
|
|
label.Foreground = Brush.Parse("#52525b");
|
|
label.FontSize = 12;
|
|
}
|
|
|
|
private static Border WarningBlock(TextBlock text)
|
|
{
|
|
text.Foreground = Brush.Parse("#9a3412");
|
|
text.TextWrapping = TextWrapping.Wrap;
|
|
return new Border
|
|
{
|
|
Background = Brush.Parse("#fff7ed"),
|
|
BorderBrush = Brush.Parse("#fed7aa"),
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(10),
|
|
Child = text
|
|
};
|
|
}
|
|
|
|
private static Separator Separator()
|
|
{
|
|
return new Separator
|
|
{
|
|
Margin = new Thickness(0, 2)
|
|
};
|
|
}
|
|
|
|
private static string QuoteShellArgument(string value)
|
|
{
|
|
return "\"" + value.Replace("\"", "\\\"", StringComparison.Ordinal) + "\"";
|
|
}
|
|
}
|