diff --git a/MainWindow.xaml b/MainWindow.xaml
index 55b5d16..a506b86 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -19,15 +19,17 @@
-
+
+
+
-
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 4101d12..ecfd650 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -636,6 +636,77 @@ public partial class MainWindow : Window
}
}
+ private void ExportConfig_Click(object sender, RoutedEventArgs e)
+ {
+ if (controller is null)
+ {
+ return;
+ }
+
+ ApplyConfigFields();
+ using SaveFileDialog dialog = new()
+ {
+ Title = "Export launcher config",
+ Filter = "JSON config files|*.json|All files|*.*",
+ FileName = $"TaskbarLauncher-config-{DateTime.Now:yyyyMMdd-HHmmss}.json",
+ InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
+ };
+
+ if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
+ {
+ return;
+ }
+
+ try
+ {
+ string exportPath = ConfigService.ExportTo(dialog.FileName, controller.Config);
+ StatusText.Text = $"Exported config to {exportPath}";
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Could not export config.\n\n{ex.Message}", "Taskbar Launcher");
+ }
+ }
+
+ private void ImportConfig_Click(object sender, RoutedEventArgs e)
+ {
+ using OpenFileDialog dialog = new()
+ {
+ Title = "Import launcher config",
+ Filter = "JSON config files|*.json|All files|*.*",
+ InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
+ };
+
+ if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
+ {
+ return;
+ }
+
+ MessageBoxResult result = MessageBox.Show(
+ "Import this config file and replace the current launcher config?\n\nA local backup will be created first.",
+ "Taskbar Launcher",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+
+ if (result != MessageBoxResult.Yes)
+ {
+ return;
+ }
+
+ try
+ {
+ string backupPath = ConfigService.Backup();
+ ConfigService.ImportFrom(dialog.FileName);
+ controller?.ReloadConfig();
+ RefreshView();
+ StatusText.Text = $"Imported config from {dialog.FileName}. Previous config backed up to {backupPath}";
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Could not import config.\n\n{ex.Message}", "Taskbar Launcher");
+ }
+ }
+
private List? FindItemPath(LauncherItem item)
{
if (controller is null)
diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs
index b8d6860..e9558f4 100644
--- a/Services/ConfigService.cs
+++ b/Services/ConfigService.cs
@@ -61,18 +61,35 @@ public static class ConfigService
return backupPath;
}
- public static LauncherConfig Restore(string sourcePath)
+ public static string ExportTo(string destinationPath, LauncherConfig config)
{
- if (!File.Exists(sourcePath))
+ if (string.IsNullOrWhiteSpace(destinationPath))
{
- throw new FileNotFoundException("The selected config file was not found.", sourcePath);
+ throw new ArgumentException("Choose a destination path for the exported config.", nameof(destinationPath));
}
- string json = File.ReadAllText(sourcePath);
- LauncherConfig config = JsonSerializer.Deserialize(json, JsonOptions)
- ?? throw new InvalidDataException("The selected config file could not be read.");
+ string? destinationFolder = Path.GetDirectoryName(destinationPath);
+ if (!string.IsNullOrWhiteSpace(destinationFolder))
+ {
+ Directory.CreateDirectory(destinationFolder);
+ }
NormalizeDisplayModes(config);
+ string json = JsonSerializer.Serialize(config, JsonOptions);
+ File.WriteAllText(destinationPath, json);
+ return destinationPath;
+ }
+
+ public static LauncherConfig ImportFrom(string sourcePath)
+ {
+ LauncherConfig config = ReadConfigFile(sourcePath);
+ Save(config);
+ return config;
+ }
+
+ public static LauncherConfig Restore(string sourcePath)
+ {
+ LauncherConfig config = ReadConfigFile(sourcePath);
Save(config);
return config;
}
@@ -135,6 +152,61 @@ public static class ConfigService
return config;
}
+ private static LauncherConfig ReadConfigFile(string sourcePath)
+ {
+ if (!File.Exists(sourcePath))
+ {
+ throw new FileNotFoundException("The selected config file was not found.", sourcePath);
+ }
+
+ string json = File.ReadAllText(sourcePath);
+ LauncherConfig config;
+ try
+ {
+ config = JsonSerializer.Deserialize(json, JsonOptions)
+ ?? throw new InvalidDataException("The selected config file could not be read.");
+ }
+ catch (JsonException ex)
+ {
+ throw new InvalidDataException("The selected file is not a valid Taskbar Launcher config.", ex);
+ }
+
+ ValidateConfig(config);
+ NormalizeDisplayModes(config);
+ return config;
+ }
+
+ private static void ValidateConfig(LauncherConfig config)
+ {
+ if (config.Items is null)
+ {
+ throw new InvalidDataException("The selected config is missing launcher items.");
+ }
+
+ foreach (LauncherItem item in config.Items)
+ {
+ ValidateItem(item);
+ }
+ }
+
+ private static void ValidateItem(LauncherItem item)
+ {
+ if (item.Children is null)
+ {
+ throw new InvalidDataException($"The item '{item.Title}' has an invalid children list.");
+ }
+
+ if (!Enum.IsDefined(item.Type))
+ {
+ throw new InvalidDataException($"The item '{item.Title}' has an invalid type.");
+ }
+
+ foreach (LauncherItem child in item.Children)
+ {
+ ValidateItem(child);
+ }
+ }
+
private static void NormalizeDisplayModes(LauncherConfig config)
{
foreach (LauncherItem item in config.Items)