From fc1949c9f3fc4be40886671a521ecf009f267e11 Mon Sep 17 00:00:00 2001 From: james-wright-rostro Date: Sat, 17 Jan 2026 20:13:31 +0000 Subject: [PATCH 1/3] Adding update service --- AllStream.Shared/Models/Settings.cs | 1 + .../Services/IAppUpdateService.cs | 9 ++ AllStream.Web/Program.cs | 3 + AllStream.Web/appsettings.json | 9 +- AllStream/MainPage.xaml.cs | 14 ++++ AllStream/MauiProgram.cs | 6 +- .../Updates/AndroidAppUpdateService.cs | 83 +++++++++++++++++++ 7 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 AllStream.Shared/Services/IAppUpdateService.cs create mode 100644 AllStream/Platforms/Android/Updates/AndroidAppUpdateService.cs diff --git a/AllStream.Shared/Models/Settings.cs b/AllStream.Shared/Models/Settings.cs index 796615b..4ececed 100644 --- a/AllStream.Shared/Models/Settings.cs +++ b/AllStream.Shared/Models/Settings.cs @@ -5,4 +5,5 @@ public class Settings public string TmdbApiKey { get; set; } = string.Empty; public string NoAdsProxyBaseUrl { get; set; } = string.Empty; public string LiveSportApiKey { get; set; } = "https://livesport.su/api/"; + public string UpdateManifestUrl { get; set; } = string.Empty; } diff --git a/AllStream.Shared/Services/IAppUpdateService.cs b/AllStream.Shared/Services/IAppUpdateService.cs new file mode 100644 index 0000000..88affa2 --- /dev/null +++ b/AllStream.Shared/Services/IAppUpdateService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace AllStream.Shared.Services +{ + public interface IAppUpdateService + { + Task CheckForUpdatesAsync(); + } +} diff --git a/AllStream.Web/Program.cs b/AllStream.Web/Program.cs index eb9105d..899776b 100644 --- a/AllStream.Web/Program.cs +++ b/AllStream.Web/Program.cs @@ -33,6 +33,9 @@ .AddInteractiveServerRenderMode() .AddAdditionalAssemblies(typeof(AllStream.Shared._Imports).Assembly); +var manifest = builder.Configuration.GetSection("Manifest").Get() ?? new UpdateManifest(); +app.MapGet("/manifest.json", () => Results.Json(manifest)); + app.Run(); public partial class Program { } diff --git a/AllStream.Web/appsettings.json b/AllStream.Web/appsettings.json index dba94a1..c215048 100644 --- a/AllStream.Web/appsettings.json +++ b/AllStream.Web/appsettings.json @@ -6,5 +6,10 @@ } }, "AllowedHosts": "*", - "TmdbApiKey": "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJkZDkyMzY0MWVhZTIxYWE1OWZhZjczYWE4OTdlNGUzYiIsIm5iZiI6MTc2Mzk4NTkzOC45ODUsInN1YiI6IjY5MjQ0YTEyNjk3YzljMWI5NWI3NDliNyIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.U-nF0JTZLIcMIXR8qFZuWwPI3bo2CuDfinmijRyuTJo" -} \ No newline at end of file + "TmdbApiKey": "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJkZDkyMzY0MWVhZTIxYWE1OWZhZjczYWE4OTdlNGUzYiIsIm5iZiI6MTc2Mzk4NTkzOC45ODUsInN1YiI6IjY5MjQ0YTEyNjk3YzljMWI5NWI3NDliNyIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.U-nF0JTZLIcMIXR8qFZuWwPI3bo2CuDfinmijRyuTJo", + "Manifest": { + "LatestVersion": "1.0.0", + "ApkUrl": "https://example.com/allstream.apk", + "ReleaseNotes": "Initial release" + } +} diff --git a/AllStream/MainPage.xaml.cs b/AllStream/MainPage.xaml.cs index d8411f0..6f49d47 100644 --- a/AllStream/MainPage.xaml.cs +++ b/AllStream/MainPage.xaml.cs @@ -23,6 +23,7 @@ public MainPage() void OnLoaded(object? sender, EventArgs e) { // WebView configuration is now handled in MauiProgram.cs via Handler Mapping + TryCheckUpdates(); } static double GetStatusBarHeightDp() @@ -35,5 +36,18 @@ static double GetStatusBarHeightDp() return density > 0 ? px / density : 0; } + + void TryCheckUpdates() + { + try + { + var svc = this.Handler?.MauiContext?.Services?.GetService(); + if (svc != null) + { + Dispatcher.Dispatch(async () => await svc.CheckForUpdatesAsync()); + } + } + catch { } + } #endif } diff --git a/AllStream/MauiProgram.cs b/AllStream/MauiProgram.cs index 001a047..d4d2c1e 100644 --- a/AllStream/MauiProgram.cs +++ b/AllStream/MauiProgram.cs @@ -144,7 +144,11 @@ public static MauiApp CreateMauiApp() } Settings settingsFromJson = builder.Configuration.Get() ?? new Settings(); builder.Services.AddMauiBlazorWebView(); - builder.Services.AddSharedServices(sp => new FormFactor(), settingsFromJson); + builder.Services.AddSharedServices(sp => new FormFactor(), + settingsFromJson); +#if ANDROID + builder.Services.AddSingleton(); +#endif #if DEBUG builder.Services.AddBlazorWebViewDeveloperTools(); diff --git a/AllStream/Platforms/Android/Updates/AndroidAppUpdateService.cs b/AllStream/Platforms/Android/Updates/AndroidAppUpdateService.cs new file mode 100644 index 0000000..d413c67 --- /dev/null +++ b/AllStream/Platforms/Android/Updates/AndroidAppUpdateService.cs @@ -0,0 +1,83 @@ +using System; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Android.App; +using Android.Content; +using Microsoft.Maui.ApplicationModel; +using AllStream.Shared.Models; +using AllStream.Shared.Services; + +namespace AllStream.Platforms.Android.Updates +{ + public class AndroidAppUpdateService : IAppUpdateService + { + private readonly Settings _settings; + private readonly IHttpClientFactory _httpClientFactory; + + public AndroidAppUpdateService(Settings settings) + { + _settings = settings; + _httpClientFactory = new HttpClientFactory(); + } + + public async Task CheckForUpdatesAsync() + { + try + { + if (string.IsNullOrWhiteSpace(_settings.UpdateManifestUrl)) return; + var client = _httpClientFactory.CreateClient(); + using var res = await client.GetAsync(_settings.UpdateManifestUrl); + if (!res.IsSuccessStatusCode) return; + var json = await res.Content.ReadAsStringAsync(); + var manifest = JsonSerializer.Deserialize(json); + if (manifest == null) return; + + var currentVersion = AppInfo.Current.VersionString; + if (IsNewer(manifest.LatestVersion, currentVersion)) + { + await ShowPromptAndUpdate(manifest.ApkUrl, manifest.ReleaseNotes); + } + } + catch { } + } + + private static bool IsNewer(string latest, string current) + { + Version vLatest, vCurrent; + if (Version.TryParse(latest, out vLatest) && Version.TryParse(current, out vCurrent)) + { + return vLatest > vCurrent; + } + return !string.Equals(latest, current, StringComparison.OrdinalIgnoreCase); + } + + private static async Task ShowPromptAndUpdate(string apkUrl, string notes) + { + try + { + if (string.IsNullOrWhiteSpace(apkUrl)) return; + var title = "Update Available"; + var message = string.IsNullOrWhiteSpace(notes) ? "A new version is available. Update now?" : notes; + var ok = await MainThread.InvokeOnMainThreadAsync(() => Microsoft.Maui.Controls.Application.Current!.MainPage!.DisplayAlert(title, message, "Update", "Later")); + if (ok) + { + await Launcher.OpenAsync(new Uri(apkUrl)); + } + } + catch { } + } + + private class HttpClientFactory : IHttpClientFactory + { + public HttpClient CreateClient(string name = "default") => new HttpClient(); + } + + private class UpdateManifest + { + public string LatestVersion { get; set; } = string.Empty; + public string ApkUrl { get; set; } = string.Empty; + public string ReleaseNotes { get; set; } = string.Empty; + } + } +} From 437c800c73d297310bc3c32aacb80f2fe3e1723d Mon Sep 17 00:00:00 2001 From: james-wright-rostro Date: Sat, 17 Jan 2026 20:14:20 +0000 Subject: [PATCH 2/3] Add update service --- AllStream.Shared/Models/UpdateManifest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 AllStream.Shared/Models/UpdateManifest.cs diff --git a/AllStream.Shared/Models/UpdateManifest.cs b/AllStream.Shared/Models/UpdateManifest.cs new file mode 100644 index 0000000..135a72c --- /dev/null +++ b/AllStream.Shared/Models/UpdateManifest.cs @@ -0,0 +1,8 @@ +namespace AllStream.Shared.Models; + +public class UpdateManifest +{ + public string LatestVersion { get; set; } = string.Empty; + public string ApkUrl { get; set; } = string.Empty; + public string ReleaseNotes { get; set; } = string.Empty; +} From ce95f0ec0165160a18f9862c293c843fb99273b3 Mon Sep 17 00:00:00 2001 From: james-wright-rostro Date: Sun, 18 Jan 2026 13:38:58 +0000 Subject: [PATCH 3/3] Adjust update service --- AllStream.Shared/Models/UpdateManifest.cs | 18 ++++++++-- AllStream.Shared/Services/AppUpdateService.cs | 34 +++++++++++++++++++ .../Services/IAppUpdateService.cs | 3 +- AllStream.sln | 7 ---- AllStream/MainPage.xaml.cs | 3 +- 5 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 AllStream.Shared/Services/AppUpdateService.cs diff --git a/AllStream.Shared/Models/UpdateManifest.cs b/AllStream.Shared/Models/UpdateManifest.cs index 135a72c..d9ed13d 100644 --- a/AllStream.Shared/Models/UpdateManifest.cs +++ b/AllStream.Shared/Models/UpdateManifest.cs @@ -1,8 +1,20 @@ +using System.Text.Json.Serialization; + namespace AllStream.Shared.Models; public class UpdateManifest { - public string LatestVersion { get; set; } = string.Empty; - public string ApkUrl { get; set; } = string.Empty; - public string ReleaseNotes { get; set; } = string.Empty; + [JsonPropertyName("tag_name")] + public required string TagName { get; set; } + + public int[] Version => TagName.Split("v")[1].Split(".").Select(x => int.Parse(x)).ToArray(); + public required bool Prerelease { get; set; } + public required Asset[] Assets { get; set; } +} + +public class Asset +{ + [JsonPropertyName("browser_download_url")] + public required string DownloadUrl { get; set; } + public required string Name { get; set; } } diff --git a/AllStream.Shared/Services/AppUpdateService.cs b/AllStream.Shared/Services/AppUpdateService.cs new file mode 100644 index 0000000..d9515f7 --- /dev/null +++ b/AllStream.Shared/Services/AppUpdateService.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Net.Http.Json; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using AllStream.Shared.Models; + +namespace AllStream.Shared.Services +{ + public class AppUpdateService(HttpClient client, string manifestUrl) : IAppUpdateService + { + public async Task CheckForUpdatesAsync(string? currentVersion, string formFactor) + { + + } + + public async Task GetLatestRelease() + { + var manifest = await client.GetFromJsonAsync(manifestUrl, new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + }); + return manifest; + switch (formFactor) + { + case "Android": + return manifest.Assets.FirstOrDefault(x => x.Name.Contains(".apk")); + default: + return null; + } + } + } +} diff --git a/AllStream.Shared/Services/IAppUpdateService.cs b/AllStream.Shared/Services/IAppUpdateService.cs index 88affa2..0622bbc 100644 --- a/AllStream.Shared/Services/IAppUpdateService.cs +++ b/AllStream.Shared/Services/IAppUpdateService.cs @@ -1,9 +1,10 @@ using System.Threading.Tasks; +using AllStream.Shared.Models; namespace AllStream.Shared.Services { public interface IAppUpdateService { - Task CheckForUpdatesAsync(); + Task CheckForUpdatesAsync(string? currentVersion, string formFactor); } } diff --git a/AllStream.sln b/AllStream.sln index 575f567..be72b4c 100644 --- a/AllStream.sln +++ b/AllStream.sln @@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AllStream.Web.Tests", "AllS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AllStream.Android.UITests", "AllStream.Android.UITests\AllStream.Android.UITests.csproj", "{F2C0A9E9-AC3D-4D6C-9329-9A3A5B268D12}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{FF9F4FC5-0330-4E83-928E-C908C8F7A58F}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,9 +56,4 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {20E6FD03-9002-4EBA-ABF2-9DDE2C488842} EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {F2C0A9E9-AC3D-4D6C-9329-9A3A5B268D12} = {FF9F4FC5-0330-4E83-928E-C908C8F7A58F} - {B8D3F38C-0E9E-4D9A-8AF6-8B9F7D5D0E10} = {FF9F4FC5-0330-4E83-928E-C908C8F7A58F} - {6D7ABF43-5B3E-4F76-8D0A-0E48798A8C3A} = {FF9F4FC5-0330-4E83-928E-C908C8F7A58F} - EndGlobalSection EndGlobal diff --git a/AllStream/MainPage.xaml.cs b/AllStream/MainPage.xaml.cs index 6f49d47..9bc5cd1 100644 --- a/AllStream/MainPage.xaml.cs +++ b/AllStream/MainPage.xaml.cs @@ -19,13 +19,14 @@ public MainPage() #endif } -#if ANDROID void OnLoaded(object? sender, EventArgs e) { // WebView configuration is now handled in MauiProgram.cs via Handler Mapping TryCheckUpdates(); } +#if ANDROID + static double GetStatusBarHeightDp() { var res = Android.App.Application.Context.Resources;