Files

320 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Jellyfin2Samsung.Helpers;
using Jellyfin2Samsung.Helpers.Core;
using Jellyfin2Samsung.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Jellyfin2Samsung.ViewModels
{
public partial class BuildInfoViewModel : ViewModelBase
{
public ObservableCollection<BuildVersion> JellyfinVersions { get; } = new();
public ObservableCollection<BuildVersion> CommunityApps { get; } = new();
public ObservableCollection<ProviderOption> ProviderOptions { get; } = new();
[ObservableProperty]
private ProviderOption? selectedProviderOption;
private static readonly HttpClient _http = new();
private readonly Dictionary<string, Bitmap?> _bitmapCache = new(StringComparer.OrdinalIgnoreCase);
private bool _isLoading;
private int _rebuildVersion;
public BuildInfoViewModel()
{
CommunityApps.CollectionChanged += (_, __) =>
{
if (_isLoading) return;
QueueRebuild();
};
JellyfinVersions.CollectionChanged += (_, __) =>
{
if (_isLoading) return;
QueueRebuild();
};
_ = LoadAsync();
}
private void QueueRebuild()
{
// Start a rebuild, but only the latest one is allowed to apply results
var version = Interlocked.Increment(ref _rebuildVersion);
_ = RebuildProviderOptionsAsync(version);
}
public async Task LoadAsync()
{
try
{
_isLoading = true;
var jellyfinMd = await _http.GetStringAsync(AppSettings.Default.ReleaseInfo);
var communityMd = await _http.GetStringAsync(AppSettings.Default.CommunityInfo);
JellyfinVersions.Clear();
CommunityApps.Clear();
ParseVersionsTable(jellyfinMd, JellyfinVersions);
JellyfinVersions.Add(new BuildVersion
{
FileName = "Moonfin",
Description = "Moonfin is optimized for the viewing experience on Samsung Smart TVs."
});
JellyfinVersions.Add(new BuildVersion
{
FileName = "Litefin",
Description = "Litefin is designed to provide a premium media browsing and playback experience, even on legacy hardware."
});
JellyfinVersions.Add(new BuildVersion
{
FileName = "Legacy",
Description = "Containing 10.8.z build for older model TVs"
});
JellyfinVersions.Add(new BuildVersion
{
FileName = "AVPlay",
Description = "Includes AVPlay video player patches for better Samsung TV compatibility"
});
JellyfinVersions.Add(new BuildVersion
{
FileName = "AVPlay 10.10.z - SmartHub",
Description = "Includes AVPlay video player patches for better Samsung TV compatibility for 10.10.z SmartHub variant"
});
ParseApplicationsTable(communityMd, CommunityApps);
}
catch (Exception ex)
{
Trace.WriteLine($"Failed to load build info: {ex}");
}
finally
{
_isLoading = false;
}
// Do a single authoritative rebuild after load finishes
var version = Interlocked.Increment(ref _rebuildVersion);
await RebuildProviderOptionsAsync(version);
if (SelectedProviderOption is null && ProviderOptions.Count > 0)
SelectedProviderOption = ProviderOptions[0];
}
private async Task RebuildProviderOptionsAsync(int version)
{
// If a newer rebuild was queued, abandon this one
if (version != Volatile.Read(ref _rebuildVersion))
return;
// Map ONLY the apps that truly need unique thumbnails
var communityPreviewUrls = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "Moonlight", Constants.PreviewImages.Moonlight },
{ "Fireplace", Constants.PreviewImages.Fireplace },
{ "TVApp", Constants.PreviewImages.TVApp },
{ "Twitch", Constants.PreviewImages.Twitch },
{ "Club Info Board", Constants.PreviewImages.ClubInfoBoard },
{ "Doom", Constants.PreviewImages.Doom },
{ "TransportTycoonDeluxe", Constants.PreviewImages.TTD },
};
// Exceptions inside JellyfinVersions that should NOT use Jellyfin image
var jellyfinOverrides = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "Moonfin", Constants.PreviewImages.Moonfin },
{ "Litefin", Constants.PreviewImages.Litefin },
};
var jellyfinBitmap = await LoadBitmapAsync(Constants.PreviewImages.Jellyfin);
// Build locally (dont mutate ObservableCollection from background thread)
var built = new List<ProviderOption>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void AddIfNew(string name, Bitmap? bmp)
{
name = (name ?? string.Empty).Trim();
if (name.Length == 0) return;
if (!seen.Add(name)) return;
built.Add(new ProviderOption
{
DisplayName = name,
PreviewImage = bmp
});
}
// 1) Jellyfin top entry
AddIfNew("Jellyfin", jellyfinBitmap);
// 2) Community apps
foreach (var app in CommunityApps)
{
var name = (app.FileName ?? string.Empty).Trim();
if (name.Length == 0) continue;
var url = communityPreviewUrls.FirstOrDefault(kvp =>
name.Contains(kvp.Key, StringComparison.OrdinalIgnoreCase)).Value;
var bmp = url is not null ? await LoadBitmapAsync(url) : null;
AddIfNew(name, bmp);
}
// 3) Jellyfin builds (default Jellyfin image, override forks like Moonfin)
foreach (var build in JellyfinVersions)
{
var name = (build.FileName ?? string.Empty).Trim();
if (name.Length == 0) continue;
Bitmap? bmp = jellyfinBitmap;
var overrideUrl = jellyfinOverrides.FirstOrDefault(kvp =>
name.Contains(kvp.Key, StringComparison.OrdinalIgnoreCase)).Value;
if (overrideUrl is not null)
bmp = await LoadBitmapAsync(overrideUrl);
AddIfNew(name, bmp);
}
// If a newer rebuild was queued while we were downloading images, abandon this one
if (version != Volatile.Read(ref _rebuildVersion))
return;
await Dispatcher.UIThread.InvokeAsync(() =>
{
ProviderOptions.Clear();
foreach (var opt in built)
ProviderOptions.Add(opt);
if (SelectedProviderOption is null && ProviderOptions.Count > 0)
SelectedProviderOption = ProviderOptions[0];
});
}
private async Task<Bitmap?> LoadBitmapAsync(string url)
{
if (_bitmapCache.TryGetValue(url, out var cached))
return cached;
try
{
var bytes = await _http.GetByteArrayAsync(url);
await using var ms = new MemoryStream(bytes);
var bmp = new Bitmap(ms);
_bitmapCache[url] = bmp;
return bmp;
}
catch (Exception ex)
{
Trace.WriteLine($"Failed to load image '{url}': {ex.Message}");
_bitmapCache[url] = null;
return null;
}
}
// -------- your existing parsing methods (unchanged) --------
private static string CleanText(string input)
{
var text = RegexPatterns.BuildInfo.MarkdownBold.Replace(input, "$1");
text = RegexPatterns.BuildInfo.EmojiRange.Replace(text, "");
return text.Trim();
}
private void ParseVersionsTable(string md, ObservableCollection<BuildVersion> target)
{
var match = RegexPatterns.BuildInfo.VersionsTable.Match(md);
if (!match.Success) return;
var table = match.Groups["table"].Value;
var rows = RegexPatterns.BuildInfo.TableRow2Columns.Matches(table);
bool headerSkipped = false;
foreach (System.Text.RegularExpressions.Match row in rows)
{
var col1 = row.Groups[1].Value.Trim();
var col2 = row.Groups[2].Value.Trim();
if (!headerSkipped && col1.Equals("File name", StringComparison.OrdinalIgnoreCase))
{
headerSkipped = true;
continue;
}
if (col1.StartsWith("-"))
continue;
target.Add(new BuildVersion
{
FileName = CleanText(col1),
Description = CleanText(col2)
});
}
}
private void ParseApplicationsTable(string md, ObservableCollection<BuildVersion> target)
{
var match = RegexPatterns.BuildInfo.ApplicationsTable.Match(md);
if (!match.Success) return;
var table = match.Groups["table"].Value;
var rows = RegexPatterns.BuildInfo.TableRow3Columns.Matches(table);
bool headerSkipped = false;
foreach (System.Text.RegularExpressions.Match row in rows)
{
var col1 = row.Groups[1].Value.Trim();
var col2 = row.Groups[2].Value.Trim();
var col3 = row.Groups[3].Value.Trim();
if (!headerSkipped && col1.Contains("Application", StringComparison.OrdinalIgnoreCase))
{
headerSkipped = true;
continue;
}
if (col1.StartsWith("-"))
continue;
target.Add(new BuildVersion
{
FileName = CleanText(col1),
Description = CleanText(col2)
});
}
}
[RelayCommand]
private void Close()
{
OnRequestClose?.Invoke();
}
public event Action? OnRequestClose;
}
}