Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ LinuxFiles is an independent Linux-only fork of [Files](https://github.com/files

## Unreleased

- The Release notes page shows the LinuxFiles GitHub release notes for the running version, rendered from Markdown with clickable links. Offline or on errors it links to the release page instead.

## 0.1.0-alpha8 - 2026-10-16

Eighth public alpha, focused on sharper text and real media/HTML previews on Wayland and X11. Expect rough edges and keep backups. Report issues at
Expand Down
6 changes: 3 additions & 3 deletions docs/linux-port/wayland-previews.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Media and HTML previews without native window embedding

Status: media playback and rendered HTML previews are implemented for the preview pane. The release notes page still opens the browser. This note records why, how, and what a fully interactive HTML view would cost.
Status: media playback and rendered HTML previews are implemented for the preview pane. The release notes page renders the GitHub release Markdown. This note records why, how, and what a fully interactive HTML view would cost.

## What the X11 host actually did

Expand Down Expand Up @@ -37,9 +37,9 @@ So the Wayland work did not need to match an existing X11 implementation. The ne
- Nix: neither library is in the flake's `runtimeLibs` yet, because WebKitGTK would add a large closure. Both previews fall back on NixOS until the flake adds them (perhaps behind an option).
- Flatpak: not covered yet.

## Release notes (not changed)
## Release notes (implemented)

`ReleaseNotesPage` points at the upstream Files blog (`files.community/blog/posts/v…?minimal`), which describes the Windows app and needs network access and interactive links. That makes it a poor fit for a snapshot. On Wayland it keeps opening the page in the default browser. A better fix is to render the LinuxFiles GitHub release body (Markdown) with the existing Markdig preview renderer, which needs a fetch and caching design of its own.
`ReleaseNotesPage` fetches the GitHub release for the running version (`GET https://api.github.com/repos/MemerGamer/LinuxFiles/releases/tags/linux-v<Version>`, 10 s timeout, User-Agent, no authentication) and renders its `body` with the Markdig preview renderer (`PreviewTextRenderer`). Raw HTML stays literal text, images show only their alt text, and only absolute http(s) links are clickable; they open in the default browser through `ILauncherService`. A successful fetch is cached for the session. When the fetch fails (offline, 404, rate limit or an empty body), the page shows a link to the GitHub release page and opens it in the default browser, as Wayland did before. `WebView2` is no longer used on Linux, since it has no native backend there.

## Interactive HTML on Wayland: options and cost

Expand Down
8 changes: 7 additions & 1 deletion src/Files.App/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,13 @@ public static class ExternalUrl
public static bool IsSupportUsAvailable => !OperatingSystem.IsLinux() || ForkSupportUsUrl.Length > 0;
public static string ActiveSupportUsUrl => OperatingSystem.IsLinux() ? ForkSupportUsUrl : SupportUsUrl;
public const string CrowdinUrl = @"https://crowdin.com/project/files-app";
public static readonly string ReleaseNotesUrl = $"https://files.community/blog/posts/v{Package.Current.Id.Version.Major}-{Package.Current.Id.Version.Minor}-{Package.Current.Id.Version.Build}?minimal";
// Release tags look like linux-v0.1.0-alpha8, built from the informational version (<Version> in Files.App.csproj).
public static readonly string LinuxReleaseTag = $"linux-v{ReleaseNotesHelper.AppVersionName}";
public static readonly string ReleaseNotesUrl = OperatingSystem.IsLinux()
? $"https://github.com/MemerGamer/LinuxFiles/releases/tag/{LinuxReleaseTag}"
: $"https://files.community/blog/posts/v{Package.Current.Id.Version.Major}-{Package.Current.Id.Version.Minor}-{Package.Current.Id.Version.Build}?minimal";

public static readonly string LinuxReleaseApiUrl = $"https://api.github.com/repos/MemerGamer/LinuxFiles/releases/tags/{LinuxReleaseTag}";
}

public static class DocsPath
Expand Down
95 changes: 95 additions & 0 deletions src/Files.App/Helpers/Application/ReleaseNotesHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Reflection;

namespace Files.App.Helpers
{
/// <summary>
/// Fetches the LinuxFiles GitHub release notes (Markdown) for the running version.
/// </summary>
public static class ReleaseNotesHelper
{
private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(10);

private const int MaxResponseBytes = 1024 * 1024;

private static readonly SemaphoreSlim fetchLock = new(1, 1);

private static string? cachedMarkdown;

/// <summary>
/// Gets the running version as written in &lt;Version&gt;, for example 0.1.0-alpha8.
/// </summary>
public static string AppVersionName { get; } = GetAppVersionName();

private static string GetAppVersionName()
{
var assembly = typeof(ReleaseNotesHelper).Assembly;
var version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? assembly.GetName().Version?.ToString(3)
?? string.Empty;

// Drop build metadata ("+<commit>") in case a build adds it.
var metadataStart = version.IndexOf('+');
return metadataStart >= 0 ? version[..metadataStart] : version;
}

/// <summary>
/// Gets the release body for the running version, or null when it is unavailable (offline, not found, rate limited or empty).
/// A successful result is cached for the session; failures are retried on the next call.
/// </summary>
public static async Task<string?> GetLinuxReleaseNotesAsync(CancellationToken cancellationToken = default)
{
if (cachedMarkdown is not null)
return cachedMarkdown;

await fetchLock.WaitAsync(cancellationToken);
try
{
if (cachedMarkdown is not null)
return cachedMarkdown;

using var client = new HttpClient() { Timeout = RequestTimeout, MaxResponseContentBufferSize = MaxResponseBytes };
using var request = new HttpRequestMessage(HttpMethod.Get, Constants.ExternalUrl.LinuxReleaseApiUrl);
if (!request.Headers.UserAgent.TryParseAdd($"LinuxFiles/{AppVersionName}"))
request.Headers.UserAgent.ParseAdd("LinuxFiles");
request.Headers.Accept.ParseAdd("application/vnd.github+json");
request.Headers.Add("X-GitHub-Api-Version", "2022-11-28");

using var response = await client.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
App.Logger.LogInformation("Release notes for {Tag} are unavailable: HTTP {StatusCode}", Constants.ExternalUrl.LinuxReleaseTag, (int)response.StatusCode);
return null;
}

var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
using var document = JsonDocument.Parse(content);
if (document.RootElement.ValueKind is JsonValueKind.Object &&
document.RootElement.TryGetProperty("body", out var body) &&
body.ValueKind is JsonValueKind.String &&
body.GetString() is { } markdown &&
!string.IsNullOrWhiteSpace(markdown))
cachedMarkdown = markdown;

return cachedMarkdown;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
App.Logger.LogInformation(ex, "Failed to fetch the release notes for {Tag}", Constants.ExternalUrl.LinuxReleaseTag);
return null;
}
finally
{
fetchLock.Release();
}
}
}
}
8 changes: 6 additions & 2 deletions src/Files.App/Services/App/AppUpdateNoneService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ public bool AreReleaseNotesAvailable
private set => SetProperty(ref _areReleaseNotesAvailable, value);
}

public new event PropertyChangedEventHandler? PropertyChanged { add { } remove { } }

public Task CheckAndUpdateFilesLauncherAsync()
{
return Task.CompletedTask;
Expand All @@ -33,6 +31,12 @@ public Task CheckForUpdatesAsync()

public async Task CheckForReleaseNotesAsync()
{
if (OperatingSystem.IsLinux())
{
AreReleaseNotesAvailable = await ReleaseNotesHelper.GetLinuxReleaseNotesAsync() is not null;
return;
}

using var client = new HttpClient();

try
Expand Down
24 changes: 2 additions & 22 deletions src/Files.App/UserControls/FilePreviews/DesktopTextPreview.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ private void BuildContent()
var dark = ActualTheme == ElementTheme.Dark
|| (ActualTheme == ElementTheme.Default && Application.Current?.RequestedTheme == ApplicationTheme.Dark);

// RichTextBlock draws nothing on Uno Skia, so the paragraphs are built in a detached block
// collection and each one is shown in its own wrapping TextBlock.
// The paragraphs are built in a detached block collection, then shown as TextBlocks (see AddParagraphTextBlocks).
var blocks = new RichTextBlock().Blocks;
switch (ViewModel.Kind)
{
Expand All @@ -54,26 +53,7 @@ private void BuildContent()
}
}

foreach (var block in blocks.OfType<Paragraph>())
{
var textBlock = new TextBlock
{
IsTextSelectionEnabled = true,
TextWrapping = TextWrapping.Wrap,
Margin = block.Margin,
FontFamily = block.FontFamily,
FontSize = block.FontSize,
FontWeight = block.FontWeight,
FontStyle = block.FontStyle,
};
foreach (var inline in block.Inlines.ToList())
{
block.Inlines.Remove(inline);
textBlock.Inlines.Add(inline);
}

ContentPanel.Children.Add(textBlock);
}
PreviewTextRenderer.AddParagraphTextBlocks(ContentPanel, blocks);

if (!string.IsNullOrEmpty(ViewModel.Footer))
{
Expand Down
Loading
Loading