Compare commits

...

9 Commits

Author SHA1 Message Date
Evan Husted
4ae9f1c0d2 UI: Use Hosted Games & Player Count localization keys in list view too 2025-02-04 23:31:31 -06:00
Evan Husted
717851985e UI: Reorganize Game Info dialog popup + localization 2025-02-04 23:28:37 -06:00
Evan Husted
bd08a111a8 UI: Show what each value is in the Game Info dialog, add game icon 2025-02-04 22:47:12 -06:00
Evan Husted
1972a47f39 UI: Game stats button on right click for Grid view users 2025-02-04 19:32:17 -06:00
Evan Husted
222ceb818b misc: chore: Use ApplicationLibrary helpers for getting DLCs & Updates for a game 2025-02-04 18:21:49 -06:00
Evan Husted
b0fcc5bee1 misc: chore: Simplify HasCompatibilityEntry
(Totally didn't realize that SelectedApplication is already an ApplicationData)
2025-02-04 18:21:24 -06:00
Evan Husted
820e8f7375 [ci skip] UI: Strip dumped file information out of the DLC name 2025-02-04 18:10:28 -06:00
Evan Husted
e8a7d5b0b7 UI: Only show DLC RomFS button under Extract Data when DLCs are available.
Also convert the constructor of DlcSelectViewModel to expect a normal title id and not one already converted to the base ID.
2025-02-04 17:21:54 -06:00
Evan Husted
fafb99c702 misc: chore: [ci skip] don't even bother looking up the application; the tag present on the control *is* a valid title ID and can't reasonably change in between the tag being set and playability information being requested.
Even if it does, worst case scenario the compat list that pops up has no results.
2025-02-04 15:57:32 -06:00
16 changed files with 566 additions and 159 deletions

View File

@@ -1525,151 +1525,151 @@
{ {
"ID": "GameListHeaderDeveloper", "ID": "GameListHeaderDeveloper",
"Translations": { "Translations": {
"ar_SA": "المطور", "ar_SA": "",
"de_DE": "Entwickler", "de_DE": "",
"el_GR": "Προγραμματιστής", "el_GR": "",
"en_US": "Developer", "en_US": "Developed by {0}",
"es_ES": "Desarrollador", "es_ES": "",
"fr_FR": "Développeur", "fr_FR": "",
"he_IL": "מפתח", "he_IL": "",
"it_IT": "Sviluppatore", "it_IT": "",
"ja_JP": "開発元", "ja_JP": "",
"ko_KR": "개발자", "ko_KR": "",
"no_NO": "Utvikler", "no_NO": "",
"pl_PL": "Twórca", "pl_PL": "",
"pt_BR": "Desenvolvedor", "pt_BR": "",
"ru_RU": "Разработчик", "ru_RU": "",
"sv_SE": "Utvecklare", "sv_SE": "",
"th_TH": "ผู้พัฒนา", "th_TH": "",
"tr_TR": "Geliştirici", "tr_TR": "",
"uk_UA": "Розробник", "uk_UA": "",
"zh_CN": "制作商", "zh_CN": "",
"zh_TW": "開發者" "zh_TW": ""
} }
}, },
{ {
"ID": "GameListHeaderVersion", "ID": "GameListHeaderVersion",
"Translations": { "Translations": {
"ar_SA": "الإصدار", "ar_SA": "",
"de_DE": "", "de_DE": "",
"el_GR": "Έκδοση", "el_GR": "Έκδοση: {0}",
"en_US": "Version", "en_US": "Version: {0}",
"es_ES": "Versión", "es_ES": "Versión: {0}",
"fr_FR": "", "fr_FR": "",
"he_IL": "גרסה", "he_IL": "",
"it_IT": "Versione", "it_IT": "Versione: {0}",
"ja_JP": "バージョン", "ja_JP": "バージョン: {0}",
"ko_KR": "버전", "ko_KR": "버전: {0}",
"no_NO": "Versjon", "no_NO": "Versjon: {0}",
"pl_PL": "Wersja", "pl_PL": "Wersja: {0}",
"pt_BR": "Versão", "pt_BR": "Versão: {0}",
"ru_RU": "Версия", "ru_RU": "Версия: {0}",
"sv_SE": "", "sv_SE": "",
"th_TH": "เวอร์ชั่น", "th_TH": "เวอร์ชั่น: {0}",
"tr_TR": "Sürüm", "tr_TR": "Sürüm: {0}",
"uk_UA": "Версія", "uk_UA": "Версія: {0}",
"zh_CN": "版本", "zh_CN": "版本: {0}",
"zh_TW": "版本" "zh_TW": "版本: {0}"
} }
}, },
{ {
"ID": "GameListHeaderTimePlayed", "ID": "GameListHeaderTimePlayed",
"Translations": { "Translations": {
"ar_SA": "وقت اللعب", "ar_SA": "",
"de_DE": "Spielzeit", "de_DE": "Spielzeit: {0}",
"el_GR": "Χρόνος", "el_GR": "Χρόνος: {0}",
"en_US": "Play Time", "en_US": "Play Time: {0}",
"es_ES": "Tiempo jugado", "es_ES": "Tiempo jugado: {0}",
"fr_FR": "Temps de jeu", "fr_FR": "Temps de jeu: {0}",
"he_IL": "זמן משחק", "he_IL": "",
"it_IT": "Tempo di gioco", "it_IT": "Tempo di gioco: {0}",
"ja_JP": "プレイ時間", "ja_JP": "プレイ時間: {0}",
"ko_KR": "플레이 타임", "ko_KR": "플레이 타임: {0}",
"no_NO": "Spilletid", "no_NO": "Spilletid: {0}",
"pl_PL": "Czas w grze:", "pl_PL": "Czas w grze: {0}",
"pt_BR": "Tempo de jogo", "pt_BR": "Tempo de jogo: {0}",
"ru_RU": "Время в игре", "ru_RU": "Время в игре: {0}",
"sv_SE": "Speltid", "sv_SE": "Speltid: {0}",
"th_TH": "เล่นไปแล้ว", "th_TH": "เล่นไปแล้ว: {0}",
"tr_TR": "Oynama Süresi", "tr_TR": "Oynama Süresi: {0}",
"uk_UA": "Зіграно часу", "uk_UA": "Зіграно часу: {0}",
"zh_CN": "游玩时长", "zh_CN": "游玩时长: {0}",
"zh_TW": "遊玩時數" "zh_TW": "遊玩時數: {0}"
} }
}, },
{ {
"ID": "GameListHeaderLastPlayed", "ID": "GameListHeaderLastPlayed",
"Translations": { "Translations": {
"ar_SA": "آخر مرة لُعبت", "ar_SA": "",
"de_DE": "Zuletzt gespielt", "de_DE": "Zuletzt gespielt: {0}",
"el_GR": "Παίχτηκε", "el_GR": "Παίχτηκε: {0}",
"en_US": "Last Played", "en_US": "Last Played: {0}",
"es_ES": "Jugado por última vez", "es_ES": "Jugado por última vez: {0}",
"fr_FR": "Dernière partie jouée", "fr_FR": "Dernière partie jouée: {0}",
"he_IL": "שוחק לאחרונה", "he_IL": "",
"it_IT": "Ultima partita", "it_IT": "Ultima partita: {0}",
"ja_JP": "最終プレイ日時", "ja_JP": "最終プレイ日時: {0}",
"ko_KR": "마지막 플레이", "ko_KR": "마지막 플레이: {0}",
"no_NO": "Sist Spilt", "no_NO": "Sist Spilt: {0}",
"pl_PL": "Ostatnio grane", "pl_PL": "Ostatnio grane: {0}",
"pt_BR": "Último jogo", "pt_BR": "Último jogo: {0}",
"ru_RU": "Последний запуск", "ru_RU": "Последний запуск: {0}",
"sv_SE": "Senast spelad", "sv_SE": "Senast spelad: {0}",
"th_TH": "เล่นล่าสุด", "th_TH": "เล่นล่าสุด: {0}",
"tr_TR": "Son Oynama Tarihi", "tr_TR": "Son Oynama Tarihi: {0}",
"uk_UA": "Востаннє зіграно", "uk_UA": "Востаннє зіграно: {0}",
"zh_CN": "最近游玩", "zh_CN": "最近游玩: {0}",
"zh_TW": "最近遊玩" "zh_TW": "最近遊玩: {0}"
} }
}, },
{ {
"ID": "GameListHeaderFileExtension", "ID": "GameListHeaderFileExtension",
"Translations": { "Translations": {
"ar_SA": "صيغة الملف", "ar_SA": "",
"de_DE": "Dateiformat", "de_DE": "Dateiformat: {0}",
"el_GR": "Κατάληξη", "el_GR": "Κατάληξη: {0}",
"en_US": "File Ext", "en_US": "Extension: {0}",
"es_ES": "Extensión", "es_ES": "Extensión: {0}",
"fr_FR": "Extension du Fichier", "fr_FR": "Extension du Fichier: {0}",
"he_IL": "סיומת קובץ", "he_IL": "",
"it_IT": "Estensione", "it_IT": "Estensione: {0}",
"ja_JP": "ファイル拡張子", "ja_JP": "ファイル拡張子: {0}",
"ko_KR": "파일 확장자", "ko_KR": "파일 확장자: {0}",
"no_NO": "Fil Eks.", "no_NO": "Fil Eks.: {0}",
"pl_PL": "Rozszerzenie pliku", "pl_PL": "Rozszerzenie pliku: {0}",
"pt_BR": "Extensão", "pt_BR": "Extensão: {0}",
"ru_RU": "Расширение файла", "ru_RU": "Расширение файла: {0}",
"sv_SE": "Filänd", "sv_SE": "Filänd: {0}",
"th_TH": "นามสกุลไฟล์", "th_TH": "นามสกุลไฟล์: {0}",
"tr_TR": "Dosya Uzantısı", "tr_TR": "Dosya Uzantısı: {0}",
"uk_UA": "Розширення файлу", "uk_UA": "Розширення файлу: {0}",
"zh_CN": "扩展名", "zh_CN": "扩展名: {0}",
"zh_TW": "副檔名" "zh_TW": "副檔名: {0}"
} }
}, },
{ {
"ID": "GameListHeaderFileSize", "ID": "GameListHeaderFileSize",
"Translations": { "Translations": {
"ar_SA": "حجم الملف", "ar_SA": "",
"de_DE": "Dateigröße", "de_DE": "Dateigröße: {0}",
"el_GR": "Μέγεθος Αρχείου", "el_GR": "Μέγεθος Αρχείου: {0}",
"en_US": "File Size", "en_US": "File Size: {0}",
"es_ES": "Tamaño del archivo", "es_ES": "Tamaño del archivo: {0}",
"fr_FR": "Taille du Fichier", "fr_FR": "Taille du Fichier: {0}",
"he_IL": "גודל הקובץ", "he_IL": "",
"it_IT": "Dimensione file", "it_IT": "Dimensione file: {0}",
"ja_JP": "ファイルサイズ", "ja_JP": "ファイルサイズ: {0}",
"ko_KR": "파일 크기", "ko_KR": "파일 크기: {0}",
"no_NO": "Fil Størrelse", "no_NO": "Fil Størrelse: {0}",
"pl_PL": "Rozmiar pliku", "pl_PL": "Rozmiar pliku: {0}",
"pt_BR": "Tamanho", "pt_BR": "Tamanho: {0}",
"ru_RU": "Размер файла", "ru_RU": "Размер файла: {0}",
"sv_SE": "Filstorlek", "sv_SE": "Filstorlek: {0}",
"th_TH": "ขนาดไฟล์", "th_TH": "ขนาดไฟล์: {0}",
"tr_TR": "Dosya Boyutu", "tr_TR": "Dosya Boyutu: {0}",
"uk_UA": "Розмір файлу", "uk_UA": "Розмір файлу: {0}",
"zh_CN": "大小", "zh_CN": "大小: {0}",
"zh_TW": "檔案大小" "zh_TW": "檔案大小: {0}"
} }
}, },
{ {
@@ -1697,6 +1697,106 @@
"zh_TW": "路徑" "zh_TW": "路徑"
} }
}, },
{
"ID": "GameListHeaderCompatibilityStatus",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Compatibility:",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GameListHeaderTitleId",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Title ID:",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GameListHeaderHostedGames",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Hosted Games: {0}",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GameListHeaderPlayerCount",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Online Players: {0}",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{ {
"ID": "GameListContextMenuOpenUserSaveDirectory", "ID": "GameListContextMenuOpenUserSaveDirectory",
"Translations": { "Translations": {
@@ -2572,6 +2672,56 @@
"zh_TW": "" "zh_TW": ""
} }
}, },
{
"ID": "GameListContextMenuShowGameData",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show Game Info",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GameListContextMenuShowGameDataToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show stats & details about the currently selected game.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{ {
"ID": "GameListContextMenuOpenModsDirectory", "ID": "GameListContextMenuOpenModsDirectory",
"Translations": { "Translations": {

View File

@@ -33,6 +33,9 @@ namespace Ryujinx.Ava
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>() .ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>()
.MainWindow.Cast<MainWindow>(); .MainWindow.Cast<MainWindow>();
public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current!
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>();
public static bool IsClipboardAvailable(out IClipboard clipboard) public static bool IsClipboardAvailable(out IClipboard clipboard)
{ {
clipboard = MainWindow.Clipboard; clipboard = MainWindow.Clipboard;

View File

@@ -25,6 +25,11 @@
Header="{ext:Locale GameListContextMenuShowCompatEntry}" Header="{ext:Locale GameListContextMenuShowCompatEntry}"
Icon="{ext:Icon mdi-gamepad}" Icon="{ext:Icon mdi-gamepad}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/> ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
<MenuItem
Click="OpenApplicationData_Click"
Header="{ext:Locale GameListContextMenuShowGameData}"
Icon="{ext:Icon mdi-chart-line}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowGameDataToolTip}"/>
<Separator /> <Separator />
<MenuItem <MenuItem
Click="OpenUserSaveDirectory_Click" Click="OpenUserSaveDirectory_Click"
@@ -117,6 +122,7 @@
Header="{ext:Locale GameListContextMenuExtractDataRomFS}" Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" /> ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem <MenuItem
IsVisible="{Binding HasDlc}"
Click="ExtractAocRomFs_Click" Click="ExtractAocRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}" Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" /> ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />

View File

@@ -334,7 +334,7 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
return; return;
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary); DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary);
if (selectedDlc is not null) if (selectedDlc is not null)
{ {
@@ -393,6 +393,12 @@ namespace Ryujinx.Ava.UI.Controls
await CompatibilityList.Show(viewModel.SelectedApplication.IdString); await CompatibilityList.Show(viewModel.SelectedApplication.IdString);
} }
public async void OpenApplicationData_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await ApplicationDataView.Show(viewModel.SelectedApplication);
}
public async void RunApplication_Click(object sender, RoutedEventArgs args) public async void RunApplication_Click(object sender, RoutedEventArgs args)
{ {
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel }) if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
@@ -401,12 +407,8 @@ namespace Ryujinx.Ava.UI.Controls
public async void TrimXCI_Click(object sender, RoutedEventArgs args) public async void TrimXCI_Click(object sender, RoutedEventArgs args)
{ {
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
if (viewModel?.SelectedApplication != null)
{
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path); await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
} }
} }
} }
}

View File

@@ -0,0 +1,114 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
x:DataType="viewModels:ApplicationDataViewModel">
<StackPanel Orientation="Horizontal">
<Image Margin="0"
MaxWidth="256"
MinWidth="256"
Source="{Binding AppData.Icon, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
<Border Margin="5, 0" Width="1" Height="256" BorderBrush="Gray" Background="Gray" />
<StackPanel Orientation="Vertical">
<Grid
RowDefinitions="Auto,Auto,Auto"
ColumnDefinitions="*">
<StackPanel Grid.Row="0">
<TextBlock HorizontalAlignment="Left"
Text="{Binding FormattedVersion}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock HorizontalAlignment="Left"
Text="{Binding FormattedDeveloper}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock HorizontalAlignment="Stretch"
Text="{Binding FormattedFileExtension}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock HorizontalAlignment="Stretch"
Text="{Binding FormattedFileSize}"
TextAlignment="Start"
TextWrapping="Wrap" />
</StackPanel>
<Separator Grid.Row="1" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<StackPanel Grid.Row="2"
HorizontalAlignment="Left"
Orientation="Vertical"
Spacing="5">
<StackPanel Orientation="Horizontal">
<TextBlock Padding="0, 0, 5, 0" Text="{ext:Locale GameListHeaderCompatibilityStatus}" />
<Button
Click="PlayabilityStatus_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding AppData.HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
Margin="1.5"
Tag="{Binding AppData.IdString}"
Text="{Binding AppData.LocalizedStatus}"
Foreground="{Binding AppData.PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Button.Styles>
<Style Selector="Button">
<Setter Property="MinWidth"
Value="0" />
<!-- avoids very wide buttons from the overall project avalonia style -->
</Style>
</Button.Styles>
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Padding="0, 0, 5, 0" Text="{ext:Locale GameListHeaderTitleId}" />
<Button
Click="IdString_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
Margin="1.5"
HorizontalAlignment="Stretch"
Text="{Binding AppData.IdString}"
TextAlignment="Start"
TextWrapping="Wrap" />
</Button>
</StackPanel>
</StackPanel>
</Grid>
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<TextBlock
HorizontalAlignment="Stretch"
IsVisible="{Binding AppData.HasLdnGames}"
Text="{Binding FormattedLdnInfo}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Separator IsVisible="{Binding AppData.HasLdnGames}" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<StackPanel
HorizontalAlignment="Left"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FormattedLastPlayed}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FormattedPlayTime}"
IsVisible="{Binding AppData.HasPlayedPreviously}"
TextAlignment="Start"
TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,86 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat;
using System.Linq;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Controls
{
public partial class ApplicationDataView : UserControl
{
public static async Task Show(ApplicationData appData)
{
ContentDialog contentDialog = new()
{
Title = appData.Name,
PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
MinWidth = 256,
Content = new ApplicationDataView { DataContext = new ApplicationDataViewModel(appData) }
};
Style closeButton = new(x => x.Name("CloseButton"));
closeButton.Setters.Add(new Setter(WidthProperty, 160d));
Style closeButtonParent = new(x => x.Name("CommandSpace"));
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
Avalonia.Layout.HorizontalAlignment.Center));
contentDialog.Styles.Add(closeButton);
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
}
public ApplicationDataView()
{
InitializeComponent();
}
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
{
if (sender is not Button { Content: TextBlock playabilityLabel })
return;
if (RyujinxApp.AppLifetime.Windows.TryGetFirst(x => x is ContentDialogOverlayWindow, out Window window))
window.Close(ContentDialogResult.None);
await CompatibilityList.Show((string)playabilityLabel.Tag);
}
private async void IdString_OnClick(object sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel mwvm)
return;
if (sender is not Button { Content: TextBlock idText })
return;
if (!RyujinxApp.IsClipboardAvailable(out IClipboard clipboard))
return;
ApplicationData appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text);
if (appData is null)
return;
await clipboard.SetTextAsync(appData.IdString);
NotificationHelper.ShowInformation(
"Copied Title ID",
$"{appData.Name} ({appData.IdString})");
}
}
}

View File

@@ -140,6 +140,7 @@
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
IsVisible="{Binding HasLdnGames}"
Text="{Binding Converter={helpers:MultiplayerInfoConverter}}" Text="{Binding Converter={helpers:MultiplayerInfoConverter}}"
TextAlignment="Start" TextAlignment="Start"
TextWrapping="Wrap"/> TextWrapping="Wrap"/>

View File

@@ -39,13 +39,7 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is not Button { Content: TextBlock playabilityLabel }) if (sender is not Button { Content: TextBlock playabilityLabel })
return; return;
if (!ulong.TryParse((string)playabilityLabel.Tag, NumberStyles.HexNumber, null, out ulong titleId)) await CompatibilityList.Show((string)playabilityLabel.Tag);
return;
if (!mwvm.ApplicationLibrary.FindApplication(titleId, out ApplicationData appData))
return;
await CompatibilityList.Show(appData.IdString);
} }
private async void IdString_OnClick(object sender, RoutedEventArgs e) private async void IdString_OnClick(object sender, RoutedEventArgs e)

View File

@@ -1,8 +1,11 @@
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using System; using System;
using System.Globalization; using System.Globalization;
using System.Text;
namespace Ryujinx.Ava.UI.Helpers namespace Ryujinx.Ava.UI.Helpers
{ {
@@ -12,16 +15,17 @@ namespace Ryujinx.Ava.UI.Helpers
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
if (value is ApplicationData applicationData) if (value is not ApplicationData { HasLdnGames: true } applicationData)
{
if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0)
{
return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}";
}
}
return ""; return "";
return new StringBuilder()
.AppendLine(
LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames]
.Format(applicationData.GameCount))
.Append(
LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount]
.Format(applicationData.PlayerCount))
.ToString();
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

View File

@@ -0,0 +1,26 @@
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.AppLibrary;
namespace Ryujinx.Ava.UI.ViewModels
{
public class ApplicationDataViewModel : BaseModel
{
public ApplicationData AppData { get; }
public ApplicationDataViewModel(ApplicationData appData) => AppData = appData;
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);
public string FormattedLastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed].Format(AppData.LastPlayedString);
public string FormattedPlayTime => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed].Format(AppData.TimePlayedString);
public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString);
public string FormattedLdnInfo =>
$"{LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames].Format(AppData.GameCount)}" +
$"\n" +
$"{LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount].Format(AppData.PlayerCount)}";
}
}

View File

@@ -14,9 +14,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary) public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
{ {
_dlcs = appLibrary.DownloadableContents.Items _dlcs = appLibrary.FindDlcsFor(titleId)
.Where(x => x.Dlc.TitleIdBase == titleId)
.Select(x => x.Dlc)
.OrderBy(it => it.IsBundled ? 0 : 1) .OrderBy(it => it.IsBundled ? 0 : 1)
.ThenBy(it => it.TitleId) .ThenBy(it => it.TitleId)
.ToArray(); .ToArray();

View File

@@ -69,8 +69,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadDownloadableContents() private void LoadDownloadableContents()
{ {
IEnumerable<(DownloadableContentModel Dlc, bool IsEnabled)> dlcs = _applicationLibrary.DownloadableContents.Items (DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id);
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
bool hasBundledContent = false; bool hasBundledContent = false;
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs) foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)

View File

@@ -349,16 +349,9 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
} }
public bool HasCompatibilityEntry public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo;
{
get
{
DynamicData.Kernel.Optional<ApplicationData> appData =
ApplicationLibrary.Applications.Lookup(SelectedApplication.Id);
return appData.HasValue && appData.Value.HasPlayabilityInfo; public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id);
}
}
public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0; public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0;

View File

@@ -41,8 +41,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadUpdates() private void LoadUpdates()
{ {
IEnumerable<(TitleUpdateModel TitleUpdate, bool IsSelected)> updates = ApplicationLibrary.TitleUpdates.Items (TitleUpdateModel TitleUpdate, bool IsSelected)[] updates = ApplicationLibrary.FindUpdateConfigurationFor(ApplicationData.Id);
.Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase);
bool hasBundledContent = false; bool hasBundledContent = false;
SelectedUpdate = new TitleUpdateViewModelNoUpdate(); SelectedUpdate = new TitleUpdateViewModelNoUpdate();

View File

@@ -49,6 +49,9 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public int PlayerCount { get; set; } public int PlayerCount { get; set; }
public int GameCount { get; set; } public int GameCount { get; set; }
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
public TimeSpan TimePlayed { get; set; } public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; } public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; } public string FileExtension { get; set; }

View File

@@ -129,10 +129,15 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
if (appData.HasValue) if (appData.HasValue)
return appData.Value.Name; return appData.Value.Name;
if (DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData)) if (!DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
return Path.GetFileNameWithoutExtension(dlcData.FileName);
return id.ToString("X16"); return id.ToString("X16");
string name = Path.GetFileNameWithoutExtension(dlcData.FileName)!;
int idx = name.IndexOf('[');
if (idx != -1)
name = name[..idx];
return name;
} }
public bool FindApplication(ulong id, out ApplicationData foundData) public bool FindApplication(ulong id, out ApplicationData foundData)
@@ -143,6 +148,30 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
return appData.HasValue; return appData.HasValue;
} }
public bool FindUpdate(ulong id, out TitleUpdateModel foundData)
{
Gommon.Optional<TitleUpdateModel> appData =
TitleUpdates.Keys.FindFirst(x => x.TitleId == id);
foundData = appData.HasValue ? appData.Value : null;
return appData.HasValue;
}
public TitleUpdateModel[] FindUpdatesFor(ulong id)
=> TitleUpdates.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public (TitleUpdateModel TitleUpdate, bool IsSelected)[] FindUpdateConfigurationFor(ulong id)
=> TitleUpdates.Items.Where(x => x.TitleUpdate.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public DownloadableContentModel[] FindDlcsFor(ulong id)
=> DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public (DownloadableContentModel Dlc, bool IsEnabled)[] FindDlcConfigurationFor(ulong id)
=> DownloadableContents.Items.Where(x => x.Dlc.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public bool HasDlcs(ulong id)
=> DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL));
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception> /// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
/// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception> /// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception>
/// <exception cref="NotSupportedException">The NCA version is not supported.</exception> /// <exception cref="NotSupportedException">The NCA version is not supported.</exception>