mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
refactor jsonrpc structure (extract setting to a standalone file PortableSettings.cs)
This commit is contained in:
parent
16dcdf01fd
commit
683f6ebce4
6 changed files with 704 additions and 451 deletions
|
|
@ -28,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
|
public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
|
||||||
public record JsonRPCQueryResponseModel(int Id,
|
public record JsonRPCQueryResponseModel(int Id,
|
||||||
[property: JsonPropertyName("result")] List<JsonRPCResult> Result,
|
[property: JsonPropertyName("result")] List<JsonRPCResult> Result,
|
||||||
Dictionary<string, object> SettingsChange = null,
|
IReadOnlyDictionary<string, object> SettingsChange = null,
|
||||||
string DebugMessage = "",
|
string DebugMessage = "",
|
||||||
JsonRPCErrorModel Error = default) : JsonRPCResponseModel(Id, Error);
|
JsonRPCErrorModel Error = default) : JsonRPCResponseModel(Id, Error);
|
||||||
|
|
||||||
public record JsonRPCRequestModel(int Id,
|
public record JsonRPCRequestModel(int Id,
|
||||||
string Method,
|
string Method,
|
||||||
object[] Parameters,
|
object[] Parameters,
|
||||||
Dictionary<string, object> Settings = default,
|
IReadOnlyDictionary<string, object> Settings = default,
|
||||||
JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
|
JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
int Id,
|
int Id,
|
||||||
string Method,
|
string Method,
|
||||||
object[] Parameters,
|
object[] Parameters,
|
||||||
Dictionary<string, object> Settings,
|
IReadOnlyDictionary<string, object> Settings,
|
||||||
bool DontHideAfterAction = false,
|
bool DontHideAfterAction = false,
|
||||||
JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error);
|
JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
/// Represent the plugin that using JsonPRC
|
/// Represent the plugin that using JsonPRC
|
||||||
/// every JsonRPC plugin should has its own plugin instance
|
/// every JsonRPC plugin should has its own plugin instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
internal abstract class JsonRPCPlugin : JsonRPCPluginBase
|
||||||
{
|
{
|
||||||
protected PluginInitContext Context;
|
|
||||||
public const string JsonRPC = "JsonRPC";
|
public const string JsonRPC = "JsonRPC";
|
||||||
|
|
||||||
protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
|
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 SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
|
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++,
|
var request = new JsonRPCRequestModel(RequestId++,
|
||||||
"context_menu",
|
"context_menu",
|
||||||
|
|
@ -77,7 +76,6 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
WriteIndented = true
|
WriteIndented = true
|
||||||
};
|
};
|
||||||
private Dictionary<string, object> Settings { get; set; }
|
|
||||||
|
|
||||||
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
|
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
|
||||||
|
|
||||||
|
|
@ -103,85 +101,42 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return ParseResults(queryResponseModel);
|
return ParseResults(queryResponseModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
|
||||||
private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
|
|
||||||
{
|
{
|
||||||
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;
|
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>();
|
return !result.JsonRPCAction.DontHideAfterAction;
|
||||||
|
|
||||||
results.AddRange(queryResponseModel.Result);
|
|
||||||
|
|
||||||
UpdateSettings(queryResponseModel.SettingsChange);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Execute external program and return the output
|
/// 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);
|
var output = await RequestAsync(request, token);
|
||||||
return await DeserializedResultAsync(output);
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
176
Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
Normal file
176
Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -64,8 +64,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
query
|
query
|
||||||
});
|
});
|
||||||
await InputMessageChannel.Writer.WriteAsync(message, token);
|
await InputMessageChannel.Writer.WriteAsync(message, token);
|
||||||
await Task.Delay(50);
|
await Task.Delay(50, token);
|
||||||
await InputStream.FlushAsync();
|
await InputStream.FlushAsync(token);
|
||||||
var task = new TaskCompletionSource<JsonRPCQueryResponseModel>();
|
var task = new TaskCompletionSource<JsonRPCQueryResponseModel>();
|
||||||
RequestTaskDictionary[currentRequestId] = task;
|
RequestTaskDictionary[currentRequestId] = task;
|
||||||
var result = await task.Task;
|
var result = await task.Task;
|
||||||
|
|
|
||||||
402
Flow.Launcher.Core/Plugin/PortableSettings.cs
Normal file
402
Flow.Launcher.Core/Plugin/PortableSettings.cs
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
|
|
@ -26,6 +27,82 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
|
|
||||||
protected string DirectoryPath { get; init; } = null!;
|
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()
|
public T Load()
|
||||||
{
|
{
|
||||||
|
|
@ -75,18 +152,9 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
var data = JsonSerializer.Deserialize<T>(File.ReadAllText(BackupFilePath));
|
var data = JsonSerializer.Deserialize<T>(File.ReadAllText(BackupFilePath));
|
||||||
|
|
||||||
if (data != null)
|
if (data != null)
|
||||||
{
|
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);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (JsonException)
|
||||||
{
|
{
|
||||||
|
|
@ -115,6 +183,20 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
|
|
||||||
File.WriteAllText(TempFilePath, serialized);
|
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))
|
if (!File.Exists(FilePath))
|
||||||
{
|
{
|
||||||
File.Move(TempFilePath, FilePath);
|
File.Move(TempFilePath, FilePath);
|
||||||
|
|
@ -124,5 +206,6 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
File.Replace(TempFilePath, FilePath, BackupFilePath);
|
File.Replace(TempFilePath, FilePath, BackupFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue