diff --git a/src/Files.App/Actions/Open/OpenReleaseNotesAction.cs b/src/Files.App/Actions/Open/OpenReleaseNotesAction.cs new file mode 100644 index 000000000000..ee4b0683c758 --- /dev/null +++ b/src/Files.App/Actions/Open/OpenReleaseNotesAction.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using CommunityToolkit.WinUI.Helpers; + +namespace Files.App.Actions +{ + internal sealed class OpenReleaseNotesAction : ObservableObject, IAction + { + private readonly IDialogService DialogService = Ioc.Default.GetRequiredService(); + private readonly IUpdateService UpdateService = Ioc.Default.GetRequiredService(); + public string Label + => Strings.WhatsNew.GetLocalizedResource(); + + public string Description + => Strings.WhatsNewDescription.GetLocalizedResource(); + + public bool IsExecutable + => UpdateService.AreReleaseNotesAvailable; + + public OpenReleaseNotesAction() + { + UpdateService.PropertyChanged += UpdateService_PropertyChanged; + } + + public Task ExecuteAsync(object? parameter = null) + { + var viewModel = new ReleaseNotesDialogViewModel(Constants.ExternalUrl.ReleaseNotesUrl); + var dialog = DialogService.GetDialog(viewModel); + + return dialog.TryShowAsync(); + } + + private void UpdateService_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(IUpdateService.AreReleaseNotesAvailable): + OnPropertyChanged(nameof(IsExecutable)); + break; + } + } + } +} diff --git a/src/Files.App/Constants.cs b/src/Files.App/Constants.cs index 31d8032ef8f5..24abed183479 100644 --- a/src/Files.App/Constants.cs +++ b/src/Files.App/Constants.cs @@ -1,6 +1,8 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. +using CommunityToolkit.WinUI.Helpers; + namespace Files.App { public static class Constants @@ -205,6 +207,7 @@ public static class ExternalUrl public const string PrivacyPolicyUrl = @"https://files.community/privacy"; public const string SupportUsUrl = @"https://github.com/sponsors/yaira2"; public const string CrowdinUrl = @"https://crowdin.com/project/files-app"; + public static readonly string ReleaseNotesUrl= $"https://files.community/blog/posts/v{SystemInformation.Instance.ApplicationVersion.Major}-{SystemInformation.Instance.ApplicationVersion.Minor}-{SystemInformation.Instance.ApplicationVersion.Build}?minimal"; } public static class DocsPath diff --git a/src/Files.App/Data/Commands/Manager/CommandCodes.cs b/src/Files.App/Data/Commands/Manager/CommandCodes.cs index ff12af5d2b06..433ca1cb7b2a 100644 --- a/src/Files.App/Data/Commands/Manager/CommandCodes.cs +++ b/src/Files.App/Data/Commands/Manager/CommandCodes.cs @@ -114,6 +114,7 @@ public enum CommandCodes OpenInVSCode, OpenRepoInVSCode, OpenProperties, + OpenReleaseNotes, OpenClassicProperties, OpenSettings, OpenStorageSense, diff --git a/src/Files.App/Data/Commands/Manager/CommandManager.cs b/src/Files.App/Data/Commands/Manager/CommandManager.cs index b79f43500eb6..4c549cab5fcf 100644 --- a/src/Files.App/Data/Commands/Manager/CommandManager.cs +++ b/src/Files.App/Data/Commands/Manager/CommandManager.cs @@ -115,6 +115,7 @@ public IRichCommand this[HotKey hotKey] public IRichCommand OpenInVSCode => commands[CommandCodes.OpenInVSCode]; public IRichCommand OpenRepoInVSCode => commands[CommandCodes.OpenRepoInVSCode]; public IRichCommand OpenProperties => commands[CommandCodes.OpenProperties]; + public IRichCommand OpenReleaseNotes => commands[CommandCodes.OpenReleaseNotes]; public IRichCommand OpenClassicProperties => commands[CommandCodes.OpenClassicProperties]; public IRichCommand OpenStorageSense => commands[CommandCodes.OpenStorageSense]; public IRichCommand OpenStorageSenseFromHome => commands[CommandCodes.OpenStorageSenseFromHome]; @@ -317,6 +318,7 @@ public IEnumerator GetEnumerator() => [CommandCodes.OpenInVSCode] = new OpenInVSCodeAction(), [CommandCodes.OpenRepoInVSCode] = new OpenRepoInVSCodeAction(), [CommandCodes.OpenProperties] = new OpenPropertiesAction(), + [CommandCodes.OpenReleaseNotes] = new OpenReleaseNotesAction(), [CommandCodes.OpenClassicProperties] = new OpenClassicPropertiesAction(), [CommandCodes.OpenStorageSense] = new OpenStorageSenseAction(), [CommandCodes.OpenStorageSenseFromHome] = new OpenStorageSenseFromHomeAction(), diff --git a/src/Files.App/Data/Commands/Manager/ICommandManager.cs b/src/Files.App/Data/Commands/Manager/ICommandManager.cs index 1f46a14f0822..480dfff743f4 100644 --- a/src/Files.App/Data/Commands/Manager/ICommandManager.cs +++ b/src/Files.App/Data/Commands/Manager/ICommandManager.cs @@ -103,6 +103,7 @@ public interface ICommandManager : IEnumerable IRichCommand OpenInVSCode { get; } IRichCommand OpenRepoInVSCode { get; } IRichCommand OpenProperties { get; } + IRichCommand OpenReleaseNotes { get; } IRichCommand OpenClassicProperties { get; } IRichCommand OpenStorageSense { get; } IRichCommand OpenStorageSenseFromHome { get; } diff --git a/src/Files.App/Data/Contracts/IUpdateService.cs b/src/Files.App/Data/Contracts/IUpdateService.cs index ce8d9b917c98..3879de820de1 100644 --- a/src/Files.App/Data/Contracts/IUpdateService.cs +++ b/src/Files.App/Data/Contracts/IUpdateService.cs @@ -16,14 +16,14 @@ public interface IUpdateService : INotifyPropertyChanged bool IsUpdating { get; } /// - /// Gets a value indicating if the apps being used the first time after an update. + /// Gets a value indicating if the app is being used the first time after an update. /// bool IsAppUpdated { get; } /// - /// Gets a value indicating if release notes are available. + /// Gets a value that indicates if there are release notes available for the current version of the app. /// - bool IsReleaseNotesAvailable { get; } + bool AreReleaseNotesAvailable { get; } Task DownloadUpdatesAsync(); @@ -31,12 +31,7 @@ public interface IUpdateService : INotifyPropertyChanged Task CheckForUpdatesAsync(); - Task CheckLatestReleaseNotesAsync(CancellationToken cancellationToken = default); - - /// - /// Gets release notes for the latest release - /// - Task GetLatestReleaseNotesAsync(CancellationToken cancellationToken = default); + Task CheckForReleaseNotesAsync(); /// /// Replace Files.App.Launcher.exe if it is used and has been updated diff --git a/src/Files.App/Dialogs/ReleaseNotesDialog.xaml b/src/Files.App/Dialogs/ReleaseNotesDialog.xaml index 0099aee78583..139c11497b9f 100644 --- a/src/Files.App/Dialogs/ReleaseNotesDialog.xaml +++ b/src/Files.App/Dialogs/ReleaseNotesDialog.xaml @@ -17,6 +17,7 @@ 0 + 1100 @@ -60,21 +61,14 @@ - - + - - + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + CoreWebView2Initialized="BlogPostWebView_CoreWebView2Initialized" + Source="{x:Bind ViewModel.BlogPostUrl, Mode=OneWay}" /> 740 ? 740 : maxHeight; + ContainerGrid.Width = maxWidth > 740 ? 740 : maxWidth; } private void Current_SizeChanged(object sender, WindowSizeChangedEventArgs e) @@ -60,10 +65,40 @@ private ContentDialog SetContentDialogRoot(ContentDialog contentDialog) return contentDialog; } - private async void ReleaseNotesMarkdownTextBlock_LinkClicked(object sender, CommunityToolkit.WinUI.UI.Controls.LinkClickedEventArgs e) + private async void BlogPostWebView_CoreWebView2Initialized(WebView2 sender, CoreWebView2InitializedEventArgs args) { - if (Uri.TryCreate(e.Link, UriKind.Absolute, out Uri? link)) - await Launcher.LaunchUriAsync(link); + BlogPostWebView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; + BlogPostWebView.CoreWebView2.Settings.AreDevToolsEnabled = false; + BlogPostWebView.CoreWebView2.Settings.AreBrowserAcceleratorKeysEnabled = false; + BlogPostWebView.CoreWebView2.Settings.IsSwipeNavigationEnabled = false; + + var script = @" + document.addEventListener('click', function(event) { + var target = event.target; + while (target && target.tagName !== 'A') { + target = target.parentElement; + } + if (target && target.href) { + event.preventDefault(); + window.chrome.webview.postMessage(target.href); + } + }); + "; + + await sender.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(script); + sender.WebMessageReceived += WebView_WebMessageReceived; } + + private async void WebView_WebMessageReceived(WebView2 sender, CoreWebView2WebMessageReceivedEventArgs args) + { + // Open link in web browser + if (Uri.TryCreate(args.TryGetWebMessageAsString(), UriKind.Absolute, out Uri? uri)) + await Launcher.LaunchUriAsync(uri); + + // Navigate back to blog post + if (sender.CoreWebView2.CanGoBack) + sender.CoreWebView2.GoBack(); + } + } } diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 6d5b75f5eba4..0a0ae22fa539 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -107,7 +107,7 @@ public static async Task CheckAppUpdate() await updateService.CheckForUpdatesAsync(); await updateService.DownloadMandatoryUpdatesAsync(); await updateService.CheckAndUpdateFilesLauncherAsync(); - await updateService.CheckLatestReleaseNotesAsync(); + await updateService.CheckForReleaseNotesAsync(); } /// diff --git a/src/Files.App/Services/App/AppUpdateNoneService.cs b/src/Files.App/Services/App/AppUpdateNoneService.cs index 5eaca32c0764..d3aa65a46a41 100644 --- a/src/Files.App/Services/App/AppUpdateNoneService.cs +++ b/src/Files.App/Services/App/AppUpdateNoneService.cs @@ -1,20 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using CommunityToolkit.WinUI.Helpers; +using System.Net.Http; namespace Files.App.Services { - internal sealed class DummyUpdateService : IUpdateService + internal sealed class DummyUpdateService : ObservableObject, IUpdateService { public bool IsUpdateAvailable => false; public bool IsUpdating => false; - public bool IsAppUpdated => true; + public bool IsAppUpdated + { + get => SystemInformation.Instance.IsAppUpdated; + } - public bool IsReleaseNotesAvailable => true; + private bool _areReleaseNotesAvailable = false; + public bool AreReleaseNotesAvailable + { + get => _areReleaseNotesAvailable; + private set => SetProperty(ref _areReleaseNotesAvailable, value); + } public event PropertyChangedEventHandler? PropertyChanged { add { } remove { } } @@ -28,9 +33,19 @@ public Task CheckForUpdatesAsync() return Task.CompletedTask; } - public Task CheckLatestReleaseNotesAsync(CancellationToken cancellationToken = default) + public async Task CheckForReleaseNotesAsync() { - return Task.CompletedTask; + using var client = new HttpClient(); + + try + { + var response = await client.GetAsync(Constants.ExternalUrl.ReleaseNotesUrl); + AreReleaseNotesAvailable = response.IsSuccessStatusCode; + } + catch + { + AreReleaseNotesAvailable = false; + } } public Task DownloadMandatoryUpdatesAsync() @@ -42,11 +57,5 @@ public Task DownloadUpdatesAsync() { return Task.CompletedTask; } - - public Task GetLatestReleaseNotesAsync(CancellationToken cancellationToken = default) - { - // No localization for dev-only string - return Task.FromResult((string?)"No release notes available for Dev build."); - } } } diff --git a/src/Files.App/Services/App/AppUpdateSideloadService.cs b/src/Files.App/Services/App/AppUpdateSideloadService.cs index 84960c7d7077..edf375def77c 100644 --- a/src/Files.App/Services/App/AppUpdateSideloadService.cs +++ b/src/Files.App/Services/App/AppUpdateSideloadService.cs @@ -58,12 +58,13 @@ public bool IsAppUpdated get => SystemInformation.Instance.IsAppUpdated; } - private bool _isReleaseNotesAvailable; - public bool IsReleaseNotesAvailable + private bool _areReleaseNotesAvailable = false; + public bool AreReleaseNotesAvailable { - get => _isReleaseNotesAvailable; - private set => SetProperty(ref _isReleaseNotesAvailable, value); + get => _areReleaseNotesAvailable; + private set => SetProperty(ref _areReleaseNotesAvailable, value); } + public async Task DownloadUpdatesAsync() { await ApplyPackageUpdateAsync(); @@ -74,34 +75,6 @@ public Task DownloadMandatoryUpdatesAsync() return Task.CompletedTask; } - public async Task GetLatestReleaseNotesAsync(CancellationToken cancellationToken = default) - { - var applicationVersion = $"{SystemInformation.Instance.ApplicationVersion.Major}.{SystemInformation.Instance.ApplicationVersion.Minor}.{SystemInformation.Instance.ApplicationVersion.Build}"; - var releaseNotesLocation = string.Concat("https://raw.githubusercontent.com/files-community/Release-Notes/main/", applicationVersion, ".md"); - - using var client = new HttpClient(); - - try - { - var result = await client.GetStringAsync(releaseNotesLocation, cancellationToken); - return result == string.Empty ? null : result; - } - catch - { - return null; - } - } - - public async Task CheckLatestReleaseNotesAsync(CancellationToken cancellationToken = default) - { - if (!IsAppUpdated) - return; - - var result = await GetLatestReleaseNotesAsync(); - if (result is not null) - IsReleaseNotesAvailable = true; - } - public async Task CheckForUpdatesAsync() { IsUpdateAvailable = false; @@ -191,6 +164,21 @@ bool HashEqual(Stream a, Stream b) } } + public async Task CheckForReleaseNotesAsync() + { + using var client = new HttpClient(); + + try + { + var response = await client.GetAsync(Constants.ExternalUrl.ReleaseNotesUrl); + AreReleaseNotesAvailable = response.IsSuccessStatusCode; + } + catch + { + AreReleaseNotesAvailable = false; + } + } + private async Task StartBackgroundDownloadAsync() { try diff --git a/src/Files.App/Services/App/AppUpdateStoreService.cs b/src/Files.App/Services/App/AppUpdateStoreService.cs index a3c93f1f6546..48c72449e019 100644 --- a/src/Files.App/Services/App/AppUpdateStoreService.cs +++ b/src/Files.App/Services/App/AppUpdateStoreService.cs @@ -34,16 +34,16 @@ public bool IsUpdating private set => SetProperty(ref _isUpdating, value); } - private bool _isReleaseNotesAvailable; - public bool IsReleaseNotesAvailable + public bool IsAppUpdated { - get => _isReleaseNotesAvailable; - private set => SetProperty(ref _isReleaseNotesAvailable, value); + get => SystemInformation.Instance.IsAppUpdated; } - public bool IsAppUpdated + private bool _areReleaseNotesAvailable = false; + public bool AreReleaseNotesAvailable { - get => SystemInformation.Instance.IsAppUpdated; + get => _areReleaseNotesAvailable; + private set => SetProperty(ref _areReleaseNotesAvailable, value); } public StoreUpdateService() @@ -107,6 +107,21 @@ public async Task CheckForUpdatesAsync() } } + public async Task CheckForReleaseNotesAsync() + { + using var client = new HttpClient(); + + try + { + var response = await client.GetAsync(Constants.ExternalUrl.ReleaseNotesUrl); + AreReleaseNotesAvailable = response.IsSuccessStatusCode; + } + catch + { + AreReleaseNotesAvailable = false; + } + } + private async Task DownloadAndInstallAsync() { // Save the updated tab list before installing the update @@ -158,35 +173,6 @@ private static async Task ShowDialogAsync() return result == ContentDialogResult.Primary; } - public async Task CheckLatestReleaseNotesAsync(CancellationToken cancellationToken = default) - { - if (!IsAppUpdated) - return; - - var result = await GetLatestReleaseNotesAsync(); - - if (result is not null) - IsReleaseNotesAvailable = true; - } - - public async Task GetLatestReleaseNotesAsync(CancellationToken cancellationToken = default) - { - var applicationVersion = $"{SystemInformation.Instance.ApplicationVersion.Major}.{SystemInformation.Instance.ApplicationVersion.Minor}.{SystemInformation.Instance.ApplicationVersion.Build}"; - var releaseNotesLocation = string.Concat("https://raw.githubusercontent.com/files-community/Release-Notes/main/", applicationVersion, ".md"); - - using var client = new HttpClient(); - - try - { - var result = await client.GetStringAsync(releaseNotesLocation, cancellationToken); - return result == string.Empty ? null : result; - } - catch - { - return null; - } - } - public async Task CheckAndUpdateFilesLauncherAsync() { var destFolderPath = Path.Combine(UserDataPaths.GetDefault().LocalAppData, "Files"); diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index c67838b4f0ad..bd78d0d61f1f 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -2195,6 +2195,9 @@ What's new + + Open Release Notes + Creating a shortcut in this location requires administrator privileges diff --git a/src/Files.App/UserControls/AddressToolbar.xaml b/src/Files.App/UserControls/AddressToolbar.xaml index fcf48e0c9648..3d1277e21e45 100644 --- a/src/Files.App/UserControls/AddressToolbar.xaml +++ b/src/Files.App/UserControls/AddressToolbar.xaml @@ -576,25 +576,26 @@ - + - +