Skip to content

Commit f290f41

Browse files
committed
Start minimal Avalonia migration PR
1 parent 6a98243 commit f290f41

138 files changed

Lines changed: 23851 additions & 7 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/dotnet.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ jobs:
5353
- name: Perform post_build tasks
5454
shell: powershell
5555
run: .\Scripts\post_build.ps1
56+
- name: Upload Avalonia Build
57+
uses: actions/upload-artifact@v7
58+
with:
59+
name: Avalonia Build
60+
path: |
61+
Output\Avalonia\Release\**\*
62+
compression-level: 0
5663
- name: Upload Plugin Nupkg
5764
uses: actions/upload-artifact@v7
5865
with:

Flow.Launcher.Avalonia/App.axaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<Application xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:local="using:Flow.Launcher.Avalonia"
4+
xmlns:sty="using:FluentAvalonia.Styling"
5+
xmlns:i18n="using:Flow.Launcher.Avalonia.Resource"
6+
x:Class="Flow.Launcher.Avalonia.App"
7+
RequestedThemeVariant="Default">
8+
<!-- "Default" ThemeVariant follows system theme variant.
9+
"Dark" or "Light" are other available options. -->
10+
11+
<Application.Styles>
12+
<sty:FluentAvaloniaTheme />
13+
<StyleInclude Source="avares://Flow.Launcher.Avalonia/Themes/Base.axaml"/>
14+
</Application.Styles>
15+
16+
<Application.Resources>
17+
<ResourceDictionary>
18+
<ResourceDictionary.MergedDictionaries>
19+
<!-- Embedded fonts -->
20+
</ResourceDictionary.MergedDictionaries>
21+
<!-- Register Segoe Fluent Icons font for glyph icons -->
22+
<FontFamily x:Key="SegoeFluentIcons">avares://Flow.Launcher.Avalonia/Resources#Segoe Fluent Icons</FontFamily>
23+
</ResourceDictionary>
24+
</Application.Resources>
25+
26+
<TrayIcon.Icons>
27+
<TrayIcons>
28+
<TrayIcon Icon="avares://Flow.Launcher.Avalonia/Images/app.ico"
29+
ToolTipText="Flow Launcher"
30+
Clicked="TrayIcon_OnClicked">
31+
<TrayIcon.Menu>
32+
<NativeMenu>
33+
<NativeMenuItem Header="{i18n:Localize show}" Click="MenuShow_OnClick" />
34+
<NativeMenuItem Header="{i18n:Localize settings}" Click="MenuSettings_OnClick" />
35+
<NativeMenuItemSeparator />
36+
<NativeMenuItem Header="{i18n:Localize exit}" Click="MenuExit_OnClick" />
37+
</NativeMenu>
38+
</TrayIcon.Menu>
39+
</TrayIcon>
40+
</TrayIcons>
41+
</TrayIcon.Icons>
42+
</Application>
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
using Avalonia;
2+
using Avalonia.Controls;
3+
using Avalonia.Controls.ApplicationLifetimes;
4+
using Avalonia.Markup.Xaml;
5+
using Avalonia.Threading;
6+
using CommunityToolkit.Mvvm.DependencyInjection;
7+
using Flow.Launcher.Avalonia.Helper;
8+
using Flow.Launcher.Avalonia.Resource;
9+
using Flow.Launcher.Avalonia.Views.Dialogs;
10+
using Flow.Launcher.Avalonia.ViewModel;
11+
using Flow.Launcher.Avalonia.Views.SettingPages;
12+
using Flow.Launcher.Core;
13+
using Flow.Launcher.Core.Configuration;
14+
using Flow.Launcher.Core.Plugin;
15+
using Flow.Launcher.Infrastructure;
16+
using Flow.Launcher.Infrastructure.Logger;
17+
using Flow.Launcher.Infrastructure.Storage;
18+
using Flow.Launcher.Infrastructure.UserSettings;
19+
using Flow.Launcher.Plugin;
20+
using Microsoft.Extensions.DependencyInjection;
21+
using System;
22+
using System.Diagnostics;
23+
using System.Threading.Tasks;
24+
25+
namespace Flow.Launcher.Avalonia;
26+
27+
public partial class App : Application
28+
{
29+
private static readonly string ClassName = nameof(App);
30+
private Settings? _settings;
31+
private MainViewModel? _mainVM;
32+
private MainWindow? _mainWindow;
33+
34+
public static IPublicAPI? API { get; private set; }
35+
36+
public override void Initialize()
37+
{
38+
// Configure DI before loading XAML so markup extensions can access services
39+
LoadSettings();
40+
ConfigureDI();
41+
42+
AvaloniaXamlLoader.Load(this);
43+
44+
// Inject translations into Application.Resources for DynamicResource bindings in plugins
45+
var i18n = Ioc.Default.GetRequiredService<Internationalization>();
46+
i18n.InjectIntoApplicationResources();
47+
48+
#if DEBUG
49+
this.AttachDeveloperTools();
50+
#endif
51+
}
52+
53+
public override void OnFrameworkInitializationCompleted()
54+
{
55+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
56+
{
57+
API = Ioc.Default.GetRequiredService<IPublicAPI>();
58+
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
59+
60+
_mainWindow = new MainWindow();
61+
// desktop.MainWindow = _mainWindow; // Prevent auto-show on startup
62+
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
63+
64+
// Initialize hotkeys after window is created
65+
HotKeyMapper.Initialize();
66+
67+
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
68+
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
69+
Dispatcher.UIThread.UnhandledException += OnUiUnhandledException;
70+
71+
AutoStartup();
72+
73+
Dispatcher.UIThread.Post(async () => await InitializePluginsAsync(), DispatcherPriority.Background);
74+
75+
// Cleanup on exit
76+
desktop.Exit += (_, _) =>
77+
{
78+
HotKeyMapper.Shutdown();
79+
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
80+
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
81+
Dispatcher.UIThread.UnhandledException -= OnUiUnhandledException;
82+
};
83+
}
84+
base.OnFrameworkInitializationCompleted();
85+
}
86+
87+
/// <summary>
88+
/// Check startup only for Release.
89+
/// </summary>
90+
[Conditional("RELEASE")]
91+
private void AutoStartup()
92+
{
93+
if (_settings?.StartFlowLauncherOnSystemStartup != true)
94+
{
95+
return;
96+
}
97+
98+
try
99+
{
100+
Helper.AutoStartup.CheckIsEnabled(_settings.UseLogonTaskForStartup);
101+
}
102+
catch (Exception e)
103+
{
104+
_settings.StartFlowLauncherOnSystemStartup = false;
105+
_settings.Save();
106+
API?.ShowMsgError(Translator.GetString("setAutoStartFailed"), e.Message);
107+
}
108+
}
109+
110+
private void TrayIcon_OnClicked(object? sender, EventArgs e)
111+
{
112+
_mainVM?.ToggleFlowLauncher();
113+
}
114+
115+
private void MenuShow_OnClick(object? sender, EventArgs e)
116+
{
117+
_mainVM?.Show();
118+
}
119+
120+
private void MenuSettings_OnClick(object? sender, EventArgs e)
121+
{
122+
var settingsWindow = new SettingsWindow();
123+
settingsWindow.Show();
124+
}
125+
126+
private void MenuExit_OnClick(object? sender, EventArgs e)
127+
{
128+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
129+
{
130+
desktop.Shutdown();
131+
}
132+
}
133+
134+
private void LoadSettings()
135+
{
136+
try
137+
{
138+
var storage = new FlowLauncherJsonStorage<Settings>();
139+
_settings = storage.Load();
140+
_settings.SetStorage(storage);
141+
}
142+
catch (Exception e)
143+
{
144+
Log.Exception(ClassName, "Settings load failed", e);
145+
var storage = new FlowLauncherJsonStorage<Settings>();
146+
_settings = new Settings
147+
{
148+
WindowSize = 580, WindowHeightSize = 42, QueryBoxFontSize = 24,
149+
ItemHeightSize = 50, ResultItemFontSize = 14, ResultSubItemFontSize = 12, MaxResultsToShow = 6
150+
};
151+
_settings.SetStorage(storage);
152+
}
153+
}
154+
155+
private void ConfigureDI()
156+
{
157+
var services = new ServiceCollection();
158+
services.AddSingleton(_settings!);
159+
services.AddSingleton(sp => new Updater(sp.GetRequiredService<IPublicAPI>(), Constant.GitHub));
160+
services.AddSingleton<Portable>();
161+
services.AddSingleton<IAlphabet, PinyinAlphabet>();
162+
services.AddSingleton<StringMatcher>();
163+
services.AddSingleton<Internationalization>();
164+
services.AddSingleton<MainViewModel>();
165+
services.AddSingleton<IPublicAPI>(sp => new AvaloniaPublicAPI(
166+
sp.GetRequiredService<Settings>(),
167+
() => sp.GetRequiredService<MainViewModel>(),
168+
sp.GetRequiredService<Internationalization>()));
169+
Ioc.Default.ConfigureServices(services.BuildServiceProvider());
170+
}
171+
172+
private async Task InitializePluginsAsync()
173+
{
174+
try
175+
{
176+
Log.Info(ClassName, "Loading plugins...");
177+
PluginManager.LoadPlugins(_settings!.PluginSettings);
178+
Log.Info(ClassName, $"Loaded {PluginManager.GetAllLoadedPlugins().Count} plugins");
179+
180+
await PluginManager.InitializePluginsAsync(_mainVM!);
181+
Log.Info(ClassName, "Plugins initialized");
182+
183+
// Update plugin translations after they are initialized
184+
var i18n = Ioc.Default.GetRequiredService<Internationalization>();
185+
i18n.UpdatePluginMetadataTranslations();
186+
187+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
188+
{
189+
desktop.MainWindow = _mainWindow;
190+
}
191+
192+
_mainVM?.OnPluginsReady();
193+
}
194+
catch (Exception e) { Log.Exception(ClassName, "Plugin init failed", e); }
195+
}
196+
197+
private void OnUiUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
198+
{
199+
Log.Exception(ClassName, "Unhandled UI exception", e.Exception);
200+
ShowReportWindow(e.Exception);
201+
e.Handled = true;
202+
}
203+
204+
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
205+
{
206+
if (e.ExceptionObject is Exception exception)
207+
{
208+
Log.Exception(ClassName, "Unhandled exception", exception);
209+
ShowReportWindow(exception);
210+
}
211+
}
212+
213+
private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
214+
{
215+
Log.Exception(ClassName, "Unobserved task exception occurred.", e.Exception);
216+
e.SetObserved();
217+
}
218+
219+
private static void ShowReportWindow(Exception exception)
220+
{
221+
Dispatcher.UIThread.Post(() =>
222+
{
223+
var window = new ReportWindow(exception);
224+
window.Show();
225+
window.Activate();
226+
});
227+
}
228+
}

0 commit comments

Comments
 (0)