diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json index 24f44deeb2..ba5af264ba 100644 --- a/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/Ryujinx.Ava/Assets/Locales/en_US.json @@ -524,7 +524,7 @@ "UserErrorUndefinedDescription": "An undefined error occured! This shouldn't happen, please contact a dev!", "OpenSetupGuideMessage": "Open the Setup Guide", "NoUpdate": "No Update", - "TitleUpdateVersionLabel": "Version {0} - {1}", + "TitleUpdateVersionLabel": "Version {0}", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Confirmation", "FileDialogAllTypes": "All types", @@ -585,7 +585,7 @@ "UserProfilesSetProfileImage": "Set Profile Image", "UserProfileEmptyNameError": "Name is required", "UserProfileNoImageError": "Profile image must be set", - "GameUpdateWindowHeading": "{0} Update(s) available for {1} ({2})", + "GameUpdateWindowHeading": "Manage Updates for {0} ({1})", "SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:", "SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:", "UserProfilesName": "Name:", diff --git a/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs b/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs index c3ba623017..c57b3a26a7 100644 --- a/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs +++ b/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs @@ -3,23 +3,17 @@ using Ryujinx.Ava.Common.Locale; namespace Ryujinx.Ava.UI.Models { - internal class TitleUpdateModel + public class TitleUpdateModel { - public bool IsEnabled { get; set; } - public bool IsNoUpdate { get; } public ApplicationControlProperty Control { get; } public string Path { get; } - public string Label => IsNoUpdate - ? LocaleManager.Instance[LocaleKeys.NoUpdate] - : string.Format(LocaleManager.Instance[LocaleKeys.TitleUpdateVersionLabel], Control.DisplayVersionString.ToString(), - Path); + public string Label => string.Format(LocaleManager.Instance[LocaleKeys.TitleUpdateVersionLabel], Control.DisplayVersionString.ToString()); - public TitleUpdateModel(ApplicationControlProperty control, string path, bool isNoUpdate = false) + public TitleUpdateModel(ApplicationControlProperty control, string path) { Control = control; Path = path; - IsNoUpdate = isNoUpdate; } } } \ No newline at end of file diff --git a/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index f86cda21a2..2954021553 100644 --- a/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -1601,13 +1601,9 @@ namespace Ryujinx.Ava.UI.ViewModels public async void OpenTitleUpdateManager() { - ApplicationData selection = SelectedApplication; - if (selection != null) + if (SelectedApplication != null) { - if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - await new TitleUpdateWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow); - } + await TitleUpdateWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName); } } diff --git a/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs new file mode 100644 index 0000000000..131ebd25b8 --- /dev/null +++ b/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs @@ -0,0 +1,226 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ns; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using SpanHelpers = LibHac.Common.SpanHelpers; +using Path = System.IO.Path; + +namespace Ryujinx.Ava.UI.ViewModels; + +public class TitleUpdateViewModel : BaseModel +{ + public TitleUpdateMetadata _titleUpdateWindowData; + public readonly string _titleUpdateJsonPath; + private VirtualFileSystem _virtualFileSystem { get; } + private ulong _titleId { get; } + private string _titleName { get; } + + private AvaloniaList<TitleUpdateModel> _titleUpdates = new(); + private AvaloniaList<object> _views = new(); + private object _selectedUpdate; + + public AvaloniaList<TitleUpdateModel> TitleUpdates + { + get => _titleUpdates; + set + { + _titleUpdates = value; + OnPropertyChanged(); + } + } + + public AvaloniaList<object> Views + { + get => _views; + set + { + _views = value; + OnPropertyChanged(); + } + } + + public object SelectedUpdate + { + get => _selectedUpdate; + set + { + _selectedUpdate = value; + OnPropertyChanged(); + } + } + + public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + _virtualFileSystem = virtualFileSystem; + + _titleId = titleId; + _titleName = titleName; + + _titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); + + try + { + _titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {_titleId} at {_titleUpdateJsonPath}"); + + _titleUpdateWindowData = new TitleUpdateMetadata + { + Selected = "", + Paths = new List<string>() + }; + } + + LoadUpdates(); + } + + private void LoadUpdates() + { + foreach (string path in _titleUpdateWindowData.Paths) + { + AddUpdate(path); + } + + TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected, null); + + SelectedUpdate = selected; + + SortUpdates(); + } + + public void SortUpdates() + { + var list = TitleUpdates.ToList(); + + list.Sort((first, second) => + { + if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString())) + { + return -1; + } + else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString())) + { + return 1; + } + + return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1; + }); + + Views.Clear(); + Views.Add(new BaseModel()); + Views.AddRange(list); + + if (SelectedUpdate == null) + { + SelectedUpdate = Views[0]; + } + else if (!TitleUpdates.Contains(SelectedUpdate)) + { + if (Views.Count > 1) + { + SelectedUpdate = Views[1]; + } + else + { + SelectedUpdate = Views[0]; + } + } + } + + private void AddUpdate(string path) + { + if (File.Exists(path) && TitleUpdates.All(x => x.Path != path)) + { + using FileStream file = new(path, FileMode.Open, FileAccess.Read); + + try + { + (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0); + + if (controlNca != null && patchNca != null) + { + ApplicationControlProperty controlData = new(); + + using UniqueRef<IFile> nacpFile = new(); + + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + + TitleUpdates.Add(new TitleUpdateModel(controlData, path)); + } + else + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]); + }); + } + } + catch (Exception ex) + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogDlcLoadNcaErrorMessage], ex.Message, path)); + }); + } + } + } + + public void RemoveUpdate(TitleUpdateModel update) + { + TitleUpdates.Remove(update); + + SortUpdates(); + } + + public async void Add() + { + OpenFileDialog dialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle], + AllowMultiple = true + }; + + dialog.Filters.Add(new FileDialogFilter + { + Name = "NSP", + Extensions = { "nsp" } + }); + + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + string[] files = await dialog.ShowAsync(desktop.MainWindow); + + if (files != null) + { + foreach (string file in files) + { + AddUpdate(file); + } + } + } + + SortUpdates(); + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml index b4f2e1014e..ec931dd965 100644 --- a/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml +++ b/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml @@ -107,6 +107,7 @@ VerticalAlignment="Stretch"> <ListBox Name="SaveList" + VirtualizationMode="None" Items="{Binding Views}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> @@ -116,6 +117,9 @@ <Setter Property="Margin" Value="5" /> <Setter Property="CornerRadius" Value="4" /> </Style> + <Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator"> + <Setter Property="IsVisible" Value="False" /> + </Style> </ListBox.Styles> <ListBox.ItemTemplate> <DataTemplate x:DataType="models:SaveModel"> diff --git a/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml index 5a69be9b09..e985803863 100644 --- a/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml +++ b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml @@ -1,115 +1,135 @@ -<window:StyleableWindow +<UserControl x:Class="Ryujinx.Ava.UI.Windows.TitleUpdateWindow" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" - Width="600" - Height="400" - MinWidth="600" - MinHeight="400" - MaxWidth="600" - MaxHeight="400" - SizeToContent="Height" - WindowStartupLocation="CenterOwner" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + Width="500" + Height="300" mc:Ignorable="d" + x:CompileBindings="True" + x:DataType="viewModels:TitleUpdateViewModel" Focusable="True"> - <Grid Margin="15"> + <Grid> <Grid.RowDefinitions> - <RowDefinition Height="Auto" /> - <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> - <TextBlock - Name="Heading" - Grid.Row="1" - MaxWidth="500" - Margin="20,15,20,20" - HorizontalAlignment="Center" - VerticalAlignment="Center" - LineHeight="18" - TextAlignment="Center" - TextWrapping="Wrap" /> <Border - Grid.Row="2" - Margin="5" + Grid.Row="0" + Margin="0 0 0 24" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - BorderBrush="Gray" - BorderThickness="1"> - <ScrollViewer - VerticalAlignment="Stretch" - HorizontalScrollBarVisibility="Auto" - VerticalScrollBarVisibility="Auto"> - <ItemsControl - Margin="10" - HorizontalAlignment="Stretch" - VerticalAlignment="Stretch" - Items="{Binding _titleUpdates}"> - <ItemsControl.ItemTemplate> - <DataTemplate> - <RadioButton - Padding="8,0" - VerticalContentAlignment="Center" - GroupName="Update" - IsChecked="{Binding IsEnabled, Mode=TwoWay}"> - <Label - Margin="0" + BorderBrush="{DynamicResource AppListHoverBackgroundColor}" + BorderThickness="1" + CornerRadius="5" + Padding="2.5"> + <ListBox + VirtualizationMode="None" + Background="Transparent" + SelectedItem="{Binding SelectedUpdate, Mode=TwoWay}" + Items="{Binding Views}"> + <ListBox.DataTemplates> + <DataTemplate + DataType="models:TitleUpdateModel"> + <Panel Margin="10"> + <TextBlock + HorizontalAlignment="Left" + VerticalAlignment="Center" + TextWrapping="Wrap" + Text="{Binding Label}" /> + <StackPanel + Spacing="10" + Orientation="Horizontal" + HorizontalAlignment="Right"> + <Button VerticalAlignment="Center" - Content="{Binding Label}" - FontSize="12" /> - </RadioButton> - </DataTemplate> - </ItemsControl.ItemTemplate> - </ItemsControl> - </ScrollViewer> + HorizontalAlignment="Right" + Padding="10" + MinWidth="0" + MinHeight="0" + Click="OpenLocation"> + <ui:SymbolIcon + Symbol="OpenFolder" + HorizontalAlignment="Center" + VerticalAlignment="Center" /> + </Button> + <Button + VerticalAlignment="Center" + HorizontalAlignment="Right" + Padding="10" + MinWidth="0" + MinHeight="0" + Click="RemoveUpdate"> + <ui:SymbolIcon + Symbol="Cancel" + HorizontalAlignment="Center" + VerticalAlignment="Center" /> + </Button> + </StackPanel> + </Panel> + </DataTemplate> + <DataTemplate + DataType="viewModels:BaseModel"> + <Panel + Height="33" + Margin="10"> + <TextBlock + HorizontalAlignment="Left" + VerticalAlignment="Center" + TextWrapping="Wrap" + Text="{locale:Locale NoUpdate}" /> + </Panel> + </DataTemplate> + </ListBox.DataTemplates> + <ListBox.Styles> + <Style Selector="ListBoxItem"> + <Setter Property="Background" Value="Transparent" /> + </Style> + </ListBox.Styles> + </ListBox> </Border> - <DockPanel - Grid.Row="3" - Margin="0" + <Panel + Grid.Row="1" HorizontalAlignment="Stretch"> - <DockPanel Margin="0" HorizontalAlignment="Left"> + <StackPanel + Orientation="Horizontal" + Spacing="10" + HorizontalAlignment="Left"> <Button Name="AddButton" MinWidth="90" - Margin="5" - Command="{Binding Add}"> + Command="{ReflectionBinding Add}"> <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" /> </Button> - <Button - Name="RemoveButton" - MinWidth="90" - Margin="5" - Command="{Binding RemoveSelected}"> - <TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" /> - </Button> <Button Name="RemoveAllButton" MinWidth="90" - Margin="5" - Command="{Binding RemoveAll}"> + Click="RemoveAll"> <TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" /> </Button> - </DockPanel> - <DockPanel Margin="0" HorizontalAlignment="Right"> + </StackPanel> + <StackPanel + Orientation="Horizontal" + Spacing="10" + HorizontalAlignment="Right"> <Button Name="SaveButton" MinWidth="90" - Margin="5" - Command="{Binding Save}"> + Click="Save"> <TextBlock Text="{locale:Locale SettingsButtonSave}" /> </Button> <Button Name="CancelButton" MinWidth="90" - Margin="5" - Command="{Binding Close}"> + Click="Close"> <TextBlock Text="{locale:Locale InputDialogCancel}" /> </Button> - </DockPanel> - </DockPanel> + </StackPanel> + </Panel> </Grid> -</window:StyleableWindow> \ No newline at end of file +</UserControl> \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs index 848c5587f1..9d8b9a7b9c 100644 --- a/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs +++ b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs @@ -1,271 +1,116 @@ -using Avalonia.Collections; using Avalonia.Controls; -using Avalonia.Threading; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.FsSystem; -using LibHac.Ns; -using LibHac.Tools.FsSystem; -using LibHac.Tools.FsSystem.NcaUtils; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; -using Ryujinx.Common.Configuration; +using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS; -using System; -using System.Collections.Generic; +using Ryujinx.Ui.Common.Helper; using System.IO; -using System.Linq; using System.Text; -using Path = System.IO.Path; -using SpanHelpers = LibHac.Common.SpanHelpers; +using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; namespace Ryujinx.Ava.UI.Windows { - public partial class TitleUpdateWindow : StyleableWindow + public partial class TitleUpdateWindow : UserControl { - private readonly string _titleUpdateJsonPath; - private TitleUpdateMetadata _titleUpdateWindowData; - - private VirtualFileSystem _virtualFileSystem { get; } - private AvaloniaList<TitleUpdateModel> _titleUpdates { get; set; } - - private ulong _titleId { get; } - private string _titleName { get; } + public TitleUpdateViewModel ViewModel; public TitleUpdateWindow() { DataContext = this; InitializeComponent(); - - Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.UpdateWindowTitle]} - {_titleName} ({_titleId:X16})"; } public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) { - _virtualFileSystem = virtualFileSystem; - _titleUpdates = new AvaloniaList<TitleUpdateModel>(); - - _titleId = titleId; - _titleName = titleName; - - _titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); - - try - { - _titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath); - } - catch - { - _titleUpdateWindowData = new TitleUpdateMetadata - { - Selected = "", - Paths = new List<string>() - }; - } - - DataContext = this; + DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId, titleName); InitializeComponent(); - - Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.UpdateWindowTitle]} - {_titleName} ({_titleId:X16})"; - - LoadUpdates(); - PrintHeading(); } - private void PrintHeading() + public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) { - Heading.Text = string.Format(LocaleManager.Instance[LocaleKeys.GameUpdateWindowHeading], _titleUpdates.Count - 1, _titleName, _titleId.ToString("X16")); - } - - private void LoadUpdates() - { - _titleUpdates.Add(new TitleUpdateModel(default, string.Empty, true)); - - foreach (string path in _titleUpdateWindowData.Paths) + ContentDialog contentDialog = new() { - AddUpdate(path); - } - - if (_titleUpdateWindowData.Selected == "") - { - _titleUpdates[0].IsEnabled = true; - } - else - { - TitleUpdateModel selected = _titleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected); - List<TitleUpdateModel> enabled = _titleUpdates.Where(x => x.IsEnabled).ToList(); - - foreach (TitleUpdateModel update in enabled) - { - update.IsEnabled = false; - } - - if (selected != null) - { - selected.IsEnabled = true; - } - } - - SortUpdates(); - } - - private void AddUpdate(string path) - { - if (File.Exists(path) && !_titleUpdates.Any(x => x.Path == path)) - { - using FileStream file = new(path, FileMode.Open, FileAccess.Read); - - try - { - (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0); - - if (controlNca != null && patchNca != null) - { - ApplicationControlProperty controlData = new(); - - using UniqueRef<IFile> nacpFile = new(); - - controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - - _titleUpdates.Add(new TitleUpdateModel(controlData, path)); - - foreach (var update in _titleUpdates) - { - update.IsEnabled = false; - } - - _titleUpdates.Last().IsEnabled = true; - } - else - { - Dispatcher.UIThread.Post(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]); - }); - } - } - catch (Exception ex) - { - Dispatcher.UIThread.Post(async () => - { - await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogDlcLoadNcaErrorMessage], ex.Message, path)); - }); - } - } - } - - private void RemoveUpdates(bool removeSelectedOnly = false) - { - if (removeSelectedOnly) - { - _titleUpdates.RemoveAll(_titleUpdates.Where(x => x.IsEnabled && !x.IsNoUpdate).ToList()); - } - else - { - _titleUpdates.RemoveAll(_titleUpdates.Where(x => !x.IsNoUpdate).ToList()); - } - - _titleUpdates.FirstOrDefault(x => x.IsNoUpdate).IsEnabled = true; - - SortUpdates(); - PrintHeading(); - } - - public void RemoveSelected() - { - RemoveUpdates(true); - } - - public void RemoveAll() - { - RemoveUpdates(); - } - - public async void Add() - { - OpenFileDialog dialog = new() - { - Title = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle], - AllowMultiple = true + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = "", + Content = new TitleUpdateWindow(virtualFileSystem, titleId, titleName), + Title = string.Format(LocaleManager.Instance[LocaleKeys.GameUpdateWindowHeading], titleName, titleId.ToString("X16")) }; - dialog.Filters.Add(new FileDialogFilter - { - Name = "NSP", - Extensions = { "nsp" } - }); + Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); - string[] files = await dialog.ShowAsync(this); + contentDialog.Styles.Add(bottomBorder); - if (files != null) - { - foreach (string file in files) - { - AddUpdate(file); - } - } - - SortUpdates(); - PrintHeading(); + await ContentDialogHelper.ShowAsync(contentDialog); } - private void SortUpdates() + private void Close(object sender, RoutedEventArgs e) { - var list = _titleUpdates.ToList(); - - list.Sort((first, second) => - { - if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString())) - { - return -1; - } - else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString())) - { - return 1; - } - - return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1; - }); - - _titleUpdates.Clear(); - _titleUpdates.AddRange(list); + ((ContentDialog)Parent).Hide(); } - public void Save() + public void Save(object sender, RoutedEventArgs e) { - _titleUpdateWindowData.Paths.Clear(); + ViewModel._titleUpdateWindowData.Paths.Clear(); - _titleUpdateWindowData.Selected = ""; + ViewModel._titleUpdateWindowData.Selected = ""; - foreach (TitleUpdateModel update in _titleUpdates) + foreach (TitleUpdateModel update in ViewModel.TitleUpdates) { - _titleUpdateWindowData.Paths.Add(update.Path); + ViewModel._titleUpdateWindowData.Paths.Add(update.Path); - if (update.IsEnabled) + if (update == ViewModel.SelectedUpdate) { - _titleUpdateWindowData.Selected = update.Path; + ViewModel._titleUpdateWindowData.Selected = update.Path; } } - using (FileStream titleUpdateJsonStream = File.Create(_titleUpdateJsonPath, 4096, FileOptions.WriteThrough)) + using (FileStream titleUpdateJsonStream = File.Create(ViewModel._titleUpdateJsonPath, 4096, FileOptions.WriteThrough)) { - titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true))); + titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(ViewModel._titleUpdateWindowData, true))); } - if (Owner is MainWindow window) + if (VisualRoot is MainWindow window) { window.ViewModel.LoadApplications(); } - Close(); + ((ContentDialog)Parent).Hide(); + } + + private void OpenLocation(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is TitleUpdateModel model) + { + OpenHelper.LocateFile(model.Path); + } + } + } + + private void RemoveUpdate(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + ViewModel.RemoveUpdate((TitleUpdateModel)button.DataContext); + } + } + + private void RemoveAll(object sender, RoutedEventArgs e) + { + ViewModel.TitleUpdates.Clear(); + + ViewModel.SortUpdates(); } } } \ No newline at end of file diff --git a/Ryujinx.Ui.Common/Helper/OpenHelper.cs b/Ryujinx.Ui.Common/Helper/OpenHelper.cs index eaaa739246..3553489215 100644 --- a/Ryujinx.Ui.Common/Helper/OpenHelper.cs +++ b/Ryujinx.Ui.Common/Helper/OpenHelper.cs @@ -1,19 +1,75 @@ using Ryujinx.Common.Logging; using System; using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; namespace Ryujinx.Ui.Common.Helper { - public static class OpenHelper + public static partial class OpenHelper { + [LibraryImport("shell32.dll", SetLastError = true)] + public static partial int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr apidl, uint dwFlags); + + [LibraryImport("shell32.dll", SetLastError = true)] + public static partial void ILFree(IntPtr pidlList); + + [LibraryImport("shell32.dll", SetLastError = true)] + public static partial IntPtr ILCreateFromPathW([MarshalAs(UnmanagedType.LPWStr)] string pszPath); + public static void OpenFolder(string path) { - Process.Start(new ProcessStartInfo + if (Directory.Exists(path)) { - FileName = path, - UseShellExecute = true, - Verb = "open" - }); + Process.Start(new ProcessStartInfo + { + FileName = path, + UseShellExecute = true, + Verb = "open" + }); + } + else + { + Logger.Notice.Print(LogClass.Application, $"Directory \"{path}\" doesn't exist!"); + } + } + + public static void LocateFile(string path) + { + if (File.Exists(path)) + { + if (OperatingSystem.IsWindows()) + { + IntPtr pidlList = ILCreateFromPathW(path); + if (pidlList != IntPtr.Zero) + { + try + { + Marshal.ThrowExceptionForHR(SHOpenFolderAndSelectItems(pidlList, 0, IntPtr.Zero, 0)); + } + finally + { + ILFree(pidlList); + } + } + } + else if (OperatingSystem.IsMacOS()) + { + Process.Start("open", $"-R \"{path}\""); + } + else if (OperatingSystem.IsLinux()) + { + Process.Start("dbus-send", $"--session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:\"file://{path}\" string:\"\""); + } + else + { + OpenFolder(Path.GetDirectoryName(path)); + } + } + else + { + Logger.Notice.Print(LogClass.Application, $"File \"{path}\" doesn't exist!"); + } } public static void OpenUrl(string url)