mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-29 22:19:39 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8962850d2a | |||
| faf49f689d | |||
| 882a6c06e0 | |||
| b21dd9fb92 | |||
| b07e4f2bc6 |
@@ -231,6 +231,11 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
@@ -258,6 +263,61 @@ jobs:
|
|||||||
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
|
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
|
||||||
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
|
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
|
||||||
|
|
||||||
|
- name: Build release notes
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
MAX_COMMITS: 200
|
||||||
|
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||||
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
||||||
|
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
|
||||||
|
VERSION: ${{ needs.init.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
previous_tag="$(git describe --tags --abbrev=0 --match 'v*' "${RELEASE_TAG}^" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [ -n "${previous_tag}" ]; then
|
||||||
|
range="${previous_tag}..${RELEASE_TAG}"
|
||||||
|
else
|
||||||
|
range="${RELEASE_TAG}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git log --no-merges --reverse --pretty=tformat:"%H%x1f%h%x1f%s" "${range}" > commits.txt
|
||||||
|
total="$(wc -l < commits.txt)"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf 'Automated SharpEmu v%s build for commit [`%s`](%s/commit/%s).\n\n' \
|
||||||
|
"${VERSION}" "${SHORT_SHA}" "${REPO_URL}" "${GITHUB_SHA}"
|
||||||
|
|
||||||
|
if [ -n "${previous_tag}" ]; then
|
||||||
|
printf '## Changes since %s\n\n' "${previous_tag}"
|
||||||
|
else
|
||||||
|
printf '## Changes\n\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${total}" -eq 0 ]; then
|
||||||
|
printf '_No commits since %s._\n' "${previous_tag}"
|
||||||
|
else
|
||||||
|
head -n "${MAX_COMMITS}" commits.txt | while IFS=$'\x1f' read -r sha short subject; do
|
||||||
|
printf -- '- %s ([`%s`](%s/commit/%s))\n' "${subject}" "${short}" "${REPO_URL}" "${sha}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${total}" -gt "${MAX_COMMITS}" ]; then
|
||||||
|
printf '\n_…and %s more commits._\n' "$((total - MAX_COMMITS))"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [ -n "${previous_tag}" ]; then
|
||||||
|
printf '**Full changelog**: %s/compare/%s...%s\n' "${REPO_URL}" "${previous_tag}" "${RELEASE_TAG}"
|
||||||
|
else
|
||||||
|
printf '**Full changelog**: %s/commits/%s\n' "${REPO_URL}" "${RELEASE_TAG}"
|
||||||
|
fi
|
||||||
|
} > release-notes.md
|
||||||
|
|
||||||
|
cat release-notes.md
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
@@ -265,7 +325,6 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
RELEASE_NAME: ${{ needs.init.outputs.release-name }}
|
RELEASE_NAME: ${{ needs.init.outputs.release-name }}
|
||||||
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||||
VERSION: ${{ needs.init.outputs.version }}
|
|
||||||
run: |
|
run: |
|
||||||
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
||||||
if [ "${#assets[@]}" -ne 3 ]; then
|
if [ "${#assets[@]}" -ne 3 ]; then
|
||||||
@@ -273,8 +332,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
notes="Automated SharpEmu v${VERSION} build for commit ${GITHUB_SHA}."
|
|
||||||
|
|
||||||
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
|
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
|
||||||
echo "Release ${RELEASE_TAG} already exists and will not be modified." >&2
|
echo "Release ${RELEASE_TAG} already exists and will not be modified." >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -283,4 +340,4 @@ jobs:
|
|||||||
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
||||||
--verify-tag \
|
--verify-tag \
|
||||||
--title "${RELEASE_NAME}" \
|
--title "${RELEASE_NAME}" \
|
||||||
--notes "${notes}"
|
--notes-file release-notes.md
|
||||||
|
|||||||
@@ -157,6 +157,62 @@ def ensure_tag_does_not_exist(
|
|||||||
raise ReleaseError(f"Tag {tag} already exists on {remote}.")
|
raise ReleaseError(f"Tag {tag} already exists on {remote}.")
|
||||||
|
|
||||||
|
|
||||||
|
def get_previous_tag(repository_root: Path) -> str:
|
||||||
|
try:
|
||||||
|
return run_git(
|
||||||
|
"describe",
|
||||||
|
"--tags",
|
||||||
|
"--abbrev=0",
|
||||||
|
"--match",
|
||||||
|
"v*",
|
||||||
|
"HEAD",
|
||||||
|
cwd=repository_root,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
except ReleaseError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_release_commits(
|
||||||
|
repository_root: Path,
|
||||||
|
previous_tag: str,
|
||||||
|
) -> list[str]:
|
||||||
|
revision_range = f"{previous_tag}..HEAD" if previous_tag else "HEAD"
|
||||||
|
|
||||||
|
output = run_git(
|
||||||
|
"log",
|
||||||
|
"--no-merges",
|
||||||
|
"--reverse",
|
||||||
|
"--pretty=tformat:%h %s",
|
||||||
|
revision_range,
|
||||||
|
cwd=repository_root,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return output.splitlines()
|
||||||
|
|
||||||
|
|
||||||
|
def print_release_notes_preview(repository_root: Path) -> None:
|
||||||
|
previous_tag = get_previous_tag(repository_root)
|
||||||
|
commits = get_release_commits(repository_root, previous_tag)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
if previous_tag:
|
||||||
|
print(f"Commits since {previous_tag} ({len(commits)}):")
|
||||||
|
else:
|
||||||
|
print(f"Commits in this release ({len(commits)}):")
|
||||||
|
|
||||||
|
if not commits:
|
||||||
|
print(" (none)")
|
||||||
|
|
||||||
|
for commit in commits:
|
||||||
|
print(f" {commit}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("These commits go into the generated release notes.")
|
||||||
|
|
||||||
|
|
||||||
def read_version(props_path: Path) -> str:
|
def read_version(props_path: Path) -> str:
|
||||||
if not props_path.exists():
|
if not props_path.exists():
|
||||||
raise ReleaseError(f"Version file not found: {props_path}")
|
raise ReleaseError(f"Version file not found: {props_path}")
|
||||||
@@ -323,6 +379,8 @@ def create_release_tag(
|
|||||||
remote,
|
remote,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print_release_notes_preview(repository_root)
|
||||||
|
|
||||||
run_git(
|
run_git(
|
||||||
"tag",
|
"tag",
|
||||||
"-a",
|
"-a",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ cascade order so individual launcher views do not redefine global visuals.
|
|||||||
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Base.axaml" />
|
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Base.axaml" />
|
||||||
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Surfaces.axaml" />
|
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Surfaces.axaml" />
|
||||||
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Buttons.axaml" />
|
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Buttons.axaml" />
|
||||||
|
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Chrome.axaml" />
|
||||||
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Inputs.axaml" />
|
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Inputs.axaml" />
|
||||||
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Console.axaml" />
|
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Console.axaml" />
|
||||||
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Library.axaml" />
|
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Library.axaml" />
|
||||||
|
|||||||
+124
-12
@@ -8,6 +8,15 @@ using Avalonia.Media.Imaging;
|
|||||||
|
|
||||||
namespace SharpEmu.GUI;
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
internal enum GameEntryChanges
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Metadata = 1,
|
||||||
|
Cover = 2,
|
||||||
|
Background = 4,
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class GameEntry : INotifyPropertyChanged
|
public sealed class GameEntry : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
// Placeholder gradients for games without cover art, picked
|
// Placeholder gradients for games without cover art, picked
|
||||||
@@ -27,29 +36,39 @@ public sealed class GameEntry : INotifyPropertyChanged
|
|||||||
private Bitmap? _cover;
|
private Bitmap? _cover;
|
||||||
private IBrush? _placeholderBrush;
|
private IBrush? _placeholderBrush;
|
||||||
private long _sizeBytes;
|
private long _sizeBytes;
|
||||||
|
private string _name;
|
||||||
|
private string? _titleId;
|
||||||
|
private string? _version;
|
||||||
|
private string? _coverPath;
|
||||||
|
private string? _backgroundPath;
|
||||||
|
private string _initials;
|
||||||
|
private FileStamp _coverStamp;
|
||||||
|
private FileStamp _backgroundStamp;
|
||||||
|
|
||||||
public GameEntry(
|
public GameEntry(
|
||||||
string name, string? titleId, string? version, string path, long sizeBytes,
|
string name, string? titleId, string? version, string path, long sizeBytes,
|
||||||
string? coverPath, string? backgroundPath)
|
string? coverPath, string? backgroundPath)
|
||||||
{
|
{
|
||||||
Name = name;
|
_name = name;
|
||||||
TitleId = titleId;
|
_titleId = titleId;
|
||||||
Version = version;
|
_version = version;
|
||||||
Path = path;
|
Path = path;
|
||||||
_sizeBytes = sizeBytes;
|
_sizeBytes = sizeBytes;
|
||||||
CoverPath = coverPath;
|
_coverPath = coverPath;
|
||||||
BackgroundPath = backgroundPath;
|
_backgroundPath = backgroundPath;
|
||||||
Initials = ComputeInitials(name);
|
_initials = ComputeInitials(name);
|
||||||
|
_coverStamp = FileStamp.Read(coverPath);
|
||||||
|
_backgroundStamp = FileStamp.Read(backgroundPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name => _name;
|
||||||
|
|
||||||
public string? TitleId { get; }
|
public string? TitleId => _titleId;
|
||||||
|
|
||||||
/// <summary>Content version from sce_sys/param.json, e.g. "01.000.000".</summary>
|
/// <summary>Content version from sce_sys/param.json, e.g. "01.000.000".</summary>
|
||||||
public string? Version { get; }
|
public string? Version => _version;
|
||||||
|
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
|
|
||||||
@@ -75,10 +94,10 @@ public sealed class GameEntry : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Path to the cover art image shipped with the game, if found.</summary>
|
/// <summary>Path to the cover art image shipped with the game, if found.</summary>
|
||||||
public string? CoverPath { get; }
|
public string? CoverPath => _coverPath;
|
||||||
|
|
||||||
/// <summary>Path to the key art (pic0/pic1) shipped with the game, if found.</summary>
|
/// <summary>Path to the key art (pic0/pic1) shipped with the game, if found.</summary>
|
||||||
public string? BackgroundPath { get; }
|
public string? BackgroundPath => _backgroundPath;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decoded key art used as the window backdrop while this game is
|
/// Decoded key art used as the window backdrop while this game is
|
||||||
@@ -86,7 +105,7 @@ public sealed class GameEntry : INotifyPropertyChanged
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Bitmap? Background { get; set; }
|
public Bitmap? Background { get; set; }
|
||||||
|
|
||||||
public string Initials { get; }
|
public string Initials => _initials;
|
||||||
|
|
||||||
// Built lazily: brushes are AvaloniaObjects that must be created on the
|
// Built lazily: brushes are AvaloniaObjects that must be created on the
|
||||||
// UI thread, while GameEntry itself is constructed on the scan thread.
|
// UI thread, while GameEntry itself is constructed on the scan thread.
|
||||||
@@ -121,6 +140,74 @@ public sealed class GameEntry : INotifyPropertyChanged
|
|||||||
/// <summary>Formatted install size badge shown in the launch bar.</summary>
|
/// <summary>Formatted install size badge shown in the launch bar.</summary>
|
||||||
public string SizeText => FormatSize(SizeBytes);
|
public string SizeText => FormatSize(SizeBytes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a fresh filesystem scan to this presentation object so its
|
||||||
|
/// ListBox container and selection remain stable
|
||||||
|
/// </summary>
|
||||||
|
internal GameEntryChanges UpdateFrom(GameEntry scanned)
|
||||||
|
{
|
||||||
|
if (!Path.Equals(scanned.Path, GameLibraryPath.Comparison))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Only matching game paths can be reconciled", nameof(scanned));
|
||||||
|
}
|
||||||
|
|
||||||
|
var changes = GameEntryChanges.None;
|
||||||
|
if (!string.Equals(_name, scanned.Name, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_name = scanned.Name;
|
||||||
|
_initials = ComputeInitials(scanned.Name);
|
||||||
|
_placeholderBrush = null;
|
||||||
|
RaisePropertyChanged(nameof(Name));
|
||||||
|
RaisePropertyChanged(nameof(Initials));
|
||||||
|
RaisePropertyChanged(nameof(PlaceholderBrush));
|
||||||
|
changes |= GameEntryChanges.Metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(_titleId, scanned.TitleId, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_titleId = scanned.TitleId;
|
||||||
|
RaisePropertyChanged(nameof(TitleId));
|
||||||
|
RaisePropertyChanged(nameof(HasTitleId));
|
||||||
|
changes |= GameEntryChanges.Metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(_version, scanned.Version, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_version = scanned.Version;
|
||||||
|
RaisePropertyChanged(nameof(Version));
|
||||||
|
RaisePropertyChanged(nameof(VersionText));
|
||||||
|
RaisePropertyChanged(nameof(HasVersion));
|
||||||
|
changes |= GameEntryChanges.Metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(_coverPath, scanned.CoverPath, StringComparison.Ordinal)
|
||||||
|
|| _coverStamp != scanned._coverStamp)
|
||||||
|
{
|
||||||
|
_coverPath = scanned.CoverPath;
|
||||||
|
_coverStamp = scanned._coverStamp;
|
||||||
|
var previousCover = Cover;
|
||||||
|
Cover = null;
|
||||||
|
previousCover?.Dispose();
|
||||||
|
changes |= GameEntryChanges.Cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(_backgroundPath, scanned.BackgroundPath, StringComparison.Ordinal)
|
||||||
|
|| _backgroundStamp != scanned._backgroundStamp)
|
||||||
|
{
|
||||||
|
_backgroundPath = scanned.BackgroundPath;
|
||||||
|
_backgroundStamp = scanned._backgroundStamp;
|
||||||
|
var previousBackground = Background;
|
||||||
|
Background = null;
|
||||||
|
previousBackground?.Dispose();
|
||||||
|
changes |= GameEntryChanges.Background;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RaisePropertyChanged(string propertyName) =>
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
|
||||||
private static string ComputeInitials(string name)
|
private static string ComputeInitials(string name)
|
||||||
{
|
{
|
||||||
var initials = name
|
var initials = name
|
||||||
@@ -164,4 +251,29 @@ public sealed class GameEntry : INotifyPropertyChanged
|
|||||||
_ => $"{bytes} B",
|
_ => $"{bytes} B",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly record struct FileStamp(long Length, long LastWriteTimeUtcTicks)
|
||||||
|
{
|
||||||
|
public static FileStamp Read(string? path)
|
||||||
|
{
|
||||||
|
if (path is null)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var file = new FileInfo(path);
|
||||||
|
return file.Exists
|
||||||
|
? new FileStamp(file.Length, file.LastWriteTimeUtc.Ticks)
|
||||||
|
: default;
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (
|
||||||
|
exception is IOException
|
||||||
|
or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
|
internal static class GameLibraryPath
|
||||||
|
{
|
||||||
|
public static StringComparer Comparer { get; } =
|
||||||
|
OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()
|
||||||
|
? StringComparer.OrdinalIgnoreCase
|
||||||
|
: StringComparer.Ordinal;
|
||||||
|
|
||||||
|
public static StringComparison Comparison { get; } =
|
||||||
|
OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()
|
||||||
|
? StringComparison.OrdinalIgnoreCase
|
||||||
|
: StringComparison.Ordinal;
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
|
internal sealed record GameLibraryReconciliation(
|
||||||
|
IReadOnlyList<GameEntry> Games,
|
||||||
|
IReadOnlyList<GameEntry> CoversToLoad,
|
||||||
|
IReadOnlySet<GameEntry> BackgroundsChanged);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies filesystem scan results while preserving presentation object
|
||||||
|
/// identity for games that remain in the library
|
||||||
|
/// </summary>
|
||||||
|
internal static class GameLibraryReconciler
|
||||||
|
{
|
||||||
|
public static GameLibraryReconciliation Reconcile(
|
||||||
|
IReadOnlyList<GameEntry> current,
|
||||||
|
IReadOnlyList<GameEntry> scanned)
|
||||||
|
{
|
||||||
|
var existingByPath = current.ToDictionary(game => game.Path, GameLibraryPath.Comparer);
|
||||||
|
var merged = new List<GameEntry>(scanned.Count);
|
||||||
|
var coversToLoad = new List<GameEntry>();
|
||||||
|
var backgroundsChanged = new HashSet<GameEntry>();
|
||||||
|
|
||||||
|
foreach (var scannedGame in scanned)
|
||||||
|
{
|
||||||
|
if (!existingByPath.TryGetValue(scannedGame.Path, out var existing))
|
||||||
|
{
|
||||||
|
merged.Add(scannedGame);
|
||||||
|
if (scannedGame.CoverPath is not null)
|
||||||
|
{
|
||||||
|
coversToLoad.Add(scannedGame);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var changes = existing.UpdateFrom(scannedGame);
|
||||||
|
merged.Add(existing);
|
||||||
|
if ((changes & GameEntryChanges.Cover) != 0 && existing.CoverPath is not null)
|
||||||
|
{
|
||||||
|
coversToLoad.Add(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((changes & GameEntryChanges.Background) != 0)
|
||||||
|
{
|
||||||
|
backgroundsChanged.Add(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GameLibraryReconciliation(merged, coversToLoad, backgroundsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reorders, inserts and removes only the items needed to reach the desired
|
||||||
|
/// visible sequence
|
||||||
|
/// </summary>
|
||||||
|
public static void ReconcileVisibleGames(
|
||||||
|
IList<GameEntry> visible,
|
||||||
|
IReadOnlyList<GameEntry> desired)
|
||||||
|
{
|
||||||
|
for (var index = 0; index < desired.Count; index++)
|
||||||
|
{
|
||||||
|
var game = desired[index];
|
||||||
|
if (index < visible.Count && ReferenceEquals(visible[index], game))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingIndex = -1;
|
||||||
|
for (var candidate = index + 1; candidate < visible.Count; candidate++)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(visible[candidate], game))
|
||||||
|
{
|
||||||
|
existingIndex = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingIndex >= 0)
|
||||||
|
{
|
||||||
|
var existing = visible[existingIndex];
|
||||||
|
visible.RemoveAt(existingIndex);
|
||||||
|
visible.Insert(index, existing);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
visible.Insert(index, game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (visible.Count > desired.Count)
|
||||||
|
{
|
||||||
|
visible.RemoveAt(visible.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Watches configured game folders and collapses filesystem bursts into one
|
||||||
|
/// library refresh request
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class GameLibraryWatcher : IDisposable
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan DefaultDebounceInterval = TimeSpan.FromMilliseconds(600);
|
||||||
|
|
||||||
|
private readonly object _sync = new();
|
||||||
|
private readonly TimeSpan _debounceInterval;
|
||||||
|
private readonly List<FileSystemWatcher> _watchers = [];
|
||||||
|
private Timer? _debounceTimer;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
internal GameLibraryWatcher(TimeSpan? debounceInterval = null)
|
||||||
|
{
|
||||||
|
_debounceInterval = debounceInterval ?? DefaultDebounceInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? RefreshRequested;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces the watched roots with the current configured game folders
|
||||||
|
/// </summary>
|
||||||
|
public void Watch(IReadOnlyList<string> folders)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
|
foreach (var watcher in _watchers)
|
||||||
|
{
|
||||||
|
watcher.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_watchers.Clear();
|
||||||
|
_debounceTimer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
|
||||||
|
foreach (var folder in folders.Distinct(GameLibraryPath.Comparer))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var watcher = new FileSystemWatcher(folder)
|
||||||
|
{
|
||||||
|
IncludeSubdirectories = true,
|
||||||
|
NotifyFilter = NotifyFilters.FileName
|
||||||
|
| NotifyFilters.DirectoryName
|
||||||
|
| NotifyFilters.LastWrite
|
||||||
|
| NotifyFilters.Size,
|
||||||
|
};
|
||||||
|
watcher.Created += OnFileSystemChanged;
|
||||||
|
watcher.Changed += OnFileSystemChanged;
|
||||||
|
watcher.Deleted += OnFileSystemChanged;
|
||||||
|
watcher.Renamed += OnFileSystemChanged;
|
||||||
|
watcher.Error += OnWatcherError;
|
||||||
|
watcher.EnableRaisingEvents = true;
|
||||||
|
_watchers.Add(watcher);
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (
|
||||||
|
exception is ArgumentException
|
||||||
|
or IOException
|
||||||
|
or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[GUI][WARN] Could not watch game folder '{folder}': {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFileSystemChanged(object sender, FileSystemEventArgs args)
|
||||||
|
=> ScheduleRefresh();
|
||||||
|
|
||||||
|
private void OnWatcherError(object sender, ErrorEventArgs args)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[GUI][WARN] Game library watcher reported an error: {args.GetException().Message}");
|
||||||
|
ScheduleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScheduleRefresh()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_debounceTimer is null)
|
||||||
|
{
|
||||||
|
_debounceTimer = new Timer(
|
||||||
|
static state => ((GameLibraryWatcher)state!).RaiseRefreshRequested(),
|
||||||
|
this,
|
||||||
|
_debounceInterval,
|
||||||
|
Timeout.InfiniteTimeSpan);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_debounceTimer.Change(_debounceInterval, Timeout.InfiniteTimeSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RaiseRefreshRequested()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
_debounceTimer?.Dispose();
|
||||||
|
_debounceTimer = null;
|
||||||
|
foreach (var watcher in _watchers)
|
||||||
|
{
|
||||||
|
watcher.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_watchers.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "ابحث في المكتبة...",
|
"Library.SearchWatermark": "ابحث في المكتبة...",
|
||||||
"Library.AddFolder": "+ إضافة مجلد",
|
"Library.AddFolder": "+ إضافة مجلد",
|
||||||
"Library.Rescan": "⟳ إعادة الفحص",
|
|
||||||
"Library.OpenFile": "فتح ملف...",
|
"Library.OpenFile": "فتح ملف...",
|
||||||
|
|
||||||
"Library.Context.Launch": "تشغيل",
|
"Library.Context.Launch": "تشغيل",
|
||||||
@@ -139,6 +138,7 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "تسجيل تخصيصات الذاكرة المباشرة وإخفاقاتها في وحدة التحكم.\nاستخدمه عندما تنهار لعبة أو تُغلق أثناء الإقلاع.",
|
"Options.Env.LogDirectMemory.Desc": "تسجيل تخصيصات الذاكرة المباشرة وإخفاقاتها في وحدة التحكم.\nاستخدمه عندما تنهار لعبة أو تُغلق أثناء الإقلاع.",
|
||||||
"Options.Env.LogIo.Desc": "تسجيل فتح الملفات وقراءتها وحلّ المسارات في وحدة التحكم.\nاستخدمه عندما لا تجد لعبة ملفات بياناتها أثناء الإقلاع.",
|
"Options.Env.LogIo.Desc": "تسجيل فتح الملفات وقراءتها وحلّ المسارات في وحدة التحكم.\nاستخدمه عندما لا تجد لعبة ملفات بياناتها أثناء الإقلاع.",
|
||||||
"Options.Env.LogNp.Desc": "تسجيل نداءات مكتبة NP (شبكة PlayStation) في وحدة التحكم.",
|
"Options.Env.LogNp.Desc": "تسجيل نداءات مكتبة NP (شبكة PlayStation) في وحدة التحكم.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "إعادة رفع أسطح الضيف التي تعيد كتابتها شيفرة المعالج الخاصة باللعبة.\nاتركه مغلقًا عادة. شغّله للألعاب التي لا تصل أسطحها المرسومة بالمعالج إلى الشاشة.\nيكلّف أداءً ويسبب مشاكل في بعض الألعاب مثل GTA V.",
|
||||||
"Common.Save": "حفظ",
|
"Common.Save": "حفظ",
|
||||||
"Common.Cancel": "إلغاء",
|
"Common.Cancel": "إلغاء",
|
||||||
"PerGame.Title": "إعدادات خاصة باللعبة — {0} ({1})",
|
"PerGame.Title": "إعدادات خاصة باللعبة — {0} ({1})",
|
||||||
@@ -168,5 +168,34 @@
|
|||||||
"Updater.Status.Timeout": "انتهت مهلة التحقق من التحديثات بعد 10 ثوانٍ.",
|
"Updater.Status.Timeout": "انتهت مهلة التحقق من التحديثات بعد 10 ثوانٍ.",
|
||||||
"Updater.Status.Failed": "تعذر التحقق من التحديثات.",
|
"Updater.Status.Failed": "تعذر التحقق من التحديثات.",
|
||||||
"Updater.Status.ChecksumFailed": "فشل التحديث المنزَّل في اجتياز تحقق SHA-256.",
|
"Updater.Status.ChecksumFailed": "فشل التحديث المنزَّل في اجتياز تحقق SHA-256.",
|
||||||
"Updater.Status.Unsupported": "يتطلب التحديث التلقائي إصدار x64 لنظام Windows أو Linux أو macOS."
|
"Updater.Status.Unsupported": "يتطلب التحديث التلقائي إصدار x64 لنظام Windows أو Linux أو macOS.",
|
||||||
|
"Options.Graphics": "الرسومات",
|
||||||
|
"Options.Section.Rendering": "التصيير",
|
||||||
|
"Options.Section.Display": "العرض",
|
||||||
|
"Options.RenderResolution.Label": "الدقة الداخلية",
|
||||||
|
"Options.RenderResolution.Desc": "تصيير الأهداف خارج الشاشة بدقة أقل من الدقة الأصلية ثم رفع دقتها عند العرض. تمنح القيم الأقل مساحة أكبر لوحدة معالجة الرسوميات مقابل جودة الصورة؛ يُطبق عند التشغيل التالي.",
|
||||||
|
"Options.RenderResolution.Native": "100% (أصلية)",
|
||||||
|
"Options.WindowMode.Label": "وضع النافذة",
|
||||||
|
"Options.WindowMode.Desc": "نافذة عادية، أو سطح مكتب بلا حدود، أو ملء شاشة حصري.",
|
||||||
|
"Options.WindowMode.Windowed": "نافذة",
|
||||||
|
"Options.WindowMode.Borderless": "بلا حدود",
|
||||||
|
"Options.WindowMode.Exclusive": "حصري",
|
||||||
|
"Options.Resolution.Label": "الدقة",
|
||||||
|
"Options.Resolution.Desc": "حجم النافذة الأولي أو دقة ملء الشاشة الحصرية.",
|
||||||
|
"Options.Display.Label": "الشاشة",
|
||||||
|
"Options.Display.Desc": "الشاشة المستخدمة للتوسيط وملء الشاشة.",
|
||||||
|
"Options.RefreshRate.Label": "معدل التحديث",
|
||||||
|
"Options.RefreshRate.Desc": "معدل تحديث ملء الشاشة الحصري. يختار الوضع التلقائي أقرب نمط.",
|
||||||
|
"Options.RefreshRate.Automatic": "تلقائي",
|
||||||
|
"Options.Scaling.Label": "التحجيم",
|
||||||
|
"Options.Scaling.Desc": "تحجيم صورة النظام الضيف الأصلية دون تغيير دقتها الداخلية.",
|
||||||
|
"Options.Scaling.Fit": "ملاءمة",
|
||||||
|
"Options.Scaling.Cover": "تغطية",
|
||||||
|
"Options.Scaling.Stretch": "تمديد",
|
||||||
|
"Options.Scaling.Integer": "عدد صحيح",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "استخدام عرض FIFO لإخراج خالٍ من تمزق الصورة.",
|
||||||
|
"Options.Hdr.Label": "إخراج HDR",
|
||||||
|
"Options.Hdr.Desc": "استخدام HDR عندما تدعمه الشاشة المحددة وواجهة الرسومات. يعود الوضع التلقائي إلى SDR.",
|
||||||
|
"Options.Hdr.Auto": "تلقائي"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Pesquisar na biblioteca…",
|
"Library.SearchWatermark": "Pesquisar na biblioteca…",
|
||||||
"Library.AddFolder": "+ Adicionar pasta",
|
"Library.AddFolder": "+ Adicionar pasta",
|
||||||
"Library.Rescan": "⟳ Atualizar biblioteca",
|
|
||||||
"Library.OpenFile": "Abrir arquivo…",
|
"Library.OpenFile": "Abrir arquivo…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Jogar",
|
"Library.Context.Launch": "Jogar",
|
||||||
@@ -35,6 +34,7 @@
|
|||||||
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
|
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
|
||||||
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
|
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
|
||||||
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
|
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixe desativado normalmente. Ative para títulos cujas superfícies desenhadas pela CPU nunca chegam à tela.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.",
|
||||||
"Options.Section.Emulation": "EMULAÇÃO",
|
"Options.Section.Emulation": "EMULAÇÃO",
|
||||||
"Options.Section.Logging": "LOGS",
|
"Options.Section.Logging": "LOGS",
|
||||||
"Options.Section.Launcher": "INICIALIZADOR",
|
"Options.Section.Launcher": "INICIALIZADOR",
|
||||||
@@ -169,5 +169,34 @@
|
|||||||
"Updater.Status.Timeout": "A verificação de atualizações expirou após 10 segundos.",
|
"Updater.Status.Timeout": "A verificação de atualizações expirou após 10 segundos.",
|
||||||
"Updater.Status.Failed": "Não foi possível verificar as atualizações.",
|
"Updater.Status.Failed": "Não foi possível verificar as atualizações.",
|
||||||
"Updater.Status.ChecksumFailed": "A atualização baixada falhou na verificação SHA-256.",
|
"Updater.Status.ChecksumFailed": "A atualização baixada falhou na verificação SHA-256.",
|
||||||
"Updater.Status.Unsupported": "A atualização automática requer um build x64 para Windows, Linux ou macOS."
|
"Updater.Status.Unsupported": "A atualização automática requer um build x64 para Windows, Linux ou macOS.",
|
||||||
|
"Options.Graphics": "Gráficos",
|
||||||
|
"Options.Section.Rendering": "RENDERIZAÇÃO",
|
||||||
|
"Options.Section.Display": "TELA",
|
||||||
|
"Options.RenderResolution.Label": "Resolução interna",
|
||||||
|
"Options.RenderResolution.Desc": "Renderiza alvos fora da tela abaixo da resolução nativa e amplia na apresentação. Valores menores trocam qualidade de imagem por folga da GPU; entra em vigor na próxima inicialização.",
|
||||||
|
"Options.RenderResolution.Native": "100% (nativa)",
|
||||||
|
"Options.WindowMode.Label": "Modo de janela",
|
||||||
|
"Options.WindowMode.Desc": "Janela normal, área de trabalho sem bordas ou tela cheia exclusiva.",
|
||||||
|
"Options.WindowMode.Windowed": "Em janela",
|
||||||
|
"Options.WindowMode.Borderless": "Sem bordas",
|
||||||
|
"Options.WindowMode.Exclusive": "Exclusiva",
|
||||||
|
"Options.Resolution.Label": "Resolução",
|
||||||
|
"Options.Resolution.Desc": "Tamanho inicial da janela ou resolução de tela cheia exclusiva.",
|
||||||
|
"Options.Display.Label": "Tela",
|
||||||
|
"Options.Display.Desc": "Monitor usado para centralização e tela cheia.",
|
||||||
|
"Options.RefreshRate.Label": "Taxa de atualização",
|
||||||
|
"Options.RefreshRate.Desc": "Taxa de atualização da tela cheia exclusiva. O modo automático seleciona o modo mais próximo.",
|
||||||
|
"Options.RefreshRate.Automatic": "Automática",
|
||||||
|
"Options.Scaling.Label": "Escala",
|
||||||
|
"Options.Scaling.Desc": "Dimensiona a imagem nativa do sistema convidado sem alterar sua resolução interna.",
|
||||||
|
"Options.Scaling.Fit": "Ajustar",
|
||||||
|
"Options.Scaling.Cover": "Preencher",
|
||||||
|
"Options.Scaling.Stretch": "Esticar",
|
||||||
|
"Options.Scaling.Integer": "Inteira",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "Usa apresentação FIFO para evitar cortes na imagem.",
|
||||||
|
"Options.Hdr.Label": "Saída HDR",
|
||||||
|
"Options.Hdr.Desc": "Usa HDR quando a tela selecionada e o backend gráfico oferecem suporte. O modo automático retorna ao SDR.",
|
||||||
|
"Options.Hdr.Auto": "Automático"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Bibliothek durchsuchen…",
|
"Library.SearchWatermark": "Bibliothek durchsuchen…",
|
||||||
"Library.AddFolder": "+ Spielordner hinzufügen",
|
"Library.AddFolder": "+ Spielordner hinzufügen",
|
||||||
"Library.Rescan": "⟳ Neu scannen",
|
|
||||||
"Library.OpenFile": "Datei öffnen…",
|
"Library.OpenFile": "Datei öffnen…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Starten",
|
"Library.Context.Launch": "Starten",
|
||||||
@@ -139,6 +138,7 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "Direkte Speicherzuweisungen und Fehler in der Konsole protokollieren.\nVerwenden, wenn ein Spiel beim Start abbricht oder sich beendet.",
|
"Options.Env.LogDirectMemory.Desc": "Direkte Speicherzuweisungen und Fehler in der Konsole protokollieren.\nVerwenden, wenn ein Spiel beim Start abbricht oder sich beendet.",
|
||||||
"Options.Env.LogIo.Desc": "Datei-Öffnen, -Lesen und Pfadauflösung in der Konsole protokollieren.\nVerwenden, wenn ein Spiel beim Start seine Datendateien nicht findet.",
|
"Options.Env.LogIo.Desc": "Datei-Öffnen, -Lesen und Pfadauflösung in der Konsole protokollieren.\nVerwenden, wenn ein Spiel beim Start seine Datendateien nicht findet.",
|
||||||
"Options.Env.LogNp.Desc": "NP-Bibliotheksaufrufe (PlayStation Network) in der Konsole protokollieren.",
|
"Options.Env.LogNp.Desc": "NP-Bibliotheksaufrufe (PlayStation Network) in der Konsole protokollieren.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Gast-Oberflächen neu hochladen, die der eigene CPU-Code des Spiels überschreibt.\nNormalerweise aus lassen. Für Titel aktivieren, deren CPU-gezeichnete Oberflächen nie auf dem Bildschirm erscheinen.\nKostet Leistung und verursacht bei einigen Titeln wie GTA V Regressionen.",
|
||||||
"Common.Save": "Speichern",
|
"Common.Save": "Speichern",
|
||||||
"Common.Cancel": "Abbrechen",
|
"Common.Cancel": "Abbrechen",
|
||||||
"PerGame.Title": "Spielspezifische Einstellungen — {0} ({1})",
|
"PerGame.Title": "Spielspezifische Einstellungen — {0} ({1})",
|
||||||
@@ -168,5 +168,34 @@
|
|||||||
"Updater.Status.Timeout": "Die Updateprüfung ist nach 10 Sekunden abgelaufen.",
|
"Updater.Status.Timeout": "Die Updateprüfung ist nach 10 Sekunden abgelaufen.",
|
||||||
"Updater.Status.Failed": "Updates konnten nicht geprüft werden.",
|
"Updater.Status.Failed": "Updates konnten nicht geprüft werden.",
|
||||||
"Updater.Status.ChecksumFailed": "Das heruntergeladene Update hat die SHA-256-Prüfung nicht bestanden.",
|
"Updater.Status.ChecksumFailed": "Das heruntergeladene Update hat die SHA-256-Prüfung nicht bestanden.",
|
||||||
"Updater.Status.Unsupported": "Automatische Updates erfordern einen x64-Build für Windows, Linux oder macOS."
|
"Updater.Status.Unsupported": "Automatische Updates erfordern einen x64-Build für Windows, Linux oder macOS.",
|
||||||
|
"Options.Graphics": "Grafik",
|
||||||
|
"Options.Section.Rendering": "DARSTELLUNG",
|
||||||
|
"Options.Section.Display": "ANZEIGE",
|
||||||
|
"Options.RenderResolution.Label": "Interne Auflösung",
|
||||||
|
"Options.RenderResolution.Desc": "Offscreen-Ziele unterhalb der nativen Auflösung rendern und bei der Ausgabe hochskalieren. Niedrigere Werte tauschen Bildqualität gegen GPU-Reserven; wird beim nächsten Start wirksam.",
|
||||||
|
"Options.RenderResolution.Native": "100 % (nativ)",
|
||||||
|
"Options.WindowMode.Label": "Fenstermodus",
|
||||||
|
"Options.WindowMode.Desc": "Normales Fenster, randloser Desktop oder exklusiver Vollbildmodus.",
|
||||||
|
"Options.WindowMode.Windowed": "Fenster",
|
||||||
|
"Options.WindowMode.Borderless": "Randlos",
|
||||||
|
"Options.WindowMode.Exclusive": "Exklusiv",
|
||||||
|
"Options.Resolution.Label": "Auflösung",
|
||||||
|
"Options.Resolution.Desc": "Anfängliche Fenstergröße oder exklusive Vollbildauflösung.",
|
||||||
|
"Options.Display.Label": "Anzeige",
|
||||||
|
"Options.Display.Desc": "Monitor für Zentrierung und Vollbilddarstellung.",
|
||||||
|
"Options.RefreshRate.Label": "Bildwiederholrate",
|
||||||
|
"Options.RefreshRate.Desc": "Bildwiederholrate im exklusiven Vollbildmodus. Automatisch wählt den nächstgelegenen Modus.",
|
||||||
|
"Options.RefreshRate.Automatic": "Automatisch",
|
||||||
|
"Options.Scaling.Label": "Skalierung",
|
||||||
|
"Options.Scaling.Desc": "Das native Gastbild skalieren, ohne seine interne Auflösung zu ändern.",
|
||||||
|
"Options.Scaling.Fit": "Einpassen",
|
||||||
|
"Options.Scaling.Cover": "Ausfüllen",
|
||||||
|
"Options.Scaling.Stretch": "Strecken",
|
||||||
|
"Options.Scaling.Integer": "Ganzzahlig",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "FIFO-Präsentation für eine Ausgabe ohne Tearing verwenden.",
|
||||||
|
"Options.Hdr.Label": "HDR-Ausgabe",
|
||||||
|
"Options.Hdr.Desc": "HDR verwenden, wenn die ausgewählte Anzeige und das Grafik-Backend es unterstützen. Automatisch fällt auf SDR zurück.",
|
||||||
|
"Options.Hdr.Auto": "Automatisch"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Søg i biblioteket…",
|
"Library.SearchWatermark": "Søg i biblioteket…",
|
||||||
"Library.AddFolder": "+ Tilføj mappe",
|
"Library.AddFolder": "+ Tilføj mappe",
|
||||||
"Library.Rescan": "⟳ Genindlæs",
|
|
||||||
"Library.OpenFile": "Åbn fil…",
|
"Library.OpenFile": "Åbn fil…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Start",
|
"Library.Context.Launch": "Start",
|
||||||
@@ -139,6 +138,7 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "Log direkte hukommelsestildelinger og fejl til konsollen.\nBrug dette, når et spil afbryder eller lukker under opstart.",
|
"Options.Env.LogDirectMemory.Desc": "Log direkte hukommelsestildelinger og fejl til konsollen.\nBrug dette, når et spil afbryder eller lukker under opstart.",
|
||||||
"Options.Env.LogIo.Desc": "Log åbning og læsning af filer samt stiopslag til konsollen.\nBrug dette, når et spil ikke kan finde sine datafiler under opstart.",
|
"Options.Env.LogIo.Desc": "Log åbning og læsning af filer samt stiopslag til konsollen.\nBrug dette, når et spil ikke kan finde sine datafiler under opstart.",
|
||||||
"Options.Env.LogNp.Desc": "Log NP-bibliotekskald (PlayStation Network) til konsollen.",
|
"Options.Env.LogNp.Desc": "Log NP-bibliotekskald (PlayStation Network) til konsollen.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Genindlæs gæsteoverflader, som spillets egen CPU-kode omskriver.\nLad den være slået fra normalt. Slå til for titler, hvis CPU-tegnede overflader aldrig når skærmen.\nKoster ydeevne og giver regressioner i nogle titler, såsom GTA V.",
|
||||||
"Common.Save": "Gem",
|
"Common.Save": "Gem",
|
||||||
"Common.Cancel": "Annuller",
|
"Common.Cancel": "Annuller",
|
||||||
"PerGame.Title": "Indstillinger pr. spil — {0} ({1})",
|
"PerGame.Title": "Indstillinger pr. spil — {0} ({1})",
|
||||||
@@ -168,5 +168,34 @@
|
|||||||
"Updater.Status.Timeout": "Opdateringstjekket fik timeout efter 10 sekunder.",
|
"Updater.Status.Timeout": "Opdateringstjekket fik timeout efter 10 sekunder.",
|
||||||
"Updater.Status.Failed": "Kunne ikke søge efter opdateringer.",
|
"Updater.Status.Failed": "Kunne ikke søge efter opdateringer.",
|
||||||
"Updater.Status.ChecksumFailed": "Den downloadede opdatering bestod ikke SHA-256-verifikationen.",
|
"Updater.Status.ChecksumFailed": "Den downloadede opdatering bestod ikke SHA-256-verifikationen.",
|
||||||
"Updater.Status.Unsupported": "Automatisk opdatering kræver et x64-build til Windows, Linux eller macOS."
|
"Updater.Status.Unsupported": "Automatisk opdatering kræver et x64-build til Windows, Linux eller macOS.",
|
||||||
|
"Options.Graphics": "Grafik",
|
||||||
|
"Options.Section.Rendering": "GENGIVELSE",
|
||||||
|
"Options.Section.Display": "SKÆRM",
|
||||||
|
"Options.RenderResolution.Label": "Intern opløsning",
|
||||||
|
"Options.RenderResolution.Desc": "Render offscreen-mål under den oprindelige opløsning, og opskaler dem ved visning. Lavere værdier bytter billedkvalitet for GPU-kapacitet; træder i kraft ved næste start.",
|
||||||
|
"Options.RenderResolution.Native": "100 % (oprindelig)",
|
||||||
|
"Options.WindowMode.Label": "Vinduestilstand",
|
||||||
|
"Options.WindowMode.Desc": "Normalt vindue, kantløst skrivebord eller eksklusiv fuldskærm.",
|
||||||
|
"Options.WindowMode.Windowed": "Vindue",
|
||||||
|
"Options.WindowMode.Borderless": "Kantløs",
|
||||||
|
"Options.WindowMode.Exclusive": "Eksklusiv",
|
||||||
|
"Options.Resolution.Label": "Opløsning",
|
||||||
|
"Options.Resolution.Desc": "Oprindelig vinduesstørrelse eller opløsning i eksklusiv fuldskærm.",
|
||||||
|
"Options.Display.Label": "Skærm",
|
||||||
|
"Options.Display.Desc": "Skærm, der bruges til centrering og fuldskærm.",
|
||||||
|
"Options.RefreshRate.Label": "Opdateringshastighed",
|
||||||
|
"Options.RefreshRate.Desc": "Opdateringshastighed i eksklusiv fuldskærm. Automatisk vælger den nærmeste tilstand.",
|
||||||
|
"Options.RefreshRate.Automatic": "Automatisk",
|
||||||
|
"Options.Scaling.Label": "Skalering",
|
||||||
|
"Options.Scaling.Desc": "Skaler det oprindelige gæstebillede uden at ændre dets interne opløsning.",
|
||||||
|
"Options.Scaling.Fit": "Tilpas",
|
||||||
|
"Options.Scaling.Cover": "Udfyld",
|
||||||
|
"Options.Scaling.Stretch": "Stræk",
|
||||||
|
"Options.Scaling.Integer": "Heltal",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "Brug FIFO-præsentation for output uden tearing.",
|
||||||
|
"Options.Hdr.Label": "HDR-output",
|
||||||
|
"Options.Hdr.Desc": "Brug HDR, når den valgte skærm og grafik-backend understøtter det. Automatisk falder tilbage til SDR.",
|
||||||
|
"Options.Hdr.Auto": "Automatisk"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Search library…",
|
"Library.SearchWatermark": "Search library…",
|
||||||
"Library.AddFolder": "+ Add folder",
|
"Library.AddFolder": "+ Add folder",
|
||||||
"Library.Rescan": "⟳ Rescan",
|
|
||||||
"Library.OpenFile": "Open file…",
|
"Library.OpenFile": "Open file…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Launch",
|
"Library.Context.Launch": "Launch",
|
||||||
@@ -38,14 +37,22 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
|
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
|
||||||
"Options.Env.LogIo.Desc": "Log file open, read, and path-resolve activity to the console.\nUse when a game cannot find its data files during boot.",
|
"Options.Env.LogIo.Desc": "Log file open, read, and path-resolve activity to the console.\nUse when a game cannot find its data files during boot.",
|
||||||
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Re-upload guest surfaces the game's own CPU code rewrites.\nLeave off normally. Turn on for titles whose CPU-drawn surfaces never reach the screen.\nCosts performance and regresses some titles, such as GTA V.",
|
||||||
"Options.Section.Emulation": "EMULATION",
|
"Options.Section.Emulation": "EMULATION",
|
||||||
"Options.Section.Logging": "LOGGING",
|
"Options.Section.Logging": "LOGGING",
|
||||||
"Options.Section.Launcher": "LAUNCHER",
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
"Options.Section.Rendering": "RENDERING",
|
||||||
"Options.Section.Display": "DISPLAY",
|
"Options.Section.Display": "DISPLAY",
|
||||||
"Options.Graphics": "Graphics",
|
"Options.Graphics": "Graphics",
|
||||||
|
|
||||||
|
"Options.RenderResolution.Label": "Internal resolution",
|
||||||
|
"Options.RenderResolution.Desc": "Render offscreen targets below native resolution and upscale on present. Lower values trade image quality for GPU headroom; takes effect on next launch.",
|
||||||
|
"Options.RenderResolution.Native": "100% (native)",
|
||||||
"Options.WindowMode.Label": "Window mode",
|
"Options.WindowMode.Label": "Window mode",
|
||||||
"Options.WindowMode.Desc": "Regular window, desktop borderless, or exclusive fullscreen.",
|
"Options.WindowMode.Desc": "Regular window, desktop borderless, or exclusive fullscreen.",
|
||||||
|
"Options.WindowMode.Windowed": "Windowed",
|
||||||
|
"Options.WindowMode.Borderless": "Borderless",
|
||||||
|
"Options.WindowMode.Exclusive": "Exclusive",
|
||||||
"Options.Resolution.Label": "Resolution",
|
"Options.Resolution.Label": "Resolution",
|
||||||
"Options.Resolution.Desc": "Initial window size or exclusive fullscreen resolution.",
|
"Options.Resolution.Desc": "Initial window size or exclusive fullscreen resolution.",
|
||||||
"Options.Display.Label": "Display",
|
"Options.Display.Label": "Display",
|
||||||
@@ -55,10 +62,15 @@
|
|||||||
"Options.RefreshRate.Automatic": "Automatic",
|
"Options.RefreshRate.Automatic": "Automatic",
|
||||||
"Options.Scaling.Label": "Scaling",
|
"Options.Scaling.Label": "Scaling",
|
||||||
"Options.Scaling.Desc": "Scale the native guest image without changing its internal resolution.",
|
"Options.Scaling.Desc": "Scale the native guest image without changing its internal resolution.",
|
||||||
|
"Options.Scaling.Fit": "Fit",
|
||||||
|
"Options.Scaling.Cover": "Cover",
|
||||||
|
"Options.Scaling.Stretch": "Stretch",
|
||||||
|
"Options.Scaling.Integer": "Integer",
|
||||||
"Options.VSync.Label": "VSync",
|
"Options.VSync.Label": "VSync",
|
||||||
"Options.VSync.Desc": "Use FIFO presentation for tear-free output.",
|
"Options.VSync.Desc": "Use FIFO presentation for tear-free output.",
|
||||||
"Options.Hdr.Label": "HDR output",
|
"Options.Hdr.Label": "HDR output",
|
||||||
"Options.Hdr.Desc": "Use HDR when the selected display and graphics backend support it. Auto falls back to SDR.",
|
"Options.Hdr.Desc": "Use HDR when the selected display and graphics backend support it. Auto falls back to SDR.",
|
||||||
|
"Options.Hdr.Auto": "Auto",
|
||||||
|
|
||||||
"Options.CpuEngine.Label": "CPU engine",
|
"Options.CpuEngine.Label": "CPU engine",
|
||||||
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Buscar en la biblioteca…",
|
"Library.SearchWatermark": "Buscar en la biblioteca…",
|
||||||
"Library.AddFolder": "+ Añadir carpeta",
|
"Library.AddFolder": "+ Añadir carpeta",
|
||||||
"Library.Rescan": "⟳ Volver a escanear",
|
|
||||||
"Library.OpenFile": "Abrir archivo…",
|
"Library.OpenFile": "Abrir archivo…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Iniciar",
|
"Library.Context.Launch": "Iniciar",
|
||||||
@@ -149,6 +148,7 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "Registrar en la consola las asignaciones de memoria directa y sus fallos.\nÚsalo cuando un juego se aborte o se cierre durante el arranque.",
|
"Options.Env.LogDirectMemory.Desc": "Registrar en la consola las asignaciones de memoria directa y sus fallos.\nÚsalo cuando un juego se aborte o se cierre durante el arranque.",
|
||||||
"Options.Env.LogIo.Desc": "Registrar en la consola la apertura y lectura de archivos y la resolución de rutas.\nÚsalo cuando un juego no encuentre sus archivos de datos durante el arranque.",
|
"Options.Env.LogIo.Desc": "Registrar en la consola la apertura y lectura de archivos y la resolución de rutas.\nÚsalo cuando un juego no encuentre sus archivos de datos durante el arranque.",
|
||||||
"Options.Env.LogNp.Desc": "Registrar en la consola las llamadas a la biblioteca NP (PlayStation Network).",
|
"Options.Env.LogNp.Desc": "Registrar en la consola las llamadas a la biblioteca NP (PlayStation Network).",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Volver a subir las superficies del invitado que reescribe el propio código de CPU del juego.\nDejar desactivado normalmente. Activar en títulos cuyas superficies dibujadas por CPU nunca llegan a la pantalla.\nCuesta rendimiento y causa regresiones en algunos títulos, como GTA V.",
|
||||||
"Common.Save": "Guardar",
|
"Common.Save": "Guardar",
|
||||||
"Common.Cancel": "Cancelar",
|
"Common.Cancel": "Cancelar",
|
||||||
"PerGame.Title": "Ajustes por juego — {0} ({1})",
|
"PerGame.Title": "Ajustes por juego — {0} ({1})",
|
||||||
@@ -169,5 +169,34 @@
|
|||||||
"Updater.Status.Timeout": "La comprobación de actualizaciones caducó tras 10 segundos.",
|
"Updater.Status.Timeout": "La comprobación de actualizaciones caducó tras 10 segundos.",
|
||||||
"Updater.Status.Failed": "No se pudieron comprobar las actualizaciones.",
|
"Updater.Status.Failed": "No se pudieron comprobar las actualizaciones.",
|
||||||
"Updater.Status.ChecksumFailed": "La actualización descargada no superó la verificación SHA-256.",
|
"Updater.Status.ChecksumFailed": "La actualización descargada no superó la verificación SHA-256.",
|
||||||
"Updater.Status.Unsupported": "La actualización automática requiere un build x64 de Windows, Linux o macOS."
|
"Updater.Status.Unsupported": "La actualización automática requiere un build x64 de Windows, Linux o macOS.",
|
||||||
|
"Options.Graphics": "Gráficos",
|
||||||
|
"Options.Section.Rendering": "RENDERIZADO",
|
||||||
|
"Options.Section.Display": "PANTALLA",
|
||||||
|
"Options.RenderResolution.Label": "Resolución interna",
|
||||||
|
"Options.RenderResolution.Desc": "Renderiza objetivos fuera de pantalla por debajo de la resolución nativa y los reescala al presentar. Los valores inferiores sacrifican calidad de imagen para liberar carga de la GPU; se aplica en el próximo inicio.",
|
||||||
|
"Options.RenderResolution.Native": "100 % (nativa)",
|
||||||
|
"Options.WindowMode.Label": "Modo de ventana",
|
||||||
|
"Options.WindowMode.Desc": "Ventana normal, escritorio sin bordes o pantalla completa exclusiva.",
|
||||||
|
"Options.WindowMode.Windowed": "En ventana",
|
||||||
|
"Options.WindowMode.Borderless": "Sin bordes",
|
||||||
|
"Options.WindowMode.Exclusive": "Exclusiva",
|
||||||
|
"Options.Resolution.Label": "Resolución",
|
||||||
|
"Options.Resolution.Desc": "Tamaño inicial de la ventana o resolución de pantalla completa exclusiva.",
|
||||||
|
"Options.Display.Label": "Pantalla",
|
||||||
|
"Options.Display.Desc": "Monitor utilizado para centrar y mostrar a pantalla completa.",
|
||||||
|
"Options.RefreshRate.Label": "Frecuencia de actualización",
|
||||||
|
"Options.RefreshRate.Desc": "Frecuencia de actualización de la pantalla completa exclusiva. El modo automático selecciona el modo más cercano.",
|
||||||
|
"Options.RefreshRate.Automatic": "Automática",
|
||||||
|
"Options.Scaling.Label": "Escalado",
|
||||||
|
"Options.Scaling.Desc": "Escala la imagen nativa del sistema invitado sin cambiar su resolución interna.",
|
||||||
|
"Options.Scaling.Fit": "Ajustar",
|
||||||
|
"Options.Scaling.Cover": "Cubrir",
|
||||||
|
"Options.Scaling.Stretch": "Estirar",
|
||||||
|
"Options.Scaling.Integer": "Entero",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "Usa presentación FIFO para evitar el desgarro de imagen.",
|
||||||
|
"Options.Hdr.Label": "Salida HDR",
|
||||||
|
"Options.Hdr.Desc": "Usa HDR cuando la pantalla seleccionada y el backend gráfico sean compatibles. El modo automático vuelve a SDR.",
|
||||||
|
"Options.Hdr.Auto": "Automático"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Rechercher dans la bibliothèque…",
|
"Library.SearchWatermark": "Rechercher dans la bibliothèque…",
|
||||||
"Library.AddFolder": "+ Ajouter un dossier",
|
"Library.AddFolder": "+ Ajouter un dossier",
|
||||||
"Library.Rescan": "⟳ Analyser à nouveau",
|
|
||||||
"Library.OpenFile": "Ouvrir un fichier…",
|
"Library.OpenFile": "Ouvrir un fichier…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Lancer",
|
"Library.Context.Launch": "Lancer",
|
||||||
@@ -35,6 +34,7 @@
|
|||||||
"Options.Env.DumpSpirv.Desc": "Exporter les shaders AGC et leurs traductions SPIR-V dans le dossier shader-dumps.\nÀ utiliser pour signaler des bugs de shader ou de rendu.",
|
"Options.Env.DumpSpirv.Desc": "Exporter les shaders AGC et leurs traductions SPIR-V dans le dossier shader-dumps.\nÀ utiliser pour signaler des bugs de shader ou de rendu.",
|
||||||
"Options.Env.LogDirectMemory.Desc": "Journaliser les allocations de mémoire directe et les échecs dans la console.\nÀ utiliser quand un jeu plante ou se ferme pendant le démarrage.",
|
"Options.Env.LogDirectMemory.Desc": "Journaliser les allocations de mémoire directe et les échecs dans la console.\nÀ utiliser quand un jeu plante ou se ferme pendant le démarrage.",
|
||||||
"Options.Env.LogNp.Desc": "Journaliser les appels de la bibliothèque NP (PlayStation Network) dans la console.",
|
"Options.Env.LogNp.Desc": "Journaliser les appels de la bibliothèque NP (PlayStation Network) dans la console.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Recharger les surfaces invité que le code CPU du jeu réécrit lui-même.\nLaisser désactivé normalement. Activer pour les titres dont les surfaces dessinées par le CPU n'atteignent jamais l'écran.\nCoûte des performances et provoque des régressions sur certains titres, comme GTA V.",
|
||||||
"Options.Section.Emulation": "ÉMULATION",
|
"Options.Section.Emulation": "ÉMULATION",
|
||||||
"Options.Section.Logging": "JOURNALISATION",
|
"Options.Section.Logging": "JOURNALISATION",
|
||||||
"Options.Section.Launcher": "LANCEUR",
|
"Options.Section.Launcher": "LANCEUR",
|
||||||
@@ -169,5 +169,34 @@
|
|||||||
"Updater.Status.Timeout": "La vérification des mises à jour a expiré après 10 secondes.",
|
"Updater.Status.Timeout": "La vérification des mises à jour a expiré après 10 secondes.",
|
||||||
"Updater.Status.Failed": "Impossible de vérifier les mises à jour.",
|
"Updater.Status.Failed": "Impossible de vérifier les mises à jour.",
|
||||||
"Updater.Status.ChecksumFailed": "La mise à jour téléchargée a échoué à la vérification SHA-256.",
|
"Updater.Status.ChecksumFailed": "La mise à jour téléchargée a échoué à la vérification SHA-256.",
|
||||||
"Updater.Status.Unsupported": "La mise à jour automatique nécessite un build x64 pour Windows, Linux ou macOS."
|
"Updater.Status.Unsupported": "La mise à jour automatique nécessite un build x64 pour Windows, Linux ou macOS.",
|
||||||
|
"Options.Graphics": "Graphismes",
|
||||||
|
"Options.Section.Rendering": "RENDU",
|
||||||
|
"Options.Section.Display": "AFFICHAGE",
|
||||||
|
"Options.RenderResolution.Label": "Résolution interne",
|
||||||
|
"Options.RenderResolution.Desc": "Effectuer le rendu des cibles hors écran sous la résolution native et les mettre à l’échelle lors de l’affichage. Les valeurs inférieures réduisent la qualité d’image pour libérer des ressources GPU ; prend effet au prochain lancement.",
|
||||||
|
"Options.RenderResolution.Native": "100 % (native)",
|
||||||
|
"Options.WindowMode.Label": "Mode fenêtre",
|
||||||
|
"Options.WindowMode.Desc": "Fenêtre standard, bureau sans bordures ou plein écran exclusif.",
|
||||||
|
"Options.WindowMode.Windowed": "Fenêtré",
|
||||||
|
"Options.WindowMode.Borderless": "Sans bordures",
|
||||||
|
"Options.WindowMode.Exclusive": "Exclusif",
|
||||||
|
"Options.Resolution.Label": "Résolution",
|
||||||
|
"Options.Resolution.Desc": "Taille initiale de la fenêtre ou résolution du plein écran exclusif.",
|
||||||
|
"Options.Display.Label": "Écran",
|
||||||
|
"Options.Display.Desc": "Moniteur utilisé pour le centrage et le plein écran.",
|
||||||
|
"Options.RefreshRate.Label": "Fréquence de rafraîchissement",
|
||||||
|
"Options.RefreshRate.Desc": "Fréquence de rafraîchissement du plein écran exclusif. Le mode automatique sélectionne le mode le plus proche.",
|
||||||
|
"Options.RefreshRate.Automatic": "Automatique",
|
||||||
|
"Options.Scaling.Label": "Mise à l’échelle",
|
||||||
|
"Options.Scaling.Desc": "Mettre à l’échelle l’image native du système invité sans modifier sa résolution interne.",
|
||||||
|
"Options.Scaling.Fit": "Ajuster",
|
||||||
|
"Options.Scaling.Cover": "Remplir",
|
||||||
|
"Options.Scaling.Stretch": "Étirer",
|
||||||
|
"Options.Scaling.Integer": "Entier",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "Utiliser la présentation FIFO pour un affichage sans déchirement.",
|
||||||
|
"Options.Hdr.Label": "Sortie HDR",
|
||||||
|
"Options.Hdr.Desc": "Utiliser le HDR lorsque l’écran sélectionné et le backend graphique le prennent en charge. Le mode automatique revient au SDR.",
|
||||||
|
"Options.Hdr.Auto": "Automatique"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Keresés a könyvtárban",
|
"Library.SearchWatermark": "Keresés a könyvtárban",
|
||||||
"Library.AddFolder": "+ Mappa hozzáadása",
|
"Library.AddFolder": "+ Mappa hozzáadása",
|
||||||
"Library.Rescan": "⟳ Újrakeresés",
|
|
||||||
"Library.OpenFile": "Fájl megnyitása…",
|
"Library.OpenFile": "Fájl megnyitása…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Inditás",
|
"Library.Context.Launch": "Inditás",
|
||||||
@@ -35,6 +34,7 @@
|
|||||||
"Options.Env.DumpSpirv.Desc": "Az AGC-shaderek és azok SPIR-V-fordításainak mentése a shader-dumps mappába.\nHasználd shader- vagy renderelési hibák jelentésekor.",
|
"Options.Env.DumpSpirv.Desc": "Az AGC-shaderek és azok SPIR-V-fordításainak mentése a shader-dumps mappába.\nHasználd shader- vagy renderelési hibák jelentésekor.",
|
||||||
"Options.Env.LogDirectMemory.Desc": "A közvetlen memóriaallokációk és hibák naplózása a konzolra.\nHasználd, ha egy játék a rendszerindítás során megszakad vagy kilép.",
|
"Options.Env.LogDirectMemory.Desc": "A közvetlen memóriaallokációk és hibák naplózása a konzolra.\nHasználd, ha egy játék a rendszerindítás során megszakad vagy kilép.",
|
||||||
"Options.Env.LogNp.Desc": "Az NP (PlayStation Network) könyvtárhívásokat naplózza a konzolra.",
|
"Options.Env.LogNp.Desc": "Az NP (PlayStation Network) könyvtárhívásokat naplózza a konzolra.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Újratölti azokat a vendégfelületeket, amelyeket a játék saját CPU-kódja ír felül.\nNormál esetben hagyd kikapcsolva. Kapcsold be azoknál a címeknél, amelyek CPU-val rajzolt felületei sosem jutnak ki a képernyőre.\nTeljesítménybe kerül, és egyes címeknél, például a GTA V-nél regressziót okoz.",
|
||||||
"Options.Section.Emulation": "EMULÁCIÓ",
|
"Options.Section.Emulation": "EMULÁCIÓ",
|
||||||
"Options.Section.Logging": "LOGOLÁS",
|
"Options.Section.Logging": "LOGOLÁS",
|
||||||
"Options.Section.Launcher": "INDITÓ",
|
"Options.Section.Launcher": "INDITÓ",
|
||||||
@@ -169,5 +169,34 @@
|
|||||||
"Updater.Status.Timeout": "A frissítés-ellenőrzés 10 másodperc után túllépte az időkorlátot.",
|
"Updater.Status.Timeout": "A frissítés-ellenőrzés 10 másodperc után túllépte az időkorlátot.",
|
||||||
"Updater.Status.Failed": "Nem sikerült frissítéseket keresni.",
|
"Updater.Status.Failed": "Nem sikerült frissítéseket keresni.",
|
||||||
"Updater.Status.ChecksumFailed": "A letöltött frissítés nem ment át az SHA-256-ellenőrzésen.",
|
"Updater.Status.ChecksumFailed": "A letöltött frissítés nem ment át az SHA-256-ellenőrzésen.",
|
||||||
"Updater.Status.Unsupported": "Az automatikus frissítéshez Windows, Linux vagy macOS x64 build szükséges."
|
"Updater.Status.Unsupported": "Az automatikus frissítéshez Windows, Linux vagy macOS x64 build szükséges.",
|
||||||
|
"Options.Graphics": "Grafika",
|
||||||
|
"Options.Section.Rendering": "RENDERELÉS",
|
||||||
|
"Options.Section.Display": "KIJELZŐ",
|
||||||
|
"Options.RenderResolution.Label": "Belső felbontás",
|
||||||
|
"Options.RenderResolution.Desc": "A képernyőn kívüli célok renderelése a natívnál kisebb felbontáson, majd felskálázás megjelenítéskor. Az alacsonyabb értékek képminőséget cserélnek GPU-tartalékra; a következő indításkor lép érvénybe.",
|
||||||
|
"Options.RenderResolution.Native": "100% (natív)",
|
||||||
|
"Options.WindowMode.Label": "Ablakmód",
|
||||||
|
"Options.WindowMode.Desc": "Normál ablak, keret nélküli asztal vagy kizárólagos teljes képernyő.",
|
||||||
|
"Options.WindowMode.Windowed": "Ablakos",
|
||||||
|
"Options.WindowMode.Borderless": "Keret nélküli",
|
||||||
|
"Options.WindowMode.Exclusive": "Kizárólagos",
|
||||||
|
"Options.Resolution.Label": "Felbontás",
|
||||||
|
"Options.Resolution.Desc": "Kezdeti ablakméret vagy kizárólagos teljes képernyős felbontás.",
|
||||||
|
"Options.Display.Label": "Kijelző",
|
||||||
|
"Options.Display.Desc": "A középre igazításhoz és teljes képernyőhöz használt monitor.",
|
||||||
|
"Options.RefreshRate.Label": "Frissítési gyakoriság",
|
||||||
|
"Options.RefreshRate.Desc": "A kizárólagos teljes képernyő frissítési gyakorisága. Az automatikus mód a legközelebbi módot választja.",
|
||||||
|
"Options.RefreshRate.Automatic": "Automatikus",
|
||||||
|
"Options.Scaling.Label": "Méretezés",
|
||||||
|
"Options.Scaling.Desc": "A natív vendégkép méretezése a belső felbontás módosítása nélkül.",
|
||||||
|
"Options.Scaling.Fit": "Illesztés",
|
||||||
|
"Options.Scaling.Cover": "Kitöltés",
|
||||||
|
"Options.Scaling.Stretch": "Nyújtás",
|
||||||
|
"Options.Scaling.Integer": "Egész szám",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "FIFO megjelenítés használata képtörésmentes kimenethez.",
|
||||||
|
"Options.Hdr.Label": "HDR-kimenet",
|
||||||
|
"Options.Hdr.Desc": "HDR használata, ha a kiválasztott kijelző és grafikus backend támogatja. Az automatikus mód SDR-re vált vissza.",
|
||||||
|
"Options.Hdr.Auto": "Automatikus"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Cerca nella libreria…",
|
"Library.SearchWatermark": "Cerca nella libreria…",
|
||||||
"Library.AddFolder": "+ Aggiungi cartella",
|
"Library.AddFolder": "+ Aggiungi cartella",
|
||||||
"Library.Rescan": "⟳ Riscansiona",
|
|
||||||
"Library.OpenFile": "Apri file…",
|
"Library.OpenFile": "Apri file…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Avvia",
|
"Library.Context.Launch": "Avvia",
|
||||||
@@ -144,6 +143,7 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "Registra in console le allocazioni di memoria diretta e i relativi errori.\nUsalo quando un gioco si interrompe o si chiude durante l'avvio.",
|
"Options.Env.LogDirectMemory.Desc": "Registra in console le allocazioni di memoria diretta e i relativi errori.\nUsalo quando un gioco si interrompe o si chiude durante l'avvio.",
|
||||||
"Options.Env.LogIo.Desc": "Registra in console l'apertura e la lettura dei file e la risoluzione dei percorsi.\nUsalo quando un gioco non trova i propri file di dati durante l'avvio.",
|
"Options.Env.LogIo.Desc": "Registra in console l'apertura e la lettura dei file e la risoluzione dei percorsi.\nUsalo quando un gioco non trova i propri file di dati durante l'avvio.",
|
||||||
"Options.Env.LogNp.Desc": "Registra in console le chiamate alla libreria NP (PlayStation Network).",
|
"Options.Env.LogNp.Desc": "Registra in console le chiamate alla libreria NP (PlayStation Network).",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Ricarica le superfici guest riscritte dal codice CPU del gioco.\nLasciare disattivato normalmente. Attivare per i titoli le cui superfici disegnate dalla CPU non raggiungono mai lo schermo.\nCosta prestazioni e causa regressioni in alcuni titoli, come GTA V.",
|
||||||
"Common.Save": "Salva",
|
"Common.Save": "Salva",
|
||||||
"Common.Cancel": "Annulla",
|
"Common.Cancel": "Annulla",
|
||||||
"PerGame.Title": "Impostazioni per gioco — {0} ({1})",
|
"PerGame.Title": "Impostazioni per gioco — {0} ({1})",
|
||||||
@@ -173,5 +173,34 @@
|
|||||||
"Updater.Status.Timeout": "Il controllo degli aggiornamenti è scaduto dopo 10 secondi.",
|
"Updater.Status.Timeout": "Il controllo degli aggiornamenti è scaduto dopo 10 secondi.",
|
||||||
"Updater.Status.Failed": "Impossibile controllare gli aggiornamenti.",
|
"Updater.Status.Failed": "Impossibile controllare gli aggiornamenti.",
|
||||||
"Updater.Status.ChecksumFailed": "L'aggiornamento scaricato non ha superato la verifica SHA-256.",
|
"Updater.Status.ChecksumFailed": "L'aggiornamento scaricato non ha superato la verifica SHA-256.",
|
||||||
"Updater.Status.Unsupported": "L'aggiornamento automatico richiede un build x64 per Windows, Linux o macOS."
|
"Updater.Status.Unsupported": "L'aggiornamento automatico richiede un build x64 per Windows, Linux o macOS.",
|
||||||
|
"Options.Graphics": "Grafica",
|
||||||
|
"Options.Section.Rendering": "RENDERING",
|
||||||
|
"Options.Section.Display": "SCHERMO",
|
||||||
|
"Options.RenderResolution.Label": "Risoluzione interna",
|
||||||
|
"Options.RenderResolution.Desc": "Renderizza i target fuori schermo sotto la risoluzione nativa e li ridimensiona in fase di presentazione. Valori inferiori sacrificano la qualità dell'immagine per lasciare margine alla GPU; ha effetto al prossimo avvio.",
|
||||||
|
"Options.RenderResolution.Native": "100% (nativa)",
|
||||||
|
"Options.WindowMode.Label": "Modalità finestra",
|
||||||
|
"Options.WindowMode.Desc": "Finestra normale, desktop senza bordi o schermo intero esclusivo.",
|
||||||
|
"Options.WindowMode.Windowed": "In finestra",
|
||||||
|
"Options.WindowMode.Borderless": "Senza bordi",
|
||||||
|
"Options.WindowMode.Exclusive": "Esclusiva",
|
||||||
|
"Options.Resolution.Label": "Risoluzione",
|
||||||
|
"Options.Resolution.Desc": "Dimensione iniziale della finestra o risoluzione dello schermo intero esclusivo.",
|
||||||
|
"Options.Display.Label": "Schermo",
|
||||||
|
"Options.Display.Desc": "Monitor utilizzato per il centraggio e lo schermo intero.",
|
||||||
|
"Options.RefreshRate.Label": "Frequenza di aggiornamento",
|
||||||
|
"Options.RefreshRate.Desc": "Frequenza di aggiornamento dello schermo intero esclusivo. La modalità automatica seleziona la modalità più vicina.",
|
||||||
|
"Options.RefreshRate.Automatic": "Automatica",
|
||||||
|
"Options.Scaling.Label": "Ridimensionamento",
|
||||||
|
"Options.Scaling.Desc": "Ridimensiona l'immagine nativa del sistema guest senza modificarne la risoluzione interna.",
|
||||||
|
"Options.Scaling.Fit": "Adatta",
|
||||||
|
"Options.Scaling.Cover": "Riempi",
|
||||||
|
"Options.Scaling.Stretch": "Estendi",
|
||||||
|
"Options.Scaling.Integer": "Intero",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "Usa la presentazione FIFO per evitare lo screen tearing.",
|
||||||
|
"Options.Hdr.Label": "Output HDR",
|
||||||
|
"Options.Hdr.Desc": "Usa HDR quando lo schermo selezionato e il backend grafico lo supportano. La modalità automatica torna a SDR.",
|
||||||
|
"Options.Hdr.Auto": "Automatico"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "ライブラリを検索…",
|
"Library.SearchWatermark": "ライブラリを検索…",
|
||||||
"Library.AddFolder": "+ フォルダーを追加",
|
"Library.AddFolder": "+ フォルダーを追加",
|
||||||
"Library.Rescan": "⟳ 再スキャン",
|
|
||||||
"Library.OpenFile": "ファイルを開く…",
|
"Library.OpenFile": "ファイルを開く…",
|
||||||
|
|
||||||
"Library.Context.Launch": "起動",
|
"Library.Context.Launch": "起動",
|
||||||
@@ -139,6 +138,7 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "ダイレクトメモリの割り当てと失敗をコンソールに記録します。\nゲームが起動中に中断・終了する場合に使用してください。",
|
"Options.Env.LogDirectMemory.Desc": "ダイレクトメモリの割り当てと失敗をコンソールに記録します。\nゲームが起動中に中断・終了する場合に使用してください。",
|
||||||
"Options.Env.LogIo.Desc": "ファイルのオープン・読み込み・パス解決の動作をコンソールに記録します。\nゲームが起動中にデータファイルを見つけられない場合に使用してください。",
|
"Options.Env.LogIo.Desc": "ファイルのオープン・読み込み・パス解決の動作をコンソールに記録します。\nゲームが起動中にデータファイルを見つけられない場合に使用してください。",
|
||||||
"Options.Env.LogNp.Desc": "NP(PlayStation Network)ライブラリの呼び出しをコンソールに記録します。",
|
"Options.Env.LogNp.Desc": "NP(PlayStation Network)ライブラリの呼び出しをコンソールに記録します。",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "ゲーム自身の CPU コードが書き換えるゲスト表面を再アップロードします。\n通常はオフのままにしてください。CPU で描画した表面が画面に反映されないタイトルで有効にします。\n性能を犠牲にし、GTA V など一部のタイトルでは不具合が生じます。",
|
||||||
"Common.Save": "保存",
|
"Common.Save": "保存",
|
||||||
"Common.Cancel": "キャンセル",
|
"Common.Cancel": "キャンセル",
|
||||||
"PerGame.Title": "ゲームごとの設定 — {0} ({1})",
|
"PerGame.Title": "ゲームごとの設定 — {0} ({1})",
|
||||||
@@ -168,5 +168,34 @@
|
|||||||
"Updater.Status.Timeout": "アップデートの確認が10秒でタイムアウトしました。",
|
"Updater.Status.Timeout": "アップデートの確認が10秒でタイムアウトしました。",
|
||||||
"Updater.Status.Failed": "アップデートを確認できませんでした。",
|
"Updater.Status.Failed": "アップデートを確認できませんでした。",
|
||||||
"Updater.Status.ChecksumFailed": "ダウンロードしたアップデートはSHA-256検証に失敗しました。",
|
"Updater.Status.ChecksumFailed": "ダウンロードしたアップデートはSHA-256検証に失敗しました。",
|
||||||
"Updater.Status.Unsupported": "自動アップデートにはWindows、Linux、またはmacOSのx64ビルドが必要です。"
|
"Updater.Status.Unsupported": "自動アップデートにはWindows、Linux、またはmacOSのx64ビルドが必要です。",
|
||||||
|
"Options.Graphics": "グラフィックス",
|
||||||
|
"Options.Section.Rendering": "レンダリング",
|
||||||
|
"Options.Section.Display": "ディスプレイ",
|
||||||
|
"Options.RenderResolution.Label": "内部解像度",
|
||||||
|
"Options.RenderResolution.Desc": "ネイティブ解像度より低い解像度でオフスクリーンターゲットを描画し、表示時にアップスケールします。値を下げると画質と引き換えにGPU負荷を軽減します。次回起動時に適用されます。",
|
||||||
|
"Options.RenderResolution.Native": "100%(ネイティブ)",
|
||||||
|
"Options.WindowMode.Label": "ウィンドウモード",
|
||||||
|
"Options.WindowMode.Desc": "通常ウィンドウ、デスクトップのボーダーレス、または排他フルスクリーン。",
|
||||||
|
"Options.WindowMode.Windowed": "ウィンドウ",
|
||||||
|
"Options.WindowMode.Borderless": "ボーダーレス",
|
||||||
|
"Options.WindowMode.Exclusive": "排他",
|
||||||
|
"Options.Resolution.Label": "解像度",
|
||||||
|
"Options.Resolution.Desc": "初期ウィンドウサイズまたは排他フルスクリーンの解像度。",
|
||||||
|
"Options.Display.Label": "ディスプレイ",
|
||||||
|
"Options.Display.Desc": "中央配置とフルスクリーンに使用するモニター。",
|
||||||
|
"Options.RefreshRate.Label": "リフレッシュレート",
|
||||||
|
"Options.RefreshRate.Desc": "排他フルスクリーンのリフレッシュレート。自動では最も近いモードを選択します。",
|
||||||
|
"Options.RefreshRate.Automatic": "自動",
|
||||||
|
"Options.Scaling.Label": "スケーリング",
|
||||||
|
"Options.Scaling.Desc": "内部解像度を変更せずにゲストのネイティブ画像を拡大縮小します。",
|
||||||
|
"Options.Scaling.Fit": "フィット",
|
||||||
|
"Options.Scaling.Cover": "カバー",
|
||||||
|
"Options.Scaling.Stretch": "引き伸ばし",
|
||||||
|
"Options.Scaling.Integer": "整数倍",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "ティアリングのない表示のためFIFOプレゼンテーションを使用します。",
|
||||||
|
"Options.Hdr.Label": "HDR出力",
|
||||||
|
"Options.Hdr.Desc": "選択したディスプレイとグラフィックスバックエンドが対応している場合にHDRを使用します。自動ではSDRにフォールバックします。",
|
||||||
|
"Options.Hdr.Auto": "自動"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "라이브러리 검색…",
|
"Library.SearchWatermark": "라이브러리 검색…",
|
||||||
"Library.AddFolder": "+ 폴더 추가",
|
"Library.AddFolder": "+ 폴더 추가",
|
||||||
"Library.Rescan": "⟳ 다시 스캔",
|
|
||||||
"Library.OpenFile": "파일 열기…",
|
"Library.OpenFile": "파일 열기…",
|
||||||
|
|
||||||
"Library.Context.Launch": "실행",
|
"Library.Context.Launch": "실행",
|
||||||
@@ -139,6 +138,7 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "다이렉트 메모리 할당과 실패를 콘솔에 기록합니다.\n게임이 부팅 중 중단되거나 종료될 때 사용하세요.",
|
"Options.Env.LogDirectMemory.Desc": "다이렉트 메모리 할당과 실패를 콘솔에 기록합니다.\n게임이 부팅 중 중단되거나 종료될 때 사용하세요.",
|
||||||
"Options.Env.LogIo.Desc": "파일 열기, 읽기, 경로 확인 동작을 콘솔에 기록합니다.\n게임이 부팅 중 데이터 파일을 찾지 못할 때 사용하세요.",
|
"Options.Env.LogIo.Desc": "파일 열기, 읽기, 경로 확인 동작을 콘솔에 기록합니다.\n게임이 부팅 중 데이터 파일을 찾지 못할 때 사용하세요.",
|
||||||
"Options.Env.LogNp.Desc": "NP(PlayStation Network) 라이브러리 호출을 콘솔에 기록합니다.",
|
"Options.Env.LogNp.Desc": "NP(PlayStation Network) 라이브러리 호출을 콘솔에 기록합니다.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "게임의 자체 CPU 코드가 다시 쓰는 게스트 표면을 다시 업로드합니다.\n평소에는 꺼 두세요. CPU로 그린 표면이 화면에 나타나지 않는 타이틀에서 켜세요.\n성능을 소모하며 GTA V 등 일부 타이틀에서는 문제가 생깁니다.",
|
||||||
"Common.Save": "저장",
|
"Common.Save": "저장",
|
||||||
"Common.Cancel": "취소",
|
"Common.Cancel": "취소",
|
||||||
"PerGame.Title": "게임별 설정 — {0} ({1})",
|
"PerGame.Title": "게임별 설정 — {0} ({1})",
|
||||||
@@ -168,5 +168,34 @@
|
|||||||
"Updater.Status.Timeout": "업데이트 확인이 10초 후 시간 초과되었습니다.",
|
"Updater.Status.Timeout": "업데이트 확인이 10초 후 시간 초과되었습니다.",
|
||||||
"Updater.Status.Failed": "업데이트를 확인할 수 없습니다.",
|
"Updater.Status.Failed": "업데이트를 확인할 수 없습니다.",
|
||||||
"Updater.Status.ChecksumFailed": "다운로드한 업데이트가 SHA-256 검증에 실패했습니다.",
|
"Updater.Status.ChecksumFailed": "다운로드한 업데이트가 SHA-256 검증에 실패했습니다.",
|
||||||
"Updater.Status.Unsupported": "자동 업데이트에는 Windows, Linux 또는 macOS x64 빌드가 필요합니다."
|
"Updater.Status.Unsupported": "자동 업데이트에는 Windows, Linux 또는 macOS x64 빌드가 필요합니다.",
|
||||||
|
"Options.Graphics": "그래픽",
|
||||||
|
"Options.Section.Rendering": "렌더링",
|
||||||
|
"Options.Section.Display": "디스플레이",
|
||||||
|
"Options.RenderResolution.Label": "내부 해상도",
|
||||||
|
"Options.RenderResolution.Desc": "네이티브 해상도보다 낮은 해상도로 오프스크린 대상을 렌더링한 뒤 표시할 때 업스케일합니다. 값이 낮을수록 화질을 희생해 GPU 여유를 확보하며 다음 실행부터 적용됩니다.",
|
||||||
|
"Options.RenderResolution.Native": "100% (네이티브)",
|
||||||
|
"Options.WindowMode.Label": "창 모드",
|
||||||
|
"Options.WindowMode.Desc": "일반 창, 데스크톱 테두리 없음 또는 독점 전체 화면.",
|
||||||
|
"Options.WindowMode.Windowed": "창",
|
||||||
|
"Options.WindowMode.Borderless": "테두리 없음",
|
||||||
|
"Options.WindowMode.Exclusive": "독점",
|
||||||
|
"Options.Resolution.Label": "해상도",
|
||||||
|
"Options.Resolution.Desc": "초기 창 크기 또는 독점 전체 화면 해상도.",
|
||||||
|
"Options.Display.Label": "디스플레이",
|
||||||
|
"Options.Display.Desc": "가운데 배치와 전체 화면에 사용할 모니터.",
|
||||||
|
"Options.RefreshRate.Label": "새로 고침 빈도",
|
||||||
|
"Options.RefreshRate.Desc": "독점 전체 화면의 새로 고침 빈도입니다. 자동은 가장 가까운 모드를 선택합니다.",
|
||||||
|
"Options.RefreshRate.Automatic": "자동",
|
||||||
|
"Options.Scaling.Label": "스케일링",
|
||||||
|
"Options.Scaling.Desc": "내부 해상도를 변경하지 않고 게스트의 네이티브 이미지를 확대 또는 축소합니다.",
|
||||||
|
"Options.Scaling.Fit": "맞춤",
|
||||||
|
"Options.Scaling.Cover": "채우기",
|
||||||
|
"Options.Scaling.Stretch": "늘이기",
|
||||||
|
"Options.Scaling.Integer": "정수배",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "티어링 없는 출력을 위해 FIFO 프레젠테이션을 사용합니다.",
|
||||||
|
"Options.Hdr.Label": "HDR 출력",
|
||||||
|
"Options.Hdr.Desc": "선택한 디스플레이와 그래픽 백엔드가 지원하는 경우 HDR을 사용합니다. 자동 모드는 SDR로 대체됩니다.",
|
||||||
|
"Options.Hdr.Auto": "자동"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Zoeken in bibliotheek…",
|
"Library.SearchWatermark": "Zoeken in bibliotheek…",
|
||||||
"Library.AddFolder": "+ Map toevoegen",
|
"Library.AddFolder": "+ Map toevoegen",
|
||||||
"Library.Rescan": "⟳ Opnieuw scannen",
|
|
||||||
"Library.OpenFile": "Bestand openen…",
|
"Library.OpenFile": "Bestand openen…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Starten",
|
"Library.Context.Launch": "Starten",
|
||||||
@@ -139,6 +138,7 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "Log directe geheugentoewijzingen en fouten naar de console.\nGebruik dit wanneer een game tijdens het opstarten afbreekt of afsluit.",
|
"Options.Env.LogDirectMemory.Desc": "Log directe geheugentoewijzingen en fouten naar de console.\nGebruik dit wanneer een game tijdens het opstarten afbreekt of afsluit.",
|
||||||
"Options.Env.LogIo.Desc": "Log het openen en lezen van bestanden en het oplossen van paden naar de console.\nGebruik dit wanneer een game zijn databestanden niet kan vinden tijdens het opstarten.",
|
"Options.Env.LogIo.Desc": "Log het openen en lezen van bestanden en het oplossen van paden naar de console.\nGebruik dit wanneer een game zijn databestanden niet kan vinden tijdens het opstarten.",
|
||||||
"Options.Env.LogNp.Desc": "Log NP-bibliotheekaanroepen (PlayStation Network) naar de console.",
|
"Options.Env.LogNp.Desc": "Log NP-bibliotheekaanroepen (PlayStation Network) naar de console.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Gastoppervlakken opnieuw uploaden die de eigen CPU-code van de game herschrijft.\nNormaal uit laten. Inschakelen voor titels waarvan de door de CPU getekende oppervlakken nooit het scherm bereiken.\nKost prestaties en veroorzaakt regressies in sommige titels, zoals GTA V.",
|
||||||
"Common.Save": "Opslaan",
|
"Common.Save": "Opslaan",
|
||||||
"Common.Cancel": "Annuleren",
|
"Common.Cancel": "Annuleren",
|
||||||
"PerGame.Title": "Instellingen per game — {0} ({1})",
|
"PerGame.Title": "Instellingen per game — {0} ({1})",
|
||||||
@@ -168,5 +168,34 @@
|
|||||||
"Updater.Status.Timeout": "De updatecontrole is na 10 seconden verlopen.",
|
"Updater.Status.Timeout": "De updatecontrole is na 10 seconden verlopen.",
|
||||||
"Updater.Status.Failed": "Kon niet controleren op updates.",
|
"Updater.Status.Failed": "Kon niet controleren op updates.",
|
||||||
"Updater.Status.ChecksumFailed": "De gedownloade update is niet door de SHA-256-verificatie gekomen.",
|
"Updater.Status.ChecksumFailed": "De gedownloade update is niet door de SHA-256-verificatie gekomen.",
|
||||||
"Updater.Status.Unsupported": "Automatisch updaten vereist een x64-build voor Windows, Linux of macOS."
|
"Updater.Status.Unsupported": "Automatisch updaten vereist een x64-build voor Windows, Linux of macOS.",
|
||||||
|
"Options.Graphics": "Grafisch",
|
||||||
|
"Options.Section.Rendering": "RENDERING",
|
||||||
|
"Options.Section.Display": "BEELDSCHERM",
|
||||||
|
"Options.RenderResolution.Label": "Interne resolutie",
|
||||||
|
"Options.RenderResolution.Desc": "Render offscreen-doelen onder de oorspronkelijke resolutie en schaal ze bij presentatie op. Lagere waarden ruilen beeldkwaliteit in voor GPU-marge; wordt bij de volgende start toegepast.",
|
||||||
|
"Options.RenderResolution.Native": "100% (native)",
|
||||||
|
"Options.WindowMode.Label": "Venstermodus",
|
||||||
|
"Options.WindowMode.Desc": "Normaal venster, randloos bureaublad of exclusief volledig scherm.",
|
||||||
|
"Options.WindowMode.Windowed": "Venster",
|
||||||
|
"Options.WindowMode.Borderless": "Randloos",
|
||||||
|
"Options.WindowMode.Exclusive": "Exclusief",
|
||||||
|
"Options.Resolution.Label": "Resolutie",
|
||||||
|
"Options.Resolution.Desc": "Initiële venstergrootte of resolutie voor exclusief volledig scherm.",
|
||||||
|
"Options.Display.Label": "Beeldscherm",
|
||||||
|
"Options.Display.Desc": "Monitor die wordt gebruikt voor centrering en volledig scherm.",
|
||||||
|
"Options.RefreshRate.Label": "Verversingssnelheid",
|
||||||
|
"Options.RefreshRate.Desc": "Verversingssnelheid voor exclusief volledig scherm. Automatisch selecteert de dichtstbijzijnde modus.",
|
||||||
|
"Options.RefreshRate.Automatic": "Automatisch",
|
||||||
|
"Options.Scaling.Label": "Schaling",
|
||||||
|
"Options.Scaling.Desc": "Schaal de oorspronkelijke gastafbeelding zonder de interne resolutie te wijzigen.",
|
||||||
|
"Options.Scaling.Fit": "Passend",
|
||||||
|
"Options.Scaling.Cover": "Vullend",
|
||||||
|
"Options.Scaling.Stretch": "Uitrekken",
|
||||||
|
"Options.Scaling.Integer": "Geheel getal",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "Gebruik FIFO-presentatie voor uitvoer zonder tearing.",
|
||||||
|
"Options.Hdr.Label": "HDR-uitvoer",
|
||||||
|
"Options.Hdr.Desc": "Gebruik HDR wanneer het geselecteerde beeldscherm en de grafische backend dit ondersteunen. Automatisch valt terug op SDR.",
|
||||||
|
"Options.Hdr.Auto": "Automatisch"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Pesquisar biblioteca…",
|
"Library.SearchWatermark": "Pesquisar biblioteca…",
|
||||||
"Library.AddFolder": "+ Adicionar pasta",
|
"Library.AddFolder": "+ Adicionar pasta",
|
||||||
"Library.Rescan": "⟳ Reanalisar",
|
|
||||||
"Library.OpenFile": "Abrir ficheiro…",
|
"Library.OpenFile": "Abrir ficheiro…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Iniciar",
|
"Library.Context.Launch": "Iniciar",
|
||||||
@@ -35,6 +34,7 @@
|
|||||||
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e as respetivas traduções SPIR-V para a pasta shader-dumps.\nUtilize ao reportar problemas de shaders ou renderização.",
|
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e as respetivas traduções SPIR-V para a pasta shader-dumps.\nUtilize ao reportar problemas de shaders ou renderização.",
|
||||||
"Options.Env.LogDirectMemory.Desc": "Regista alocações de memória direta e falhas na consola.\nUtilize quando um jogo aborta ou fecha durante o arranque.",
|
"Options.Env.LogDirectMemory.Desc": "Regista alocações de memória direta e falhas na consola.\nUtilize quando um jogo aborta ou fecha durante o arranque.",
|
||||||
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
|
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixar desativado normalmente. Ativar para títulos cujas superfícies desenhadas pela CPU nunca chegam ao ecrã.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.",
|
||||||
"Options.Section.Emulation": "EMULAÇÃO",
|
"Options.Section.Emulation": "EMULAÇÃO",
|
||||||
"Options.Section.Logging": "REGISTOS",
|
"Options.Section.Logging": "REGISTOS",
|
||||||
"Options.Section.Launcher": "LANÇADOR",
|
"Options.Section.Launcher": "LANÇADOR",
|
||||||
@@ -169,5 +169,34 @@
|
|||||||
"Updater.Status.Timeout": "A verificação de atualizações expirou após 10 segundos.",
|
"Updater.Status.Timeout": "A verificação de atualizações expirou após 10 segundos.",
|
||||||
"Updater.Status.Failed": "Não foi possível procurar atualizações.",
|
"Updater.Status.Failed": "Não foi possível procurar atualizações.",
|
||||||
"Updater.Status.ChecksumFailed": "A atualização transferida falhou a verificação SHA-256.",
|
"Updater.Status.ChecksumFailed": "A atualização transferida falhou a verificação SHA-256.",
|
||||||
"Updater.Status.Unsupported": "A atualização automática requer um build x64 para Windows, Linux ou macOS."
|
"Updater.Status.Unsupported": "A atualização automática requer um build x64 para Windows, Linux ou macOS.",
|
||||||
|
"Options.Graphics": "Gráficos",
|
||||||
|
"Options.Section.Rendering": "RENDERIZAÇÃO",
|
||||||
|
"Options.Section.Display": "ECRÃ",
|
||||||
|
"Options.RenderResolution.Label": "Resolução interna",
|
||||||
|
"Options.RenderResolution.Desc": "Renderiza alvos fora do ecrã abaixo da resolução nativa e amplia-os na apresentação. Valores inferiores trocam qualidade de imagem por margem da GPU; entra em vigor no próximo arranque.",
|
||||||
|
"Options.RenderResolution.Native": "100% (nativa)",
|
||||||
|
"Options.WindowMode.Label": "Modo de janela",
|
||||||
|
"Options.WindowMode.Desc": "Janela normal, ambiente de trabalho sem margens ou ecrã inteiro exclusivo.",
|
||||||
|
"Options.WindowMode.Windowed": "Em janela",
|
||||||
|
"Options.WindowMode.Borderless": "Sem margens",
|
||||||
|
"Options.WindowMode.Exclusive": "Exclusivo",
|
||||||
|
"Options.Resolution.Label": "Resolução",
|
||||||
|
"Options.Resolution.Desc": "Tamanho inicial da janela ou resolução de ecrã inteiro exclusivo.",
|
||||||
|
"Options.Display.Label": "Ecrã",
|
||||||
|
"Options.Display.Desc": "Monitor utilizado para centrar e apresentar em ecrã inteiro.",
|
||||||
|
"Options.RefreshRate.Label": "Taxa de atualização",
|
||||||
|
"Options.RefreshRate.Desc": "Taxa de atualização do ecrã inteiro exclusivo. O modo automático seleciona o modo mais próximo.",
|
||||||
|
"Options.RefreshRate.Automatic": "Automática",
|
||||||
|
"Options.Scaling.Label": "Escala",
|
||||||
|
"Options.Scaling.Desc": "Dimensiona a imagem nativa do sistema convidado sem alterar a respetiva resolução interna.",
|
||||||
|
"Options.Scaling.Fit": "Ajustar",
|
||||||
|
"Options.Scaling.Cover": "Preencher",
|
||||||
|
"Options.Scaling.Stretch": "Esticar",
|
||||||
|
"Options.Scaling.Integer": "Inteira",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "Utiliza apresentação FIFO para evitar cortes na imagem.",
|
||||||
|
"Options.Hdr.Label": "Saída HDR",
|
||||||
|
"Options.Hdr.Desc": "Utiliza HDR quando o ecrã selecionado e o backend gráfico o suportam. O modo automático regressa a SDR.",
|
||||||
|
"Options.Hdr.Auto": "Automático"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Поиск…",
|
"Library.SearchWatermark": "Поиск…",
|
||||||
"Library.AddFolder": "+ Добавить папку",
|
"Library.AddFolder": "+ Добавить папку",
|
||||||
"Library.Rescan": "⟳ Сканировать",
|
|
||||||
"Library.OpenFile": "Открыть файл…",
|
"Library.OpenFile": "Открыть файл…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Запустить",
|
"Library.Context.Launch": "Запустить",
|
||||||
@@ -38,9 +37,40 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "Выводить в консоль выделения прямой памяти и ошибки выделения.\nИспользуйте, если игра аварийно завершает работу или закрывается при запуске.",
|
"Options.Env.LogDirectMemory.Desc": "Выводить в консоль выделения прямой памяти и ошибки выделения.\nИспользуйте, если игра аварийно завершает работу или закрывается при запуске.",
|
||||||
"Options.Env.LogIo.Desc": "Выводить в консоль операции открытия и чтения файлов, а также разрешение путей.\nИспользуйте, если игра не может найти файлы данных при запуске.",
|
"Options.Env.LogIo.Desc": "Выводить в консоль операции открытия и чтения файлов, а также разрешение путей.\nИспользуйте, если игра не может найти файлы данных при запуске.",
|
||||||
"Options.Env.LogNp.Desc": "Выводить в консоль вызовы библиотеки NP (PlayStation Network).",
|
"Options.Env.LogNp.Desc": "Выводить в консоль вызовы библиотеки NP (PlayStation Network).",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Повторно загружать гостевые поверхности, которые переписывает собственный код ЦП игры.\nОбычно оставляйте выключенным. Включайте для игр, чьи отрисованные ЦП поверхности не попадают на экран.\nСнижает производительность и вызывает регрессии в некоторых играх, например в GTA V.",
|
||||||
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
|
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
|
||||||
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
|
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
|
||||||
"Options.Section.Launcher": "ЛАУНЧЕР",
|
"Options.Section.Launcher": "ЛАУНЧЕР",
|
||||||
|
"Options.Section.Rendering": "РЕНДЕРИНГ",
|
||||||
|
"Options.Section.Display": "ЭКРАН",
|
||||||
|
"Options.Graphics": "Графика",
|
||||||
|
|
||||||
|
"Options.RenderResolution.Label": "Внутреннее разрешение",
|
||||||
|
"Options.RenderResolution.Desc": "Рендерить внеэкранные буферы ниже нативного разрешения и масштабировать при выводе. Меньшие значения снижают качество изображения, но уменьшают нагрузку на GPU; применяется при следующем запуске.",
|
||||||
|
"Options.RenderResolution.Native": "100% (нативное)",
|
||||||
|
"Options.WindowMode.Label": "Режим окна",
|
||||||
|
"Options.WindowMode.Desc": "Обычное окно, безрамочный режим рабочего стола или эксклюзивный полноэкранный режим.",
|
||||||
|
"Options.WindowMode.Windowed": "Оконный",
|
||||||
|
"Options.WindowMode.Borderless": "Без рамки",
|
||||||
|
"Options.WindowMode.Exclusive": "Эксклюзивный",
|
||||||
|
"Options.Resolution.Label": "Разрешение",
|
||||||
|
"Options.Resolution.Desc": "Начальный размер окна или разрешение эксклюзивного полноэкранного режима.",
|
||||||
|
"Options.Display.Label": "Монитор",
|
||||||
|
"Options.Display.Desc": "Монитор, используемый для центрирования окна и полноэкранного режима.",
|
||||||
|
"Options.RefreshRate.Label": "Частота обновления",
|
||||||
|
"Options.RefreshRate.Desc": "Частота обновления эксклюзивного полноэкранного режима. Автоматический режим выбирает ближайшее значение.",
|
||||||
|
"Options.RefreshRate.Automatic": "Автоматически",
|
||||||
|
"Options.Scaling.Label": "Масштабирование",
|
||||||
|
"Options.Scaling.Desc": "Масштабировать нативное изображение игры без изменения внутреннего разрешения.",
|
||||||
|
"Options.Scaling.Fit": "Вписать",
|
||||||
|
"Options.Scaling.Cover": "Заполнить",
|
||||||
|
"Options.Scaling.Stretch": "Растянуть",
|
||||||
|
"Options.Scaling.Integer": "Целочисленное",
|
||||||
|
"Options.VSync.Label": "VSync",
|
||||||
|
"Options.VSync.Desc": "Использовать режим представления FIFO для вывода без разрывов изображения.",
|
||||||
|
"Options.Hdr.Label": "HDR-вывод",
|
||||||
|
"Options.Hdr.Desc": "Использовать HDR, если выбранный монитор и графический бэкенд его поддерживают. Автоматический режим при необходимости переключается на SDR.",
|
||||||
|
"Options.Hdr.Auto": "Авто",
|
||||||
|
|
||||||
"Options.CpuEngine.Label": "Движок ЦП",
|
"Options.CpuEngine.Label": "Движок ЦП",
|
||||||
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
|
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
|
||||||
@@ -51,12 +81,12 @@
|
|||||||
|
|
||||||
"Options.LogLevel.Label": "Уровень логгирования",
|
"Options.LogLevel.Label": "Уровень логгирования",
|
||||||
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
|
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
|
||||||
"Options.LogLevel.Trace": "Trace",
|
"Options.LogLevel.Trace": "Трассировка",
|
||||||
"Options.LogLevel.Debug": "Debug",
|
"Options.LogLevel.Debug": "Отладка",
|
||||||
"Options.LogLevel.Info": "Info",
|
"Options.LogLevel.Info": "Информация",
|
||||||
"Options.LogLevel.Warning": "Warning",
|
"Options.LogLevel.Warning": "Предупреждение",
|
||||||
"Options.LogLevel.Error": "Error",
|
"Options.LogLevel.Error": "Ошибка",
|
||||||
"Options.LogLevel.Critical": "Critical",
|
"Options.LogLevel.Critical": "Критический",
|
||||||
|
|
||||||
"Options.TraceImports.Label": "Лимит трассировки импортов",
|
"Options.TraceImports.Label": "Лимит трассировки импортов",
|
||||||
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
|
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
|
||||||
@@ -87,6 +117,8 @@
|
|||||||
|
|
||||||
"PerGame.Title": "Настройки игры — {0} ({1})",
|
"PerGame.Title": "Настройки игры — {0} ({1})",
|
||||||
"PerGame.InheritNote": "Неотмеченные строки наследуют глобальные настройки.",
|
"PerGame.InheritNote": "Неотмеченные строки наследуют глобальные настройки.",
|
||||||
|
"PerGame.Tab.General": "Основные",
|
||||||
|
"PerGame.Tab.Graphics": "Графика",
|
||||||
"PerGame.EnvToggles.Label": "Переключатели окружения",
|
"PerGame.EnvToggles.Label": "Переключатели окружения",
|
||||||
"PerGame.EnvToggles.Desc": "Переопределить глобальный набор переключателей SHARPEMU_* для этой игры.",
|
"PerGame.EnvToggles.Desc": "Переопределить глобальный набор переключателей SHARPEMU_* для этой игры.",
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
"Library.SearchWatermark": "Kütüphanede ara…",
|
"Library.SearchWatermark": "Kütüphanede ara…",
|
||||||
"Library.AddFolder": "+ Klasör ekle",
|
"Library.AddFolder": "+ Klasör ekle",
|
||||||
"Library.Rescan": "⟳ Yeniden tara",
|
|
||||||
"Library.OpenFile": "Dosya aç…",
|
"Library.OpenFile": "Dosya aç…",
|
||||||
|
|
||||||
"Library.Context.Launch": "Başlat",
|
"Library.Context.Launch": "Başlat",
|
||||||
@@ -173,6 +172,7 @@
|
|||||||
"Options.Env.LogDirectMemory.Desc": "Doğrudan bellek tahsislerini ve hatalarını konsola günlükle.\nBir oyun açılış sırasında çöküyor veya kapanıyorsa kullanın.",
|
"Options.Env.LogDirectMemory.Desc": "Doğrudan bellek tahsislerini ve hatalarını konsola günlükle.\nBir oyun açılış sırasında çöküyor veya kapanıyorsa kullanın.",
|
||||||
"Options.Env.LogIo.Desc": "Dosya açma, okuma ve yol çözümleme etkinliğini konsola günlükle.\nBir oyun açılışta veri dosyalarını bulamıyorsa kullanın.",
|
"Options.Env.LogIo.Desc": "Dosya açma, okuma ve yol çözümleme etkinliğini konsola günlükle.\nBir oyun açılışta veri dosyalarını bulamıyorsa kullanın.",
|
||||||
"Options.Env.LogNp.Desc": "NP (PlayStation Network) kütüphane çağrılarını konsola günlükle.",
|
"Options.Env.LogNp.Desc": "NP (PlayStation Network) kütüphane çağrılarını konsola günlükle.",
|
||||||
|
"Options.Env.GuestImageCpuSync.Desc": "Oyunun kendi CPU kodunun yeniden yazdığı misafir yüzeyleri tekrar yükler.\nNormalde kapalı bırakın. CPU ile çizilen yüzeyleri ekrana ulaşmayan oyunlarda açın.\nPerformansa mal olur ve GTA V gibi bazı oyunlarda soruna yol açar.",
|
||||||
"Common.Save": "Kaydet",
|
"Common.Save": "Kaydet",
|
||||||
"Common.Cancel": "İptal",
|
"Common.Cancel": "İptal",
|
||||||
"PerGame.Title": "Oyuna özel ayarlar — {0} ({1})",
|
"PerGame.Title": "Oyuna özel ayarlar — {0} ({1})",
|
||||||
@@ -189,5 +189,17 @@
|
|||||||
"About.Discord.Label": "Discord",
|
"About.Discord.Label": "Discord",
|
||||||
"About.Discord.Desc": "Topluluğa katılın, destek alın ve geliştirmeyi takip edin.",
|
"About.Discord.Desc": "Topluluğa katılın, destek alın ve geliştirmeyi takip edin.",
|
||||||
"About.GithubButton": "GitHub'da katkıda bulun!",
|
"About.GithubButton": "GitHub'da katkıda bulun!",
|
||||||
"About.DiscordButton": "Discord'umuza katıl!"
|
"About.DiscordButton": "Discord'umuza katıl!",
|
||||||
|
"Options.Section.Rendering": "GÖRÜNTÜ İŞLEME",
|
||||||
|
"Options.RenderResolution.Label": "Dahili çözünürlük",
|
||||||
|
"Options.RenderResolution.Desc": "Ekran dışı hedefleri doğal çözünürlüğün altında işle ve sunum sırasında ölçeklendir. Daha düşük değerler GPU payı karşılığında görüntü kalitesini azaltır; bir sonraki başlatmada etkili olur.",
|
||||||
|
"Options.RenderResolution.Native": "%100 (doğal)",
|
||||||
|
"Options.WindowMode.Windowed": "Pencereli",
|
||||||
|
"Options.WindowMode.Borderless": "Kenarlıksız",
|
||||||
|
"Options.WindowMode.Exclusive": "Özel",
|
||||||
|
"Options.Scaling.Fit": "Sığdır",
|
||||||
|
"Options.Scaling.Cover": "Kapla",
|
||||||
|
"Options.Scaling.Stretch": "Uzat",
|
||||||
|
"Options.Scaling.Integer": "Tam sayı",
|
||||||
|
"Options.Hdr.Auto": "Otomatik"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace SharpEmu.GUI;
|
namespace SharpEmu.GUI;
|
||||||
@@ -14,25 +16,50 @@ public sealed record LanguageInfo(string Code, string NativeName);
|
|||||||
/// executable overrides the embedded copy for that code, so a translation
|
/// executable overrides the embedded copy for that code, so a translation
|
||||||
/// fix or a brand-new language never needs a rebuild.
|
/// fix or a brand-new language never needs a rebuild.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Localization
|
public sealed class Localization : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
public static Localization Instance { get; } = new();
|
public static Localization Instance { get; } = new();
|
||||||
|
|
||||||
private const string EmbeddedResourcePrefix = "Languages.";
|
private const string EmbeddedResourcePrefix = "Languages.";
|
||||||
private const string EmbeddedResourceSuffix = ".json";
|
private const string EmbeddedResourceSuffix = ".json";
|
||||||
|
private const string IndexerPropertyName = "Item";
|
||||||
|
|
||||||
|
private readonly string _languagesDirectory;
|
||||||
private Dictionary<string, string> _strings = new();
|
private Dictionary<string, string> _strings = new();
|
||||||
private Dictionary<string, string> _fallbackStrings = new();
|
private Dictionary<string, string> _fallbackStrings = new();
|
||||||
|
private string _currentCode = "en";
|
||||||
|
|
||||||
|
private Localization() : this(LanguagesDirectory)
|
||||||
private Localization()
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal Localization(string languagesDirectory)
|
||||||
|
{
|
||||||
|
_languagesDirectory = languagesDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
|
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
|
||||||
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
|
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
|
||||||
|
|
||||||
public string CurrentCode { get; private set; } = "en";
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
/// <summary>Exposes localized strings to XAML bindings by key.</summary>
|
||||||
|
public string this[string key] => Get(key);
|
||||||
|
|
||||||
|
public string CurrentCode
|
||||||
|
{
|
||||||
|
get => _currentCode;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (_currentCode == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentCode = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string Get(string key)
|
public string Get(string key)
|
||||||
{
|
{
|
||||||
@@ -67,7 +94,7 @@ public sealed class Localization
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
|
foreach (var file in Directory.EnumerateFiles(_languagesDirectory, "*.json"))
|
||||||
{
|
{
|
||||||
var code = Path.GetFileNameWithoutExtension(file);
|
var code = Path.GetFileNameWithoutExtension(file);
|
||||||
using var stream = File.OpenRead(file);
|
using var stream = File.OpenRead(file);
|
||||||
@@ -84,47 +111,36 @@ public sealed class Localization
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
/// <summary>
|
||||||
/// english is the fallback language
|
/// Loads a language by code. Loose files overlay the embedded language,
|
||||||
|
/// while English supplies values missing from the selected language.
|
||||||
|
/// </summary>
|
||||||
public void Load(string code)
|
public void Load(string code)
|
||||||
{
|
{
|
||||||
if (_fallbackStrings.Count == 0 && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
_fallbackStrings = LoadMergedLanguage("en");
|
||||||
|
_strings = string.Equals(code, "en", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? _fallbackStrings
|
||||||
|
: LoadMergedLanguage(code);
|
||||||
|
|
||||||
|
CurrentCode = code;
|
||||||
|
OnPropertyChanged(IndexerPropertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> LoadMergedLanguage(string code)
|
||||||
|
{
|
||||||
|
var merged = TryLoadEmbedded(code, out var embedded)
|
||||||
|
? embedded
|
||||||
|
: new Dictionary<string, string>();
|
||||||
|
|
||||||
|
if (TryLoadLooseFile(code, out var loose))
|
||||||
{
|
{
|
||||||
if (!TryLoadLooseFile("en", out var fallback) && !TryLoadEmbedded("en", out fallback))
|
foreach (var (key, value) in loose)
|
||||||
{
|
{
|
||||||
fallback = new Dictionary<string, string>();
|
merged[key] = value;
|
||||||
}
|
}
|
||||||
_fallbackStrings = fallback;
|
|
||||||
}
|
|
||||||
else if (string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (TryLoadLooseFile("en", out var enDict) || TryLoadEmbedded("en", out enDict))
|
|
||||||
{
|
|
||||||
_strings = enDict;
|
|
||||||
_fallbackStrings = enDict;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_strings = new Dictionary<string, string>();
|
|
||||||
_fallbackStrings = new Dictionary<string, string>();
|
|
||||||
}
|
|
||||||
CurrentCode = "en";
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the requested language
|
return merged;
|
||||||
if (TryLoadLooseFile(code, out var loaded) || TryLoadEmbedded(code, out loaded))
|
|
||||||
{
|
|
||||||
_strings = loaded;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_fallbackStrings.Count > 0)
|
|
||||||
_strings = new Dictionary<string, string>(_fallbackStrings);
|
|
||||||
else
|
|
||||||
_strings = new Dictionary<string, string>();
|
|
||||||
}
|
|
||||||
CurrentCode = code;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> EmbeddedLanguageCodes()
|
private static IEnumerable<string> EmbeddedLanguageCodes()
|
||||||
@@ -161,44 +177,12 @@ public sealed class Localization
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryLoadLooseFile(string code)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
|
||||||
return File.Exists(path) && TryLoad(code, File.ReadAllText(path));
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryLoadEmbedded(string code)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var stream = OpenEmbeddedLanguageStream(code);
|
|
||||||
if (stream is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var reader = new StreamReader(stream);
|
|
||||||
return TryLoad(code, reader.ReadToEnd());
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryLoadLooseFile(string code, out Dictionary<string, string> result)
|
private bool TryLoadLooseFile(string code, out Dictionary<string, string> result)
|
||||||
{
|
{
|
||||||
result = new Dictionary<string, string>();
|
result = new Dictionary<string, string>();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
var path = Path.Combine(_languagesDirectory, $"{code}.json");
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -243,14 +227,6 @@ public sealed class Localization
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryLoad(string code, string json)
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
{
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
if (TryLoad(json, out var dict))
|
}
|
||||||
{
|
|
||||||
_strings = dict;
|
|
||||||
CurrentCode = code;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A stable settings value with a display label that can be refreshed when
|
||||||
|
/// the active UI language changes.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LocalizedChoice : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private string _label;
|
||||||
|
|
||||||
|
private LocalizedChoice(string value, string label, string? localizationKey)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
_label = label;
|
||||||
|
LocalizationKey = localizationKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public string Label
|
||||||
|
{
|
||||||
|
get => _label;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (_label == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_label = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? LocalizationKey { get; }
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
public static LocalizedChoice FromKey(string value, string localizationKey) =>
|
||||||
|
new(value, localizationKey, localizationKey);
|
||||||
|
|
||||||
|
public static LocalizedChoice Literal(string value, string label) =>
|
||||||
|
new(value, label, null);
|
||||||
|
|
||||||
|
public void Refresh(Localization localization)
|
||||||
|
{
|
||||||
|
if (LocalizationKey is { } key)
|
||||||
|
{
|
||||||
|
Label = localization.Get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
+343
-139
@@ -14,12 +14,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
WindowState="Maximized"
|
WindowState="Maximized"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
Background="{StaticResource BgBrush}"
|
Background="{StaticResource BgBrush}"
|
||||||
ExtendClientAreaToDecorationsHint="True"
|
WindowDecorations="None"
|
||||||
WindowDecorations="Full"
|
|
||||||
ExtendClientAreaTitleBarHeightHint="44"
|
|
||||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
|
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
|
||||||
KeyDown="OnKeyDown">
|
KeyDown="OnKeyDown">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<DataTemplate x:Key="LocalizedChoiceTemplate" x:DataType="local:LocalizedChoice">
|
||||||
|
<TextBlock Text="{Binding Label}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
TextAlignment="Left" />
|
||||||
|
</DataTemplate>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
<Grid x:Name="RootLayout" RowDefinitions="Auto,*,Auto">
|
<Grid x:Name="RootLayout" RowDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
||||||
@@ -46,7 +52,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
|
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
|
||||||
the game gets the whole screen. -->
|
the game gets the whole screen. -->
|
||||||
<Grid x:Name="TitleBar" Grid.Row="0" Height="44" Background="{StaticResource ChromeBrush}">
|
<Grid x:Name="TitleBar"
|
||||||
|
Grid.Row="0"
|
||||||
|
Height="44"
|
||||||
|
ColumnDefinitions="*,Auto"
|
||||||
|
Background="{StaticResource ChromeBrush}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||||
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||||
@@ -55,6 +65,29 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel x:Name="WindowChromeButtons"
|
||||||
|
Grid.Column="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right">
|
||||||
|
<Button x:Name="MinimizeButton"
|
||||||
|
Classes="windowChrome"
|
||||||
|
ToolTip.Tip="Minimize"
|
||||||
|
AutomationProperties.Name="Minimize window">
|
||||||
|
<TextBlock Text="—" FontSize="16" />
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="MaximizeButton"
|
||||||
|
Classes="windowChrome"
|
||||||
|
ToolTip.Tip="Restore"
|
||||||
|
AutomationProperties.Name="Restore window">
|
||||||
|
<TextBlock x:Name="MaximizeGlyph" Text="❐" FontSize="13" />
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="CloseButton"
|
||||||
|
Classes="windowChrome windowClose"
|
||||||
|
ToolTip.Tip="Close"
|
||||||
|
AutomationProperties.Name="Close window">
|
||||||
|
<TextBlock Text="×" FontSize="20" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
@@ -69,8 +102,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Border Classes="padHint" VerticalAlignment="Center">
|
<Border Classes="padHint" VerticalAlignment="Center">
|
||||||
<TextBlock Text="LB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
<TextBlock Text="LB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
||||||
</Border>
|
</Border>
|
||||||
<Button x:Name="LibraryTabButton" Classes="segment active" Content="Library" />
|
<Button x:Name="LibraryTabButton"
|
||||||
<Button x:Name="OptionsTabButton" Classes="segment" Content="Options" />
|
Classes="segment active"
|
||||||
|
Content="{Binding [Page.Library], Source={x:Static local:Localization.Instance}}" />
|
||||||
|
<Button x:Name="OptionsTabButton"
|
||||||
|
Classes="segment"
|
||||||
|
Content="{Binding [Page.Options], Source={x:Static local:Localization.Instance}}" />
|
||||||
<Border Classes="padHint" VerticalAlignment="Center">
|
<Border Classes="padHint" VerticalAlignment="Center">
|
||||||
<TextBlock Text="RB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
<TextBlock Text="RB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
||||||
</Border>
|
</Border>
|
||||||
@@ -78,10 +115,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<StackPanel Grid.Column="2" x:Name="LibraryToolbar" Orientation="Horizontal" Spacing="8"
|
<StackPanel Grid.Column="2" x:Name="LibraryToolbar" Orientation="Horizontal" Spacing="8"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<TextBox x:Name="SearchBox" PlaceholderText="Search library…" Width="240" VerticalAlignment="Center" />
|
<TextBox x:Name="SearchBox"
|
||||||
<Button x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder" VerticalAlignment="Center" />
|
PlaceholderText="{Binding [Library.SearchWatermark], Source={x:Static local:Localization.Instance}}"
|
||||||
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" VerticalAlignment="Center" />
|
Width="240"
|
||||||
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
|
<Button x:Name="AddFolderButton"
|
||||||
|
Classes="ghost"
|
||||||
|
Content="{Binding [Library.AddFolder], Source={x:Static local:Localization.Instance}}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<Button x:Name="OpenFileButton"
|
||||||
|
Classes="ghost"
|
||||||
|
Content="{Binding [Library.OpenFile], Source={x:Static local:Localization.Instance}}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -95,39 +140,46 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
SelectionMode="Single" Padding="0">
|
SelectionMode="Single" Padding="0">
|
||||||
<ListBox.ContextMenu>
|
<ListBox.ContextMenu>
|
||||||
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
||||||
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
<MenuItem x:Name="CtxLaunch"
|
||||||
|
Header="{Binding [Library.Context.Launch], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontWeight="SemiBold">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
<MenuItem x:Name="CtxOpenFolder"
|
||||||
|
Header="{Binding [Library.Context.OpenFolder], Source={x:Static local:Localization.Instance}}">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
<MenuItem x:Name="CtxCopyPath"
|
||||||
|
Header="{Binding [Library.Context.CopyPath], Source={x:Static local:Localization.Instance}}">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
<MenuItem x:Name="CtxCopyTitleId"
|
||||||
|
Header="{Binding [Library.Context.CopyTitleId], Source={x:Static local:Localization.Instance}}">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem x:Name="CtxGameSettings" Header="Game settings…">
|
<MenuItem x:Name="CtxGameSettings"
|
||||||
|
Header="{Binding [Library.Context.GameSettings], Source={x:Static local:Localization.Instance}}">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<TextBlock Text="⚙" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
<TextBlock Text="⚙" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
<MenuItem x:Name="CtxRemove"
|
||||||
|
Header="{Binding [Library.Context.Remove], Source={x:Static local:Localization.Instance}}"
|
||||||
Foreground="{StaticResource DangerHoverBrush}">
|
Foreground="{StaticResource DangerHoverBrush}">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
||||||
@@ -172,7 +224,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
HorizontalAlignment="Center" />
|
HorizontalAlignment="Center" />
|
||||||
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
||||||
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||||
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
<Button x:Name="EmptyAddFolderButton" Classes="accent"
|
||||||
|
Content="{Binding [Library.Empty.AddFolder], Source={x:Static local:Localization.Instance}}"
|
||||||
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
@@ -182,7 +235,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<StackPanel x:Name="LoadingState" Spacing="14" HorizontalAlignment="Center" VerticalAlignment="Center"
|
<StackPanel x:Name="LoadingState" Spacing="14" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
IsVisible="False">
|
IsVisible="False">
|
||||||
<ProgressBar IsIndeterminate="True" Width="180" Height="3" />
|
<ProgressBar IsIndeterminate="True" Width="180" Height="3" />
|
||||||
<TextBlock x:Name="LoadingStateText" Text="Loading library…" FontSize="13"
|
<TextBlock x:Name="LoadingStateText"
|
||||||
|
Text="{Binding [Library.Loading], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontSize="13"
|
||||||
Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -192,25 +247,32 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
language used by the console panel and launch bar below. -->
|
language used by the console panel and launch bar below. -->
|
||||||
<Grid x:Name="OptionsPage" IsVisible="False">
|
<Grid x:Name="OptionsPage" IsVisible="False">
|
||||||
<TabControl>
|
<TabControl>
|
||||||
<TabItem x:Name="GeneralTabItem" Header="General" FontSize="15">
|
<TabItem x:Name="GeneralTabItem"
|
||||||
|
Header="{Binding [Options.General], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontSize="15">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||||
|
|
||||||
<Border Classes="card">
|
<Border Classes="card">
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" />
|
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle"
|
||||||
|
Text="{Binding [Options.Section.Emulation], Source={x:Static local:Localization.Instance}}" />
|
||||||
|
|
||||||
<local:SettingRow x:Name="CpuEngineRow" Label="CPU engine"
|
<local:SettingRow x:Name="CpuEngineRow"
|
||||||
Description="Execution engine used to run game code.">
|
Label="{Binding [Options.CpuEngine.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ComboBox x:Name="CpuEngineBox" Width="160" SelectedIndex="0"
|
Description="{Binding [Options.CpuEngine.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
VerticalAlignment="Center" CornerRadius="8">
|
<ComboBox x:Name="CpuEngineBox" Width="160"
|
||||||
<ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" />
|
ItemTemplate="{StaticResource LocalizedChoiceTemplate}"
|
||||||
</ComboBox>
|
HorizontalContentAlignment="Left"
|
||||||
|
VerticalAlignment="Center" CornerRadius="8" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="StrictRow" Label="Strict dynlib resolution"
|
<local:SettingRow x:Name="StrictRow"
|
||||||
Description="Fail the launch when an imported symbol cannot be resolved.">
|
Label="{Binding [Options.Strict.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off"
|
Description="{Binding [Options.Strict.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
|
<ToggleSwitch x:Name="StrictToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -218,43 +280,49 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<Border Classes="card">
|
<Border Classes="card">
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" />
|
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle"
|
||||||
|
Text="{Binding [Options.Section.Logging], Source={x:Static local:Localization.Instance}}" />
|
||||||
|
|
||||||
<local:SettingRow x:Name="LogLevelRow" Label="Log level"
|
<local:SettingRow x:Name="LogLevelRow"
|
||||||
Description="Verbosity of the emulator console output.">
|
Label="{Binding [Options.LogLevel.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ComboBox x:Name="LogLevelBox" Width="160" SelectedIndex="2"
|
Description="{Binding [Options.LogLevel.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
VerticalAlignment="Center" CornerRadius="8">
|
<ComboBox x:Name="LogLevelBox" Width="160"
|
||||||
<ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" />
|
ItemTemplate="{StaticResource LocalizedChoiceTemplate}"
|
||||||
<ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" />
|
HorizontalContentAlignment="Left"
|
||||||
<ComboBoxItem x:Name="LogLevelInfoItem" Content="Info" />
|
VerticalAlignment="Center" CornerRadius="8" />
|
||||||
<ComboBoxItem x:Name="LogLevelWarningItem" Content="Warning" />
|
|
||||||
<ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" />
|
|
||||||
<ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" />
|
|
||||||
</ComboBox>
|
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="TraceImportsRow" Label="Import trace limit"
|
<local:SettingRow x:Name="TraceImportsRow"
|
||||||
Description="Trace the first N imports per module (0 = off).">
|
Label="{Binding [Options.TraceImports.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
|
Description="{Binding [Options.TraceImports.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<NumericUpDown x:Name="TraceImportsBox" Width="160" Minimum="0"
|
<NumericUpDown x:Name="TraceImportsBox" Width="160" Minimum="0"
|
||||||
Maximum="4096" Increment="16" Value="0" FormatString="0"
|
Maximum="4096" Increment="16" Value="0" FormatString="0"
|
||||||
VerticalAlignment="Center" CornerRadius="8" />
|
VerticalAlignment="Center" CornerRadius="8" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="LogToFileRow" Label="Log to file"
|
<local:SettingRow x:Name="LogToFileRow"
|
||||||
Description="Mirror emulator output to a log file.">
|
Label="{Binding [Options.LogToFile.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off"
|
Description="{Binding [Options.LogToFile.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
|
<ToggleSwitch x:Name="LogToFileToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="LogFilePathRow" Label="Log file path"
|
<local:SettingRow x:Name="LogFilePathRow"
|
||||||
|
Label="{Binding [Options.LogFilePath.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
Description="No custom path">
|
Description="No custom path">
|
||||||
<Button x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
|
<Button x:Name="SelectLogFilePathButton" Classes="ghost"
|
||||||
|
Content="{Binding [Options.LogFilePath.Select], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="OverrideLogFileRow" Label="Override log file"
|
<local:SettingRow x:Name="OverrideLogFileRow"
|
||||||
Description="Use the exact file path instead of appending title ID and timestamp.">
|
Label="{Binding [Options.OverrideLogFile.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ToggleSwitch x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off"
|
Description="{Binding [Options.OverrideLogFile.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
|
<ToggleSwitch x:Name="OverrideLogFileToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -262,10 +330,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<Border Classes="card">
|
<Border Classes="card">
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" />
|
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle"
|
||||||
|
Text="{Binding [Options.Section.Launcher], Source={x:Static local:Localization.Instance}}" />
|
||||||
|
|
||||||
<local:SettingRow x:Name="LanguageRow" Label="Emulator language"
|
<local:SettingRow x:Name="LanguageRow"
|
||||||
Description="Language used throughout the launcher. Applies immediately.">
|
Label="{Binding [Options.Language.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
|
Description="{Binding [Options.Language.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ComboBox x:Name="LanguageBox" Width="160"
|
<ComboBox x:Name="LanguageBox" Width="160"
|
||||||
VerticalAlignment="Center" CornerRadius="8">
|
VerticalAlignment="Center" CornerRadius="8">
|
||||||
<ComboBox.ItemTemplate>
|
<ComboBox.ItemTemplate>
|
||||||
@@ -276,21 +346,30 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</ComboBox>
|
</ComboBox>
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="TitleMusicRow" Label="Title music"
|
<local:SettingRow x:Name="TitleMusicRow"
|
||||||
Description="Loop the selected game's preview music in the library.">
|
Label="{Binding [Options.TitleMusic.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off"
|
Description="{Binding [Options.TitleMusic.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
|
<ToggleSwitch x:Name="TitleMusicToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
IsChecked="True" VerticalAlignment="Center" />
|
IsChecked="True" VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="DiscordRow" Label="Discord presence"
|
<local:SettingRow x:Name="DiscordRow"
|
||||||
Description="Show the running game on your Discord profile.">
|
Label="{Binding [Options.Discord.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ToggleSwitch x:Name="DiscordToggle" OnContent="On" OffContent="Off"
|
Description="{Binding [Options.Discord.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
|
<ToggleSwitch x:Name="DiscordToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="AutoUpdateRow" Label="Check for updates on startup"
|
<local:SettingRow x:Name="AutoUpdateRow"
|
||||||
Description="Checks GitHub without delaying startup.">
|
Label="{Binding [Updater.Auto.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ToggleSwitch x:Name="AutoUpdateToggle" OnContent="On" OffContent="Off"
|
Description="{Binding [Updater.Auto.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
|
<ToggleSwitch x:Name="AutoUpdateToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
IsChecked="True" VerticalAlignment="Center" />
|
IsChecked="True" VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -299,7 +378,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<TextBlock x:Name="AboutSectionTitle"
|
<TextBlock x:Name="AboutSectionTitle"
|
||||||
Classes="sectionTitle"
|
Classes="sectionTitle"
|
||||||
Text="ABOUT" />
|
Text="{Binding [Options.About], Source={x:Static local:Localization.Instance}}" />
|
||||||
|
|
||||||
<!--Latest commit info-->
|
<!--Latest commit info-->
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
@@ -307,8 +386,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Image Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
|
<Image Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
|
||||||
Width="24" Height="24" VerticalAlignment="Center" />
|
Width="24" Height="24" VerticalAlignment="Center" />
|
||||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||||
<TextBlock x:Name="LatestCommitLabel" Text="Latest commit" FontSize="13"/>
|
<TextBlock x:Name="LatestCommitLabel"
|
||||||
<TextBlock x:Name="LatestCommitDescription" Text="Latest commit on the main branch" FontSize="11" Foreground="{StaticResource MutedBrush}"/>
|
Text="{Binding [About.Github.LatestCommitLabel], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontSize="13"/>
|
||||||
|
<TextBlock x:Name="LatestCommitDescription"
|
||||||
|
Text="{Binding [About.Github.LatestCommitDescription], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
@@ -326,7 +409,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Height="24"
|
Height="24"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||||
<TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" />
|
<TextBlock x:Name="UpdateLabel"
|
||||||
|
Text="{Binding [Updater.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontSize="13" />
|
||||||
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
|
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
|
||||||
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -348,10 +433,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<StackPanel VerticalAlignment="Center"
|
<StackPanel VerticalAlignment="Center"
|
||||||
Spacing="2">
|
Spacing="2">
|
||||||
<TextBlock x:Name="GithubLabel"
|
<TextBlock x:Name="GithubLabel"
|
||||||
Text="GitHub"
|
Text="{Binding [About.Github.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
FontSize="13" />
|
FontSize="13" />
|
||||||
<TextBlock x:Name="GithubDesc"
|
<TextBlock x:Name="GithubDesc"
|
||||||
Text="Source code, issues and project development."
|
Text="{Binding [About.Github.Desc], Source={x:Static local:Localization.Instance}}"
|
||||||
FontSize="11"
|
FontSize="11"
|
||||||
Foreground="{StaticResource MutedBrush}"
|
Foreground="{StaticResource MutedBrush}"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
@@ -361,7 +446,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
x:Name="GithubButton"
|
x:Name="GithubButton"
|
||||||
Classes="ghost"
|
Classes="ghost"
|
||||||
Content="Open"
|
Content="{Binding [About.GithubButton], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -378,10 +463,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<StackPanel VerticalAlignment="Center"
|
<StackPanel VerticalAlignment="Center"
|
||||||
Spacing="2">
|
Spacing="2">
|
||||||
<TextBlock x:Name="DiscordServerLabel"
|
<TextBlock x:Name="DiscordServerLabel"
|
||||||
Text="Discord"
|
Text="{Binding [About.Discord.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
FontSize="13" />
|
FontSize="13" />
|
||||||
<TextBlock x:Name="DiscordServerDesc"
|
<TextBlock x:Name="DiscordServerDesc"
|
||||||
Text="Join the community, get support and follow development."
|
Text="{Binding [About.Discord.Desc], Source={x:Static local:Localization.Instance}}"
|
||||||
FontSize="11"
|
FontSize="11"
|
||||||
Foreground="{StaticResource MutedBrush}"
|
Foreground="{StaticResource MutedBrush}"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
@@ -391,7 +476,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
x:Name="DiscordButton"
|
x:Name="DiscordButton"
|
||||||
Classes="ghost"
|
Classes="ghost"
|
||||||
Content="Join"
|
Content="{Binding [About.DiscordButton], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -399,130 +484,165 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem x:Name="GraphicsTabItem" Header="Graphics" FontSize="15">
|
<TabItem x:Name="GraphicsTabItem"
|
||||||
|
Header="{Binding [Options.Graphics], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontSize="15">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||||
<Border Classes="card">
|
<Border Classes="card">
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<TextBlock x:Name="RenderingSectionTitle" Classes="sectionTitle" Text="RENDERING" />
|
<TextBlock x:Name="RenderingSectionTitle" Classes="sectionTitle"
|
||||||
|
Text="{Binding [Options.Section.Rendering], Source={x:Static local:Localization.Instance}}" />
|
||||||
|
|
||||||
<local:SettingRow x:Name="RenderResolutionRow" Label="Internal resolution"
|
<local:SettingRow x:Name="RenderResolutionRow"
|
||||||
Description="Render offscreen targets below native resolution and upscale on present. Lower values trade image quality for GPU headroom; takes effect on next launch.">
|
Label="{Binding [Options.RenderResolution.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ComboBox x:Name="RenderResolutionBox" Width="160" SelectedIndex="0"
|
Description="{Binding [Options.RenderResolution.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
VerticalAlignment="Center" CornerRadius="8">
|
<ComboBox x:Name="RenderResolutionBox" Width="160"
|
||||||
<ComboBoxItem x:Name="RenderResolution100Item" Content="100% (native)" Tag="1.0" />
|
ItemTemplate="{StaticResource LocalizedChoiceTemplate}"
|
||||||
<ComboBoxItem x:Name="RenderResolution75Item" Content="75%" Tag="0.75" />
|
HorizontalContentAlignment="Left"
|
||||||
<ComboBoxItem x:Name="RenderResolution50Item" Content="50%" Tag="0.5" />
|
VerticalAlignment="Center" CornerRadius="8" />
|
||||||
<ComboBoxItem x:Name="RenderResolution25Item" Content="25%" Tag="0.25" />
|
|
||||||
</ComboBox>
|
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Classes="card">
|
<Border Classes="card">
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<TextBlock x:Name="DisplaySectionTitle" Classes="sectionTitle" Text="DISPLAY" />
|
<TextBlock x:Name="DisplaySectionTitle" Classes="sectionTitle"
|
||||||
<local:SettingRow x:Name="WindowModeRow" Label="Window mode" Description="Regular window, desktop borderless, or exclusive fullscreen.">
|
Text="{Binding [Options.Section.Display], Source={x:Static local:Localization.Instance}}" />
|
||||||
<ComboBox x:Name="WindowModeBox" Width="180" SelectedIndex="0" CornerRadius="8">
|
<local:SettingRow x:Name="WindowModeRow"
|
||||||
<ComboBoxItem Content="Windowed" />
|
Label="{Binding [Options.WindowMode.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ComboBoxItem Content="Borderless" />
|
Description="{Binding [Options.WindowMode.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ComboBoxItem Content="Exclusive" />
|
<ComboBox x:Name="WindowModeBox" Width="180"
|
||||||
</ComboBox>
|
ItemTemplate="{StaticResource LocalizedChoiceTemplate}"
|
||||||
|
HorizontalContentAlignment="Left"
|
||||||
|
CornerRadius="8" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
<local:SettingRow x:Name="ResolutionRow" Label="Resolution" Description="Initial window size or exclusive fullscreen resolution.">
|
<local:SettingRow x:Name="ResolutionRow"
|
||||||
|
Label="{Binding [Options.Resolution.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
|
Description="{Binding [Options.Resolution.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ComboBox x:Name="ResolutionBox" Width="180" CornerRadius="8" />
|
<ComboBox x:Name="ResolutionBox" Width="180" CornerRadius="8" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
<local:SettingRow x:Name="DisplayRow" Label="Display" Description="Monitor used for centering and fullscreen.">
|
<local:SettingRow x:Name="DisplayRow"
|
||||||
|
Label="{Binding [Options.Display.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
|
Description="{Binding [Options.Display.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ComboBox x:Name="DisplayBox" Width="260" CornerRadius="8" />
|
<ComboBox x:Name="DisplayBox" Width="260" CornerRadius="8" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
<local:SettingRow x:Name="RefreshRateRow" Label="Refresh rate" Description="Exclusive fullscreen refresh rate. Automatic selects the closest mode.">
|
<local:SettingRow x:Name="RefreshRateRow"
|
||||||
|
Label="{Binding [Options.RefreshRate.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
|
Description="{Binding [Options.RefreshRate.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ComboBox x:Name="RefreshRateBox" Width="180" CornerRadius="8" />
|
<ComboBox x:Name="RefreshRateBox" Width="180" CornerRadius="8" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
<local:SettingRow x:Name="ScalingRow" Label="Scaling" Description="Scale the native guest image without changing its internal resolution.">
|
<local:SettingRow x:Name="ScalingRow"
|
||||||
<ComboBox x:Name="ScalingModeBox" Width="180" SelectedIndex="0" CornerRadius="8">
|
Label="{Binding [Options.Scaling.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ComboBoxItem Content="Fit" />
|
Description="{Binding [Options.Scaling.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ComboBoxItem Content="Cover" />
|
<ComboBox x:Name="ScalingModeBox" Width="180"
|
||||||
<ComboBoxItem Content="Stretch" />
|
ItemTemplate="{StaticResource LocalizedChoiceTemplate}"
|
||||||
<ComboBoxItem Content="Integer" />
|
HorizontalContentAlignment="Left"
|
||||||
</ComboBox>
|
CornerRadius="8" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
<local:SettingRow x:Name="VSyncRow" Label="VSync" Description="Use FIFO presentation for tear-free output.">
|
<local:SettingRow x:Name="VSyncRow"
|
||||||
<ToggleSwitch x:Name="VSyncToggle" IsChecked="True" OnContent="On" OffContent="Off" />
|
Label="{Binding [Options.VSync.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
|
Description="{Binding [Options.VSync.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
|
<ToggleSwitch x:Name="VSyncToggle" IsChecked="True"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
<local:SettingRow x:Name="HdrRow" Label="HDR" Description="Use HDR output when the selected display and graphics backend support it.">
|
<local:SettingRow x:Name="HdrRow"
|
||||||
<ComboBox x:Name="HdrModeBox" Width="180" SelectedIndex="0" CornerRadius="8">
|
Label="{Binding [Options.Hdr.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
<ComboBoxItem Content="Auto" />
|
Description="{Binding [Options.Hdr.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ComboBoxItem Content="On" />
|
<ComboBox x:Name="HdrModeBox" Width="180"
|
||||||
<ComboBoxItem Content="Off" />
|
ItemTemplate="{StaticResource LocalizedChoiceTemplate}"
|
||||||
</ComboBox>
|
HorizontalContentAlignment="Left"
|
||||||
|
CornerRadius="8" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
<TabItem x:Name="EnvTabItem"
|
||||||
|
Header="{Binding [Options.Env.Tab], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontSize="15">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||||
|
|
||||||
<Border Classes="card">
|
<Border Classes="card">
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<TextBlock x:Name="EnvSectionTitle" Classes="sectionTitle" Text="ENVIRONMENT VARIABLES" />
|
<TextBlock x:Name="EnvSectionTitle" Classes="sectionTitle"
|
||||||
|
Text="{Binding [Options.Section.Environment], Source={x:Static local:Localization.Instance}}" />
|
||||||
<TextBlock x:Name="EnvDesc"
|
<TextBlock x:Name="EnvDesc"
|
||||||
Text="Switches passed to the emulator as environment variables at launch."
|
Text="{Binding [Options.Env.Desc], Source={x:Static local:Localization.Instance}}"
|
||||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
|
||||||
<local:SettingRow x:Name="EnvBthidRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_BTHID_UNAVAILABLE"
|
<local:SettingRow x:Name="EnvBthidRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_BTHID_UNAVAILABLE"
|
||||||
Description="Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever. Leave off normally. Some titles freeze when init fails.">
|
Description="{Binding [Options.Env.Bthid.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ToggleSwitch x:Name="EnvBthidToggle" OnContent="On" OffContent="Off"
|
<ToggleSwitch x:Name="EnvBthidToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="EnvLoopGuardRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"
|
<local:SettingRow x:Name="EnvLoopGuardRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"
|
||||||
Description="Do not force quit titles that repeat the same call for too long. Try this when a game exits on its own while loading.">
|
Description="{Binding [Options.Env.LoopGuard.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ToggleSwitch x:Name="EnvLoopGuardToggle" OnContent="On" OffContent="Off"
|
<ToggleSwitch x:Name="EnvLoopGuardToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="EnvWritableApp0Row" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_WRITABLE_APP0"
|
<local:SettingRow x:Name="EnvWritableApp0Row" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_WRITABLE_APP0"
|
||||||
Description="Allow titles to create and write files inside their install folder. Needed by unpackaged dumps that write their save or config data under /app0.">
|
Description="{Binding [Options.Env.WritableApp0.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ToggleSwitch x:Name="EnvWritableApp0Toggle" OnContent="On" OffContent="Off"
|
<ToggleSwitch x:Name="EnvWritableApp0Toggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="EnvVkValidationRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_VK_VALIDATION"
|
<local:SettingRow x:Name="EnvVkValidationRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_VK_VALIDATION"
|
||||||
Description="Enable Vulkan validation layers for GPU debugging. Slow. Requires the Vulkan SDK to be installed.">
|
Description="{Binding [Options.Env.VkValidation.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ToggleSwitch x:Name="EnvVkValidationToggle" OnContent="On" OffContent="Off"
|
<ToggleSwitch x:Name="EnvVkValidationToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="EnvDumpSpirvRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_DUMP_SPIRV"
|
<local:SettingRow x:Name="EnvDumpSpirvRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_DUMP_SPIRV"
|
||||||
Description="Dump AGC shaders and their SPIR-V translations to the shader-dumps folder. Use when reporting shader or rendering bugs.">
|
Description="{Binding [Options.Env.DumpSpirv.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ToggleSwitch x:Name="EnvDumpSpirvToggle" OnContent="On" OffContent="Off"
|
<ToggleSwitch x:Name="EnvDumpSpirvToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="EnvLogDirectMemoryRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_LOG_DIRECT_MEMORY"
|
<local:SettingRow x:Name="EnvLogDirectMemoryRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_LOG_DIRECT_MEMORY"
|
||||||
Description="Log direct memory allocations and failures to the console. Use when a game aborts or exits during boot.">
|
Description="{Binding [Options.Env.LogDirectMemory.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ToggleSwitch x:Name="EnvLogDirectMemoryToggle" OnContent="On" OffContent="Off"
|
<ToggleSwitch x:Name="EnvLogDirectMemoryToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="EnvLogIoRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_LOG_IO"
|
<local:SettingRow x:Name="EnvLogIoRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_LOG_IO"
|
||||||
Description="Log file open, read, and path-resolve activity to the console. Use when a game cannot find its data files during boot.">
|
Description="{Binding [Options.Env.LogIo.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ToggleSwitch x:Name="EnvLogIoToggle" OnContent="On" OffContent="Off"
|
<ToggleSwitch x:Name="EnvLogIoToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="EnvLogNpRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_LOG_NP"
|
<local:SettingRow x:Name="EnvLogNpRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_LOG_NP"
|
||||||
Description="Log NP (PlayStation Network) library calls to the console.">
|
Description="{Binding [Options.Env.LogNp.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ToggleSwitch x:Name="EnvLogNpToggle" OnContent="On" OffContent="Off"
|
<ToggleSwitch x:Name="EnvLogNpToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="EnvGuestImageCpuSyncRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_GUEST_IMAGE_CPU_SYNC"
|
<local:SettingRow x:Name="EnvGuestImageCpuSyncRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_GUEST_IMAGE_CPU_SYNC"
|
||||||
Description="Re-upload guest surfaces the game's own CPU code rewrites. Enabled by default for compatibility. Disable only for titles that regress with it, such as GTA V.">
|
Description="{Binding [Options.Env.GuestImageCpuSync.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
<ToggleSwitch x:Name="EnvGuestImageCpuSyncToggle" OnContent="On" OffContent="Off"
|
<ToggleSwitch x:Name="EnvGuestImageCpuSyncToggle"
|
||||||
|
OnContent="{Binding [Common.On], Source={x:Static local:Localization.Instance}}"
|
||||||
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -540,16 +660,27 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Margin="0,12,0,0" IsVisible="False">
|
Margin="0,12,0,0" IsVisible="False">
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*">
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||||
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle"
|
||||||
|
Text="{Binding [Console.Title], Source={x:Static local:Localization.Instance}}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
|
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
|
||||||
PlaceholderText="Search..." Width="320" />
|
PlaceholderText="{Binding [Console.SearchWatermark], Source={x:Static local:Localization.Instance}}"
|
||||||
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
Width="320" />
|
||||||
|
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck"
|
||||||
|
Content="{Binding [Console.AutoScroll], Source={x:Static local:Localization.Instance}}"
|
||||||
|
IsChecked="True"
|
||||||
FontSize="12" Margin="0,0,12,0" />
|
FontSize="12" Margin="0,0,12,0" />
|
||||||
<Button Grid.Column="3" x:Name="DetachConsoleButton" Classes="ghost" Content="Split" FontSize="12"
|
<Button Grid.Column="3" x:Name="DetachConsoleButton" Classes="ghost"
|
||||||
|
Content="{Binding [Console.Split], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontSize="12"
|
||||||
Padding="10,4" Margin="0,0,8,0" />
|
Padding="10,4" Margin="0,0,8,0" />
|
||||||
<Button Grid.Column="4" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
<Button Grid.Column="4" x:Name="CopyLogButton" Classes="ghost"
|
||||||
|
Content="{Binding [Console.Copy], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontSize="12"
|
||||||
Padding="10,4" Margin="0,0,8,0" />
|
Padding="10,4" Margin="0,0,8,0" />
|
||||||
<Button Grid.Column="5" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
<Button Grid.Column="5" x:Name="ClearLogButton" Classes="ghost"
|
||||||
|
Content="{Binding [Console.Clear], Source={x:Static local:Localization.Instance}}"
|
||||||
|
FontSize="12"
|
||||||
Padding="10,4" />
|
Padding="10,4" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
||||||
@@ -625,9 +756,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
|
<ToggleButton x:Name="ConsoleToggle" Classes="ghost"
|
||||||
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
Content="{Binding [Launch.Console], Source={x:Static local:Localization.Instance}}" />
|
||||||
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
<Button x:Name="LaunchButton" Classes="accent"
|
||||||
|
Content="{Binding [Launch.Launch], Source={x:Static local:Localization.Instance}}"
|
||||||
|
IsEnabled="False" />
|
||||||
|
<Button x:Name="StopButton" Classes="danger"
|
||||||
|
Content="{Binding [Launch.Stop], Source={x:Static local:Localization.Instance}}"
|
||||||
|
IsEnabled="False" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -663,5 +799,73 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<TextBlock x:Name="StatusBarRight" Grid.Column="1" Text="" FontSize="11"
|
<TextBlock x:Name="StatusBarRight" Grid.Column="1" Text="" FontSize="11"
|
||||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0" />
|
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Portable resize hit targets for the frameless desktop window. They
|
||||||
|
are disabled while maximized or fullscreen in code-behind. -->
|
||||||
|
<Panel x:Name="ResizeHandles"
|
||||||
|
Grid.RowSpan="3"
|
||||||
|
ZIndex="1000"
|
||||||
|
IsVisible="False">
|
||||||
|
<Border Tag="North"
|
||||||
|
Height="6"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="#01000000"
|
||||||
|
Cursor="TopSide"
|
||||||
|
PointerPressed="OnResizeHandlePointerPressed" />
|
||||||
|
<Border Tag="South"
|
||||||
|
Height="6"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Background="#01000000"
|
||||||
|
Cursor="BottomSide"
|
||||||
|
PointerPressed="OnResizeHandlePointerPressed" />
|
||||||
|
<Border Tag="West"
|
||||||
|
Width="6"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Background="#01000000"
|
||||||
|
Cursor="LeftSide"
|
||||||
|
PointerPressed="OnResizeHandlePointerPressed" />
|
||||||
|
<Border Tag="East"
|
||||||
|
Width="6"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Background="#01000000"
|
||||||
|
Cursor="RightSide"
|
||||||
|
PointerPressed="OnResizeHandlePointerPressed" />
|
||||||
|
<Border Tag="NorthWest"
|
||||||
|
Width="12"
|
||||||
|
Height="12"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="#01000000"
|
||||||
|
Cursor="TopLeftCorner"
|
||||||
|
PointerPressed="OnResizeHandlePointerPressed" />
|
||||||
|
<Border Tag="NorthEast"
|
||||||
|
Width="12"
|
||||||
|
Height="12"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="#01000000"
|
||||||
|
Cursor="TopRightCorner"
|
||||||
|
PointerPressed="OnResizeHandlePointerPressed" />
|
||||||
|
<Border Tag="SouthWest"
|
||||||
|
Width="12"
|
||||||
|
Height="12"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Background="#01000000"
|
||||||
|
Cursor="BottomLeftCorner"
|
||||||
|
PointerPressed="OnResizeHandlePointerPressed" />
|
||||||
|
<Border Tag="SouthEast"
|
||||||
|
Width="12"
|
||||||
|
Height="12"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Background="#01000000"
|
||||||
|
Cursor="BottomRightCorner"
|
||||||
|
PointerPressed="OnResizeHandlePointerPressed" />
|
||||||
|
</Panel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
using Avalonia.Automation;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
@@ -41,15 +42,48 @@ public partial class MainWindow : Window
|
|||||||
private static readonly IBrush WarningLineBrush = new SolidColorBrush(Color.Parse("#E8B341"));
|
private static readonly IBrush WarningLineBrush = new SolidColorBrush(Color.Parse("#E8B341"));
|
||||||
private static readonly IBrush ErrorLineBrush = new SolidColorBrush(Color.Parse("#F2777C"));
|
private static readonly IBrush ErrorLineBrush = new SolidColorBrush(Color.Parse("#F2777C"));
|
||||||
private static readonly IBrush SuccessLineBrush = new SolidColorBrush(Color.Parse("#63D489"));
|
private static readonly IBrush SuccessLineBrush = new SolidColorBrush(Color.Parse("#63D489"));
|
||||||
private static readonly StringComparer FilePathComparer = OperatingSystem.IsWindows()
|
private readonly LocalizedChoice[] _cpuEngineChoices =
|
||||||
? StringComparer.OrdinalIgnoreCase
|
[
|
||||||
: StringComparer.Ordinal;
|
LocalizedChoice.FromKey("Native", "Options.CpuEngine.Native"),
|
||||||
private static readonly StringComparison FilePathComparison = OperatingSystem.IsWindows()
|
];
|
||||||
? StringComparison.OrdinalIgnoreCase
|
private readonly LocalizedChoice[] _logLevelChoices =
|
||||||
: StringComparison.Ordinal;
|
[
|
||||||
|
LocalizedChoice.FromKey("Trace", "Options.LogLevel.Trace"),
|
||||||
|
LocalizedChoice.FromKey("Debug", "Options.LogLevel.Debug"),
|
||||||
|
LocalizedChoice.FromKey("Info", "Options.LogLevel.Info"),
|
||||||
|
LocalizedChoice.FromKey("Warning", "Options.LogLevel.Warning"),
|
||||||
|
LocalizedChoice.FromKey("Error", "Options.LogLevel.Error"),
|
||||||
|
LocalizedChoice.FromKey("Critical", "Options.LogLevel.Critical"),
|
||||||
|
];
|
||||||
|
private readonly LocalizedChoice[] _renderResolutionChoices =
|
||||||
|
[
|
||||||
|
LocalizedChoice.FromKey("1.0", "Options.RenderResolution.Native"),
|
||||||
|
LocalizedChoice.Literal("0.75", "75%"),
|
||||||
|
LocalizedChoice.Literal("0.5", "50%"),
|
||||||
|
LocalizedChoice.Literal("0.25", "25%"),
|
||||||
|
];
|
||||||
|
private readonly LocalizedChoice[] _windowModeChoices =
|
||||||
|
[
|
||||||
|
LocalizedChoice.FromKey("Windowed", "Options.WindowMode.Windowed"),
|
||||||
|
LocalizedChoice.FromKey("Borderless", "Options.WindowMode.Borderless"),
|
||||||
|
LocalizedChoice.FromKey("Exclusive", "Options.WindowMode.Exclusive"),
|
||||||
|
];
|
||||||
|
private readonly LocalizedChoice[] _scalingModeChoices =
|
||||||
|
[
|
||||||
|
LocalizedChoice.FromKey("Fit", "Options.Scaling.Fit"),
|
||||||
|
LocalizedChoice.FromKey("Cover", "Options.Scaling.Cover"),
|
||||||
|
LocalizedChoice.FromKey("Stretch", "Options.Scaling.Stretch"),
|
||||||
|
LocalizedChoice.FromKey("Integer", "Options.Scaling.Integer"),
|
||||||
|
];
|
||||||
|
private readonly LocalizedChoice[] _hdrModeChoices =
|
||||||
|
[
|
||||||
|
LocalizedChoice.FromKey("Auto", "Options.Hdr.Auto"),
|
||||||
|
LocalizedChoice.FromKey("On", "Common.On"),
|
||||||
|
LocalizedChoice.FromKey("Off", "Common.Off"),
|
||||||
|
];
|
||||||
private readonly List<GameEntry> _allGames = new();
|
private readonly List<GameEntry> _allGames = new();
|
||||||
private readonly ObservableCollection<GameEntry> _visibleGames = new();
|
private readonly ObservableCollection<GameEntry> _visibleGames = new();
|
||||||
|
private readonly GameLibraryWatcher _libraryWatcher = new();
|
||||||
private readonly AvaloniaList<LogLine> _consoleLines = new();
|
private readonly AvaloniaList<LogLine> _consoleLines = new();
|
||||||
private readonly List<LogLine> _allConsoleLines = new();
|
private readonly List<LogLine> _allConsoleLines = new();
|
||||||
private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new();
|
private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new();
|
||||||
@@ -85,8 +119,10 @@ public partial class MainWindow : Window
|
|||||||
private string? _runningGameName;
|
private string? _runningGameName;
|
||||||
private string? _runningGameTitleId;
|
private string? _runningGameTitleId;
|
||||||
private long _runningSinceUnixSeconds;
|
private long _runningSinceUnixSeconds;
|
||||||
|
private int _libraryScanGeneration;
|
||||||
private int _detailLoadGeneration;
|
private int _detailLoadGeneration;
|
||||||
private int _backdropGeneration;
|
private int _backdropGeneration;
|
||||||
|
private bool _isClosing;
|
||||||
|
|
||||||
// Bundled key art shown whenever no game-specific backdrop applies; the
|
// Bundled key art shown whenever no game-specific backdrop applies; the
|
||||||
// plain window color remains the fallback when the asset fails to load.
|
// plain window color remains the fallback when the asset fails to load.
|
||||||
@@ -119,6 +155,7 @@ public partial class MainWindow : Window
|
|||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
InitializeLocalizedChoiceBoxes();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -133,6 +170,7 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
GameList.ItemsSource = _visibleGames;
|
GameList.ItemsSource = _visibleGames;
|
||||||
|
_libraryWatcher.RefreshRequested += OnLibraryRefreshRequested;
|
||||||
ConsoleList.ItemsSource = _consoleLines;
|
ConsoleList.ItemsSource = _consoleLines;
|
||||||
_consoleMirror = GuiConsoleMirror.Install((line, isError) =>
|
_consoleMirror = GuiConsoleMirror.Install((line, isError) =>
|
||||||
_pendingLines.Enqueue((line, isError)));
|
_pendingLines.Enqueue((line, isError)));
|
||||||
@@ -167,13 +205,28 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
|
|
||||||
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
||||||
|
TitleBar.DoubleTapped += OnTitleBarDoubleTapped;
|
||||||
|
MinimizeButton.Click += (_, _) => WindowState = WindowState.Minimized;
|
||||||
|
MaximizeButton.Click += (_, _) => ToggleMaximized();
|
||||||
|
CloseButton.Click += (_, _) => Close();
|
||||||
|
Opened += (_, _) =>
|
||||||
|
{
|
||||||
|
// Some compositors ignore the initial maximized state while a
|
||||||
|
// frameless native window is still being created.
|
||||||
|
if (WindowState == WindowState.Normal)
|
||||||
|
{
|
||||||
|
WindowState = WindowState.Maximized;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateWindowChromeState();
|
||||||
|
};
|
||||||
|
UpdateWindowChromeState();
|
||||||
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
||||||
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
||||||
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
||||||
ConsoleSearchBox.TextChanged += (_, _) => RefreshVisibleConsoleLines();
|
ConsoleSearchBox.TextChanged += (_, _) => RefreshVisibleConsoleLines();
|
||||||
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||||
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||||
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
|
|
||||||
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
||||||
LaunchButton.Click += (_, _) => LaunchSelected();
|
LaunchButton.Click += (_, _) => LaunchSelected();
|
||||||
ClearLogButton.Click += (_, _) => { _consoleLines.Clear(); _allConsoleLines.Clear(); };
|
ClearLogButton.Click += (_, _) => { _consoleLines.Clear(); _allConsoleLines.Clear(); };
|
||||||
@@ -190,9 +243,9 @@ public partial class MainWindow : Window
|
|||||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||||
RenderResolutionBox.SelectionChanged += (_, _) =>
|
RenderResolutionBox.SelectionChanged += (_, _) =>
|
||||||
{
|
{
|
||||||
if (RenderResolutionBox.SelectedItem is ComboBoxItem { Tag: string tag } &&
|
if (RenderResolutionBox.SelectedItem is LocalizedChoice { Value: var value } &&
|
||||||
double.TryParse(
|
double.TryParse(
|
||||||
tag,
|
value,
|
||||||
System.Globalization.NumberStyles.Float,
|
System.Globalization.NumberStyles.Float,
|
||||||
System.Globalization.CultureInfo.InvariantCulture,
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
out var scale))
|
out var scale))
|
||||||
@@ -606,135 +659,15 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Re-applies every UI string from the current language, so switching
|
/// Recomputes localized strings that also depend on runtime state.
|
||||||
/// languages in Options takes effect immediately without reopening the
|
/// Static labels update automatically through XAML bindings.
|
||||||
/// window.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ApplyLocalization()
|
private void ApplyLocalization()
|
||||||
{
|
{
|
||||||
var loc = Localization.Instance;
|
RefreshLocalizedChoices();
|
||||||
|
|
||||||
LibraryTabButton.Content = loc.Get("Page.Library");
|
|
||||||
OptionsTabButton.Content = loc.Get("Page.Options");
|
|
||||||
|
|
||||||
SearchBox.PlaceholderText = loc.Get("Library.SearchWatermark");
|
|
||||||
AddFolderButton.Content = loc.Get("Library.AddFolder");
|
|
||||||
RescanButton.Content = loc.Get("Library.Rescan");
|
|
||||||
OpenFileButton.Content = loc.Get("Library.OpenFile");
|
|
||||||
|
|
||||||
CtxLaunch.Header = loc.Get("Library.Context.Launch");
|
|
||||||
CtxOpenFolder.Header = loc.Get("Library.Context.OpenFolder");
|
|
||||||
CtxCopyPath.Header = loc.Get("Library.Context.CopyPath");
|
|
||||||
CtxCopyTitleId.Header = loc.Get("Library.Context.CopyTitleId");
|
|
||||||
CtxGameSettings.Header = loc.Get("Library.Context.GameSettings");
|
|
||||||
CtxRemove.Header = loc.Get("Library.Context.Remove");
|
|
||||||
|
|
||||||
EmptyAddFolderButton.Content = loc.Get("Library.Empty.AddFolder");
|
|
||||||
LoadingStateText.Text = loc.Get("Library.Loading");
|
|
||||||
|
|
||||||
GeneralTabItem.Header = loc.Get("Options.General");
|
|
||||||
EnvTabItem.Header = loc.Get("Options.Env.Tab");
|
|
||||||
EnvSectionTitle.Text = loc.Get("Options.Section.Environment");
|
|
||||||
EnvDesc.Text = loc.Get("Options.Env.Desc");
|
|
||||||
EnvBthidRow.Description = loc.Get("Options.Env.Bthid.Desc");
|
|
||||||
EnvLoopGuardRow.Description = loc.Get("Options.Env.LoopGuard.Desc");
|
|
||||||
EnvWritableApp0Row.Description = loc.Get("Options.Env.WritableApp0.Desc");
|
|
||||||
EnvVkValidationRow.Description = loc.Get("Options.Env.VkValidation.Desc");
|
|
||||||
EnvDumpSpirvRow.Description = loc.Get("Options.Env.DumpSpirv.Desc");
|
|
||||||
EnvLogDirectMemoryRow.Description = loc.Get("Options.Env.LogDirectMemory.Desc");
|
|
||||||
EnvLogIoRow.Description = loc.Get("Options.Env.LogIo.Desc");
|
|
||||||
EnvLogNpRow.Description = loc.Get("Options.Env.LogNp.Desc");
|
|
||||||
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
|
||||||
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
|
||||||
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
|
|
||||||
|
|
||||||
CpuEngineRow.Label = loc.Get("Options.CpuEngine.Label");
|
|
||||||
CpuEngineRow.Description = loc.Get("Options.CpuEngine.Desc");
|
|
||||||
CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native");
|
|
||||||
|
|
||||||
StrictRow.Label = loc.Get("Options.Strict.Label");
|
|
||||||
StrictRow.Description = loc.Get("Options.Strict.Desc");
|
|
||||||
|
|
||||||
LogLevelRow.Label = loc.Get("Options.LogLevel.Label");
|
|
||||||
LogLevelRow.Description = loc.Get("Options.LogLevel.Desc");
|
|
||||||
LogLevelTraceItem.Content = loc.Get("Options.LogLevel.Trace");
|
|
||||||
LogLevelDebugItem.Content = loc.Get("Options.LogLevel.Debug");
|
|
||||||
LogLevelInfoItem.Content = loc.Get("Options.LogLevel.Info");
|
|
||||||
LogLevelWarningItem.Content = loc.Get("Options.LogLevel.Warning");
|
|
||||||
LogLevelErrorItem.Content = loc.Get("Options.LogLevel.Error");
|
|
||||||
LogLevelCriticalItem.Content = loc.Get("Options.LogLevel.Critical");
|
|
||||||
|
|
||||||
TraceImportsRow.Label = loc.Get("Options.TraceImports.Label");
|
|
||||||
TraceImportsRow.Description = loc.Get("Options.TraceImports.Desc");
|
|
||||||
|
|
||||||
LogToFileRow.Label = loc.Get("Options.LogToFile.Label");
|
|
||||||
LogToFileRow.Description = loc.Get("Options.LogToFile.Desc");
|
|
||||||
|
|
||||||
LogFilePathRow.Label = loc.Get("Options.LogFilePath.Label");
|
|
||||||
SelectLogFilePathButton.Content = loc.Get("Options.LogFilePath.Select");
|
|
||||||
UpdateLogFilePathText();
|
UpdateLogFilePathText();
|
||||||
|
|
||||||
OverrideLogFileRow.Label = loc.Get("Options.OverrideLogFile.Label");
|
|
||||||
OverrideLogFileRow.Description = loc.Get("Options.OverrideLogFile.Desc");
|
|
||||||
|
|
||||||
LanguageRow.Label = loc.Get("Options.Language.Label");
|
|
||||||
LanguageRow.Description = loc.Get("Options.Language.Desc");
|
|
||||||
|
|
||||||
TitleMusicRow.Label = loc.Get("Options.TitleMusic.Label");
|
|
||||||
TitleMusicRow.Description = loc.Get("Options.TitleMusic.Desc");
|
|
||||||
|
|
||||||
DiscordRow.Label = loc.Get("Options.Discord.Label");
|
|
||||||
DiscordRow.Description = loc.Get("Options.Discord.Desc");
|
|
||||||
AutoUpdateRow.Label = loc.Get("Updater.Auto.Label");
|
|
||||||
AutoUpdateRow.Description = loc.Get("Updater.Auto.Desc");
|
|
||||||
|
|
||||||
GraphicsTabItem.Header = loc.Get("Options.Graphics");
|
|
||||||
DisplaySectionTitle.Text = loc.Get("Options.Section.Display");
|
|
||||||
WindowModeRow.Label = loc.Get("Options.WindowMode.Label");
|
|
||||||
WindowModeRow.Description = loc.Get("Options.WindowMode.Desc");
|
|
||||||
ResolutionRow.Label = loc.Get("Options.Resolution.Label");
|
|
||||||
ResolutionRow.Description = loc.Get("Options.Resolution.Desc");
|
|
||||||
DisplayRow.Label = loc.Get("Options.Display.Label");
|
|
||||||
DisplayRow.Description = loc.Get("Options.Display.Desc");
|
|
||||||
RefreshRateRow.Label = loc.Get("Options.RefreshRate.Label");
|
|
||||||
RefreshRateRow.Description = loc.Get("Options.RefreshRate.Desc");
|
|
||||||
ScalingRow.Label = loc.Get("Options.Scaling.Label");
|
|
||||||
ScalingRow.Description = loc.Get("Options.Scaling.Desc");
|
|
||||||
VSyncRow.Label = loc.Get("Options.VSync.Label");
|
|
||||||
VSyncRow.Description = loc.Get("Options.VSync.Desc");
|
|
||||||
HdrRow.Label = loc.Get("Options.Hdr.Label");
|
|
||||||
HdrRow.Description = loc.Get("Options.Hdr.Desc");
|
|
||||||
RefreshHostRefreshRates(_settings.RefreshRate);
|
RefreshHostRefreshRates(_settings.RefreshRate);
|
||||||
|
|
||||||
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle, AutoUpdateToggle, VSyncToggle })
|
|
||||||
{
|
|
||||||
toggle.OnContent = loc.Get("Common.On");
|
|
||||||
toggle.OffContent = loc.Get("Common.Off");
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleSectionTitle.Text = loc.Get("Console.Title");
|
|
||||||
ConsoleSearchBox.PlaceholderText = loc.Get("Console.SearchWatermark");
|
|
||||||
AutoScrollCheck.Content = loc.Get("Console.AutoScroll");
|
|
||||||
DetachConsoleButton.Content = loc.Get("Console.Split");
|
|
||||||
CopyLogButton.Content = loc.Get("Console.Copy");
|
|
||||||
ClearLogButton.Content = loc.Get("Console.Clear");
|
|
||||||
|
|
||||||
ConsoleToggle.Content = loc.Get("Launch.Console");
|
|
||||||
LaunchButton.Content = loc.Get("Launch.Launch");
|
|
||||||
StopButton.Content = loc.Get("Launch.Stop");
|
|
||||||
|
|
||||||
AboutSectionTitle.Text = loc.Get("Options.About");
|
|
||||||
GithubLabel.Text = loc.Get("About.Github.Label");
|
|
||||||
GithubDesc.Text = loc.Get("About.Github.Desc");
|
|
||||||
DiscordServerLabel.Text = loc.Get("About.Discord.Label");
|
|
||||||
DiscordServerDesc.Text = loc.Get("About.Discord.Desc");
|
|
||||||
GithubButton.Content = loc.Get("About.GithubButton");
|
|
||||||
DiscordButton.Content = loc.Get("About.DiscordButton");
|
|
||||||
UpdateLabel.Text = loc.Get("Updater.Label");
|
|
||||||
LatestCommitLabel.Text = loc.Get("About.Github.LatestCommitLabel");
|
|
||||||
LatestCommitDescription.Text = loc.Get("About.Github.LatestCommitDescription");
|
|
||||||
RefreshUpdateText();
|
RefreshUpdateText();
|
||||||
|
|
||||||
UpdateEmptyStateTexts();
|
UpdateEmptyStateTexts();
|
||||||
UpdateSelectedGameTexts();
|
UpdateSelectedGameTexts();
|
||||||
}
|
}
|
||||||
@@ -798,6 +731,10 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void OnWindowClosing()
|
private void OnWindowClosing()
|
||||||
{
|
{
|
||||||
|
_isClosing = true;
|
||||||
|
Interlocked.Increment(ref _libraryScanGeneration);
|
||||||
|
Interlocked.Increment(ref _detailLoadGeneration);
|
||||||
|
_libraryWatcher.Dispose();
|
||||||
_settings.Save();
|
_settings.Save();
|
||||||
_consoleFlushTimer.Stop();
|
_consoleFlushTimer.Stop();
|
||||||
_libraryBlurTimer.Stop();
|
_libraryBlurTimer.Stop();
|
||||||
@@ -813,16 +750,114 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
|
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
if (e.Source is Visual source &&
|
||||||
|
source.FindAncestorOfType<Button>(includeSelf: true) is null &&
|
||||||
|
e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
{
|
{
|
||||||
BeginMoveDrag(e);
|
BeginMoveDrag(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnTitleBarDoubleTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Source is Visual source &&
|
||||||
|
source.FindAncestorOfType<Button>(includeSelf: true) is null)
|
||||||
|
{
|
||||||
|
ToggleMaximized();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleMaximized()
|
||||||
|
{
|
||||||
|
WindowState = WindowState == WindowState.Maximized
|
||||||
|
? WindowState.Normal
|
||||||
|
: WindowState.Maximized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMaximizeButton()
|
||||||
|
{
|
||||||
|
if (MaximizeGlyph is not { } glyph || MaximizeButton is not { } button)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isMaximized = WindowState == WindowState.Maximized;
|
||||||
|
glyph.Text = isMaximized ? "❐" : "□";
|
||||||
|
ToolTip.SetTip(button, isMaximized ? "Restore" : "Maximize");
|
||||||
|
AutomationProperties.SetName(
|
||||||
|
button,
|
||||||
|
isMaximized ? "Restore window" : "Maximize window");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateWindowChromeState()
|
||||||
|
{
|
||||||
|
UpdateMaximizeButton();
|
||||||
|
var isFullscreen = WindowState == WindowState.FullScreen;
|
||||||
|
|
||||||
|
if (TitleBar is { } titleBar)
|
||||||
|
{
|
||||||
|
titleBar.IsVisible = !isFullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StatusBar is { } statusBar)
|
||||||
|
{
|
||||||
|
statusBar.IsVisible = !isFullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ResizeHandles is { } handles)
|
||||||
|
{
|
||||||
|
handles.IsVisible = CanResize && WindowState == WindowState.Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResizeHandlePointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (WindowState != WindowState.Normal ||
|
||||||
|
!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed ||
|
||||||
|
sender is not Control { Tag: string edgeName } ||
|
||||||
|
!Enum.TryParse<WindowEdge>(edgeName, out var edge))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BeginResizeDrag(edge, e);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Settings ----
|
// ---- Settings ----
|
||||||
|
|
||||||
|
private void InitializeLocalizedChoiceBoxes()
|
||||||
|
{
|
||||||
|
CpuEngineBox.ItemsSource = _cpuEngineChoices;
|
||||||
|
LogLevelBox.ItemsSource = _logLevelChoices;
|
||||||
|
RenderResolutionBox.ItemsSource = _renderResolutionChoices;
|
||||||
|
WindowModeBox.ItemsSource = _windowModeChoices;
|
||||||
|
ScalingModeBox.ItemsSource = _scalingModeChoices;
|
||||||
|
HdrModeBox.ItemsSource = _hdrModeChoices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshLocalizedChoices()
|
||||||
|
{
|
||||||
|
RefreshChoices(_cpuEngineChoices);
|
||||||
|
RefreshChoices(_logLevelChoices);
|
||||||
|
RefreshChoices(_renderResolutionChoices);
|
||||||
|
RefreshChoices(_windowModeChoices);
|
||||||
|
RefreshChoices(_scalingModeChoices);
|
||||||
|
RefreshChoices(_hdrModeChoices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RefreshChoices(IEnumerable<LocalizedChoice> choices)
|
||||||
|
{
|
||||||
|
foreach (var choice in choices)
|
||||||
|
{
|
||||||
|
choice.Refresh(Localization.Instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplySettingsToControls()
|
private void ApplySettingsToControls()
|
||||||
{
|
{
|
||||||
|
CpuEngineBox.SelectedIndex = 0;
|
||||||
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
|
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"trace" => 0,
|
"trace" => 0,
|
||||||
@@ -868,7 +903,7 @@ public partial class MainWindow : Window
|
|||||||
private static string SelectedComboText(ComboBox comboBox, string fallback) =>
|
private static string SelectedComboText(ComboBox comboBox, string fallback) =>
|
||||||
comboBox.SelectedItem switch
|
comboBox.SelectedItem switch
|
||||||
{
|
{
|
||||||
ComboBoxItem item => item.Content?.ToString() ?? fallback,
|
LocalizedChoice { Value: var value } => value,
|
||||||
string value => value,
|
string value => value,
|
||||||
_ => fallback,
|
_ => fallback,
|
||||||
};
|
};
|
||||||
@@ -1176,7 +1211,7 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var changed = false;
|
var changed = false;
|
||||||
if (!_settings.GameFolders.Contains(path, FilePathComparer))
|
if (!_settings.GameFolders.Contains(path, GameLibraryPath.Comparer))
|
||||||
{
|
{
|
||||||
_settings.GameFolders.Add(path);
|
_settings.GameFolders.Add(path);
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -1186,7 +1221,7 @@ public partial class MainWindow : Window
|
|||||||
// games beneath it that were removed from the library earlier.
|
// games beneath it that were removed from the library earlier.
|
||||||
var prefix = Path.TrimEndingDirectorySeparator(path) + Path.DirectorySeparatorChar;
|
var prefix = Path.TrimEndingDirectorySeparator(path) + Path.DirectorySeparatorChar;
|
||||||
changed |= _settings.ExcludedGames.RemoveAll(excluded =>
|
changed |= _settings.ExcludedGames.RemoveAll(excluded =>
|
||||||
excluded.StartsWith(prefix, FilePathComparison)) > 0;
|
excluded.StartsWith(prefix, GameLibraryPath.Comparison)) > 0;
|
||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
{
|
{
|
||||||
@@ -1196,25 +1231,72 @@ public partial class MainWindow : Window
|
|||||||
await RescanLibraryAsync();
|
await RescanLibraryAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RescanLibraryAsync()
|
private void OnLibraryRefreshRequested(object? sender, EventArgs args)
|
||||||
{
|
{
|
||||||
|
Dispatcher.UIThread.Post(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (!_isClosing)
|
||||||
|
{
|
||||||
|
_ = RescanLibraryFromWatcherAsync();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RescanLibraryFromWatcherAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await RescanLibraryAsync(showProgress: false);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[GUI][WARN] Automatic library refresh failed: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RescanLibraryAsync(bool showProgress = true)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.VerifyAccess();
|
||||||
|
|
||||||
|
var scanGeneration = Interlocked.Increment(ref _libraryScanGeneration);
|
||||||
var folders = _settings.GameFolders.ToArray();
|
var folders = _settings.GameFolders.ToArray();
|
||||||
var excluded = new HashSet<string>(_settings.ExcludedGames, FilePathComparer);
|
var excluded = new HashSet<string>(_settings.ExcludedGames, GameLibraryPath.Comparer);
|
||||||
StatusBarRight.Text = Localization.Instance.Get("Status.ScanningLibrary");
|
_libraryWatcher.Watch(folders);
|
||||||
EmptyState.IsVisible = false;
|
var showLoadingState = showProgress && _allGames.Count == 0;
|
||||||
LoadingState.IsVisible = true;
|
if (showProgress)
|
||||||
|
{
|
||||||
|
StatusBarRight.Text = Localization.Instance.Get("Status.ScanningLibrary");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLoadingState)
|
||||||
|
{
|
||||||
|
EmptyState.IsVisible = false;
|
||||||
|
LoadingState.IsVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
var games = await Task.Run(() => ScanFolders(folders, excluded));
|
var games = await Task.Run(() => ScanFolders(folders, excluded));
|
||||||
|
if (_isClosing || scanGeneration != Volatile.Read(ref _libraryScanGeneration))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher.UIThread.VerifyAccess();
|
||||||
|
var reconciliation = GameLibraryReconciler.Reconcile(_allGames, games);
|
||||||
_allGames.Clear();
|
_allGames.Clear();
|
||||||
_allGames.AddRange(games);
|
_allGames.AddRange(reconciliation.Games);
|
||||||
RefreshVisibleGames();
|
RefreshVisibleGames(reconciliation.BackgroundsChanged);
|
||||||
LoadingState.IsVisible = false;
|
LoadingState.IsVisible = false;
|
||||||
LoadGameDetailsInBackground(games);
|
LoadGameDetailsInBackground(reconciliation.CoversToLoad, reconciliation.Games);
|
||||||
UpdateDiscordPresence();
|
UpdateDiscordPresence();
|
||||||
StatusBarRight.Text = folders.Length == 0
|
if (showProgress)
|
||||||
? Localization.Instance.Get("Status.AddFolderPrompt")
|
{
|
||||||
: Localization.Instance.Format("Status.LibraryScanned", games.Count, folders.Length);
|
StatusBarRight.Text = folders.Length == 0
|
||||||
|
? Localization.Instance.Get("Status.AddFolderPrompt")
|
||||||
|
: Localization.Instance.Format("Status.LibraryScanned", games.Count, folders.Length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1222,44 +1304,57 @@ public partial class MainWindow : Window
|
|||||||
/// game's install folder size — posting results back as they become
|
/// game's install folder size — posting results back as they become
|
||||||
/// ready. A newer scan invalidates older loads.
|
/// ready. A newer scan invalidates older loads.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void LoadGameDetailsInBackground(IReadOnlyList<GameEntry> games)
|
private void LoadGameDetailsInBackground(
|
||||||
|
IReadOnlyList<GameEntry> coversToLoad,
|
||||||
|
IReadOnlyList<GameEntry> gamesToMeasure)
|
||||||
{
|
{
|
||||||
var generation = ++_detailLoadGeneration;
|
var generation = ++_detailLoadGeneration;
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
// Covers first: they are cheap and the most visible, so the grid
|
// Covers first: they are cheap and the most visible, so the grid
|
||||||
// fills with art before the (potentially slow) size pass runs.
|
// fills with art before the (potentially slow) size pass runs.
|
||||||
foreach (var game in games)
|
foreach (var game in coversToLoad)
|
||||||
{
|
{
|
||||||
if (generation != _detailLoadGeneration)
|
if (generation != _detailLoadGeneration)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game.CoverPath is null)
|
var requestedCoverPath = game.CoverPath;
|
||||||
|
if (requestedCoverPath is null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = File.OpenRead(game.CoverPath);
|
using var stream = File.OpenRead(requestedCoverPath);
|
||||||
var bitmap = Bitmap.DecodeToWidth(stream, 312);
|
var bitmap = Bitmap.DecodeToWidth(stream, 312);
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
if (generation == _detailLoadGeneration)
|
if (generation == _detailLoadGeneration
|
||||||
|
&& _allGames.Contains(game)
|
||||||
|
&& string.Equals(
|
||||||
|
game.CoverPath,
|
||||||
|
requestedCoverPath,
|
||||||
|
StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
game.Cover = bitmap;
|
game.Cover = bitmap;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bitmap.Dispose();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
// A missing or undecodable image keeps the placeholder.
|
Console.Error.WriteLine(
|
||||||
|
$"[GUI][WARN] Could not load cover '{requestedCoverPath}': {exception.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var game in games)
|
foreach (var game in gamesToMeasure)
|
||||||
{
|
{
|
||||||
if (generation != _detailLoadGeneration)
|
if (generation != _detailLoadGeneration)
|
||||||
{
|
{
|
||||||
@@ -1271,7 +1366,7 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
if (generation == _detailLoadGeneration)
|
if (generation == _detailLoadGeneration && _allGames.Contains(game))
|
||||||
{
|
{
|
||||||
game.SizeBytes = size;
|
game.SizeBytes = size;
|
||||||
}
|
}
|
||||||
@@ -1306,9 +1401,10 @@ public partial class MainWindow : Window
|
|||||||
total += file.Length;
|
total += file.Length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
// Fall back to whatever was accumulated so far.
|
Console.Error.WriteLine(
|
||||||
|
$"[GUI][WARN] Could not measure game folder '{directory}': {exception.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return total;
|
return total;
|
||||||
@@ -1317,7 +1413,7 @@ public partial class MainWindow : Window
|
|||||||
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders, IReadOnlySet<string> excludedPaths)
|
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders, IReadOnlySet<string> excludedPaths)
|
||||||
{
|
{
|
||||||
var games = new List<GameEntry>();
|
var games = new List<GameEntry>();
|
||||||
var seen = new HashSet<string>(FilePathComparer);
|
var seen = new HashSet<string>(GameLibraryPath.Comparer);
|
||||||
var enumeration = new EnumerationOptions
|
var enumeration = new EnumerationOptions
|
||||||
{
|
{
|
||||||
IgnoreInaccessible = true,
|
IgnoreInaccessible = true,
|
||||||
@@ -1347,8 +1443,10 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
size = new FileInfo(fullPath).Length;
|
size = new FileInfo(fullPath).Length;
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException exception)
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[GUI][WARN] Could not inspect executable '{fullPath}': {exception.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var (title, titleId, version) = TryReadParamJson(fullPath);
|
var (title, titleId, version) = TryReadParamJson(fullPath);
|
||||||
@@ -1357,9 +1455,10 @@ public partial class MainWindow : Window
|
|||||||
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
|
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
// Skip folders that fail to enumerate.
|
Console.Error.WriteLine(
|
||||||
|
$"[GUI][WARN] Could not scan game folder '{folder}': {exception.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1600,24 +1699,26 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_settings.ExcludedGames.Contains(game.Path, FilePathComparer))
|
if (!_settings.ExcludedGames.Contains(game.Path, GameLibraryPath.Comparer))
|
||||||
{
|
{
|
||||||
_settings.ExcludedGames.Add(game.Path);
|
_settings.ExcludedGames.Add(game.Path);
|
||||||
_settings.Save();
|
_settings.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, FilePathComparison));
|
_allGames.RemoveAll(g =>
|
||||||
|
string.Equals(g.Path, game.Path, GameLibraryPath.Comparison));
|
||||||
GameList.SelectedItem = null;
|
GameList.SelectedItem = null;
|
||||||
RefreshVisibleGames();
|
RefreshVisibleGames();
|
||||||
StatusBarRight.Text = Localization.Instance.Format("Status.RemovedFromLibrary", game.Name);
|
StatusBarRight.Text = Localization.Instance.Format("Status.RemovedFromLibrary", game.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshVisibleGames()
|
private void RefreshVisibleGames(IReadOnlySet<GameEntry>? backgroundsChanged = null)
|
||||||
{
|
{
|
||||||
var query = SearchBox.Text?.Trim() ?? string.Empty;
|
var query = SearchBox.Text?.Trim() ?? string.Empty;
|
||||||
var selectedPath = (GameList.SelectedItem as GameEntry)?.Path;
|
var selectedBefore = GameList.SelectedItem as GameEntry;
|
||||||
|
var selectedPath = selectedBefore?.Path;
|
||||||
|
var desired = new List<GameEntry>(_allGames.Count);
|
||||||
|
|
||||||
_visibleGames.Clear();
|
|
||||||
foreach (var game in _allGames)
|
foreach (var game in _allGames)
|
||||||
{
|
{
|
||||||
if (query.Length == 0 ||
|
if (query.Length == 0 ||
|
||||||
@@ -1625,21 +1726,37 @@ public partial class MainWindow : Window
|
|||||||
game.Path.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
game.Path.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||||
(game.TitleId?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false))
|
(game.TitleId?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false))
|
||||||
{
|
{
|
||||||
_visibleGames.Add(game);
|
desired.Add(game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedPath is not null &&
|
GameLibraryReconciler.ReconcileVisibleGames(_visibleGames, desired);
|
||||||
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, FilePathComparison))
|
|
||||||
is { } reselected)
|
var selectedAfter = selectedPath is null
|
||||||
|
? null
|
||||||
|
: _visibleGames.FirstOrDefault(game =>
|
||||||
|
game.Path.Equals(selectedPath, GameLibraryPath.Comparison));
|
||||||
|
if (!ReferenceEquals(GameList.SelectedItem, selectedAfter))
|
||||||
{
|
{
|
||||||
GameList.SelectedItem = reselected;
|
GameList.SelectedItem = selectedAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
EmptyState.IsVisible = _visibleGames.Count == 0;
|
EmptyState.IsVisible = _visibleGames.Count == 0;
|
||||||
UpdateEmptyStateTexts();
|
UpdateEmptyStateTexts();
|
||||||
|
|
||||||
UpdateSelectedGame();
|
if (!ReferenceEquals(selectedBefore, selectedAfter))
|
||||||
|
{
|
||||||
|
UpdateSelectedGame();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateSelectedGameTexts();
|
||||||
|
UpdateRunButtons();
|
||||||
|
if (selectedAfter is not null && backgroundsChanged?.Contains(selectedAfter) == true)
|
||||||
|
{
|
||||||
|
_ = UpdateBackdropAsync(selectedAfter);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1749,6 +1866,8 @@ public partial class MainWindow : Window
|
|||||||
base.OnPropertyChanged(change);
|
base.OnPropertyChanged(change);
|
||||||
if (change.Property == WindowStateProperty)
|
if (change.Property == WindowStateProperty)
|
||||||
{
|
{
|
||||||
|
UpdateWindowChromeState();
|
||||||
|
|
||||||
// The XAML WindowState="Maximized" assignment raises this change
|
// The XAML WindowState="Maximized" assignment raises this change
|
||||||
// during InitializeComponent, before named controls are wired up.
|
// during InitializeComponent, before named controls are wired up.
|
||||||
if (WindowState == WindowState.Minimized)
|
if (WindowState == WindowState.Minimized)
|
||||||
@@ -1861,7 +1980,8 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var resolvedTitleId = string.IsNullOrWhiteSpace(titleId)
|
var resolvedTitleId = string.IsNullOrWhiteSpace(titleId)
|
||||||
? _allGames.FirstOrDefault(game => game.Path.Equals(ebootPath, FilePathComparison))?.TitleId
|
? _allGames.FirstOrDefault(game =>
|
||||||
|
game.Path.Equals(ebootPath, GameLibraryPath.Comparison))?.TitleId
|
||||||
: titleId;
|
: titleId;
|
||||||
var effective = EffectiveLaunchSettings.Resolve(_settings, PerGameSettings.Load(resolvedTitleId));
|
var effective = EffectiveLaunchSettings.Resolve(_settings, PerGameSettings.Load(resolvedTitleId));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
Window chrome button sizing and interaction states.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Style Selector="Button.windowChrome">
|
||||||
|
<Setter Property="Width" Value="46" />
|
||||||
|
<Setter Property="Height" Value="44" />
|
||||||
|
<Setter Property="MinWidth" Value="0" />
|
||||||
|
<Setter Property="MinHeight" Value="0" />
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
<Setter Property="CornerRadius" Value="0" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.windowChrome:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.windowClose:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource DangerBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="White" />
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using SharpEmu.GUI;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.GUI;
|
||||||
|
|
||||||
|
public sealed class GameLibraryReconcilerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Reconcile_PreservesExistingEntriesAndAppliesMetadataChanges()
|
||||||
|
{
|
||||||
|
var retained = CreateGame("retained", name: "Old name", version: "01.000");
|
||||||
|
var removed = CreateGame("removed");
|
||||||
|
var scannedRetained = CreateGame(
|
||||||
|
"retained",
|
||||||
|
name: "New name",
|
||||||
|
version: "02.000");
|
||||||
|
var added = CreateGame("added");
|
||||||
|
|
||||||
|
var result = GameLibraryReconciler.Reconcile(
|
||||||
|
[retained, removed],
|
||||||
|
[scannedRetained, added]);
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Games.Count);
|
||||||
|
Assert.Same(retained, result.Games[0]);
|
||||||
|
Assert.Same(added, result.Games[1]);
|
||||||
|
Assert.Equal("New name", retained.Name);
|
||||||
|
Assert.Equal("02.000", retained.Version);
|
||||||
|
Assert.DoesNotContain(removed, result.Games);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reconcile_ChangedCoverAndBackgroundReloadsExistingEntry()
|
||||||
|
{
|
||||||
|
var retained = CreateGame(
|
||||||
|
"retained",
|
||||||
|
coverPath: AssetPath("old-cover.png"),
|
||||||
|
backgroundPath: AssetPath("old-background.png"));
|
||||||
|
var scanned = CreateGame(
|
||||||
|
"retained",
|
||||||
|
coverPath: AssetPath("new-cover.png"),
|
||||||
|
backgroundPath: AssetPath("new-background.png"));
|
||||||
|
|
||||||
|
var result = GameLibraryReconciler.Reconcile([retained], [scanned]);
|
||||||
|
|
||||||
|
Assert.Same(retained, Assert.Single(result.Games));
|
||||||
|
Assert.Same(retained, Assert.Single(result.CoversToLoad));
|
||||||
|
Assert.Contains(retained, result.BackgroundsChanged);
|
||||||
|
Assert.Equal(scanned.CoverPath, retained.CoverPath);
|
||||||
|
Assert.Equal(scanned.BackgroundPath, retained.BackgroundPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReconcileVisibleGames_UsesMinimalIdentityPreservingChanges()
|
||||||
|
{
|
||||||
|
var first = CreateGame("first");
|
||||||
|
var second = CreateGame("second");
|
||||||
|
var removed = CreateGame("removed");
|
||||||
|
var added = CreateGame("added");
|
||||||
|
var visible = new ObservableCollection<GameEntry>
|
||||||
|
{
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
removed,
|
||||||
|
};
|
||||||
|
|
||||||
|
GameLibraryReconciler.ReconcileVisibleGames(
|
||||||
|
visible,
|
||||||
|
[second, first, added]);
|
||||||
|
|
||||||
|
Assert.Equal([second, first, added], visible);
|
||||||
|
Assert.Same(second, visible[0]);
|
||||||
|
Assert.Same(first, visible[1]);
|
||||||
|
Assert.Same(added, visible[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameEntry CreateGame(
|
||||||
|
string id,
|
||||||
|
string? name = null,
|
||||||
|
string? version = null,
|
||||||
|
string? coverPath = null,
|
||||||
|
string? backgroundPath = null)
|
||||||
|
=> new(
|
||||||
|
name ?? id,
|
||||||
|
$"PPSA-{id}",
|
||||||
|
version,
|
||||||
|
GamePath(id),
|
||||||
|
1,
|
||||||
|
coverPath,
|
||||||
|
backgroundPath);
|
||||||
|
|
||||||
|
private static string GamePath(string id)
|
||||||
|
=> Path.GetFullPath(Path.Combine("library-tests", id, "eboot.bin"));
|
||||||
|
|
||||||
|
private static string AssetPath(string name)
|
||||||
|
=> Path.GetFullPath(Path.Combine("library-tests", "assets", name));
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.GUI;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.GUI;
|
||||||
|
|
||||||
|
public sealed class GameLibraryWatcherTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Watch_FileCreation_RequestsRefresh()
|
||||||
|
{
|
||||||
|
var directory = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
$"sharpemu-library-watcher-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var watcher = new GameLibraryWatcher(TimeSpan.FromMilliseconds(25));
|
||||||
|
var refreshRequested = new TaskCompletionSource(
|
||||||
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
watcher.RefreshRequested += (_, _) => refreshRequested.TrySetResult();
|
||||||
|
watcher.Watch([directory]);
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(
|
||||||
|
Path.Combine(directory, "eboot.bin"),
|
||||||
|
"test");
|
||||||
|
|
||||||
|
await refreshRequested.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Data;
|
||||||
|
using SharpEmu.GUI;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.GUI;
|
||||||
|
|
||||||
|
public sealed class LocalizationTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _languagesDirectory = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
$"sharpemu-localization-{Guid.NewGuid():N}");
|
||||||
|
|
||||||
|
public LocalizationTests()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_languagesDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_RaisesNotificationsForCurrentCodeAndIndexer()
|
||||||
|
{
|
||||||
|
var localization = new Localization(_languagesDirectory);
|
||||||
|
var changedProperties = new List<string?>();
|
||||||
|
localization.PropertyChanged += (_, args) =>
|
||||||
|
changedProperties.Add(args.PropertyName);
|
||||||
|
|
||||||
|
localization.Load("ru");
|
||||||
|
|
||||||
|
Assert.Equal("ru", localization.CurrentCode);
|
||||||
|
Assert.Contains(nameof(Localization.CurrentCode), changedProperties);
|
||||||
|
Assert.Contains("Item", changedProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndexerBinding_UpdatesWhenLanguageChanges()
|
||||||
|
{
|
||||||
|
var localization = new Localization(_languagesDirectory);
|
||||||
|
localization.Load("en");
|
||||||
|
var englishLibraryLabel = localization.Get("Page.Library");
|
||||||
|
localization.Load("ru");
|
||||||
|
var russianLibraryLabel = localization.Get("Page.Library");
|
||||||
|
Assert.NotEqual(englishLibraryLabel, russianLibraryLabel);
|
||||||
|
|
||||||
|
localization.Load("en");
|
||||||
|
var text = new TextBlock();
|
||||||
|
text.Bind(
|
||||||
|
TextBlock.TextProperty,
|
||||||
|
new Binding("[Page.Library]") { Source = localization });
|
||||||
|
|
||||||
|
Assert.Equal(englishLibraryLabel, text.Text);
|
||||||
|
|
||||||
|
localization.Load("ru");
|
||||||
|
|
||||||
|
Assert.Equal(russianLibraryLabel, text.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LocalizedChoice_UpdatesLabelWithoutReplacingStableValue()
|
||||||
|
{
|
||||||
|
var localization = new Localization(_languagesDirectory);
|
||||||
|
var choice = LocalizedChoice.FromKey(
|
||||||
|
"Native",
|
||||||
|
"Options.CpuEngine.Native");
|
||||||
|
var changedProperties = new List<string?>();
|
||||||
|
choice.PropertyChanged += (_, args) =>
|
||||||
|
changedProperties.Add(args.PropertyName);
|
||||||
|
|
||||||
|
localization.Load("en");
|
||||||
|
choice.Refresh(localization);
|
||||||
|
var englishLabel = choice.Label;
|
||||||
|
|
||||||
|
localization.Load("ru");
|
||||||
|
choice.Refresh(localization);
|
||||||
|
|
||||||
|
Assert.Equal("Native", choice.Value);
|
||||||
|
Assert.NotEqual(englishLabel, choice.Label);
|
||||||
|
Assert.Equal(localization.Get("Options.CpuEngine.Native"), choice.Label);
|
||||||
|
Assert.Contains(nameof(LocalizedChoice.Label), changedProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComboBoxSelection_KeepsLiveLocalizedChoiceAsSelectionBoxItem()
|
||||||
|
{
|
||||||
|
var localization = new Localization(_languagesDirectory);
|
||||||
|
var choice = LocalizedChoice.FromKey(
|
||||||
|
"Native",
|
||||||
|
"Options.CpuEngine.Native");
|
||||||
|
localization.Load("en");
|
||||||
|
choice.Refresh(localization);
|
||||||
|
var comboBox = new ComboBox
|
||||||
|
{
|
||||||
|
ItemsSource = new[] { choice },
|
||||||
|
SelectedIndex = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Same(choice, comboBox.SelectionBoxItem);
|
||||||
|
|
||||||
|
localization.Load("ru");
|
||||||
|
choice.Refresh(localization);
|
||||||
|
|
||||||
|
Assert.Same(choice, comboBox.SelectionBoxItem);
|
||||||
|
Assert.Equal(localization.Get("Options.CpuEngine.Native"), choice.Label);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmbeddedLanguages_ContainEveryEnglishOptionsKey()
|
||||||
|
{
|
||||||
|
var assembly = typeof(Localization).Assembly;
|
||||||
|
var resourceNames = assembly.GetManifestResourceNames()
|
||||||
|
.Where(name =>
|
||||||
|
name.StartsWith("Languages.", StringComparison.Ordinal) &&
|
||||||
|
name.EndsWith(".json", StringComparison.Ordinal))
|
||||||
|
.Order()
|
||||||
|
.ToArray();
|
||||||
|
var english = ReadEmbeddedLanguage(assembly, "Languages.en.json");
|
||||||
|
var optionKeys = english.Keys
|
||||||
|
.Where(key => key.StartsWith("Options.", StringComparison.Ordinal))
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var resourceName in resourceNames)
|
||||||
|
{
|
||||||
|
var language = ReadEmbeddedLanguage(assembly, resourceName);
|
||||||
|
var missing = optionKeys
|
||||||
|
.Where(key => !language.ContainsKey(key))
|
||||||
|
.Order()
|
||||||
|
.ToArray();
|
||||||
|
var empty = optionKeys
|
||||||
|
.Where(key =>
|
||||||
|
language.TryGetValue(key, out var value) &&
|
||||||
|
string.IsNullOrWhiteSpace(value))
|
||||||
|
.Order()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
missing.Length == 0,
|
||||||
|
$"{resourceName} is missing option keys: {string.Join(", ", missing)}");
|
||||||
|
Assert.True(
|
||||||
|
empty.Length == 0,
|
||||||
|
$"{resourceName} has empty option values: {string.Join(", ", empty)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("en")]
|
||||||
|
[InlineData("ru")]
|
||||||
|
public void LooseLanguageFile_OverridesEmbeddedValuesWithoutReplacingThem(
|
||||||
|
string languageCode)
|
||||||
|
{
|
||||||
|
var localization = new Localization(_languagesDirectory);
|
||||||
|
localization.Load(languageCode);
|
||||||
|
var embeddedOptionsLabel = localization.Get("Page.Options");
|
||||||
|
File.WriteAllText(
|
||||||
|
Path.Combine(_languagesDirectory, $"{languageCode}.json"),
|
||||||
|
$$"""
|
||||||
|
{
|
||||||
|
"_languageName": "Test {{languageCode}}",
|
||||||
|
"Page.Library": "Custom library"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
localization.Load(languageCode);
|
||||||
|
|
||||||
|
Assert.Equal("Custom library", localization.Get("Page.Library"));
|
||||||
|
Assert.Equal(embeddedOptionsLabel, localization.Get("Page.Options"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingLanguage_FallsBackToEnglish()
|
||||||
|
{
|
||||||
|
var localization = new Localization(_languagesDirectory);
|
||||||
|
localization.Load("en");
|
||||||
|
var englishLibraryLabel = localization.Get("Page.Library");
|
||||||
|
|
||||||
|
localization.Load("missing");
|
||||||
|
|
||||||
|
Assert.Equal("missing", localization.CurrentCode);
|
||||||
|
Assert.Equal(englishLibraryLabel, localization.Get("Page.Library"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Directory.Delete(_languagesDirectory, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> ReadEmbeddedLanguage(
|
||||||
|
System.Reflection.Assembly assembly,
|
||||||
|
string resourceName)
|
||||||
|
{
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
|
Assert.NotNull(stream);
|
||||||
|
return JsonSerializer.Deserialize<Dictionary<string, string>>(stream)
|
||||||
|
?? throw new InvalidDataException($"Could not parse {resourceName}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user