diff --git a/backend/FwLite/FwLiteMaui/App.xaml.cs b/backend/FwLite/FwLiteMaui/App.xaml.cs index c4d2cc6b53..88511ac633 100644 --- a/backend/FwLite/FwLiteMaui/App.xaml.cs +++ b/backend/FwLite/FwLiteMaui/App.xaml.cs @@ -1,22 +1,24 @@ -using FwLiteShared.Services; namespace FwLiteMaui; public partial class App : Application { + public IServiceProvider ServiceProvider { get; } private readonly MainPage _mainPage; + public static string? OverrideStartupUrl { get; set; } - public App(MainPage mainPage, IPreferencesService preferences) + public App(MainPage mainPage, IServiceProvider serviceProvider) { + ServiceProvider = serviceProvider; _mainPage = mainPage; - var lastUrl = preferences.Get(nameof(PreferenceKey.AppLastUrl)); - if (lastUrl?.StartsWith('/') == true) - { - mainPage.StartPath = lastUrl; - } InitializeComponent(); } + internal void LoadAppUrl(string url) + { + _mainPage.LoadAppUrl(url); + } + protected override Window CreateWindow(IActivationState? activationState) { return CreateWindow(_mainPage); diff --git a/backend/FwLite/FwLiteMaui/MainPage.xaml.cs b/backend/FwLite/FwLiteMaui/MainPage.xaml.cs index f6b60aa407..a20532ff48 100644 --- a/backend/FwLite/FwLiteMaui/MainPage.xaml.cs +++ b/backend/FwLite/FwLiteMaui/MainPage.xaml.cs @@ -1,28 +1,62 @@ +using FwLiteShared.Services; using Microsoft.AspNetCore.Components.WebView; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; +using Microsoft.JSInterop; namespace FwLiteMaui; public partial class MainPage : ContentPage { - public MainPage() + private readonly ILogger _logger; + + public MainPage(IPreferencesService preferences, ILogger logger) { + _logger = logger; InitializeComponent(); + var lastUrlFromPrefs = preferences.Get(nameof(PreferenceKey.AppLastUrl)); blazorWebView.BlazorWebViewInitializing += BlazorWebViewInitializing; blazorWebView.BlazorWebViewInitialized += BlazorWebViewInitialized; + blazorWebView.BlazorWebViewInitialized += (s, e) => + { + // Decide initial StartPath here (after Android intent is processed) to avoid cold-start race + var initial = App.OverrideStartupUrl ?? lastUrlFromPrefs ?? "/"; + //only change it if it's still the default, might have been changed already, for example when opening a new window + if (blazorWebView.StartPath == "/") + blazorWebView.StartPath = initial; + App.OverrideStartupUrl = null; + }; blazorWebView.UrlLoading += BlazorWebViewOnUrlLoading; } - - internal string StartPath { get => blazorWebView.StartPath; set => blazorWebView.StartPath = value; } + internal void LoadAppUrl(string url) + { + blazorWebView.StartPath = url; + _ = blazorWebView.TryDispatchAsync(services => + { + var jsRuntime = services.GetService(); + if (jsRuntime != null) + _ = NavigateAsync(jsRuntime, url); + }); + } + + private async Task NavigateAsync(IJSRuntime jsRuntime, string url) + { + try + { + await jsRuntime.InvokeVoidAsync("lexbox.SvelteNavigate", url, new { replace = true }); + } + catch (Exception e) + { + _logger.LogError(e, "Failed to navigate to {Url} via SvelteNavigate", url); + } + } + #if ANDROID || WINDOWS private partial void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e); private partial void BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e); diff --git a/backend/FwLite/FwLiteMaui/MauiProgram.cs b/backend/FwLite/FwLiteMaui/MauiProgram.cs index 30630df99c..7a66e0b314 100644 --- a/backend/FwLite/FwLiteMaui/MauiProgram.cs +++ b/backend/FwLite/FwLiteMaui/MauiProgram.cs @@ -1,3 +1,4 @@ +using FwLiteShared.Services; using Microsoft.Extensions.Logging; using Microsoft.Maui.LifecycleEvents; @@ -52,6 +53,41 @@ public static MauiApp CreateMauiApp() builder.ConfigureEssentials(essentialsBuilder => { essentialsBuilder.UseVersionTracking(); + + // Register all shortcuts from a single central place + foreach (var action in Shortcuts.Declarations) + { + essentialsBuilder.AddAppAction(action); + } + + essentialsBuilder.OnAppAction(action => + { + var app = (App?)Application.Current; + if (app is null) return; + if (Shortcuts.TryGetUrl(action.Id, out var url)) + { + app.Dispatcher.Dispatch(() => + { + app.LoadAppUrl(url); + }); + } + else if (action.Id == Shortcuts.ShareLogOut) + { + _ = app.Dispatcher.DispatchAsync(async () => + { + try + { + await app.ServiceProvider.GetRequiredService() + .ShareLogFile(); + } + catch (Exception e) + { + app.ServiceProvider.GetService>()? + .LogError(e, "Failed to share log file from app action"); + } + }); + } + }); }); builder.Services.AddFwLiteMauiServices(builder.Configuration, builder.Logging); diff --git a/backend/FwLite/FwLiteMaui/Platforms/Android/MainActivity.cs b/backend/FwLite/FwLiteMaui/Platforms/Android/MainActivity.cs index c8b0d36594..be23b14aa0 100644 --- a/backend/FwLite/FwLiteMaui/Platforms/Android/MainActivity.cs +++ b/backend/FwLite/FwLiteMaui/Platforms/Android/MainActivity.cs @@ -12,16 +12,41 @@ namespace FwLiteMaui; [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, + ResizeableActivity = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +[IntentFilter([Platform.Intent.ActionAppAction], + Categories = [Intent.CategoryDefault])] public class MainActivity : MauiAppCompatActivity { protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); + + if (Intent?.Action == Platform.Intent.ActionAppAction) + { + //name comes from internal maui code: https://github.com/dotnet/maui/blob/271d2505eb436600bb84002c8941670abb0ae23b/src/Essentials/src/AppActions/AppActions.android.cs#L83 + var actionId = Intent.GetStringExtra("EXTRA_XE_APP_ACTION_ID"); + if (Shortcuts.TryGetUrl(actionId, out var url)) + { + App.OverrideStartupUrl = url; + } + } + ApplyBrandedSystemBars(); } + protected override void OnResume() + { + base.OnResume(); + Platform.OnResume(this); + } + + protected override void OnNewIntent(Intent? intent) + { + base.OnNewIntent(intent); + Platform.OnNewIntent(intent); + } public override void OnConfigurationChanged(Configuration newConfig) { base.OnConfigurationChanged(newConfig); diff --git a/backend/FwLite/FwLiteMaui/Shortcuts.cs b/backend/FwLite/FwLiteMaui/Shortcuts.cs new file mode 100644 index 0000000000..591112bf5a --- /dev/null +++ b/backend/FwLite/FwLiteMaui/Shortcuts.cs @@ -0,0 +1,35 @@ +namespace FwLiteMaui; + +public static class Shortcuts +{ + public const string Home = "home"; + public const string ShareLogOut = "share-log-out"; + + private static readonly IReadOnlyDictionary IdToUrl = new Dictionary + { + [Home] = "/", + }; + + // Titles/subtitles shown in the system UI (if supported) + public static readonly IReadOnlyList Declarations = + [ + new(Home, "Home"), + new(ShareLogOut, "Share Debug Log"), + ]; + + public static bool TryGetUrl(string? id, out string url) + { + if (id is null) + { + url = string.Empty; + return false; + } + if (IdToUrl.TryGetValue(id, out var found)) + { + url = found; + return true; + } + url = string.Empty; + return false; + } +} diff --git a/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs b/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs index 03e1b416e4..df5bfa04d7 100644 --- a/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs +++ b/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs @@ -53,6 +53,13 @@ static int Fail(string message) if (cctor is null || !cctor.HasBody) return Fail("SqlTransparentExpression .cctor not found (or has no body)."); + if (cctor.Body.Instructions.Count == 1 && cctor.Body.Instructions[0].OpCode == OpCodes.Ret) + { + Console.WriteLine($"Already patched (inferred from IL): {dllPath}"); + File.WriteAllText(markerPath, DateTime.UtcNow.ToString("O")); + return 0; + } + // Sanity-check the cctor shape: at least one stsfld targeting the _ctor field. // If upstream renames _ctor or restructures the field init, we want to know. var storesCtorField = cctor.Body.Instructions.Any(ins => diff --git a/frontend/viewer/src/lib/services/service-declaration.ts b/frontend/viewer/src/lib/services/service-declaration.ts index 81f8cb1c55..42a63eade1 100644 --- a/frontend/viewer/src/lib/services/service-declaration.ts +++ b/frontend/viewer/src/lib/services/service-declaration.ts @@ -10,6 +10,8 @@ declare global { ServiceProvider: LexboxServiceProvider; Search: { openSearch: (search: string) => void }; IsDotnetHosted?: boolean; + // Expose svelte-routing navigate for native hosts (MAUI) and other integrations + SvelteNavigate?: (url: string, options?: { replace?: boolean }) => void; /* eslint-enable @typescript-eslint/naming-convention */ } diff --git a/frontend/viewer/src/main.ts b/frontend/viewer/src/main.ts index c657072fee..92ebdd6d2c 100644 --- a/frontend/viewer/src/main.ts +++ b/frontend/viewer/src/main.ts @@ -7,6 +7,7 @@ import '@formatjs/intl-durationformat/polyfill'; import App from './App.svelte'; import {mount} from 'svelte'; +import {navigate} from 'svelte-routing'; import {setupDotnetServiceProvider} from './lib/services/service-provider-dotnet'; import {setupServiceProvider} from '$lib/services/service-provider'; import {setupBrowserAppServices} from '$lib/services/browser-app-services'; @@ -20,6 +21,9 @@ if (!window.lexbox.IsDotnetHosted) { } useEventBus(); +// Wire up globally-accessible helpers for hosts (e.g., MAUI) +window.lexbox.SvelteNavigate = (url: string, options?: { replace?: boolean }) => navigate(url, options); + //don't mount the app until after we've loaded the local void setLanguage('default') .then(() => {