mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-03 00:19:46 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e28069c7e3 |
@@ -1,168 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace SharpEmu.GUI;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remembers the last completed library scan so a cold start can paint the
|
|
||||||
/// grid immediately instead of waiting on a recursive walk of every game
|
|
||||||
/// folder. The cache is a display seed, never an authority: startup still
|
|
||||||
/// runs the normal scan and reconciles over it, so a stale file can only
|
|
||||||
/// ever cost one frame of wrong content, not a wrong library.
|
|
||||||
/// </summary>
|
|
||||||
internal static class GameLibraryCache
|
|
||||||
{
|
|
||||||
private const int CurrentVersion = 1;
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static string CachePath =>
|
|
||||||
Path.Combine(AppContext.BaseDirectory, "user", "library_cache.json");
|
|
||||||
|
|
||||||
internal sealed class CachedGame
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string? TitleId { get; set; }
|
|
||||||
public string? Version { get; set; }
|
|
||||||
public string Path { get; set; } = string.Empty;
|
|
||||||
public long SizeBytes { get; set; }
|
|
||||||
public string? CoverPath { get; set; }
|
|
||||||
public string? BackgroundPath { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class CacheDocument
|
|
||||||
{
|
|
||||||
public int Version { get; set; }
|
|
||||||
public List<string> Folders { get; set; } = [];
|
|
||||||
public List<CachedGame> Games { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the cached games when the file matches the configured folder
|
|
||||||
/// set and the executables still exist. Entries whose executable is gone
|
|
||||||
/// are dropped so a removed game never flashes on screen.
|
|
||||||
/// </summary>
|
|
||||||
internal static List<GameEntry> Load(IReadOnlyList<string> folders)
|
|
||||||
{
|
|
||||||
var games = new List<GameEntry>();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(CachePath))
|
|
||||||
{
|
|
||||||
return games;
|
|
||||||
}
|
|
||||||
|
|
||||||
var document = JsonSerializer.Deserialize<CacheDocument>(
|
|
||||||
File.ReadAllText(CachePath),
|
|
||||||
SerializerOptions);
|
|
||||||
if (document is null || document.Version != CurrentVersion)
|
|
||||||
{
|
|
||||||
return games;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!SameFolders(document.Folders, folders))
|
|
||||||
{
|
|
||||||
return games;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var cached in document.Games)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(cached.Path) || !File.Exists(cached.Path))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
games.Add(new GameEntry(
|
|
||||||
cached.Name,
|
|
||||||
cached.TitleId,
|
|
||||||
cached.Version,
|
|
||||||
cached.Path,
|
|
||||||
cached.SizeBytes,
|
|
||||||
Existing(cached.CoverPath),
|
|
||||||
Existing(cached.BackgroundPath)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[GUI][WARN] Could not read the library cache: {exception.Message}");
|
|
||||||
games.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
return games;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void Save(IReadOnlyList<string> folders, IReadOnlyList<GameEntry> games)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var directory = Path.GetDirectoryName(CachePath);
|
|
||||||
if (!string.IsNullOrEmpty(directory))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
var document = new CacheDocument
|
|
||||||
{
|
|
||||||
Version = CurrentVersion,
|
|
||||||
Folders = [.. folders],
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var game in games)
|
|
||||||
{
|
|
||||||
document.Games.Add(new CachedGame
|
|
||||||
{
|
|
||||||
Name = game.Name,
|
|
||||||
TitleId = game.TitleId,
|
|
||||||
Version = game.Version,
|
|
||||||
Path = game.Path,
|
|
||||||
SizeBytes = game.SizeBytes,
|
|
||||||
CoverPath = game.CoverPath,
|
|
||||||
BackgroundPath = game.BackgroundPath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
File.WriteAllText(
|
|
||||||
CachePath,
|
|
||||||
JsonSerializer.Serialize(document, SerializerOptions));
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[GUI][WARN] Could not write the library cache: {exception.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? Existing(string? path) =>
|
|
||||||
!string.IsNullOrWhiteSpace(path) && File.Exists(path) ? path : null;
|
|
||||||
|
|
||||||
private static bool SameFolders(
|
|
||||||
IReadOnlyList<string> cached,
|
|
||||||
IReadOnlyList<string> configured)
|
|
||||||
{
|
|
||||||
if (cached.Count != configured.Count)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var known = new HashSet<string>(cached, GameLibraryPath.Comparer);
|
|
||||||
foreach (var folder in configured)
|
|
||||||
{
|
|
||||||
if (!known.Contains(folder))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -86,7 +86,6 @@ public partial class MainWindow
|
|||||||
CloseGameSettings();
|
CloseGameSettings();
|
||||||
LaunchSelected();
|
LaunchSelected();
|
||||||
};
|
};
|
||||||
GameOptionsCloseButton.Click += (_, _) => CloseGameSettings();
|
|
||||||
GameOptionsOpenFolderButton.Click += (_, _) => OpenSelectedGameFolder();
|
GameOptionsOpenFolderButton.Click += (_, _) => OpenSelectedGameFolder();
|
||||||
GameOptionsCopyPathButton.Click += async (_, _) =>
|
GameOptionsCopyPathButton.Click += async (_, _) =>
|
||||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path);
|
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path);
|
||||||
@@ -141,7 +140,6 @@ public partial class MainWindow
|
|||||||
!string.IsNullOrWhiteSpace(game.TitleId);
|
!string.IsNullOrWhiteSpace(game.TitleId);
|
||||||
|
|
||||||
_isGameSettingsOpen = true;
|
_isGameSettingsOpen = true;
|
||||||
SetGameOptionsPagesSpan(coversConsoleRow: true);
|
|
||||||
SetGameOptionsOpenClass(BackdropLayer, active: true);
|
SetGameOptionsOpenClass(BackdropLayer, active: true);
|
||||||
SetGameOptionsOpenClass(CarouselHost, active: true);
|
SetGameOptionsOpenClass(CarouselHost, active: true);
|
||||||
SetGameOptionsOpenClass(LibrarySelectedDetails, active: true);
|
SetGameOptionsOpenClass(LibrarySelectedDetails, active: true);
|
||||||
@@ -160,7 +158,6 @@ public partial class MainWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isGameSettingsOpen = false;
|
_isGameSettingsOpen = false;
|
||||||
SetGameOptionsPagesSpan(coversConsoleRow: false);
|
|
||||||
SetGameOptionsNavigationIndicator(_gameOptionsIndicatorIndex, animate: false);
|
SetGameOptionsNavigationIndicator(_gameOptionsIndicatorIndex, animate: false);
|
||||||
_gameSettingsTitleId = null;
|
_gameSettingsTitleId = null;
|
||||||
_gameEnvironmentPassthrough.Clear();
|
_gameEnvironmentPassthrough.Clear();
|
||||||
@@ -441,11 +438,6 @@ public partial class MainWindow
|
|||||||
animate);
|
animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetGameOptionsPagesSpan(bool coversConsoleRow)
|
|
||||||
{
|
|
||||||
Grid.SetRowSpan(PagesHost, coversConsoleRow ? 2 : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Button[] GameOptionsNavigationButtons() =>
|
private Button[] GameOptionsNavigationButtons() =>
|
||||||
[
|
[
|
||||||
GameOptionsGeneralNav,
|
GameOptionsGeneralNav,
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Panel Grid.Row="1" x:Name="PagesHost" ZIndex="1">
|
<Panel Grid.Row="1" x:Name="PagesHost">
|
||||||
|
|
||||||
<!-- Library page. Covers use a horizontal virtualized rail so the
|
<!-- Library page. Covers use a horizontal virtualized rail so the
|
||||||
number of realized images stays bounded for large libraries. -->
|
number of realized images stays bounded for large libraries. -->
|
||||||
@@ -311,7 +311,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Duration="0:0:0.18" />
|
Duration="0:0:0.18" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</StackPanel.Transitions>
|
</StackPanel.Transitions>
|
||||||
<Border Classes="selectedDetailsDivider" />
|
|
||||||
<TextBlock Classes="selectedGameTitle"
|
<TextBlock Classes="selectedGameTitle"
|
||||||
Text="{Binding Name}"
|
Text="{Binding Name}"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
@@ -450,7 +449,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Padding="260,18,32,24">
|
<Border Padding="260,18,32,24">
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto"
|
<Grid ColumnDefinitions="*,Auto"
|
||||||
ColumnSpacing="34">
|
ColumnSpacing="34">
|
||||||
<StackPanel Spacing="3"
|
<StackPanel Spacing="3"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
@@ -501,20 +500,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Text="{Binding TitleId}" />
|
Text="{Binding TitleId}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button x:Name="GameOptionsCloseButton"
|
|
||||||
Grid.Column="2"
|
|
||||||
Classes="optionsCircle compact"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
ToolTip.Tip="{Binding [Common.Back],
|
|
||||||
Source={x:Static local:Localization.Instance},
|
|
||||||
x:CompileBindings=False}"
|
|
||||||
AutomationProperties.Name="{Binding [Common.Back],
|
|
||||||
Source={x:Static local:Localization.Instance},
|
|
||||||
x:CompileBindings=False}">
|
|
||||||
<TextBlock Classes="materialSymbol"
|
|
||||||
Text="close" />
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -526,18 +511,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
RowDefinitions="Auto,*">
|
RowDefinitions="Auto,*">
|
||||||
<Button x:Name="GameOptionsLaunchButton"
|
<Button x:Name="GameOptionsLaunchButton"
|
||||||
Classes="gameOptionsLaunch">
|
Classes="gameOptionsLaunch">
|
||||||
<StackPanel Orientation="Horizontal"
|
|
||||||
Spacing="8"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center">
|
|
||||||
<TextBlock Classes="materialSymbol"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Text="play_arrow" />
|
|
||||||
<TextBlock VerticalAlignment="Center"
|
<TextBlock VerticalAlignment="Center"
|
||||||
Text="{Binding [Library.Context.Launch],
|
Text="{Binding [Library.Context.Launch],
|
||||||
Source={x:Static local:Localization.Instance},
|
Source={x:Static local:Localization.Instance},
|
||||||
x:CompileBindings=False}" />
|
x:CompileBindings=False}" />
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ScrollViewer x:Name="GameOptionsNavScroll"
|
<ScrollViewer x:Name="GameOptionsNavScroll"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public partial class MainWindow : Window
|
|||||||
private const int MaxConsoleLines = 4000;
|
private const int MaxConsoleLines = 4000;
|
||||||
private const int MaxConsoleLinesPerFlush = 500;
|
private const int MaxConsoleLinesPerFlush = 500;
|
||||||
private static readonly TimeSpan NavigationIndicatorAnimationDuration =
|
private static readonly TimeSpan NavigationIndicatorAnimationDuration =
|
||||||
TimeSpan.FromMilliseconds(180);
|
TimeSpan.FromMilliseconds(240);
|
||||||
|
|
||||||
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
|
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
|
||||||
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
|
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
|
||||||
@@ -555,7 +555,7 @@ public partial class MainWindow : Window
|
|||||||
translationAnimation.InsertExpressionKeyFrame(
|
translationAnimation.InsertExpressionKeyFrame(
|
||||||
1f,
|
1f,
|
||||||
"this.FinalValue",
|
"this.FinalValue",
|
||||||
new CubicEaseOut());
|
new SineEaseInOut());
|
||||||
|
|
||||||
var animations = visual.Compositor.CreateImplicitAnimationCollection();
|
var animations = visual.Compositor.CreateImplicitAnimationCollection();
|
||||||
animations[nameof(CompositionVisual.Translation)] = translationAnimation;
|
animations[nameof(CompositionVisual.Translation)] = translationAnimation;
|
||||||
@@ -848,8 +848,6 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
_ = CheckForUpdatesAsync();
|
_ = CheckForUpdatesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
SeedLibraryFromCache();
|
|
||||||
await RescanLibraryAsync();
|
await RescanLibraryAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1549,31 +1547,6 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Paints the previous scan's result before the real scan starts. The
|
|
||||||
/// scan that follows reconciles over this, so the cache only ever
|
|
||||||
/// shortens the blank period; it never decides what the library holds.
|
|
||||||
/// </summary>
|
|
||||||
private void SeedLibraryFromCache()
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.VerifyAccess();
|
|
||||||
|
|
||||||
if (_allGames.Count != 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cached = GameLibraryCache.Load(_settings.GameFolders.ToArray());
|
|
||||||
if (cached.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_allGames.AddRange(cached);
|
|
||||||
RefreshVisibleGames(new HashSet<GameEntry>(cached));
|
|
||||||
LoadGameDetailsInBackground(cached, cached);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RescanLibraryAsync(bool showProgress = true)
|
private async Task RescanLibraryAsync(bool showProgress = true)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.VerifyAccess();
|
Dispatcher.UIThread.VerifyAccess();
|
||||||
@@ -1603,7 +1576,6 @@ public partial class MainWindow : Window
|
|||||||
LoadingState.IsVisible = false;
|
LoadingState.IsVisible = false;
|
||||||
LoadGameDetailsInBackground(reconciliation.CoversToLoad, reconciliation.Games);
|
LoadGameDetailsInBackground(reconciliation.CoversToLoad, reconciliation.Games);
|
||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
GameLibraryCache.Save(folders, reconciliation.Games);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -103,12 +103,6 @@ Contextual per-game options reveal, actions, and selected-game motion.
|
|||||||
<Setter Property="Foreground" Value="White" />
|
<Setter Property="Foreground" Value="White" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Button.optionsCircle.compact, ToggleButton.optionsCircle.compact">
|
|
||||||
<Setter Property="Width" Value="40" />
|
|
||||||
<Setter Property="Height" Value="40" />
|
|
||||||
<Setter Property="CornerRadius" Value="20" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="StackPanel.selectedDetailsHost">
|
<Style Selector="StackPanel.selectedDetailsHost">
|
||||||
<Setter Property="Spacing" Value="18" />
|
<Setter Property="Spacing" Value="18" />
|
||||||
</Style>
|
</Style>
|
||||||
@@ -125,21 +119,6 @@ Contextual per-game options reveal, actions, and selected-game motion.
|
|||||||
<Setter Property="FontSize" Value="26" />
|
<Setter Property="FontSize" Value="26" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.selectedDetailsDivider">
|
|
||||||
<Setter Property="Height" Value="1" />
|
|
||||||
<Setter Property="MinWidth" Value="440" />
|
|
||||||
<Setter Property="Margin" Value="0,0,0,18" />
|
|
||||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
|
||||||
<Setter Property="Background">
|
|
||||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,0%">
|
|
||||||
<GradientStop Offset="0" Color="#00FFFFFF" />
|
|
||||||
<GradientStop Offset="0.06" Color="#2EFFFFFF" />
|
|
||||||
<GradientStop Offset="0.55" Color="#14FFFFFF" />
|
|
||||||
<GradientStop Offset="1" Color="#00FFFFFF" />
|
|
||||||
</LinearGradientBrush>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="StackPanel.selectedDetailsHost.gridLayout Button.playButton">
|
<Style Selector="StackPanel.selectedDetailsHost.gridLayout Button.playButton">
|
||||||
<Setter Property="MinHeight" Value="44" />
|
<Setter Property="MinHeight" Value="44" />
|
||||||
<Setter Property="MinWidth" Value="168" />
|
<Setter Property="MinWidth" Value="168" />
|
||||||
|
|||||||
Reference in New Issue
Block a user