mirror of
https://github.com/Ryujinx/Ryujinx.git
synced 2024-08-18 14:48:44 +02:00
Avalonia - Save Manager (#3476)
* Add save manager to account selector * add fallback to app metadata for titlename if app is not in gamelist * Allow recovering lost accounts
This commit is contained in:
parent
c25e8427aa
commit
d9053bbe37
20 changed files with 693 additions and 43 deletions
|
@ -596,7 +596,18 @@
|
||||||
"RyujinxUpdaterMessage": "Do you want to update Ryujinx to the latest version?",
|
"RyujinxUpdaterMessage": "Do you want to update Ryujinx to the latest version?",
|
||||||
"SettingsTabHotkeysVolumeUpHotkey": "Increase Volume:",
|
"SettingsTabHotkeysVolumeUpHotkey": "Increase Volume:",
|
||||||
"SettingsTabHotkeysVolumeDownHotkey": "Decrease Volume:",
|
"SettingsTabHotkeysVolumeDownHotkey": "Decrease Volume:",
|
||||||
"VolumeShort": "Vol",
|
|
||||||
"SettingsEnableMacroHLE": "Enable Macro HLE",
|
"SettingsEnableMacroHLE": "Enable Macro HLE",
|
||||||
"SettingsEnableMacroHLETooltip": "High-level emulation of GPU Macro code.\n\nImproves performance, but may cause graphical glitches in some games.\n\nLeave ON if unsure."
|
"SettingsEnableMacroHLETooltip": "High-level emulation of GPU Macro code.\n\nImproves performance, but may cause graphical glitches in some games.\n\nLeave ON if unsure.",
|
||||||
|
"VolumeShort": "Vol",
|
||||||
|
"UserProfilesManageSaves": "Manage Saves",
|
||||||
|
"DeleteUserSave": "Do you want to delete user save for this game?",
|
||||||
|
"IrreversibleActionNote": "This action is not reversible.",
|
||||||
|
"SaveManagerHeading": "Manage Saves for {0}",
|
||||||
|
"SaveManagerTitle": "Save Manager",
|
||||||
|
"Name": "Name",
|
||||||
|
"Size": "Size",
|
||||||
|
"Search": "Search",
|
||||||
|
"UserProfilesRecoverLostAccounts": "Recover Lost Accounts",
|
||||||
|
"Recover": "Recover",
|
||||||
|
"UserProfilesRecoverHeading" : "Saves were found for the following accounts"
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,9 @@
|
||||||
<SolidColorBrush x:Key="DataGridSelectionBackgroundBrush" Color="{DynamicResource DataGridSelectionColor}" />
|
<SolidColorBrush x:Key="DataGridSelectionBackgroundBrush" Color="{DynamicResource DataGridSelectionColor}" />
|
||||||
<SolidColorBrush x:Key="ThemeAccentColorBrush" Color="{DynamicResource SystemAccentColor}" />
|
<SolidColorBrush x:Key="ThemeAccentColorBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||||
<SolidColorBrush x:Key="ThemeAccentBrush4" Color="{DynamicResource ThemeAccentColor4}" />
|
<SolidColorBrush x:Key="ThemeAccentBrush4" Color="{DynamicResource ThemeAccentColor4}" />
|
||||||
|
<Color x:Key="ControlFillColorSecondary">#008AA8</Color>
|
||||||
|
<SolidColorBrush x:Key="ControlFillColorSecondaryBrush" Color="{StaticResource ControlFillColorSecondary}" />
|
||||||
|
<StaticResource x:Key="ButtonBackgroundPointerOver" ResourceKey="ControlFillColorSecondaryBrush" />
|
||||||
<Color x:Key="SystemAccentColor">#FF00C3E3</Color>
|
<Color x:Key="SystemAccentColor">#FF00C3E3</Color>
|
||||||
<Color x:Key="SystemAccentColorDark1">#FF99b000</Color>
|
<Color x:Key="SystemAccentColorDark1">#FF99b000</Color>
|
||||||
<Color x:Key="SystemAccentColorDark2">#FF006d7d</Color>
|
<Color x:Key="SystemAccentColorDark2">#FF006d7d</Color>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<Styles
|
<Styles
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:sys="clr-namespace:System;assembly=netstandard"
|
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
|
||||||
<Design.PreviewWith>
|
<Design.PreviewWith>
|
||||||
<Border Height="2000" Padding="20">
|
<Border Height="2000" Padding="20">
|
||||||
|
@ -269,13 +268,15 @@
|
||||||
<Color x:Key="DataGridSelectionColor">#FF00FABB</Color>
|
<Color x:Key="DataGridSelectionColor">#FF00FABB</Color>
|
||||||
<Color x:Key="ThemeContentBackgroundColor">#FF2D2D2D</Color>
|
<Color x:Key="ThemeContentBackgroundColor">#FF2D2D2D</Color>
|
||||||
<Color x:Key="ThemeControlBorderColor">#FF505050</Color>
|
<Color x:Key="ThemeControlBorderColor">#FF505050</Color>
|
||||||
<sys:Double x:Key="ScrollBarThickness">15</sys:Double>
|
<x:Double x:Key="ScrollBarThickness">15</x:Double>
|
||||||
<sys:Double x:Key="FontSizeSmall">8</sys:Double>
|
<x:Double x:Key="FontSizeSmall">8</x:Double>
|
||||||
<sys:Double x:Key="FontSizeNormal">10</sys:Double>
|
<x:Double x:Key="FontSizeNormal">10</x:Double>
|
||||||
<sys:Double x:Key="FontSize">12</sys:Double>
|
<x:Double x:Key="FontSize">12</x:Double>
|
||||||
<sys:Double x:Key="FontSizeLarge">15</sys:Double>
|
<x:Double x:Key="FontSizeLarge">15</x:Double>
|
||||||
<sys:Double x:Key="ControlContentThemeFontSize">13</sys:Double>
|
<x:Double x:Key="ControlContentThemeFontSize">13</x:Double>
|
||||||
<x:Double x:Key="MenuItemHeight">26</x:Double>
|
<x:Double x:Key="MenuItemHeight">26</x:Double>
|
||||||
<x:Double x:Key="TabItemMinHeight">28</x:Double>
|
<x:Double x:Key="TabItemMinHeight">28</x:Double>
|
||||||
|
<x:Double x:Key="ContentDialogMaxWidth">600</x:Double>
|
||||||
|
<x:Double x:Key="ContentDialogMaxHeight">756</x:Double>
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
</Styles>
|
</Styles>
|
|
@ -113,6 +113,11 @@ namespace Ryujinx.Ava.Common
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OpenSaveDir(saveDataId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void OpenSaveDir(ulong saveDataId)
|
||||||
|
{
|
||||||
string saveRootPath = Path.Combine(_virtualFileSystem.GetNandPath(), $"user/save/{saveDataId:x16}");
|
string saveRootPath = Path.Combine(_virtualFileSystem.GetNandPath(), $"user/save/{saveDataId:x16}");
|
||||||
|
|
||||||
if (!Directory.Exists(saveRootPath))
|
if (!Directory.Exists(saveRootPath))
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using LibHac;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.ViewModels;
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
@ -14,6 +15,8 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||||
{
|
{
|
||||||
public AccountManager AccountManager { get; }
|
public AccountManager AccountManager { get; }
|
||||||
public ContentManager ContentManager { get; }
|
public ContentManager ContentManager { get; }
|
||||||
|
public VirtualFileSystem VirtualFileSystem { get; }
|
||||||
|
public HorizonClient HorizonClient { get; }
|
||||||
public UserProfileViewModel ViewModel { get; set; }
|
public UserProfileViewModel ViewModel { get; set; }
|
||||||
|
|
||||||
public NavigationDialogHost()
|
public NavigationDialogHost()
|
||||||
|
@ -22,10 +25,12 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||||
}
|
}
|
||||||
|
|
||||||
public NavigationDialogHost(AccountManager accountManager, ContentManager contentManager,
|
public NavigationDialogHost(AccountManager accountManager, ContentManager contentManager,
|
||||||
VirtualFileSystem virtualFileSystem)
|
VirtualFileSystem virtualFileSystem, HorizonClient horizonClient)
|
||||||
{
|
{
|
||||||
AccountManager = accountManager;
|
AccountManager = accountManager;
|
||||||
ContentManager = contentManager;
|
ContentManager = contentManager;
|
||||||
|
VirtualFileSystem = virtualFileSystem;
|
||||||
|
HorizonClient = horizonClient;
|
||||||
ViewModel = new UserProfileViewModel(this);
|
ViewModel = new UserProfileViewModel(this);
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,9 +59,10 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||||
ContentFrame.Navigate(sourcePageType, parameter);
|
ContentFrame.Navigate(sourcePageType, parameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Show(AccountManager ownerAccountManager, ContentManager ownerContentManager, VirtualFileSystem ownerVirtualFileSystem)
|
public static async Task Show(AccountManager ownerAccountManager, ContentManager ownerContentManager,
|
||||||
|
VirtualFileSystem ownerVirtualFileSystem, HorizonClient ownerHorizonClient)
|
||||||
{
|
{
|
||||||
var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem);
|
var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
|
||||||
ContentDialog contentDialog = new ContentDialog
|
ContentDialog contentDialog = new ContentDialog
|
||||||
{
|
{
|
||||||
Title = LocaleManager.Instance["UserProfileWindowTitle"],
|
Title = LocaleManager.Instance["UserProfileWindowTitle"],
|
||||||
|
|
102
Ryujinx.Ava/Ui/Controls/SaveManager.axaml
Normal file
102
Ryujinx.Ava/Ui/Controls/SaveManager.axaml
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
||||||
|
xmlns:models="clr-namespace:Ryujinx.Ava.Ui.Models"
|
||||||
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
Height="400"
|
||||||
|
Width="550"
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Controls.SaveManager">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||||
|
</UserControl.Resources>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid Grid.Row="0" HorizontalAlignment="Stretch">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Spacing="10" Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||||
|
<Label Content="{locale:Locale CommonSort}" VerticalAlignment="Center" />
|
||||||
|
<ComboBox SelectedIndex="{Binding SortIndex}" Width="100">
|
||||||
|
<ComboBoxItem>
|
||||||
|
<Label VerticalAlignment="Center" HorizontalContentAlignment="Left"
|
||||||
|
Content="{locale:Locale Name}" />
|
||||||
|
</ComboBoxItem>
|
||||||
|
<ComboBoxItem>
|
||||||
|
<Label VerticalAlignment="Center" HorizontalContentAlignment="Left"
|
||||||
|
Content="{locale:Locale Size}" />
|
||||||
|
</ComboBoxItem>
|
||||||
|
</ComboBox>
|
||||||
|
<ComboBox SelectedIndex="{Binding OrderIndex}" Width="150">
|
||||||
|
<ComboBoxItem>
|
||||||
|
<Label VerticalAlignment="Center" HorizontalContentAlignment="Left"
|
||||||
|
Content="{locale:Locale OrderAscending}" />
|
||||||
|
</ComboBoxItem>
|
||||||
|
<ComboBoxItem>
|
||||||
|
<Label VerticalAlignment="Center" HorizontalContentAlignment="Left"
|
||||||
|
Content="{locale:Locale Descending}" />
|
||||||
|
</ComboBoxItem>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
<Grid Grid.Column="1" HorizontalAlignment="Stretch" Margin="10,0, 0, 0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Label Content="{locale:Locale Search}" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Margin="5,0,0,0" Grid.Column="1" HorizontalAlignment="Stretch" Text="{Binding Search}"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Border Grid.Row="1" Margin="0,5" BorderThickness="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
|
<ListBox Name="SaveList" Items="{Binding View}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="models:SaveModel">
|
||||||
|
<Grid HorizontalAlignment="Stretch" Margin="0,5">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||||
|
<Border Height="42" Margin="2" Width="42" Padding="10"
|
||||||
|
IsVisible="{Binding !InGameList}">
|
||||||
|
<ui:SymbolIcon Symbol="Help" FontSize="30" HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
<Image IsVisible="{Binding InGameList}"
|
||||||
|
Margin="2"
|
||||||
|
Width="42"
|
||||||
|
Height="42"
|
||||||
|
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
|
||||||
|
<TextBlock MaxLines="3" Width="320" Margin="5" TextWrapping="Wrap"
|
||||||
|
Text="{Binding Title}" VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1" Spacing="10" HorizontalAlignment="Right"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<Label Content="{Binding SizeString}" IsVisible="{Binding SizeAvailable}"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Right" />
|
||||||
|
<Button VerticalAlignment="Center" HorizontalAlignment="Right" Padding="10"
|
||||||
|
MinWidth="0" MinHeight="0" Name="OpenLocation" Command="{Binding OpenLocation}">
|
||||||
|
<ui:SymbolIcon Symbol="OpenFolder" HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Button>
|
||||||
|
<Button VerticalAlignment="Center" HorizontalAlignment="Right" Padding="10"
|
||||||
|
MinWidth="0" MinHeight="0" Name="Delete" Command="{Binding Delete}">
|
||||||
|
<ui:SymbolIcon Symbol="Delete" HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
160
Ryujinx.Ava/Ui/Controls/SaveManager.axaml.cs
Normal file
160
Ryujinx.Ava/Ui/Controls/SaveManager.axaml.cs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DynamicData;
|
||||||
|
using DynamicData.Binding;
|
||||||
|
using LibHac;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Shim;
|
||||||
|
using Ryujinx.Ava.Common;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.Ui.App.Common;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Controls
|
||||||
|
{
|
||||||
|
public partial class SaveManager : UserControl
|
||||||
|
{
|
||||||
|
private readonly UserProfile _userProfile;
|
||||||
|
private readonly HorizonClient _horizonClient;
|
||||||
|
private readonly VirtualFileSystem _virtualFileSystem;
|
||||||
|
private int _sortIndex;
|
||||||
|
private int _orderIndex;
|
||||||
|
private ObservableCollection<SaveModel> _view = new ObservableCollection<SaveModel>();
|
||||||
|
private string _search;
|
||||||
|
|
||||||
|
public ObservableCollection<SaveModel> Saves { get; set; } = new ObservableCollection<SaveModel>();
|
||||||
|
|
||||||
|
public ObservableCollection<SaveModel> View
|
||||||
|
{
|
||||||
|
get => _view;
|
||||||
|
set => _view = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SortIndex
|
||||||
|
{
|
||||||
|
get => _sortIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_sortIndex = value;
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int OrderIndex
|
||||||
|
{
|
||||||
|
get => _orderIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_orderIndex = value;
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Search
|
||||||
|
{
|
||||||
|
get => _search;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_search = value;
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SaveManager()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SaveManager(UserProfile userProfile, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem)
|
||||||
|
{
|
||||||
|
_userProfile = userProfile;
|
||||||
|
_horizonClient = horizonClient;
|
||||||
|
_virtualFileSystem = virtualFileSystem;
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
Task.Run(LoadSaves);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadSaves()
|
||||||
|
{
|
||||||
|
Saves.Clear();
|
||||||
|
var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account,
|
||||||
|
new UserId((ulong)_userProfile.UserId.High, (ulong)_userProfile.UserId.Low), saveDataId: default, index: default);
|
||||||
|
|
||||||
|
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
|
||||||
|
|
||||||
|
_horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
|
||||||
|
|
||||||
|
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
|
||||||
|
|
||||||
|
if (readCount == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < readCount; i++)
|
||||||
|
{
|
||||||
|
var save = saveDataInfo[i];
|
||||||
|
if (save.ProgramId.Value != 0)
|
||||||
|
{
|
||||||
|
var saveModel = new SaveModel(save, _horizonClient, _virtualFileSystem);
|
||||||
|
Saves.Add(saveModel);
|
||||||
|
saveModel.DeleteAction = () => { Saves.Remove(saveModel); };
|
||||||
|
}
|
||||||
|
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Sort()
|
||||||
|
{
|
||||||
|
Saves.AsObservableChangeSet()
|
||||||
|
.Filter(Filter)
|
||||||
|
.Sort(GetComparer())
|
||||||
|
.Bind(out var view).AsObservableList();
|
||||||
|
|
||||||
|
_view.Clear();
|
||||||
|
_view.AddRange(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IComparer<SaveModel> GetComparer()
|
||||||
|
{
|
||||||
|
switch (SortIndex)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return OrderIndex == 0
|
||||||
|
? SortExpressionComparer<SaveModel>.Ascending(save => save.Title)
|
||||||
|
: SortExpressionComparer<SaveModel>.Descending(save => save.Title);
|
||||||
|
case 1:
|
||||||
|
return OrderIndex == 0
|
||||||
|
? SortExpressionComparer<SaveModel>.Ascending(save => save.Size)
|
||||||
|
: SortExpressionComparer<SaveModel>.Descending(save => save.Size);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Filter(object arg)
|
||||||
|
{
|
||||||
|
if (arg is SaveModel save)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(_search) || save.Title.ToLower().Contains(_search.ToLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||||
Margin="0"
|
Margin="0"
|
||||||
|
MinWidth="500"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
|
@ -63,7 +64,7 @@
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
MaxLength="{Binding MaxProfileNameLength}"
|
MaxLength="{Binding MaxProfileNameLength}"
|
||||||
Text="{Binding Name}" />
|
Text="{Binding Name}" />
|
||||||
<TextBlock Text="{Locale:Locale UserProfilesUserId}" />
|
<TextBlock Name="IdText" Text="{Locale:Locale UserProfilesUserId}" />
|
||||||
<TextBlock Name="IdLabel" Text="{Binding UserId}" />
|
<TextBlock Name="IdLabel" Text="{Binding UserId}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
|
|
|
@ -36,15 +36,8 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||||
case NavigationMode.New:
|
case NavigationMode.New:
|
||||||
var args = ((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter;
|
var args = ((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter;
|
||||||
_isNewUser = args.isNewUser;
|
_isNewUser = args.isNewUser;
|
||||||
if (!_isNewUser)
|
_profile = args.profile;
|
||||||
{
|
TempProfile = new TempProfile(_profile);
|
||||||
_profile = args.profile;
|
|
||||||
TempProfile = new TempProfile(_profile);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TempProfile = new TempProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
_parent = args.parent;
|
_parent = args.parent;
|
||||||
break;
|
break;
|
||||||
|
@ -53,7 +46,8 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||||
DataContext = TempProfile;
|
DataContext = TempProfile;
|
||||||
|
|
||||||
AddPictureButton.IsVisible = _isNewUser;
|
AddPictureButton.IsVisible = _isNewUser;
|
||||||
IdLabel.IsVisible = !_isNewUser;
|
IdLabel.IsVisible = _profile != null;
|
||||||
|
IdText.IsVisible = _profile != null;
|
||||||
ChangePictureButton.IsVisible = !_isNewUser;
|
ChangePictureButton.IsVisible = !_isNewUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +81,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_profile != null)
|
if (_profile != null && !_isNewUser)
|
||||||
{
|
{
|
||||||
_profile.Name = TempProfile.Name;
|
_profile.Name = TempProfile.Name;
|
||||||
_profile.Image = TempProfile.Image;
|
_profile.Image = TempProfile.Image;
|
||||||
|
@ -97,7 +91,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
||||||
}
|
}
|
||||||
else if (_isNewUser)
|
else if (_isNewUser)
|
||||||
{
|
{
|
||||||
_parent.AccountManager.AddUser(TempProfile.Name, TempProfile.Image);
|
_parent.AccountManager.AddUser(TempProfile.Name, TempProfile.Image, TempProfile.UserId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
70
Ryujinx.Ava/Ui/Controls/UserRecoverer.axaml
Normal file
70
Ryujinx.Ava/Ui/Controls/UserRecoverer.axaml
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
MinWidth="500"
|
||||||
|
MinHeight="400"
|
||||||
|
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Controls.UserRecoverer">
|
||||||
|
<Design.DataContext>
|
||||||
|
<viewModels:UserProfileViewModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
<Grid HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Button Grid.Row="0"
|
||||||
|
Margin="5"
|
||||||
|
Height="30"
|
||||||
|
Width="50"
|
||||||
|
MinWidth="50"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Command="{Binding GoBack}">
|
||||||
|
<ui:SymbolIcon Symbol="Back"/>
|
||||||
|
</Button>
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Text="{Locale:Locale UserProfilesRecoverHeading}"/>
|
||||||
|
<ListBox
|
||||||
|
Margin="5"
|
||||||
|
Grid.Row="2"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Items="{Binding LostProfiles}">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border
|
||||||
|
Margin="2"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
ClipToBounds="True"
|
||||||
|
CornerRadius="5">
|
||||||
|
<Grid Margin="0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Text="{Binding UserId}"
|
||||||
|
TextAlignment="Left"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Command="{Binding Recover}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Content="{Locale:Locale Recover}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
44
Ryujinx.Ava/Ui/Controls/UserRecoverer.axaml.cs
Normal file
44
Ryujinx.Ava/Ui/Controls/UserRecoverer.axaml.cs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using FluentAvalonia.UI.Navigation;
|
||||||
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Controls
|
||||||
|
{
|
||||||
|
public partial class UserRecoverer : UserControl
|
||||||
|
{
|
||||||
|
private UserProfileViewModel _viewModel;
|
||||||
|
private NavigationDialogHost _parent;
|
||||||
|
|
||||||
|
public UserRecoverer()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||||
|
{
|
||||||
|
NavigatedTo(e);
|
||||||
|
}, RoutingStrategies.Direct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigatedTo(NavigationEventArgs arg)
|
||||||
|
{
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
switch (arg.NavigationMode)
|
||||||
|
{
|
||||||
|
case NavigationMode.New:
|
||||||
|
var args = ((NavigationDialogHost parent, UserProfileViewModel viewModel))arg.Parameter;
|
||||||
|
|
||||||
|
_viewModel = args.viewModel;
|
||||||
|
_parent = args.parent;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataContext = _viewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||||
d:DesignHeight="450"
|
d:DesignHeight="450"
|
||||||
|
MinWidth="500"
|
||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<ListBox
|
<ListBox
|
||||||
Margin="5"
|
Margin="5"
|
||||||
|
MaxHeight="300"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
DoubleTapped="ProfilesList_DoubleTapped"
|
DoubleTapped="ProfilesList_DoubleTapped"
|
||||||
|
@ -88,21 +90,56 @@
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
<StackPanel
|
<Grid
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Margin="10,0"
|
HorizontalAlignment="Center">
|
||||||
HorizontalAlignment="Center"
|
<Grid.RowDefinitions>
|
||||||
Orientation="Horizontal"
|
<RowDefinition Height="Auto"/>
|
||||||
Spacing="10">
|
<RowDefinition Height="Auto"/>
|
||||||
<Button Command="{Binding AddUser}" Content="{Locale:Locale UserProfilesAddNewProfile}" />
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
<Button
|
<Button
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="2"
|
||||||
|
Command="{Binding AddUser}"
|
||||||
|
Content="{Locale:Locale UserProfilesAddNewProfile}" />
|
||||||
|
<Button
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="2"
|
||||||
|
Grid.Column="1"
|
||||||
Command="{Binding EditUser}"
|
Command="{Binding EditUser}"
|
||||||
Content="{Locale:Locale UserProfilesEditProfile}"
|
Content="{Locale:Locale UserProfilesEditProfile}"
|
||||||
IsEnabled="{Binding IsSelectedProfiledEditable}" />
|
IsEnabled="{Binding IsSelectedProfiledEditable}" />
|
||||||
<Button
|
<Button
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="2"
|
||||||
|
Content="{Locale:Locale UserProfilesManageSaves}"
|
||||||
|
Command="{Binding ManageSaves}" />
|
||||||
|
<Button
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="2"
|
||||||
Command="{Binding DeleteUser}"
|
Command="{Binding DeleteUser}"
|
||||||
Content="{Locale:Locale UserProfilesDeleteSelectedProfile}"
|
Content="{Locale:Locale UserProfilesDeleteSelectedProfile}"
|
||||||
IsEnabled="{Binding IsSelectedProfileDeletable}" />
|
IsEnabled="{Binding IsSelectedProfileDeletable}" />
|
||||||
</StackPanel>
|
<Button
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="2"
|
||||||
|
Command="{Binding RecoverLostAccounts}"
|
||||||
|
Content="{Locale:Locale UserProfilesRecoverLostAccounts}" />
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
122
Ryujinx.Ava/Ui/Models/SaveModel.cs
Normal file
122
Ryujinx.Ava/Ui/Models/SaveModel.cs
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
using LibHac;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Shim;
|
||||||
|
using LibHac.Ncm;
|
||||||
|
using Ryujinx.Ava.Common;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
|
using Ryujinx.Ava.Ui.Windows;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.Ui.App.Common;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Models
|
||||||
|
{
|
||||||
|
public class SaveModel : BaseModel
|
||||||
|
{
|
||||||
|
private readonly HorizonClient _horizonClient;
|
||||||
|
private long _size;
|
||||||
|
|
||||||
|
public Action DeleteAction { get; set; }
|
||||||
|
public ulong SaveId { get; }
|
||||||
|
public ProgramId TitleId { get; }
|
||||||
|
public string TitleIdString => $"{TitleId.Value:X16}";
|
||||||
|
public UserId UserId { get; }
|
||||||
|
public bool InGameList { get; }
|
||||||
|
public string Title { get; }
|
||||||
|
public byte[] Icon { get; }
|
||||||
|
|
||||||
|
public long Size
|
||||||
|
{
|
||||||
|
get => _size; set
|
||||||
|
{
|
||||||
|
_size = value;
|
||||||
|
SizeAvailable = true;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(SizeString));
|
||||||
|
OnPropertyChanged(nameof(SizeAvailable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SizeAvailable { get; set; }
|
||||||
|
|
||||||
|
public string SizeString => $"{((float)_size * 0.000000954):0.###}MB";
|
||||||
|
|
||||||
|
public SaveModel(SaveDataInfo info, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem)
|
||||||
|
{
|
||||||
|
_horizonClient = horizonClient;
|
||||||
|
SaveId = info.SaveDataId;
|
||||||
|
TitleId = info.ProgramId;
|
||||||
|
UserId = info.UserId;
|
||||||
|
|
||||||
|
var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString);
|
||||||
|
|
||||||
|
InGameList = appData != null;
|
||||||
|
|
||||||
|
if (InGameList)
|
||||||
|
{
|
||||||
|
Icon = appData.Icon;
|
||||||
|
Title = appData.TitleName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var appMetadata = MainWindow.MainWindowViewModel.ApplicationLibrary.LoadAndSaveMetaData(TitleIdString);
|
||||||
|
Title = appMetadata.Title ?? TitleIdString;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
var saveRoot = System.IO.Path.Combine(virtualFileSystem.GetNandPath(), $"user/save/{info.SaveDataId:x16}");
|
||||||
|
|
||||||
|
long total_size = GetDirectorySize(saveRoot);
|
||||||
|
long GetDirectorySize(string path)
|
||||||
|
{
|
||||||
|
long size = 0;
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
var directories = Directory.GetDirectories(path);
|
||||||
|
foreach (var directory in directories)
|
||||||
|
{
|
||||||
|
size += GetDirectorySize(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = Directory.GetFiles(path);
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
size += new FileInfo(file).Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Size = total_size;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenLocation()
|
||||||
|
{
|
||||||
|
ApplicationHelper.OpenSaveDir(SaveId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Delete()
|
||||||
|
{
|
||||||
|
var result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance["DeleteUserSave"],
|
||||||
|
LocaleManager.Instance["IrreversibleActionNote"],
|
||||||
|
LocaleManager.Instance["InputDialogYes"],
|
||||||
|
LocaleManager.Instance["InputDialogNo"], "");
|
||||||
|
|
||||||
|
if (result == UserResult.Yes)
|
||||||
|
{
|
||||||
|
_horizonClient.Fs.DeleteSaveData(SaveDataSpaceId.User, SaveId);
|
||||||
|
|
||||||
|
DeleteAction?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,9 +45,12 @@ namespace Ryujinx.Ava.Ui.Models
|
||||||
{
|
{
|
||||||
_profile = profile;
|
_profile = profile;
|
||||||
|
|
||||||
Image = profile.Image;
|
if (_profile != null)
|
||||||
Name = profile.Name;
|
{
|
||||||
UserId = profile.UserId;
|
Image = profile.Image;
|
||||||
|
Name = profile.Name;
|
||||||
|
UserId = profile.UserId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TempProfile(){}
|
public TempProfile(){}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
using Ryujinx.Ava.Ui.ViewModels;
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
|
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
|
||||||
|
@ -7,6 +8,7 @@ namespace Ryujinx.Ava.Ui.Models
|
||||||
public class UserProfile : BaseModel
|
public class UserProfile : BaseModel
|
||||||
{
|
{
|
||||||
private readonly Profile _profile;
|
private readonly Profile _profile;
|
||||||
|
private readonly NavigationDialogHost _owner;
|
||||||
private byte[] _image;
|
private byte[] _image;
|
||||||
private string _name;
|
private string _name;
|
||||||
private UserId _userId;
|
private UserId _userId;
|
||||||
|
@ -41,9 +43,10 @@ namespace Ryujinx.Ava.Ui.Models
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserProfile(Profile profile)
|
public UserProfile(Profile profile, NavigationDialogHost owner)
|
||||||
{
|
{
|
||||||
_profile = profile;
|
_profile = profile;
|
||||||
|
_owner = owner;
|
||||||
|
|
||||||
Image = profile.Image;
|
Image = profile.Image;
|
||||||
Name = profile.Name;
|
Name = profile.Name;
|
||||||
|
@ -57,5 +60,10 @@ namespace Ryujinx.Ava.Ui.Models
|
||||||
OnPropertyChanged(nameof(IsOpened));
|
OnPropertyChanged(nameof(IsOpened));
|
||||||
OnPropertyChanged(nameof(Name));
|
OnPropertyChanged(nameof(Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Recover(UserProfile userProfile)
|
||||||
|
{
|
||||||
|
_owner.Navigate(typeof(UserEditor), (_owner, userProfile, true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -76,6 +76,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
private bool _showAll;
|
private bool _showAll;
|
||||||
private string _lastScannedAmiiboId;
|
private string _lastScannedAmiiboId;
|
||||||
private ReadOnlyObservableCollection<ApplicationData> _appsObservableList;
|
private ReadOnlyObservableCollection<ApplicationData> _appsObservableList;
|
||||||
|
public ApplicationLibrary ApplicationLibrary => _owner.ApplicationLibrary;
|
||||||
|
|
||||||
public string TitleName { get; internal set; }
|
public string TitleName { get; internal set; }
|
||||||
|
|
||||||
|
@ -103,8 +104,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
_owner.ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
|
ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
|
||||||
_owner.ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
|
ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
|
||||||
|
|
||||||
Ptc.PtcStateChanged -= ProgressHandler;
|
Ptc.PtcStateChanged -= ProgressHandler;
|
||||||
Ptc.PtcStateChanged += ProgressHandler;
|
Ptc.PtcStateChanged += ProgressHandler;
|
||||||
|
@ -817,7 +818,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
|
|
||||||
Thread thread = new(() =>
|
Thread thread = new(() =>
|
||||||
{
|
{
|
||||||
_owner.ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs.Value, ConfigurationState.Instance.System.Language);
|
ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs.Value, ConfigurationState.Instance.System.Language);
|
||||||
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
})
|
})
|
||||||
|
@ -1005,7 +1006,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
|
|
||||||
public async void ManageProfiles()
|
public async void ManageProfiles()
|
||||||
{
|
{
|
||||||
await NavigationDialogHost.Show(_owner.AccountManager, _owner.ContentManager, _owner.VirtualFileSystem);
|
await NavigationDialogHost.Show(_owner.AccountManager, _owner.ContentManager, _owner.VirtualFileSystem, _owner.LibHacHorizonManager.RyujinxClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OpenAboutWindow()
|
public async void OpenAboutWindow()
|
||||||
|
@ -1098,7 +1099,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
{
|
{
|
||||||
selection.Favorite = !selection.Favorite;
|
selection.Favorite = !selection.Favorite;
|
||||||
|
|
||||||
_owner.ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata =>
|
ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata =>
|
||||||
{
|
{
|
||||||
appMetadata.Favorite = selection.Favorite;
|
appMetadata.Favorite = selection.Favorite;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Shim;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Controls;
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
|
using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
|
||||||
|
@ -19,6 +25,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
public UserProfileViewModel()
|
public UserProfileViewModel()
|
||||||
{
|
{
|
||||||
Profiles = new ObservableCollection<UserProfile>();
|
Profiles = new ObservableCollection<UserProfile>();
|
||||||
|
LostProfiles = new ObservableCollection<UserProfile>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserProfileViewModel(NavigationDialogHost owner) : this()
|
public UserProfileViewModel(NavigationDialogHost owner) : this()
|
||||||
|
@ -30,6 +37,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
|
|
||||||
public ObservableCollection<UserProfile> Profiles { get; set; }
|
public ObservableCollection<UserProfile> Profiles { get; set; }
|
||||||
|
|
||||||
|
public ObservableCollection<UserProfile> LostProfiles { get; set; }
|
||||||
|
|
||||||
public UserProfile SelectedProfile
|
public UserProfile SelectedProfile
|
||||||
{
|
{
|
||||||
get => _selectedProfile;
|
get => _selectedProfile;
|
||||||
|
@ -65,12 +74,13 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
public void LoadProfiles()
|
public void LoadProfiles()
|
||||||
{
|
{
|
||||||
Profiles.Clear();
|
Profiles.Clear();
|
||||||
|
LostProfiles.Clear();
|
||||||
|
|
||||||
var profiles = _owner.AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open);
|
var profiles = _owner.AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open);
|
||||||
|
|
||||||
foreach (var profile in profiles)
|
foreach (var profile in profiles)
|
||||||
{
|
{
|
||||||
Profiles.Add(new UserProfile(profile));
|
Profiles.Add(new UserProfile(profile, _owner));
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId);
|
SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId);
|
||||||
|
@ -84,6 +94,42 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
_owner.AccountManager.OpenUser(_selectedProfile.UserId);
|
_owner.AccountManager.OpenUser(_selectedProfile.UserId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account,
|
||||||
|
default, saveDataId: default, index: default);
|
||||||
|
|
||||||
|
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
|
||||||
|
|
||||||
|
_owner.HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
|
||||||
|
|
||||||
|
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
|
||||||
|
|
||||||
|
HashSet<HLE.HOS.Services.Account.Acc.UserId> lostAccounts = new HashSet<HLE.HOS.Services.Account.Acc.UserId>();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
|
||||||
|
|
||||||
|
if (readCount == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < readCount; i++)
|
||||||
|
{
|
||||||
|
var save = saveDataInfo[i];
|
||||||
|
var id = new HLE.HOS.Services.Account.Acc.UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High);
|
||||||
|
if (Profiles.FirstOrDefault( x=> x.UserId == id) == null)
|
||||||
|
{
|
||||||
|
lostAccounts.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(var account in lostAccounts)
|
||||||
|
{
|
||||||
|
LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), _owner));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddUser()
|
public void AddUser()
|
||||||
|
@ -93,6 +139,25 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
|
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void ManageSaves()
|
||||||
|
{
|
||||||
|
UserProfile userProfile = _highlightedProfile ?? SelectedProfile;
|
||||||
|
|
||||||
|
SaveManager manager = new SaveManager(userProfile, _owner.HorizonClient, _owner.VirtualFileSystem);
|
||||||
|
|
||||||
|
ContentDialog contentDialog = new ContentDialog
|
||||||
|
{
|
||||||
|
Title = string.Format(LocaleManager.Instance["SaveManagerHeading"], userProfile.Name),
|
||||||
|
PrimaryButtonText = "",
|
||||||
|
SecondaryButtonText = "",
|
||||||
|
CloseButtonText = LocaleManager.Instance["UserProfilesClose"],
|
||||||
|
Content = manager,
|
||||||
|
Padding = new Thickness(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
await contentDialog.ShowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public void EditUser()
|
public void EditUser()
|
||||||
{
|
{
|
||||||
_owner.Navigate(typeof(UserEditor), (this._owner, _highlightedProfile ?? SelectedProfile, false));
|
_owner.Navigate(typeof(UserEditor), (this._owner, _highlightedProfile ?? SelectedProfile, false));
|
||||||
|
@ -134,5 +199,15 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
|
|
||||||
LoadProfiles();
|
LoadProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void GoBack()
|
||||||
|
{
|
||||||
|
_owner.GoBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecoverLostAccounts()
|
||||||
|
{
|
||||||
|
_owner.Navigate(typeof(UserRecoverer), (this._owner, this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -36,6 +36,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||||
{
|
{
|
||||||
public partial class MainWindow : StyleableWindow
|
public partial class MainWindow : StyleableWindow
|
||||||
{
|
{
|
||||||
|
internal static MainWindowViewModel MainWindowViewModel { get; private set; }
|
||||||
private bool _canUpdate;
|
private bool _canUpdate;
|
||||||
private bool _isClosing;
|
private bool _isClosing;
|
||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
|
@ -81,6 +82,8 @@ namespace Ryujinx.Ava.Ui.Windows
|
||||||
{
|
{
|
||||||
ViewModel = new MainWindowViewModel(this);
|
ViewModel = new MainWindowViewModel(this);
|
||||||
|
|
||||||
|
MainWindowViewModel = ViewModel;
|
||||||
|
|
||||||
DataContext = ViewModel;
|
DataContext = ViewModel;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
|
@ -444,7 +444,10 @@ namespace Ryujinx.Ui.App.Common
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId);
|
ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata =>
|
||||||
|
{
|
||||||
|
appMetadata.Title = titleName;
|
||||||
|
});
|
||||||
|
|
||||||
if (appMetadata.LastPlayed != "Never" && !DateTime.TryParse(appMetadata.LastPlayed, out _))
|
if (appMetadata.LastPlayed != "Never" && !DateTime.TryParse(appMetadata.LastPlayed, out _))
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
{
|
{
|
||||||
public class ApplicationMetadata
|
public class ApplicationMetadata
|
||||||
{
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
public bool Favorite { get; set; }
|
public bool Favorite { get; set; }
|
||||||
public double TimePlayed { get; set; }
|
public double TimePlayed { get; set; }
|
||||||
public string LastPlayed { get; set; } = "Never";
|
public string LastPlayed { get; set; } = "Never";
|
||||||
|
|
Loading…
Reference in a new issue