refactor jsonrpc structure (extract setting to a standalone file PortableSettings.cs)

This commit is contained in:
Hongtao Zhang 2023-03-26 02:24:31 -05:00
parent 16dcdf01fd
commit 683f6ebce4
No known key found for this signature in database
GPG key ID: 75F655B91C7AC9BB
6 changed files with 704 additions and 451 deletions

View file

@ -28,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin
public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
public record JsonRPCQueryResponseModel(int Id,
[property: JsonPropertyName("result")] List<JsonRPCResult> Result,
Dictionary<string, object> SettingsChange = null,
IReadOnlyDictionary<string, object> SettingsChange = null,
string DebugMessage = "",
JsonRPCErrorModel Error = default) : JsonRPCResponseModel(Id, Error);
public record JsonRPCRequestModel(int Id,
string Method,
object[] Parameters,
Dictionary<string, object> Settings = default,
IReadOnlyDictionary<string, object> Settings = default,
JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
@ -46,7 +46,7 @@ namespace Flow.Launcher.Core.Plugin
int Id,
string Method,
object[] Parameters,
Dictionary<string, object> Settings,
IReadOnlyDictionary<string, object> Settings,
bool DontHideAfterAction = false,
JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error);

View file

@ -33,9 +33,8 @@ namespace Flow.Launcher.Core.Plugin
/// Represent the plugin that using JsonPRC
/// every JsonRPC plugin should has its own plugin instance
/// </summary>
internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
internal abstract class JsonRPCPlugin : JsonRPCPluginBase
{
protected PluginInitContext Context;
public const string JsonRPC = "JsonRPC";
protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
@ -48,7 +47,7 @@ namespace Flow.Launcher.Core.Plugin
private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
public List<Result> LoadContextMenus(Result selectedResult)
public override List<Result> LoadContextMenus(Result selectedResult)
{
var request = new JsonRPCRequestModel(RequestId++,
"context_menu",
@ -77,7 +76,6 @@ namespace Flow.Launcher.Core.Plugin
{
WriteIndented = true
};
private Dictionary<string, object> Settings { get; set; }
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
@ -103,85 +101,42 @@ namespace Flow.Launcher.Core.Plugin
return ParseResults(queryResponseModel);
}
private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
{
if (queryResponseModel.Result == null) return null;
if (result.JsonRPCAction == null) return false;
if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
{
Context.API.ShowMsg(queryResponseModel.DebugMessage);
return !result.JsonRPCAction.DontHideAfterAction;
}
foreach (var result in queryResponseModel.Result)
if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
{
result.AsyncAction = async c =>
ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..],
result.JsonRPCAction.Parameters);
}
else
{
await using var actionResponse = await RequestAsync(result.JsonRPCAction);
if (actionResponse.Length == 0)
{
UpdateSettings(result.SettingsChange);
if (result.JsonRPCAction == null) return false;
if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
{
return !result.JsonRPCAction.DontHideAfterAction;
}
if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
{
ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..],
result.JsonRPCAction.Parameters);
}
else
{
await using var actionResponse = await RequestAsync(result.JsonRPCAction);
if (actionResponse.Length == 0)
{
return !result.JsonRPCAction.DontHideAfterAction;
}
var jsonRpcRequestModel = await
JsonSerializer.DeserializeAsync<JsonRPCRequestModel>(actionResponse, options);
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
{
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
jsonRpcRequestModel.Parameters);
}
}
return !result.JsonRPCAction.DontHideAfterAction;
};
}
var jsonRpcRequestModel = await
JsonSerializer.DeserializeAsync<JsonRPCRequestModel>(actionResponse, options);
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
{
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
jsonRpcRequestModel.Parameters);
}
}
var results = new List<Result>();
results.AddRange(queryResponseModel.Result);
UpdateSettings(queryResponseModel.SettingsChange);
return results;
return !result.JsonRPCAction.DontHideAfterAction;
}
private void ExecuteFlowLauncherAPI(string method, object[] parameters)
{
var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray);
if (methodInfo == null)
{
return;
}
try
{
methodInfo.Invoke(PluginManager.API, parameters);
}
catch (Exception)
{
#if (DEBUG)
throw;
#endif
}
}
/// <summary>
/// Execute external program and return the output
@ -297,373 +252,10 @@ namespace Flow.Launcher.Core.Plugin
}
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
protected override async Task<List<Result>> QueryRequestAsync(JsonRPCRequestModel request, CancellationToken token)
{
var request = new JsonRPCRequestModel(RequestId++,
"query",
new object[]{ query.Search },
Settings);
var output = await RequestAsync(request, token);
return await DeserializedResultAsync(output);
}
private async Task InitSettingAsync()
{
if (!File.Exists(SettingConfigurationPath))
return;
if (File.Exists(SettingPath))
{
await using var fileStream = File.OpenRead(SettingPath);
Settings = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(fileStream, options);
}
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
_settingsTemplate = deserializer.Deserialize<JsonRpcConfigurationModel>(await File.ReadAllTextAsync(SettingConfigurationPath));
Settings ??= new Dictionary<string, object>();
foreach (var (type, attribute) in _settingsTemplate.Body)
{
if (type == "textBlock")
continue;
if (!Settings.ContainsKey(attribute.Name))
{
Settings[attribute.Name] = attribute.DefaultValue;
}
}
}
public virtual async Task InitAsync(PluginInitContext context)
{
this.Context = context;
await InitSettingAsync();
}
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9);
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
private JsonRpcConfigurationModel _settingsTemplate;
public Control CreateSettingPanel()
{
if (Settings == null)
return new();
var settingWindow = new UserControl();
var mainPanel = new Grid
{
Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center
};
ColumnDefinition gridCol1 = new ColumnDefinition();
ColumnDefinition gridCol2 = new ColumnDefinition();
gridCol1.Width = new GridLength(70, GridUnitType.Star);
gridCol2.Width = new GridLength(30, GridUnitType.Star);
mainPanel.ColumnDefinitions.Add(gridCol1);
mainPanel.ColumnDefinitions.Add(gridCol2);
settingWindow.Content = mainPanel;
int rowCount = 0;
foreach (var (type, attribute) in _settingsTemplate.Body)
{
Separator sep = new Separator();
sep.VerticalAlignment = VerticalAlignment.Top;
sep.Margin = settingSepMargin;
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
var panel = new StackPanel
{
Orientation = Orientation.Vertical,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingLabelPanelMargin
};
RowDefinition gridRow = new RowDefinition();
mainPanel.RowDefinitions.Add(gridRow);
var name = new TextBlock()
{
Text = attribute.Label,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingLabelMargin,
TextWrapping = TextWrapping.WrapWithOverflow
};
var desc = new TextBlock()
{
Text = attribute.Description,
FontSize = 12,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingDescMargin,
TextWrapping = TextWrapping.WrapWithOverflow
};
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
if (attribute.Description == null) /* if no description, hide */
desc.Visibility = Visibility.Collapsed;
if (type != "textBlock") /* if textBlock, hide desc */
{
panel.Children.Add(name);
panel.Children.Add(desc);
}
Grid.SetColumn(panel, 0);
Grid.SetRow(panel, rowCount);
FrameworkElement contentControl;
switch (type)
{
case "textBlock":
{
contentControl = new TextBlock
{
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
Margin = settingTextBlockMargin,
Padding = new Thickness(0, 0, 0, 0),
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
TextAlignment = TextAlignment.Left,
TextWrapping = TextWrapping.Wrap
};
Grid.SetColumn(contentControl, 0);
Grid.SetColumnSpan(contentControl, 2);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "input":
{
var textBox = new TextBox()
{
Text = Settings[attribute.Name] as string ?? string.Empty,
Margin = settingControlMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
contentControl = textBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "inputWithFileBtn":
{
var textBox = new TextBox()
{
Margin = new Thickness(10, 0, 0, 0),
Text = Settings[attribute.Name] as string ?? string.Empty,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
var Btn = new System.Windows.Controls.Button()
{
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
};
var dockPanel = new DockPanel()
{
Margin = settingControlMargin
};
DockPanel.SetDock(Btn, Dock.Right);
dockPanel.Children.Add(Btn);
dockPanel.Children.Add(textBox);
contentControl = dockPanel;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "textarea":
{
var textBox = new TextBox()
{
Height = 120,
Margin = settingControlMargin,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.WrapWithOverflow,
AcceptsReturn = true,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
Text = Settings[attribute.Name] as string ?? string.Empty,
ToolTip = attribute.Description
};
textBox.TextChanged += (sender, _) =>
{
Settings[attribute.Name] = ((TextBox)sender).Text;
};
contentControl = textBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "passwordBox":
{
var passwordBox = new PasswordBox()
{
Margin = settingControlMargin,
Password = Settings[attribute.Name] as string ?? string.Empty,
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
passwordBox.PasswordChanged += (sender, _) =>
{
Settings[attribute.Name] = ((PasswordBox)sender).Password;
};
contentControl = passwordBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "dropdown":
{
var comboBox = new System.Windows.Controls.ComboBox()
{
ItemsSource = attribute.Options,
SelectedItem = Settings[attribute.Name],
Margin = settingControlMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
ToolTip = attribute.Description
};
comboBox.SelectionChanged += (sender, _) =>
{
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
};
contentControl = comboBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "checkbox":
var checkBox = new CheckBox
{
IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue),
Margin = settingCheckboxMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
ToolTip = attribute.Description
};
checkBox.Click += (sender, _) =>
{
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
};
contentControl = checkBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
case "hyperlink":
var hyperlink = new Hyperlink
{
ToolTip = attribute.Description, NavigateUri = attribute.url
};
var linkbtn = new System.Windows.Controls.Button
{
HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin
};
linkbtn.Content = attribute.urlLabel;
contentControl = linkbtn;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
default:
continue;
}
if (type != "textBlock")
_settingControls[attribute.Name] = contentControl;
mainPanel.Children.Add(panel);
mainPanel.Children.Add(contentControl);
rowCount++;
}
return settingWindow;
}
public void Save()
{
if (Settings != null)
{
Helper.ValidateDirectory(Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name));
File.WriteAllText(SettingPath, JsonSerializer.Serialize(Settings, settingSerializeOption));
}
}
public void UpdateSettings(Dictionary<string, object> settings)
{
if (settings == null || settings.Count == 0)
return;
foreach (var (key, value) in settings)
{
if (Settings.ContainsKey(key))
{
Settings[key] = value;
}
if (_settingControls.ContainsKey(key))
{
switch (_settingControls[key])
{
case TextBox textBox:
textBox.Dispatcher.Invoke(() => textBox.Text = value as string);
break;
case PasswordBox passwordBox:
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string);
break;
case System.Windows.Controls.ComboBox comboBox:
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
break;
case CheckBox checkBox:
checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string));
break;
}
}
}
}
}
}

View file

@ -0,0 +1,176 @@
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Microsoft.IO;
using System.Windows;
using System.Windows.Controls;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using CheckBox = System.Windows.Controls.CheckBox;
using Control = System.Windows.Controls.Control;
using Orientation = System.Windows.Controls.Orientation;
using TextBox = System.Windows.Controls.TextBox;
using UserControl = System.Windows.Controls.UserControl;
using System.Windows.Documents;
using static System.Windows.Forms.LinkLabel;
using Droplex;
using System.Windows.Forms;
using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.Core.Plugin
{
/// <summary>
/// Represent the plugin that using JsonPRC
/// every JsonRPC plugin should has its own plugin instance
/// </summary>
internal abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
{
protected PluginInitContext Context;
public const string JsonRPC = "JsonRPC";
private int RequestId { get; set; }
private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
public abstract List<Result> LoadContextMenus(Result selectedResult);
private static readonly JsonSerializerOptions options = new()
{
PropertyNameCaseInsensitive = true,
#pragma warning disable SYSLIB0020
// IgnoreNullValues is obsolete, but the replacement JsonIgnoreCondition.WhenWritingNull still
// deserializes null, instead of ignoring it and leaving the default (empty list). We can change the behaviour
// to accept null and fallback to a default etc, or just keep IgnoreNullValues for now
// see: https://github.com/dotnet/runtime/issues/39152
IgnoreNullValues = true,
#pragma warning restore SYSLIB0020 // Type or member is obsolete
Converters =
{
new JsonObjectConverter()
}
};
private static readonly JsonSerializerOptions settingSerializeOption = new()
{
WriteIndented = true
};
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
protected abstract Task<bool> ExecuteResultAsync(JsonRPCResult result);
protected abstract Task<List<Result>> QueryRequestAsync(JsonRPCRequestModel request, CancellationToken token);
protected PortableSettings Settings { get; set; }
protected List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
{
if (queryResponseModel.Result == null) return null;
if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
{
Context.API.ShowMsg(queryResponseModel.DebugMessage);
}
foreach (var result in queryResponseModel.Result)
{
result.AsyncAction = async c =>
{
Settings.UpdateSettings(result.SettingsChange);
return await ExecuteResultAsync(result);
};
}
var results = new List<Result>();
results.AddRange(queryResponseModel.Result);
Settings.UpdateSettings(queryResponseModel.SettingsChange);
return results;
}
protected void ExecuteFlowLauncherAPI(string method, object[] parameters)
{
var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray);
if (methodInfo == null)
{
return;
}
try
{
methodInfo.Invoke(Context.API, parameters);
}
catch (Exception)
{
#if (DEBUG)
throw;
#endif
}
}
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{
var request = new JsonRPCRequestModel(RequestId++,
"query",
new object[]
{
query.Search
},
Settings.Inner);
return await QueryRequestAsync(request, token);
}
private async Task InitSettingAsync()
{
if (!File.Exists(SettingConfigurationPath))
return;
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
var configuration = deserializer.Deserialize<JsonRpcConfigurationModel>(await File.ReadAllTextAsync(SettingConfigurationPath));
Settings ??= new PortableSettings
{
Configuration = configuration,
SettingPath = SettingPath,
API = Context.API
};
}
public virtual async Task InitAsync(PluginInitContext context)
{
this.Context = context;
await InitSettingAsync();
}
public void Save()
{
Settings.Save();
}
public Control CreateSettingPanel()
{
return Settings.CreateSettingPanel();
}
}
}

View file

@ -64,8 +64,8 @@ namespace Flow.Launcher.Core.Plugin
query
});
await InputMessageChannel.Writer.WriteAsync(message, token);
await Task.Delay(50);
await InputStream.FlushAsync();
await Task.Delay(50, token);
await InputStream.FlushAsync(token);
var task = new TaskCompletionSource<JsonRPCQueryResponseModel>();
RequestTaskDictionary[currentRequestId] = task;
var result = await task.Task;

View file

@ -0,0 +1,402 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin
{
public class PortableSettings
{
public required JsonRpcConfigurationModel Configuration { get; init; }
public required string SettingPath { get; init; }
public Dictionary<string, FrameworkElement> SettingControls { get; } = new();
public IReadOnlyDictionary<string, object> Inner => Settings;
protected Dictionary<string, object> Settings { get; set; }
public required IPublicAPI API { get; init; }
private JsonStorage<Dictionary<string, object>> _storage;
// maybe move to resource?
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9);
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
public async Task InitializeAsync()
{
_storage = new JsonStorage<Dictionary<string, object>>(SettingPath);
Settings = await _storage.LoadAsync();
}
public void UpdateSettings(IReadOnlyDictionary<string, object> settings)
{
if (settings == null || settings.Count == 0)
return;
foreach (var (key, value) in settings)
{
if (Settings.ContainsKey(key))
{
Settings[key] = value;
}
if (SettingControls.TryGetValue(key, out var control))
{
switch (control)
{
case TextBox textBox:
textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty);
break;
case PasswordBox passwordBox:
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty);
break;
case ComboBox comboBox:
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
break;
case CheckBox checkBox:
checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string ?? string.Empty));
break;
}
}
}
}
public async Task SaveAsync()
{
await _storage.SaveAsync();
}
public void Save()
{
_storage.Save();
}
public Control CreateSettingPanel()
{
if (Settings == null)
return new();
var settingWindow = new UserControl();
var mainPanel = new Grid
{
Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center
};
ColumnDefinition gridCol1 = new ColumnDefinition();
ColumnDefinition gridCol2 = new ColumnDefinition();
gridCol1.Width = new GridLength(70, GridUnitType.Star);
gridCol2.Width = new GridLength(30, GridUnitType.Star);
mainPanel.ColumnDefinitions.Add(gridCol1);
mainPanel.ColumnDefinitions.Add(gridCol2);
settingWindow.Content = mainPanel;
int rowCount = 0;
foreach (var (type, attribute) in Configuration.Body)
{
Separator sep = new Separator();
sep.VerticalAlignment = VerticalAlignment.Top;
sep.Margin = settingSepMargin;
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
var panel = new StackPanel
{
Orientation = Orientation.Vertical,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingLabelPanelMargin
};
RowDefinition gridRow = new RowDefinition();
mainPanel.RowDefinitions.Add(gridRow);
var name = new TextBlock()
{
Text = attribute.Label,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingLabelMargin,
TextWrapping = TextWrapping.WrapWithOverflow
};
var desc = new TextBlock()
{
Text = attribute.Description,
FontSize = 12,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingDescMargin,
TextWrapping = TextWrapping.WrapWithOverflow
};
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
if (attribute.Description == null) /* if no description, hide */
desc.Visibility = Visibility.Collapsed;
if (type != "textBlock") /* if textBlock, hide desc */
{
panel.Children.Add(name);
panel.Children.Add(desc);
}
Grid.SetColumn(panel, 0);
Grid.SetRow(panel, rowCount);
FrameworkElement contentControl;
switch (type)
{
case "textBlock":
{
contentControl = new TextBlock
{
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
Margin = settingTextBlockMargin,
Padding = new Thickness(0, 0, 0, 0),
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
TextAlignment = TextAlignment.Left,
TextWrapping = TextWrapping.Wrap
};
Grid.SetColumn(contentControl, 0);
Grid.SetColumnSpan(contentControl, 2);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "input":
{
var textBox = new TextBox()
{
Text = Settings[attribute.Name] as string ?? string.Empty,
Margin = settingControlMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
contentControl = textBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "inputWithFileBtn":
{
var textBox = new TextBox()
{
Margin = new Thickness(10, 0, 0, 0),
Text = Settings[attribute.Name] as string ?? string.Empty,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
var Btn = new System.Windows.Controls.Button()
{
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
};
var dockPanel = new DockPanel()
{
Margin = settingControlMargin
};
DockPanel.SetDock(Btn, Dock.Right);
dockPanel.Children.Add(Btn);
dockPanel.Children.Add(textBox);
contentControl = dockPanel;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "textarea":
{
var textBox = new TextBox()
{
Height = 120,
Margin = settingControlMargin,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.WrapWithOverflow,
AcceptsReturn = true,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
Text = Settings[attribute.Name] as string ?? string.Empty,
ToolTip = attribute.Description
};
textBox.TextChanged += (sender, _) =>
{
Settings[attribute.Name] = ((TextBox)sender).Text;
};
contentControl = textBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "passwordBox":
{
var passwordBox = new PasswordBox()
{
Margin = settingControlMargin,
Password = Settings[attribute.Name] as string ?? string.Empty,
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
passwordBox.PasswordChanged += (sender, _) =>
{
Settings[attribute.Name] = ((PasswordBox)sender).Password;
};
contentControl = passwordBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "dropdown":
{
var comboBox = new System.Windows.Controls.ComboBox()
{
ItemsSource = attribute.Options,
SelectedItem = Settings[attribute.Name],
Margin = settingControlMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
ToolTip = attribute.Description
};
comboBox.SelectionChanged += (sender, _) =>
{
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
};
contentControl = comboBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "checkbox":
var checkBox = new CheckBox
{
IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue),
Margin = settingCheckboxMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
ToolTip = attribute.Description
};
checkBox.Click += (sender, _) =>
{
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
};
contentControl = checkBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
case "hyperlink":
var hyperlink = new Hyperlink
{
ToolTip = attribute.Description, NavigateUri = attribute.url
};
var linkbtn = new System.Windows.Controls.Button
{
HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin
};
linkbtn.Content = attribute.urlLabel;
contentControl = linkbtn;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
default:
continue;
}
if (type != "textBlock")
SettingControls[attribute.Name] = contentControl;
mainPanel.Children.Add(panel);
mainPanel.Children.Add(contentControl);
rowCount++;
}
return settingWindow;
}
}
}

View file

@ -3,6 +3,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher.Infrastructure.Storage
@ -26,6 +27,82 @@ namespace Flow.Launcher.Infrastructure.Storage
protected string DirectoryPath { get; init; } = null!;
// Let the derived class to set the file path
protected JsonStorage()
{
}
public JsonStorage(string filePath)
{
FilePath = filePath;
}
public async Task<T> LoadAsync()
{
if (Data != null)
return Data;
string? serialized = null;
if (File.Exists(FilePath))
{
serialized = await File.ReadAllTextAsync(FilePath);
}
if (!string.IsNullOrEmpty(serialized))
{
try
{
Data = JsonSerializer.Deserialize<T>(serialized) ?? await LoadBackupOrDefaultAsync();
}
catch (JsonException)
{
Data = await LoadBackupOrDefaultAsync();
}
}
else
{
Data = await LoadBackupOrDefaultAsync();
}
return Data.NonNull();
}
private async ValueTask<T> LoadBackupOrDefaultAsync()
{
var backup = await TryLoadBackupAsync();
return backup ?? LoadDefault();
}
private async ValueTask<T?> TryLoadBackupAsync()
{
if (!File.Exists(BackupFilePath))
return default;
try
{
await using var source = File.OpenRead(BackupFilePath);
var data = await JsonSerializer.DeserializeAsync<T>(source) ?? default;
if (data != null)
RestoreBackup();
return data;
}
catch (JsonException)
{
return default;
}
}
private void RestoreBackup()
{
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
if (File.Exists(FilePath))
File.Replace(BackupFilePath, FilePath, null);
else
File.Move(BackupFilePath, FilePath);
}
public T Load()
{
@ -75,18 +152,9 @@ namespace Flow.Launcher.Infrastructure.Storage
var data = JsonSerializer.Deserialize<T>(File.ReadAllText(BackupFilePath));
if (data != null)
{
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
if(File.Exists(FilePath))
File.Replace(BackupFilePath, FilePath, null);
else
File.Move(BackupFilePath, FilePath);
RestoreBackup();
return data;
}
return default;
return data;
}
catch (JsonException)
{
@ -115,6 +183,20 @@ namespace Flow.Launcher.Infrastructure.Storage
File.WriteAllText(TempFilePath, serialized);
AtomicWriteSetting();
}
public async Task SaveAsync()
{
var tempOutput = File.OpenWrite(TempFilePath);
await JsonSerializer.SerializeAsync(tempOutput, Data,
new JsonSerializerOptions
{
WriteIndented = true
});
AtomicWriteSetting();
}
private void AtomicWriteSetting()
{
if (!File.Exists(FilePath))
{
File.Move(TempFilePath, FilePath);
@ -124,5 +206,6 @@ namespace Flow.Launcher.Infrastructure.Storage
File.Replace(TempFilePath, FilePath, BackupFilePath);
}
}
}
}