diff options
Diffstat (limited to 'Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs')
-rw-r--r-- | Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs | 1446 |
1 files changed, 927 insertions, 519 deletions
diff --git a/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 514a8bb3..9571af47 100644 --- a/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,5 @@ -using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Media; using Avalonia.Threading; @@ -7,12 +7,12 @@ using DynamicData; using DynamicData.Binding; using LibHac.Fs; using LibHac.FsSystem; -using LibHac.Ncm; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -21,11 +21,13 @@ using Ryujinx.Cpu; using Ryujinx.HLE; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; -using Ryujinx.Modules; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.HLE.Ui; using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; +using SixLabors.ImageSharp.PixelFormats; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -35,14 +37,14 @@ using System.Threading; using System.Threading.Tasks; using Path = System.IO.Path; using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; +using UserId = LibHac.Fs.UserId; namespace Ryujinx.Ava.UI.ViewModels { - internal class MainWindowViewModel : BaseModel + public class MainWindowViewModel : BaseModel { private const int HotKeyPressDelayMs = 500; - private readonly MainWindow _owner; private ObservableCollection<ApplicationData> _applications; private string _aspectStatusText; @@ -57,7 +59,7 @@ namespace Ryujinx.Ava.UI.ViewModels private string _gpuStatusText; private bool _isAmiiboRequested; private bool _isGameRunning; - private bool _isLoading; + private bool _isFullScreen; private int _progressMaximum; private int _progressValue; private long _lastFullscreenToggle = Environment.TickCount64; @@ -76,15 +78,29 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _isLoadingIndeterminate = true; private bool _showAll; private string _lastScannedAmiiboId; + private bool _statusBarVisible; private ReadOnlyObservableCollection<ApplicationData> _appsObservableList; - public ApplicationLibrary ApplicationLibrary => _owner.ApplicationLibrary; - public string TitleName { get; internal set; } + private string _showUiKey = "F4"; + private string _pauseKey = "F5"; + private string _screenshotKey = "F8"; + private float _volume; + private string _backendText; - public MainWindowViewModel(MainWindow owner) : this() - { - _owner = owner; - } + public ApplicationData ListSelectedApplication; + public ApplicationData GridSelectedApplication; + private bool _canUpdate; + private Cursor _cursor; + private string _title; + private string _currentEmulatedGamePath; + private AutoResetEvent _rendererWaitEvent; + private WindowState _windowState; + private bool _isActive; + + public event Action ReloadGameList; + + private string TitleName { get; set; } + internal AppHost AppHost { get; set; } public MainWindowViewModel() { @@ -95,6 +111,8 @@ namespace Ryujinx.Ava.UI.ViewModels .Sort(GetComparer()) .Bind(out _appsObservableList).AsObservableList(); + _rendererWaitEvent = new AutoResetEvent(false); + if (Program.PreviewerDetached) { LoadConfigurableHotKeys(); @@ -103,12 +121,37 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public void Initialize() + public void Initialize( + ContentManager contentManager, + ApplicationLibrary applicationLibrary, + VirtualFileSystem virtualFileSystem, + AccountManager accountManager, + Ryujinx.Input.HLE.InputManager inputManager, + UserChannelPersistence userChannelPersistence, + LibHacHorizonManager libHacHorizonManager, + IHostUiHandler uiHandler, + Action<bool> showLoading, + Action<bool> switchToGameControl, + Action<Control> setMainContent, + TopLevel topLevel) { - ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; - ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; + ContentManager = contentManager; + ApplicationLibrary = applicationLibrary; + VirtualFileSystem = virtualFileSystem; + AccountManager = accountManager; + InputManager = inputManager; + UserChannelPersistence = userChannelPersistence; + LibHacHorizonManager = libHacHorizonManager; + UiHandler = uiHandler; + + ShowLoading = showLoading; + SwitchToGameControl = switchToGameControl; + SetMainContent = setMainContent; + TopLevel = topLevel; } +#region Properties + public string SearchText { get => _searchText; @@ -130,6 +173,27 @@ namespace Ryujinx.Ava.UI.ViewModels _searchTimer = null; } + public bool CanUpdate + { + get => _canUpdate; + set + { + _canUpdate = value; + + OnPropertyChanged(); + } + } + + public Cursor Cursor + { + get => _cursor; + set + { + _cursor = value; + OnPropertyChanged(); + } + } + public ReadOnlyObservableCollection<ApplicationData> AppsObservableList { get => _appsObservableList; @@ -152,6 +216,28 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public long LastFullscreenToggle + { + get => _lastFullscreenToggle; + set + { + _lastFullscreenToggle = value; + + OnPropertyChanged(); + } + } + + public bool StatusBarVisible + { + get => _statusBarVisible && EnableNonGameRunningControls; + set + { + _statusBarVisible = value; + + OnPropertyChanged(); + } + } + public bool EnableNonGameRunningControls => !IsGameRunning; public bool ShowFirmwareStatus => !ShowLoadProgress; @@ -170,6 +256,7 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(); OnPropertyChanged(nameof(EnableNonGameRunningControls)); + OnPropertyChanged(nameof(StatusBarVisible)); OnPropertyChanged(nameof(ShowFirmwareStatus)); } } @@ -208,11 +295,38 @@ namespace Ryujinx.Ava.UI.ViewModels } } - private string _showUikey = "F4"; - private string _pauseKey = "F5"; - private string _screenshotkey = "F8"; - private float _volume; - private string _backendText; + public bool IsFullScreen + { + get => _isFullScreen; + set + { + _isFullScreen = value; + + OnPropertyChanged(); + } + } + + public bool ShowAll + { + get => _showAll; + set + { + _showAll = value; + + OnPropertyChanged(); + } + } + + public string LastScannedAmiiboId + { + get => _lastScannedAmiiboId; + set + { + _lastScannedAmiiboId = value; + + OnPropertyChanged(); + } + } public ApplicationData SelectedApplication { @@ -220,9 +334,9 @@ namespace Ryujinx.Ava.UI.ViewModels { return Glyph switch { - Glyph.List => _owner.GameList.SelectedApplication, - Glyph.Grid => _owner.GameGrid.SelectedApplication, - _ => null, + Glyph.List => ListSelectedApplication, + Glyph.Grid => GridSelectedApplication, + _ => null, }; } } @@ -414,7 +528,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (_isGameRunning) { - _owner.AppHost.Device.SetVolume(_volume); + AppHost.Device.SetVolume(_volume); } OnPropertyChanged(nameof(VolumeStatusText)); @@ -456,6 +570,18 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool IsActive + { + get => _isActive; + set + { + _isActive = value; + + OnPropertyChanged(); + } + } + + public bool ShowContent { get => _showContent; @@ -478,6 +604,17 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public WindowState WindowState + { + get => _windowState; + internal set + { + _windowState = value; + + OnPropertyChanged(); + } + } + public bool IsGrid => Glyph == Glyph.Grid; public bool IsList => Glyph == Glyph.List; @@ -495,44 +632,6 @@ namespace Ryujinx.Ava.UI.ViewModels RefreshView(); } - private IComparer<ApplicationData> GetComparer() - { - return SortMode switch - { - ApplicationSort.LastPlayed => new Models.Generic.LastPlayedSortComparer(IsAscending), - ApplicationSort.FileSize => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileSizeBytes) - : SortExpressionComparer<ApplicationData>.Descending(app => app.FileSizeBytes), - ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TimePlayedNum) - : SortExpressionComparer<ApplicationData>.Descending(app => app.TimePlayedNum), - ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName) - : SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName), - ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Favorite) - : SortExpressionComparer<ApplicationData>.Descending(app => app.Favorite), - ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer) - : SortExpressionComparer<ApplicationData>.Descending(app => app.Developer), - ApplicationSort.FileType => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension) - : SortExpressionComparer<ApplicationData>.Descending(app => app.FileExtension), - ApplicationSort.Path => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Path) - : SortExpressionComparer<ApplicationData>.Descending(app => app.Path), - _ => null, - }; - } - - private void RefreshView() - { - RefreshGrid(); - } - - private void RefreshGrid() - { - Applications.ToObservableChangeSet() - .Filter(Filter) - .Sort(GetComparer()) - .Bind(out _appsObservableList).AsObservableList(); - - OnPropertyChanged(nameof(AppsObservableList)); - } - public bool StartGamesInFullscreen { get => ConfigurationState.Instance.Ui.StartFullscreen; @@ -559,6 +658,17 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public string Title + { + get => _title; + set + { + _title = value; + + OnPropertyChanged(); + } + } + public bool ShowConsoleVisible { get => ConsoleHelper.SetConsoleWindowStateSupported; @@ -598,6 +708,7 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(); OnPropertyChanged(nameof(GridSizeScale)); + OnPropertyChanged(nameof(GridItemSelectorSize)); ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); } @@ -617,14 +728,70 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public bool IsSortedByFavorite => SortMode == ApplicationSort.Favorite; - public bool IsSortedByTitle => SortMode == ApplicationSort.Title; - public bool IsSortedByDeveloper => SortMode == ApplicationSort.Developer; - public bool IsSortedByLastPlayed => SortMode == ApplicationSort.LastPlayed; - public bool IsSortedByTimePlayed => SortMode == ApplicationSort.TotalTimePlayed; - public bool IsSortedByType => SortMode == ApplicationSort.FileType; - public bool IsSortedBySize => SortMode == ApplicationSort.FileSize; - public bool IsSortedByPath => SortMode == ApplicationSort.Path; + public int ListItemSelectorSize + { + get + { + switch (ConfigurationState.Instance.Ui.GridSize) + { + case 1: + return 78; + case 2: + return 100; + case 3: + return 120; + case 4: + return 140; + default: + return 16; + } + } + } + + public int GridItemSelectorSize + { + get + { + switch (ConfigurationState.Instance.Ui.GridSize) + { + case 1: + return 120; + case 2: + return ShowNames ? 210 : 150; + case 3: + return ShowNames ? 240 : 180; + case 4: + return ShowNames ? 280 : 220; + default: + return 16; + } + } + } + + public int GridSizeScale + { + get => ConfigurationState.Instance.Ui.GridSize; + set + { + ConfigurationState.Instance.Ui.GridSize.Value = value; + + if (value < 2) + { + ShowNames = false; + } + + OnPropertyChanged(); + OnPropertyChanged(nameof(IsGridSmall)); + OnPropertyChanged(nameof(IsGridMedium)); + OnPropertyChanged(nameof(IsGridLarge)); + OnPropertyChanged(nameof(IsGridHuge)); + OnPropertyChanged(nameof(ListItemSelectorSize)); + OnPropertyChanged(nameof(GridItemSelectorSize)); + OnPropertyChanged(nameof(ShowNames)); + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + } public string SortName { @@ -632,15 +799,15 @@ namespace Ryujinx.Ava.UI.ViewModels { return SortMode switch { - ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], - ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper], - ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed], + ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication], + ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper], + ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed], ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed], - ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension], - ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize], - ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath], - ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite], - _ => string.Empty, + ApplicationSort.FileType => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension], + ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize], + ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath], + ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite], + _ => string.Empty, }; } } @@ -662,9 +829,10 @@ namespace Ryujinx.Ava.UI.ViewModels public KeyGesture ShowUiKey { - get => KeyGesture.Parse(_showUikey); set + get => KeyGesture.Parse(_showUiKey); + set { - _showUikey = value.ToString(); + _showUiKey = value.ToString(); OnPropertyChanged(); } @@ -672,9 +840,10 @@ namespace Ryujinx.Ava.UI.ViewModels public KeyGesture ScreenshotKey { - get => KeyGesture.Parse(_screenshotkey); set + get => KeyGesture.Parse(_screenshotKey); + set { - _screenshotkey = value.ToString(); + _screenshotKey = value.ToString(); OnPropertyChanged(); } @@ -690,222 +859,393 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public bool IsGridSmall => ConfigurationState.Instance.Ui.GridSize == 1; + public ContentManager ContentManager { get; private set; } + public ApplicationLibrary ApplicationLibrary { get; private set; } + public VirtualFileSystem VirtualFileSystem { get; private set; } + public AccountManager AccountManager { get; private set; } + public Ryujinx.Input.HLE.InputManager InputManager { get; private set; } + public UserChannelPersistence UserChannelPersistence { get; private set; } + public Action<bool> ShowLoading { get; private set; } + public Action<bool> SwitchToGameControl { get; private set; } + public Action<Control> SetMainContent { get; private set; } + public TopLevel TopLevel { get; private set; } + public RendererHost RendererControl { get; private set; } + public bool IsClosing { get; set; } + public LibHacHorizonManager LibHacHorizonManager { get; internal set; } + public IHostUiHandler UiHandler { get; internal set; } + public bool IsSortedByFavorite => SortMode == ApplicationSort.Favorite; + public bool IsSortedByTitle => SortMode == ApplicationSort.Title; + public bool IsSortedByDeveloper => SortMode == ApplicationSort.Developer; + public bool IsSortedByLastPlayed => SortMode == ApplicationSort.LastPlayed; + public bool IsSortedByTimePlayed => SortMode == ApplicationSort.TotalTimePlayed; + public bool IsSortedByType => SortMode == ApplicationSort.FileType; + public bool IsSortedBySize => SortMode == ApplicationSort.FileSize; + public bool IsSortedByPath => SortMode == ApplicationSort.Path; + public bool IsGridSmall => ConfigurationState.Instance.Ui.GridSize == 1; public bool IsGridMedium => ConfigurationState.Instance.Ui.GridSize == 2; - public bool IsGridLarge => ConfigurationState.Instance.Ui.GridSize == 3; - public bool IsGridHuge => ConfigurationState.Instance.Ui.GridSize == 4; + public bool IsGridLarge => ConfigurationState.Instance.Ui.GridSize == 3; + public bool IsGridHuge => ConfigurationState.Instance.Ui.GridSize == 4; - public int GridSizeScale +#endregion + +#region PrivateMethods + + private IComparer<ApplicationData> GetComparer() { - get => ConfigurationState.Instance.Ui.GridSize; - set + return SortMode switch { - ConfigurationState.Instance.Ui.GridSize.Value = value; + ApplicationSort.LastPlayed => new Models.Generic.LastPlayedSortComparer(IsAscending), + ApplicationSort.FileSize => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileSizeBytes) + : SortExpressionComparer<ApplicationData>.Descending(app => app.FileSizeBytes), + ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TimePlayedNum) + : SortExpressionComparer<ApplicationData>.Descending(app => app.TimePlayedNum), + ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName) + : SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName), + ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Favorite) + : SortExpressionComparer<ApplicationData>.Descending(app => app.Favorite), + ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer) + : SortExpressionComparer<ApplicationData>.Descending(app => app.Developer), + ApplicationSort.FileType => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension) + : SortExpressionComparer<ApplicationData>.Descending(app => app.FileExtension), + ApplicationSort.Path => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Path) + : SortExpressionComparer<ApplicationData>.Descending(app => app.Path), + _ => null, + }; + } - if (value < 2) - { - ShowNames = false; - } + private void RefreshView() + { + RefreshGrid(); + } - OnPropertyChanged(); - OnPropertyChanged(nameof(IsGridSmall)); - OnPropertyChanged(nameof(IsGridMedium)); - OnPropertyChanged(nameof(IsGridLarge)); - OnPropertyChanged(nameof(IsGridHuge)); - OnPropertyChanged(nameof(ShowNames)); + private void RefreshGrid() + { + Applications.ToObservableChangeSet() + .Filter(Filter) + .Sort(GetComparer()) + .Bind(out _appsObservableList).AsObservableList(); - ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); - } + OnPropertyChanged(nameof(AppsObservableList)); } - public async void OpenAmiiboWindow() + private bool Filter(object arg) { - if (!_isAmiiboRequested) + if (arg is ApplicationData app) { - return; + return string.IsNullOrWhiteSpace(_searchText) || app.TitleName.ToLower().Contains(_searchText.ToLower()); } - if (_owner.AppHost.Device.System.SearchingForAmiibo(out int deviceId)) + return false; + } + + private async Task HandleFirmwareInstallation(string filename) + { + try { - string titleId = _owner.AppHost.Device.Application.TitleIdText.ToUpper(); - AmiiboWindow window = new(_showAll, _lastScannedAmiiboId, titleId); + SystemVersion firmwareVersion = ContentManager.VerifyFirmwarePackage(filename); + + if (firmwareVersion == null) + { + await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareNotFoundErrorMessage], filename)); + + return; + } + + string dialogTitle = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallTitle], firmwareVersion.VersionString); + + SystemVersion currentVersion = ContentManager.GetCurrentFirmwareVersion(); + + string dialogMessage = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallMessage], firmwareVersion.VersionString); + + if (currentVersion != null) + { + dialogMessage += string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallSubMessage], currentVersion.VersionString); + } + + dialogMessage += LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallConfirmMessage]; + + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + dialogTitle, + dialogMessage, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); - await window.ShowDialog(_owner); + UpdateWaitWindow waitingDialog = ContentDialogHelper.CreateWaitingDialog(dialogTitle, LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallWaitMessage]); - if (window.IsScanned) + if (result == UserResult.Yes) { - _showAll = window.ViewModel.ShowAllAmiibo; - _lastScannedAmiiboId = window.ScannedAmiibo.GetId(); + Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); + + Thread thread = new(() => + { + Dispatcher.UIThread.InvokeAsync(delegate + { + waitingDialog.Show(); + }); + + try + { + ContentManager.InstallFirmware(filename); + + Dispatcher.UIThread.InvokeAsync(async delegate + { + waitingDialog.Close(); + + string message = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallSuccessMessage], firmwareVersion.VersionString); + + await ContentDialogHelper.CreateInfoDialog(dialogTitle, message, LocaleManager.Instance[LocaleKeys.InputDialogOk], "", LocaleManager.Instance[LocaleKeys.RyujinxInfo]); + + Logger.Info?.Print(LogClass.Application, message); + + // Purge Applet Cache. + + DirectoryInfo miiEditorCacheFolder = new DirectoryInfo(Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache")); - _owner.AppHost.Device.System.ScanAmiibo(deviceId, _lastScannedAmiiboId, window.ViewModel.UseRandomUuid); + if (miiEditorCacheFolder.Exists) + { + miiEditorCacheFolder.Delete(true); + } + }); + } + catch (Exception ex) + { + Dispatcher.UIThread.InvokeAsync(async () => + { + waitingDialog.Close(); + + await ContentDialogHelper.CreateErrorDialog(ex.Message); + }); + } + finally + { + RefreshFirmwareStatus(); + } + }) { Name = "GUI.FirmwareInstallerThread" }; + + thread.Start(); } } - } + catch (LibHac.Common.Keys.MissingKeyException ex) + { + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + Logger.Error?.Print(LogClass.Application, ex.ToString()); - public void SetUiProgressHandlers(Switch emulationContext) - { - if (emulationContext.Application.DiskCacheLoadState != null) + async void Action() => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, (desktop.MainWindow as MainWindow)); + + Dispatcher.UIThread.Post(Action); + } + } + catch (Exception ex) { - emulationContext.Application.DiskCacheLoadState.StateChanged -= ProgressHandler; - emulationContext.Application.DiskCacheLoadState.StateChanged += ProgressHandler; + await ContentDialogHelper.CreateErrorDialog(ex.Message); } - - emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler; - emulationContext.Gpu.ShaderCacheStateChanged += ProgressHandler; } - private bool Filter(object arg) + private void ProgressHandler<T>(T state, int current, int total) where T : Enum { - if (arg is ApplicationData app) + Dispatcher.UIThread.Post((() => { - return string.IsNullOrWhiteSpace(_searchText) || app.TitleName.ToLower().Contains(_searchText.ToLower()); - } + ProgressMaximum = total; + ProgressValue = current; - return false; + switch (state) + { + case LoadState ptcState: + CacheLoadStatus = $"{current} / {total}"; + switch (ptcState) + { + case LoadState.Unloaded: + case LoadState.Loading: + LoadHeading = LocaleManager.Instance[LocaleKeys.CompilingPPTC]; + IsLoadingIndeterminate = false; + break; + case LoadState.Loaded: + LoadHeading = string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], TitleName); + IsLoadingIndeterminate = true; + CacheLoadStatus = ""; + break; + } + break; + case ShaderCacheLoadingState shaderCacheState: + CacheLoadStatus = $"{current} / {total}"; + switch (shaderCacheState) + { + case ShaderCacheLoadingState.Start: + case ShaderCacheLoadingState.Loading: + LoadHeading = LocaleManager.Instance[LocaleKeys.CompilingShaders]; + IsLoadingIndeterminate = false; + break; + case ShaderCacheLoadingState.Loaded: + LoadHeading = string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], TitleName); + IsLoadingIndeterminate = true; + CacheLoadStatus = ""; + break; + } + break; + default: + throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"); + } + })); } - private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e) + private void OpenSaveDirectory(in SaveDataFilter filter, ApplicationData data, ulong titleId) { - AddApplication(e.AppData); + ApplicationHelper.OpenSaveDir(in filter, titleId, data.ControlHolder, data.TitleName); } - private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e) + private async void ExtractLogo() { - StatusBarProgressValue = e.NumAppsLoaded; - StatusBarProgressMaximum = e.NumAppsFound; - - LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, StatusBarProgressValue, StatusBarProgressMaximum); - - Dispatcher.UIThread.Post(() => + var selection = SelectedApplication; + if (selection != null) { - if (e.NumAppsFound == 0) - { - _owner.LoadProgressBar.IsVisible = false; - } - - if (e.NumAppsLoaded == e.NumAppsFound) - { - _owner.LoadProgressBar.IsVisible = false; - } - }); + await ApplicationHelper.ExtractSection(NcaSectionType.Logo, selection.Path); + } } - public void AddApplication(ApplicationData applicationData) + private async void ExtractRomFs() { - Dispatcher.UIThread.InvokeAsync(() => + var selection = SelectedApplication; + if (selection != null) { - Applications.Add(applicationData); - }); + await ApplicationHelper.ExtractSection(NcaSectionType.Data, selection.Path); + } } - public async void LoadApplications() + private async void ExtractExeFs() { - await Dispatcher.UIThread.InvokeAsync(() => + var selection = SelectedApplication; + if (selection != null) { - Applications.Clear(); + await ApplicationHelper.ExtractSection(NcaSectionType.Code, selection.Path); + } + } - _owner.LoadProgressBar.IsVisible = true; - StatusBarProgressMaximum = 0; - StatusBarProgressValue = 0; + private void PrepareLoadScreen() + { + using MemoryStream stream = new(SelectedIcon); + using var gameIconBmp = SixLabors.ImageSharp.Image.Load<Bgra32>(stream); - LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0); - }); + var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp).ToPixel<Bgra32>(); + + const float colorMultiple = 0.5f; - ReloadGameList(); + Color progressFgColor = Color.FromRgb(dominantColor.R, dominantColor.G, dominantColor.B); + Color progressBgColor = Color.FromRgb( + (byte)(dominantColor.R * colorMultiple), + (byte)(dominantColor.G * colorMultiple), + (byte)(dominantColor.B * colorMultiple)); + + ProgressBarForegroundColor = new SolidColorBrush(progressFgColor); + ProgressBarBackgroundColor = new SolidColorBrush(progressBgColor); } - private void ReloadGameList() + private void InitializeGame() { - if (_isLoading) - { - return; - } + RendererControl.RendererInitialized += GlRenderer_Created; - _isLoading = true; + AppHost.StatusUpdatedEvent += Update_StatusBar; + AppHost.AppExit += AppHost_AppExit; - Thread thread = new(() => - { - ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs.Value, ConfigurationState.Instance.System.Language); + _rendererWaitEvent.WaitOne(); - _isLoading = false; - }) - { Name = "GUI.AppListLoadThread", Priority = ThreadPriority.AboveNormal }; + AppHost?.Start(); - thread.Start(); + AppHost?.DisposeContext(); } - public async void OpenFile() + private void HandleRelaunch() { - OpenFileDialog dialog = new() + if (UserChannelPersistence.PreviousIndex != -1 && UserChannelPersistence.ShouldRestart) { - Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle] - }; + UserChannelPersistence.ShouldRestart = false; - dialog.Filters.Add(new FileDialogFilter + Dispatcher.UIThread.Post(() => + { + LoadApplication(_currentEmulatedGamePath); + }); + } + else { - Name = LocaleManager.Instance[LocaleKeys.AllSupportedFormats], - Extensions = + // Otherwise, clear state. + UserChannelPersistence = new UserChannelPersistence(); + _currentEmulatedGamePath = null; + } + } + + private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) + { + if (ShowMenuAndStatusBar && !ShowLoadProgress) + { + Dispatcher.UIThread.InvokeAsync(() => { - "nsp", - "pfs0", - "xci", - "nca", - "nro", - "nso" - } - }); + Avalonia.Application.Current.Styles.TryGetResource(args.VSyncEnabled + ? "VsyncEnabled" + : "VsyncDisabled", out object color); - dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "PFS0", Extensions = { "pfs0" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "NCA", Extensions = { "nca" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "NRO", Extensions = { "nro" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "NSO", Extensions = { "nso" } }); + if (color is not null) + { + VsyncColor = new SolidColorBrush((Color)color); + } - string[] files = await dialog.ShowAsync(_owner); + DockedStatusText = args.DockedMode; + AspectRatioStatusText = args.AspectRatio; + GameStatusText = args.GameStatus; + VolumeStatusText = args.VolumeStatus; + FifoStatusText = args.FifoStatus; + GpuNameText = args.GpuName; + BackendText = args.GpuBackend; - if (files != null && files.Length > 0) - { - _owner.LoadApplication(files[0]); + ShowStatusSeparator = true; + }); } } - public async void OpenFolder() + private void GlRenderer_Created(object sender, EventArgs e) { - OpenFolderDialog dialog = new() - { - Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle] - }; + ShowLoading(false); - string folder = await dialog.ShowAsync(_owner); + _rendererWaitEvent.Set(); + } + +#endregion - if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder)) +#region PublicMethods + + public void SetUIProgressHandlers(Switch emulationContext) + { + if (emulationContext.Application.DiskCacheLoadState != null) { - _owner.LoadApplication(folder); + emulationContext.Application.DiskCacheLoadState.StateChanged -= ProgressHandler; + emulationContext.Application.DiskCacheLoadState.StateChanged += ProgressHandler; } + + emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler; + emulationContext.Gpu.ShaderCacheStateChanged += ProgressHandler; } public void LoadConfigurableHotKeys() { if (AvaloniaKeyboardMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUi, out var showUiKey)) { - ShowUiKey = new KeyGesture(showUiKey, KeyModifiers.None); + ShowUiKey = new KeyGesture(showUiKey); } if (AvaloniaKeyboardMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot, out var screenshotKey)) { - ScreenshotKey = new KeyGesture(screenshotKey, KeyModifiers.None); + ScreenshotKey = new KeyGesture(screenshotKey); } if (AvaloniaKeyboardMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause, out var pauseKey)) { - PauseKey = new KeyGesture(pauseKey, KeyModifiers.None); + PauseKey = new KeyGesture(pauseKey); } } public void TakeScreenshot() { - _owner.AppHost.ScreenshotRequested = true; + AppHost.ScreenshotRequested = true; } public void HideUi() @@ -923,13 +1263,36 @@ namespace Ryujinx.Ava.UI.ViewModels Glyph = Glyph.Grid; } - public void OpenMiiApplet() + public async void InstallFirmwareFromFile() { - string contentPath = _owner.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + OpenFileDialog dialog = new() { AllowMultiple = false }; + dialog.Filters.Add(new FileDialogFilter { Name = LocaleManager.Instance[LocaleKeys.FileDialogAllTypes], Extensions = { "xci", "zip" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "ZIP", Extensions = { "zip" } }); + + string[] file = await dialog.ShowAsync(desktop.MainWindow); - if (!string.IsNullOrWhiteSpace(contentPath)) + if (file != null && file.Length > 0) + { + await HandleFirmwareInstallation(file[0]); + } + } + } + + public async void InstallFirmwareFromFolder() + { + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - _owner.LoadApplication(contentPath, false, "Mii Applet"); + OpenFolderDialog dialog = new(); + + string folder = await dialog.ShowAsync(desktop.MainWindow); + + if (!string.IsNullOrEmpty(folder)) + { + await HandleFirmwareInstallation(folder); + } } } @@ -947,39 +1310,6 @@ namespace Ryujinx.Ava.UI.ViewModels OpenHelper.OpenFolder(logPath); } - public void ToggleFullscreen() - { - if (Environment.TickCount64 - _lastFullscreenToggle < HotKeyPressDelayMs) - { - return; - } - - _lastFullscreenToggle = Environment.TickCount64; - - if (_owner.WindowState == WindowState.FullScreen) - { - _owner.WindowState = WindowState.Normal; - - if (IsGameRunning) - { - ShowMenuAndStatusBar = true; - } - } - else - { - _owner.WindowState = WindowState.FullScreen; - - if (IsGameRunning) - { - ShowMenuAndStatusBar = false; - } - } - - OnPropertyChanged(nameof(IsFullScreen)); - } - - public bool IsFullScreen => _owner.WindowState == WindowState.FullScreen; - public void ToggleDockMode() { if (IsGameRunning) @@ -990,7 +1320,7 @@ namespace Ryujinx.Ava.UI.ViewModels public async void ExitCurrentState() { - if (_owner.WindowState == WindowState.FullScreen) + if (WindowState == WindowState.FullScreen) { ToggleFullscreen(); } @@ -998,146 +1328,19 @@ namespace Ryujinx.Ava.UI.ViewModels { await Task.Delay(100); - _owner.AppHost?.ShowExitPrompt(); + AppHost?.ShowExitPrompt(); } } - public async void OpenSettings() - { - _owner.SettingsWindow = new(_owner.VirtualFileSystem, _owner.ContentManager); - - await _owner.SettingsWindow.ShowDialog(_owner); - - LoadConfigurableHotKeys(); - } - - public async void ManageProfiles() - { - await NavigationDialogHost.Show(_owner.AccountManager, _owner.ContentManager, _owner.VirtualFileSystem, _owner.LibHacHorizonManager.RyujinxClient); - } - - public async void OpenAboutWindow() - { - await AboutWindow.Show(); - } - public void ChangeLanguage(object obj) { LocaleManager.Instance.LoadDefaultLanguage(); LocaleManager.Instance.LoadLanguage((string)obj); } - private void ProgressHandler<T>(T state, int current, int total) where T : Enum - { - try - { - ProgressMaximum = total; - ProgressValue = current; - - switch (state) - { - case LoadState ptcState: - CacheLoadStatus = $"{current} / {total}"; - switch (ptcState) - { - case LoadState.Unloaded: - case LoadState.Loading: - LoadHeading = LocaleManager.Instance[LocaleKeys.CompilingPPTC]; - IsLoadingIndeterminate = false; - break; - case LoadState.Loaded: - LoadHeading = string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], TitleName); - IsLoadingIndeterminate = true; - CacheLoadStatus = ""; - break; - } - break; - case ShaderCacheLoadingState shaderCacheState: - CacheLoadStatus = $"{current} / {total}"; - switch (shaderCacheState) - { - case ShaderCacheLoadingState.Start: - case ShaderCacheLoadingState.Loading: - LoadHeading = LocaleManager.Instance[LocaleKeys.CompilingShaders]; - IsLoadingIndeterminate = false; - break; - case ShaderCacheLoadingState.Loaded: - LoadHeading = string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], TitleName); - IsLoadingIndeterminate = true; - CacheLoadStatus = ""; - break; - } - break; - default: - throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"); - } - } - catch (Exception) { } - } - - public void OpenUserSaveDirectory() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - Task.Run(() => - { - if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - Dispatcher.UIThread.Post(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - }); - - return; - } - - UserId userId = new((ulong)_owner.AccountManager.LastOpenedUser.UserId.High, (ulong)_owner.AccountManager.LastOpenedUser.UserId.Low); - SaveDataFilter saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveType: default, userId, saveDataId: default, index: default); - OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber); - }); - } - } - - public void ToggleFavorite() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - selection.Favorite = !selection.Favorite; - - ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata => - { - appMetadata.Favorite = selection.Favorite; - }); - - RefreshView(); - } - } - - public void OpenModsDirectory() - { - ApplicationData selection = SelectedApplication; - if (selection != null) - { - string modsBasePath = _owner.VirtualFileSystem.ModLoader.GetModsBasePath(); - string titleModsPath = _owner.VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, selection.TitleId); - - OpenHelper.OpenFolder(titleModsPath); - } - } - - public void OpenSdModsDirectory() + public async void ManageProfiles() { - ApplicationData selection = SelectedApplication; - - if (selection != null) - { - string sdModsBasePath = _owner.VirtualFileSystem.ModLoader.GetSdModsBasePath(); - string titleModsPath = _owner.VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, selection.TitleId); - - OpenHelper.OpenFolder(titleModsPath); - } + await NavigationDialogHost.Show(AccountManager, ContentManager, VirtualFileSystem, LibHacHorizonManager.RyujinxClient); } public void OpenPtcDirectory() @@ -1145,8 +1348,8 @@ namespace Ryujinx.Ava.UI.ViewModels ApplicationData selection = SelectedApplication; if (selection != null) { - string ptcDir = Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu"); - string mainPath = Path.Combine(ptcDir, "0"); + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu"); + string mainPath = Path.Combine(ptcDir, "0"); string backupPath = Path.Combine(ptcDir, "1"); if (!Directory.Exists(ptcDir)) @@ -1165,7 +1368,7 @@ namespace Ryujinx.Ava.UI.ViewModels ApplicationData selection = SelectedApplication; if (selection != null) { - DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "0")); + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "0")); DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "1")); // FIXME: Found a way to reproduce the bold effect on the title name (fork?). @@ -1222,7 +1425,7 @@ namespace Ryujinx.Ava.UI.ViewModels public void SimulateWakeUpMessage() { - _owner.AppHost.Device.System.SimulateWakeUpMessage(); + AppHost.Device.System.SimulateWakeUpMessage(); } public async void PurgeShaderCache() @@ -1240,7 +1443,7 @@ namespace Ryujinx.Ava.UI.ViewModels LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); List<DirectoryInfo> oldCacheDirectories = new(); - List<FileInfo> newCacheFiles = new(); + List<FileInfo> newCacheFiles = new(); if (shaderCacheDir.Exists) { @@ -1278,58 +1481,73 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public async void CheckForUpdates() - { - if (Updater.CanUpdate(true, _owner)) - { - await Updater.BeginParse(_owner, true); - } - } - - public async void OpenTitleUpdateManager() + public void OpenDeviceSaveDirectory() { ApplicationData selection = SelectedApplication; if (selection != null) { - await new TitleUpdateWindow(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(_owner); + Task.Run(() => + { + if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) + { + async void Action() + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); + } + + Dispatcher.UIThread.Post(Action); + + return; + } + + var saveDataFilter = SaveDataFilter.Make(titleIdNumber, SaveDataType.Device, userId: default, saveDataId: default, index: default); + OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber); + }); } } - public async void OpenDownloadableContentManager() + public void OpenBcatSaveDirectory() { ApplicationData selection = SelectedApplication; if (selection != null) { - await new DownloadableContentManagerWindow(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(_owner); + Task.Run(() => + { + if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) + { + async void Action() + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); + } + + Dispatcher.UIThread.Post(Action); + + return; + } + + var saveDataFilter = SaveDataFilter.Make(titleIdNumber, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); + OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber); + }); } } - public async void OpenCheatManager() + public void ToggleFavorite() { ApplicationData selection = SelectedApplication; if (selection != null) { - await new CheatWindow(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName).ShowDialog(_owner); - } - } - - public async void OpenCheatManagerForCurrentApp() - { - if (!IsGameRunning) - { - return; - } + selection.Favorite = !selection.Favorite; - ApplicationLoader application = _owner.AppHost.Device.Application; - if (application != null) - { - await new CheatWindow(_owner.VirtualFileSystem, application.TitleIdText, application.TitleName).ShowDialog(_owner); + ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata => + { + appMetadata.Favorite = selection.Favorite; + }); - _owner.AppHost.Device.EnableCheats(); + RefreshView(); } } - public void OpenDeviceSaveDirectory() + public void OpenUserSaveDirectory() { ApplicationData selection = SelectedApplication; if (selection != null) @@ -1338,206 +1556,396 @@ namespace Ryujinx.Ava.UI.ViewModels { if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) { - Dispatcher.UIThread.Post(async () => + async void Action() { await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - }); + } + + Dispatcher.UIThread.Post(Action); return; } - var saveDataFilter = SaveDataFilter.Make(titleIdNumber, SaveDataType.Device, userId: default, saveDataId: default, index: default); + UserId userId = new((ulong)AccountManager.LastOpenedUser.UserId.High, (ulong)AccountManager.LastOpenedUser.UserId.Low); + SaveDataFilter saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveType: default, userId, saveDataId: default, index: default); OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber); }); } } - public void OpenBcatSaveDirectory() + public void OpenModsDirectory() { ApplicationData selection = SelectedApplication; if (selection != null) { - Task.Run(() => - { - if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - Dispatcher.UIThread.Post(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - }); + string modsBasePath = VirtualFileSystem.ModLoader.GetModsBasePath(); + string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, selection.TitleId); - return; - } - - var saveDataFilter = SaveDataFilter.Make(titleIdNumber, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); - OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber); - }); + OpenHelper.OpenFolder(titleModsPath); } } - private void OpenSaveDirectory(in SaveDataFilter filter, ApplicationData data, ulong titleId) + public void OpenSdModsDirectory() { - ApplicationHelper.OpenSaveDir(in filter, titleId, data.ControlHolder, data.TitleName); + ApplicationData selection = SelectedApplication; + + if (selection != null) + { + string sdModsBasePath = VirtualFileSystem.ModLoader.GetSdModsBasePath(); + string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, selection.TitleId); + + OpenHelper.OpenFolder(titleModsPath); + } } - private async void ExtractLogo() + public async void OpenTitleUpdateManager() { - var selection = SelectedApplication; + ApplicationData selection = SelectedApplication; if (selection != null) { - await ApplicationHelper.ExtractSection(NcaSectionType.Logo, selection.Path); + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + await new TitleUpdateWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow); + } } } - private async void ExtractRomFs() + public async void OpenDownloadableContentManager() { - var selection = SelectedApplication; + ApplicationData selection = SelectedApplication; if (selection != null) { - await ApplicationHelper.ExtractSection(NcaSectionType.Data, selection.Path); + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow); + } } } - private async void ExtractExeFs() + public async void OpenCheatManager() { - var selection = SelectedApplication; + ApplicationData selection = SelectedApplication; if (selection != null) { - await ApplicationHelper.ExtractSection(NcaSectionType.Code, selection.Path); + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + await new CheatWindow(VirtualFileSystem, selection.TitleId, selection.TitleName).ShowDialog(desktop.MainWindow); + } } } - public void CloseWindow() + public async void LoadApplications() { - _owner.Close(); + await Dispatcher.UIThread.InvokeAsync(() => + { + Applications.Clear(); + + StatusBarVisible = true; + StatusBarProgressMaximum = 0; + StatusBarProgressValue = 0; + + LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0); + }); + + ReloadGameList?.Invoke(); } - private async Task HandleFirmwareInstallation(string filename) + public async void OpenFile() { - try + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - SystemVersion firmwareVersion = _owner.ContentManager.VerifyFirmwarePackage(filename); + OpenFileDialog dialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle] + }; - if (firmwareVersion == null) + dialog.Filters.Add(new FileDialogFilter { - await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareNotFoundErrorMessage], filename)); + Name = LocaleManager.Instance[LocaleKeys.AllSupportedFormats], + Extensions = + { + "nsp", + "pfs0", + "xci", + "nca", + "nro", + "nso" + } + }); - return; - } + dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "PFS0", Extensions = { "pfs0" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "NCA", Extensions = { "nca" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "NRO", Extensions = { "nro" } }); + dialog.Filters.Add(new FileDialogFilter { Name = "NSO", Extensions = { "nso" } }); - string dialogTitle = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallTitle], firmwareVersion.VersionString); + string[] files = await dialog.ShowAsync(desktop.MainWindow); + + if (files != null && files.Length > 0) + { + LoadApplication(files[0]); + } + } + } - SystemVersion currentVersion = _owner.ContentManager.GetCurrentFirmwareVersion(); + public async void OpenFolder() + { + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + OpenFolderDialog dialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle] + }; - string dialogMessage = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallMessage], firmwareVersion.VersionString); + string folder = await dialog.ShowAsync(desktop.MainWindow); - if (currentVersion != null) + if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder)) { - dialogMessage += string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallSubMessage], currentVersion.VersionString); + LoadApplication(folder); } + } + } - dialogMessage += LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallConfirmMessage]; + public async void LoadApplication(string path, bool startFullscreen = false, string titleName = "") + { + if (AppHost != null) + { + await ContentDialogHelper.CreateInfoDialog( + LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedMessage], + LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedSubMessage], + LocaleManager.Instance[LocaleKeys.InputDialogOk], + "", + LocaleManager.Instance[LocaleKeys.RyujinxInfo]); - UserResult result = await ContentDialogHelper.CreateConfirmationDialog( - dialogTitle, - dialogMessage, - LocaleManager.Instance[LocaleKeys.InputDialogYes], - LocaleManager.Instance[LocaleKeys.InputDialogNo], - LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + return; + } - UpdateWaitWindow waitingDialog = ContentDialogHelper.CreateWaitingDialog(dialogTitle, LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallWaitMessage]); +#if RELEASE + await PerformanceCheck(); +#endif - if (result == UserResult.Yes) + Logger.RestartTime(); + + if (SelectedIcon == null) + { + SelectedIcon = ApplicationLibrary.GetApplicationIcon(path); + } + + PrepareLoadScreen(); + + RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel); + if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl) + { + RendererControl.CreateOpenGL(); + } + else + { + RendererControl.CreateVulkan(); + } + + AppHost = new AppHost( + RendererControl, + InputManager, + path, + VirtualFileSystem, + ContentManager, + AccountManager, + UserChannelPersistence, + this, + TopLevel); + + async void Action() + { + if (!await AppHost.LoadGuestApplication()) { - Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); + AppHost.DisposeContext(); + AppHost = null; - Thread thread = new(() => - { - Dispatcher.UIThread.InvokeAsync(delegate - { - waitingDialog.Show(); - }); + return; + } - try - { - _owner.ContentManager.InstallFirmware(filename); + CanUpdate = false; + LoadHeading = string.IsNullOrWhiteSpace(titleName) ? string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], AppHost.Device.Application.TitleName) : titleName; + TitleName = string.IsNullOrWhiteSpace(titleName) ? AppHost.Device.Application.TitleName : titleName; - Dispatcher.UIThread.InvokeAsync(async delegate - { - waitingDialog.Close(); + SwitchToRenderer(startFullscreen); - string message = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallSuccessMessage], firmwareVersion.VersionString); + _currentEmulatedGamePath = path; - await ContentDialogHelper.CreateInfoDialog(dialogTitle, message, LocaleManager.Instance[LocaleKeys.InputDialogOk], "", LocaleManager.Instance[LocaleKeys.RyujinxInfo]); + Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; + gameThread.Start(); + } - Logger.Info?.Print(LogClass.Application, message); + Dispatcher.UIThread.Post(Action); + } - // Purge Applet Cache. + public void SwitchToRenderer(bool startFullscreen) + { + Dispatcher.UIThread.Post(() => + { + SwitchToGameControl(startFullscreen); - DirectoryInfo miiEditorCacheFolder = new DirectoryInfo(Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache")); + SetMainContent(RendererControl); - if (miiEditorCacheFolder.Exists) - { - miiEditorCacheFolder.Delete(true); - } - }); - } - catch (Exception ex) - { - Dispatcher.UIThread.InvokeAsync(async () => - { - waitingDialog.Close(); + RendererControl.Focus(); + }); + } - await ContentDialogHelper.CreateErrorDialog(ex.Message); - }); - } - finally - { - _owner.RefreshFirmwareStatus(); - } - }); + public void UpdateGameMetadata(string titleId) + { + ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => + { + if (DateTime.TryParse(appMetadata.LastPlayed, out DateTime lastPlayedDateTime)) + { + double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds; - thread.Name = "GUI.FirmwareInstallerThread"; - thread.Start(); + appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); } + }); + } + + public void RefreshFirmwareStatus() + { + SystemVersion version = null; + try + { + version = ContentManager.GetCurrentFirmwareVersion(); } - catch (LibHac.Common.Keys.MissingKeyException ex) + catch (Exception) { } + + bool hasApplet = false; + + if (version != null) { - Logger.Error?.Print(LogClass.Application, ex.ToString()); + LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion, + version.VersionString); - Dispatcher.UIThread.Post(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, _owner)); + hasApplet = version.Major > 3; } - catch (Exception ex) + else { - await ContentDialogHelper.CreateErrorDialog(ex.Message); + LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion, "0.0"); } + + IsAppletMenuActive = hasApplet; } - public async void InstallFirmwareFromFile() + public void AppHost_AppExit(object sender, EventArgs e) { - OpenFileDialog dialog = new() { AllowMultiple = false }; - dialog.Filters.Add(new FileDialogFilter { Name = LocaleManager.Instance[LocaleKeys.FileDialogAllTypes], Extensions = { "xci", "zip" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "XCI", Extensions = { "xci" } }); - dialog.Filters.Add(new FileDialogFilter { Name = "ZIP", Extensions = { "zip" } }); + if (IsClosing) + { + return; + } - string[] file = await dialog.ShowAsync(_owner); + IsGameRunning = false; - if (file != null && file.Length > 0) + Dispatcher.UIThread.InvokeAsync(() => { - await HandleFirmwareInstallation(file[0]); + ShowMenuAndStatusBar = true; + ShowContent = true; + ShowLoadProgress = false; + IsLoadingIndeterminate = false; + CanUpdate = true; + Cursor = Cursor.Default; + + SetMainContent(null); + + AppHost = null; + + HandleRelaunch(); + }); + + RendererControl.RendererInitialized -= GlRenderer_Created; + RendererControl = null; + + SelectedIcon = null; + + Dispatcher.UIThread.InvokeAsync(() => + { + Title = $"Ryujinx {Program.Version}"; + }); + } + + public void ToggleFullscreen() + { + if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs) + { + return; } + + LastFullscreenToggle = Environment.TickCount64; + + if (WindowState == WindowState.FullScreen) + { + WindowState = WindowState.Normal; + + if (IsGameRunning) + { + ShowMenuAndStatusBar = true; + } + } + else + { + WindowState = WindowState.FullScreen; + + if (IsGameRunning) + { + ShowMenuAndStatusBar = false; + } + } + + IsFullScreen = WindowState == WindowState.FullScreen; } - public async void InstallFirmwareFromFolder() + public static void SaveConfig() { - OpenFolderDialog dialog = new(); + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + + public static async Task PerformanceCheck() + { + if (ConfigurationState.Instance.Logger.EnableTrace.Value) + { + string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledMessage]; + string secondaryMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledConfirmMessage]; + + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + mainMessage, + secondaryMessage, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result != UserResult.Yes) + { + ConfigurationState.Instance.Logger.EnableTrace.Value = false; - string folder = await dialog.ShowAsync(_owner); + SaveConfig(); + } + } - if (!string.IsNullOrWhiteSpace(folder)) + if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value)) { - await HandleFirmwareInstallation(folder); + string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledMessage]; + string secondaryMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledConfirmMessage]; + + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + mainMessage, + secondaryMessage, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result != UserResult.Yes) + { + ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = ""; + + SaveConfig(); + } } } + +#endregion } }
\ No newline at end of file |