UI - Avalonia Part 3 (#3441)
* Add all other windows * addreesed review * Prevent "No Update" option from being deleted * Select no update is the current update is removed from the title update window * fix amiibo crash
This commit is contained in:
parent
bccf5e8b5a
commit
3af42d6c7e
@ -553,6 +553,20 @@
|
|||||||
"SettingsTabHotkeysToggleMuteHotkey": "Mute:",
|
"SettingsTabHotkeysToggleMuteHotkey": "Mute:",
|
||||||
"ControllerMotionTitle": "Motion Control Settings",
|
"ControllerMotionTitle": "Motion Control Settings",
|
||||||
"ControllerRumbleTitle": "Rumble Settings",
|
"ControllerRumbleTitle": "Rumble Settings",
|
||||||
"SettingsSelectThemeFileDialogTitle" : "Select Theme File",
|
"SettingsSelectThemeFileDialogTitle": "Select Theme File",
|
||||||
"SettingsXamlThemeFile" : "Xaml Theme File"
|
"SettingsXamlThemeFile": "Xaml Theme File",
|
||||||
|
"AvatarWindowTitle": "Manage Accounts - Avatar",
|
||||||
|
"Amiibo": "Amiibo",
|
||||||
|
"Unknown": "Unknown",
|
||||||
|
"Usage": "Usage",
|
||||||
|
"Writable": "Writable",
|
||||||
|
"SelectDlcDialogTitle": "Select DLC files",
|
||||||
|
"SelectUpdateDialogTitle": "Select update files",
|
||||||
|
"UserProfileWindowTitle": "Manage User Profiles",
|
||||||
|
"CheatWindowTitle": "Manage Game Cheats",
|
||||||
|
"DlcWindowTitle": "Manage Game DLC",
|
||||||
|
"UpdateWindowTitle": "Manage Game Updates",
|
||||||
|
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
||||||
|
"DlcWindowHeading": "DLC Available for {0} [{1}]",
|
||||||
|
"GameUpdateWindowHeading": "DLC Available for {0} [{1}]"
|
||||||
}
|
}
|
||||||
|
35
Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml
Normal file
35
Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<Window 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"
|
||||||
|
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Controls.ProfileImageSelectionDialog"
|
||||||
|
SizeToContent="WidthAndHeight"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Title="{Locale:Locale ProfileImageSelectionTitle}"
|
||||||
|
CanResize="false">
|
||||||
|
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="5,10,5, 5">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="70" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock FontWeight="Bold" FontSize="18" HorizontalAlignment="Center" Grid.Row="1"
|
||||||
|
Text="{Locale:Locale ProfileImageSelectionHeader}" />
|
||||||
|
<TextBlock FontWeight="Bold" Grid.Row="2" Margin="10" MaxWidth="400" TextWrapping="Wrap"
|
||||||
|
HorizontalAlignment="Center" TextAlignment="Center" Text="{Locale:Locale ProfileImageSelectionNote}" />
|
||||||
|
<StackPanel Margin="5,0" Spacing="10" Grid.Row="4" HorizontalAlignment="Center"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<Button Name="Import" Click="Import_OnClick" Width="200">
|
||||||
|
<TextBlock Text="{Locale:Locale ProfileImageSelectionImportImage}" />
|
||||||
|
</Button>
|
||||||
|
<Button Name="SelectFirmwareImage" IsEnabled="{Binding FirmwareFound}" Click="SelectFirmwareImage_OnClick"
|
||||||
|
Width="200">
|
||||||
|
<TextBlock Text="{Locale:Locale ProfileImageSelectionSelectAvatar}" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
105
Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs
Normal file
105
Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Windows;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using System.IO;
|
||||||
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Controls
|
||||||
|
{
|
||||||
|
public class ProfileImageSelectionDialog : StyleableWindow
|
||||||
|
{
|
||||||
|
private readonly ContentManager _contentManager;
|
||||||
|
|
||||||
|
public bool FirmwareFound => _contentManager.GetCurrentFirmwareVersion() != null;
|
||||||
|
|
||||||
|
public byte[] BufferImageProfile { get; set; }
|
||||||
|
|
||||||
|
public ProfileImageSelectionDialog(ContentManager contentManager)
|
||||||
|
{
|
||||||
|
_contentManager = contentManager;
|
||||||
|
DataContext = this;
|
||||||
|
InitializeComponent();
|
||||||
|
#if DEBUG
|
||||||
|
this.AttachDevTools();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProfileImageSelectionDialog()
|
||||||
|
{
|
||||||
|
DataContext = this;
|
||||||
|
InitializeComponent();
|
||||||
|
#if DEBUG
|
||||||
|
this.AttachDevTools();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Import_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
OpenFileDialog dialog = new();
|
||||||
|
dialog.Filters.Add(new FileDialogFilter
|
||||||
|
{
|
||||||
|
Name = LocaleManager.Instance["AllSupportedFormats"],
|
||||||
|
Extensions = { "jpg", "jpeg", "png", "bmp" }
|
||||||
|
});
|
||||||
|
dialog.Filters.Add(new FileDialogFilter { Name = "JPEG", Extensions = { "jpg", "jpeg" } });
|
||||||
|
dialog.Filters.Add(new FileDialogFilter { Name = "PNG", Extensions = { "png" } });
|
||||||
|
dialog.Filters.Add(new FileDialogFilter { Name = "BMP", Extensions = { "bmp" } });
|
||||||
|
|
||||||
|
dialog.AllowMultiple = false;
|
||||||
|
|
||||||
|
string[] image = await dialog.ShowAsync(this);
|
||||||
|
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
if (image.Length > 0)
|
||||||
|
{
|
||||||
|
string imageFile = image[0];
|
||||||
|
|
||||||
|
ProcessProfileImage(File.ReadAllBytes(imageFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (FirmwareFound)
|
||||||
|
{
|
||||||
|
AvatarWindow window = new(_contentManager);
|
||||||
|
|
||||||
|
await window.ShowDialog(this);
|
||||||
|
|
||||||
|
BufferImageProfile = window.SelectedImage;
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessProfileImage(byte[] buffer)
|
||||||
|
{
|
||||||
|
using (Image image = Image.Load(buffer))
|
||||||
|
{
|
||||||
|
image.Mutate(x => x.Resize(256, 256));
|
||||||
|
|
||||||
|
using (MemoryStream streamJpg = new())
|
||||||
|
{
|
||||||
|
image.SaveAsJpeg(streamJpg);
|
||||||
|
|
||||||
|
BufferImageProfile = streamJpg.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
Ryujinx.Ava/Ui/Models/Amiibo.cs
Normal file
72
Ryujinx.Ava/Ui/Models/Amiibo.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Models
|
||||||
|
{
|
||||||
|
public class Amiibo
|
||||||
|
{
|
||||||
|
public struct AmiiboJson
|
||||||
|
{
|
||||||
|
[JsonPropertyName("amiibo")] public List<AmiiboApi> Amiibo { get; set; }
|
||||||
|
[JsonPropertyName("lastUpdated")] public DateTime LastUpdated { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AmiiboApi
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")] public string Name { get; set; }
|
||||||
|
[JsonPropertyName("head")] public string Head { get; set; }
|
||||||
|
[JsonPropertyName("tail")] public string Tail { get; set; }
|
||||||
|
[JsonPropertyName("image")] public string Image { get; set; }
|
||||||
|
[JsonPropertyName("amiiboSeries")] public string AmiiboSeries { get; set; }
|
||||||
|
[JsonPropertyName("character")] public string Character { get; set; }
|
||||||
|
[JsonPropertyName("gameSeries")] public string GameSeries { get; set; }
|
||||||
|
[JsonPropertyName("type")] public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("release")] public Dictionary<string, string> Release { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("gamesSwitch")] public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetId()
|
||||||
|
{
|
||||||
|
return Head + Tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj is AmiiboApi amiibo)
|
||||||
|
{
|
||||||
|
return amiibo.Head + amiibo.Tail == Head + Tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return base.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AmiiboApiGamesSwitch
|
||||||
|
{
|
||||||
|
[JsonPropertyName("amiiboUsage")] public List<AmiiboApiUsage> AmiiboUsage { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("gameID")] public List<string> GameId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("gameName")] public string GameName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AmiiboApiUsage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("Usage")] public string Usage { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("write")] public bool Write { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
Ryujinx.Ava/Ui/Models/CheatModel.cs
Normal file
37
Ryujinx.Ava/Ui/Models/CheatModel.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Models
|
||||||
|
{
|
||||||
|
public class CheatModel : BaseModel
|
||||||
|
{
|
||||||
|
private bool _isEnabled;
|
||||||
|
|
||||||
|
public event EventHandler<bool> EnableToggled;
|
||||||
|
|
||||||
|
public CheatModel(string name, string buildId, bool isEnabled)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
BuildId = buildId;
|
||||||
|
IsEnabled = isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEnabled
|
||||||
|
{
|
||||||
|
get => _isEnabled;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isEnabled = value;
|
||||||
|
EnableToggled?.Invoke(this, _isEnabled);
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BuildId { get; }
|
||||||
|
|
||||||
|
public string BuildIdKey => $"{BuildId}-{Name}";
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public string CleanName => Name.Substring(1, Name.Length - 8);
|
||||||
|
}
|
||||||
|
}
|
51
Ryujinx.Ava/Ui/Models/CheatsList.cs
Normal file
51
Ryujinx.Ava/Ui/Models/CheatsList.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Models
|
||||||
|
{
|
||||||
|
public class CheatsList : ObservableCollection<CheatModel>
|
||||||
|
{
|
||||||
|
public CheatsList(string buildId, string path)
|
||||||
|
{
|
||||||
|
BuildId = buildId;
|
||||||
|
Path = path;
|
||||||
|
CollectionChanged += CheatsList_CollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheatsList_CollectionChanged(object sender,
|
||||||
|
NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Action == NotifyCollectionChangedAction.Add)
|
||||||
|
{
|
||||||
|
(e.NewItems[0] as CheatModel).EnableToggled += Item_EnableToggled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Item_EnableToggled(object sender, bool e)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BuildId { get; }
|
||||||
|
public string Path { get; }
|
||||||
|
|
||||||
|
public bool IsEnabled
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return this.ToList().TrueForAll(x => x.IsEnabled);
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
foreach (var cheat in this)
|
||||||
|
{
|
||||||
|
cheat.IsEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
Ryujinx.Ava/Ui/Models/DlcModel.cs
Normal file
18
Ryujinx.Ava/Ui/Models/DlcModel.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace Ryujinx.Ava.Ui.Models
|
||||||
|
{
|
||||||
|
public class DlcModel
|
||||||
|
{
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
public string TitleId { get; }
|
||||||
|
public string ContainerPath { get; }
|
||||||
|
public string FullPath { get; }
|
||||||
|
|
||||||
|
public DlcModel(string titleId, string containerPath, string fullPath, bool isEnabled)
|
||||||
|
{
|
||||||
|
TitleId = titleId;
|
||||||
|
ContainerPath = containerPath;
|
||||||
|
FullPath = fullPath;
|
||||||
|
IsEnabled = isEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
namespace Ryujinx.Ava.Ui.Models
|
namespace Ryujinx.Ava.Ui.Models
|
||||||
{
|
{
|
||||||
internal class ProfileImageModel
|
public class ProfileImageModel
|
||||||
{
|
{
|
||||||
public ProfileImageModel(string name, byte[] data)
|
public ProfileImageModel(string name, byte[] data)
|
||||||
{
|
{
|
||||||
|
@ -9,8 +9,11 @@ namespace Ryujinx.Ava.Ui.Models
|
|||||||
public bool IsNoUpdate { get; }
|
public bool IsNoUpdate { get; }
|
||||||
public ApplicationControlProperty Control { get; }
|
public ApplicationControlProperty Control { get; }
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
public string Label => IsNoUpdate ? LocaleManager.Instance["NoUpdate"] :
|
|
||||||
string.Format(LocaleManager.Instance["TitleUpdateVersionLabel"], Control.DisplayVersionString.ToString(), Path);
|
public string Label => IsNoUpdate
|
||||||
|
? LocaleManager.Instance["NoUpdate"]
|
||||||
|
: string.Format(LocaleManager.Instance["TitleUpdateVersionLabel"], Control.DisplayVersionString.ToString(),
|
||||||
|
Path);
|
||||||
|
|
||||||
public TitleUpdateModel(ApplicationControlProperty control, string path, bool isNoUpdate = false)
|
public TitleUpdateModel(ApplicationControlProperty control, string path, bool isNoUpdate = false)
|
||||||
{
|
{
|
||||||
|
61
Ryujinx.Ava/Ui/Models/UserProfile.cs
Normal file
61
Ryujinx.Ava/Ui/Models/UserProfile.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
|
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Models
|
||||||
|
{
|
||||||
|
public class UserProfile : BaseModel
|
||||||
|
{
|
||||||
|
private readonly Profile _profile;
|
||||||
|
private byte[] _image;
|
||||||
|
private string _name;
|
||||||
|
private UserId _userId;
|
||||||
|
|
||||||
|
public byte[] Image
|
||||||
|
{
|
||||||
|
get => _image;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_image = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserId UserId
|
||||||
|
{
|
||||||
|
get => _userId;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_userId = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_name = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserProfile(Profile profile)
|
||||||
|
{
|
||||||
|
_profile = profile;
|
||||||
|
|
||||||
|
Image = profile.Image;
|
||||||
|
Name = profile.Name;
|
||||||
|
UserId = profile.UserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsOpened => _profile.AccountState == AccountState.Open;
|
||||||
|
|
||||||
|
public void UpdateState()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsOpened));
|
||||||
|
OnPropertyChanged(nameof(Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
450
Ryujinx.Ava/Ui/ViewModels/AmiiboWindowViewModel.cs
Normal file
450
Ryujinx.Ava/Ui/ViewModels/AmiiboWindowViewModel.cs
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
using Ryujinx.Ava.Ui.Windows;
|
||||||
|
using Ryujinx.Common;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
|
{
|
||||||
|
public class AmiiboWindowViewModel : BaseModel, IDisposable
|
||||||
|
{
|
||||||
|
private const string DefaultJson = "{ \"amiibo\": [] }";
|
||||||
|
private const float AmiiboImageSize = 350f;
|
||||||
|
|
||||||
|
private readonly string _amiiboJsonPath;
|
||||||
|
private readonly byte[] _amiiboLogoBytes;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly StyleableWindow _owner;
|
||||||
|
|
||||||
|
private Bitmap _amiiboImage;
|
||||||
|
private List<Amiibo.AmiiboApi> _amiiboList;
|
||||||
|
private AvaloniaList<Amiibo.AmiiboApi> _amiibos;
|
||||||
|
private ObservableCollection<string> _amiiboSeries;
|
||||||
|
|
||||||
|
private int _amiiboSelectedIndex;
|
||||||
|
private int _seriesSelectedIndex;
|
||||||
|
private bool _enableScanning;
|
||||||
|
private bool _showAllAmiibo;
|
||||||
|
private bool _useRandomUuid;
|
||||||
|
private string _usage;
|
||||||
|
|
||||||
|
public AmiiboWindowViewModel(StyleableWindow owner, string lastScannedAmiiboId, string titleId)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_httpClient = new HttpClient { Timeout = TimeSpan.FromMilliseconds(5000) };
|
||||||
|
LastScannedAmiiboId = lastScannedAmiiboId;
|
||||||
|
TitleId = titleId;
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
|
||||||
|
|
||||||
|
_amiiboJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json");
|
||||||
|
_amiiboList = new List<Amiibo.AmiiboApi>();
|
||||||
|
_amiiboSeries = new ObservableCollection<string>();
|
||||||
|
_amiibos = new AvaloniaList<Amiibo.AmiiboApi>();
|
||||||
|
|
||||||
|
_amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png");
|
||||||
|
|
||||||
|
_ = LoadContentAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmiiboWindowViewModel() { }
|
||||||
|
|
||||||
|
public string TitleId { get; set; }
|
||||||
|
public string LastScannedAmiiboId { get; set; }
|
||||||
|
|
||||||
|
public UserResult Response { get; private set; }
|
||||||
|
|
||||||
|
public bool UseRandomUuid
|
||||||
|
{
|
||||||
|
get => _useRandomUuid;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_useRandomUuid = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowAllAmiibo
|
||||||
|
{
|
||||||
|
get => _showAllAmiibo;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_showAllAmiibo = value;
|
||||||
|
|
||||||
|
#pragma warning disable 4014
|
||||||
|
ParseAmiiboData();
|
||||||
|
#pragma warning restore 4014
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvaloniaList<Amiibo.AmiiboApi> AmiiboList
|
||||||
|
{
|
||||||
|
get => _amiibos;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_amiibos = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<string> AmiiboSeries
|
||||||
|
{
|
||||||
|
get => _amiiboSeries;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_amiiboSeries = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SeriesSelectedIndex
|
||||||
|
{
|
||||||
|
get => _seriesSelectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_seriesSelectedIndex = value;
|
||||||
|
|
||||||
|
FilterAmiibo();
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int AmiiboSelectedIndex
|
||||||
|
{
|
||||||
|
get => _amiiboSelectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_amiiboSelectedIndex = value;
|
||||||
|
|
||||||
|
EnableScanning = _amiiboSelectedIndex >= 0 && _amiiboSelectedIndex < _amiibos.Count;
|
||||||
|
|
||||||
|
SetAmiiboDetails();
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap AmiiboImage
|
||||||
|
{
|
||||||
|
get => _amiiboImage;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_amiiboImage = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Usage
|
||||||
|
{
|
||||||
|
get => _usage;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_usage = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool EnableScanning
|
||||||
|
{
|
||||||
|
get => _enableScanning;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_enableScanning = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_httpClient.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadContentAsync()
|
||||||
|
{
|
||||||
|
string amiiboJsonString = DefaultJson;
|
||||||
|
|
||||||
|
if (File.Exists(_amiiboJsonPath))
|
||||||
|
{
|
||||||
|
amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
|
||||||
|
|
||||||
|
if (await NeedsUpdate(JsonSerializer.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).LastUpdated))
|
||||||
|
{
|
||||||
|
amiiboJsonString = await DownloadAmiiboJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
amiiboJsonString = await DownloadAmiiboJson();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ShowInfoDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_amiiboList = JsonSerializer.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).Amiibo;
|
||||||
|
_amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
|
||||||
|
|
||||||
|
ParseAmiiboData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseAmiiboData()
|
||||||
|
{
|
||||||
|
_amiiboSeries.Clear();
|
||||||
|
_amiibos.Clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < _amiiboList.Count; i++)
|
||||||
|
{
|
||||||
|
if (!_amiiboSeries.Contains(_amiiboList[i].AmiiboSeries))
|
||||||
|
{
|
||||||
|
if (!ShowAllAmiibo)
|
||||||
|
{
|
||||||
|
foreach (Amiibo.AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
|
||||||
|
{
|
||||||
|
if (game != null)
|
||||||
|
{
|
||||||
|
if (game.GameId.Contains(TitleId))
|
||||||
|
{
|
||||||
|
AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LastScannedAmiiboId != "")
|
||||||
|
{
|
||||||
|
SelectLastScannedAmiibo();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SeriesSelectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectLastScannedAmiibo()
|
||||||
|
{
|
||||||
|
Amiibo.AmiiboApi scanned = _amiiboList.FirstOrDefault(amiibo => amiibo.GetId() == LastScannedAmiiboId);
|
||||||
|
|
||||||
|
SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries);
|
||||||
|
AmiiboSelectedIndex = AmiiboList.IndexOf(scanned);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FilterAmiibo()
|
||||||
|
{
|
||||||
|
_amiibos.Clear();
|
||||||
|
|
||||||
|
if (_seriesSelectedIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Amiibo.AmiiboApi> amiiboSortedList = _amiiboList
|
||||||
|
.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex])
|
||||||
|
.OrderBy(amiibo => amiibo.Name).ToList();
|
||||||
|
|
||||||
|
for (int i = 0; i < amiiboSortedList.Count; i++)
|
||||||
|
{
|
||||||
|
if (!_amiibos.Contains(amiiboSortedList[i]))
|
||||||
|
{
|
||||||
|
if (!_showAllAmiibo)
|
||||||
|
{
|
||||||
|
foreach (Amiibo.AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
|
||||||
|
{
|
||||||
|
if (game != null)
|
||||||
|
{
|
||||||
|
if (game.GameId.Contains(TitleId))
|
||||||
|
{
|
||||||
|
_amiibos.Add(amiiboSortedList[i]);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_amiibos.Add(amiiboSortedList[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AmiiboSelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetAmiiboDetails()
|
||||||
|
{
|
||||||
|
ResetAmiiboPreview();
|
||||||
|
|
||||||
|
Usage = string.Empty;
|
||||||
|
|
||||||
|
if (_amiiboSelectedIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Amiibo.AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
|
||||||
|
|
||||||
|
string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image;
|
||||||
|
|
||||||
|
string usageString = "";
|
||||||
|
|
||||||
|
for (int i = 0; i < _amiiboList.Count; i++)
|
||||||
|
{
|
||||||
|
if (_amiiboList[i].Equals(selected))
|
||||||
|
{
|
||||||
|
bool writable = false;
|
||||||
|
|
||||||
|
foreach (Amiibo.AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
|
||||||
|
{
|
||||||
|
if (item.GameId.Contains(TitleId))
|
||||||
|
{
|
||||||
|
foreach (Amiibo.AmiiboApiUsage usageItem in item.AmiiboUsage)
|
||||||
|
{
|
||||||
|
usageString += Environment.NewLine +
|
||||||
|
$"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
|
||||||
|
|
||||||
|
writable = usageItem.Write;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usageString.Length == 0)
|
||||||
|
{
|
||||||
|
usageString = LocaleManager.Instance["Unknown"] + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
Usage = $"{LocaleManager.Instance["Usage"]} {(writable ? $" ({LocaleManager.Instance["Writable"]})" : "")} : {usageString}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = UpdateAmiiboPreview(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> NeedsUpdate(DateTime oldLastModified)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpResponseMessage response =
|
||||||
|
await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://amiibo.ryujinx.org/"));
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return response.Content.Headers.LastModified != oldLastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ShowInfoDialog();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> DownloadAmiiboJson()
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = await _httpClient.GetAsync("https://amiibo.ryujinx.org/");
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
string amiiboJsonString = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
using (FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
|
||||||
|
{
|
||||||
|
dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
|
||||||
|
}
|
||||||
|
|
||||||
|
return amiiboJsonString;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ContentDialogHelper.CreateInfoDialog(_owner, LocaleManager.Instance["DialogAmiiboApiTitle"],
|
||||||
|
LocaleManager.Instance["DialogAmiiboApiFailFetchMessage"],
|
||||||
|
LocaleManager.Instance["InputDialogOk"],
|
||||||
|
"",
|
||||||
|
LocaleManager.Instance["RyujinxInfo"]);
|
||||||
|
|
||||||
|
Close();
|
||||||
|
|
||||||
|
return DefaultJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close()
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(_owner.Close);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateAmiiboPreview(string imageUrl)
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = await _httpClient.GetAsync(imageUrl);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync();
|
||||||
|
using (MemoryStream memoryStream = new(amiiboPreviewBytes))
|
||||||
|
{
|
||||||
|
Bitmap bitmap = new(memoryStream);
|
||||||
|
|
||||||
|
double ratio = Math.Min(AmiiboImageSize / bitmap.Size.Width,
|
||||||
|
AmiiboImageSize / bitmap.Size.Height);
|
||||||
|
|
||||||
|
int resizeHeight = (int)(bitmap.Size.Height * ratio);
|
||||||
|
int resizeWidth = (int)(bitmap.Size.Width * ratio);
|
||||||
|
|
||||||
|
AmiiboImage = bitmap.CreateScaledBitmap(new PixelSize(resizeWidth, resizeHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetAmiiboPreview()
|
||||||
|
{
|
||||||
|
using (MemoryStream memoryStream = new(_amiiboLogoBytes))
|
||||||
|
{
|
||||||
|
Bitmap bitmap = new(memoryStream);
|
||||||
|
|
||||||
|
AmiiboImage = bitmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ShowInfoDialog()
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateInfoDialog(_owner, LocaleManager.Instance["DialogAmiiboApiTitle"],
|
||||||
|
LocaleManager.Instance["DialogAmiiboApiConnectErrorMessage"],
|
||||||
|
LocaleManager.Instance["InputDialogOk"],
|
||||||
|
"",
|
||||||
|
LocaleManager.Instance["RyujinxInfo"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
363
Ryujinx.Ava/Ui/ViewModels/AvatarProfileViewModel.cs
Normal file
363
Ryujinx.Ava/Ui/ViewModels/AvatarProfileViewModel.cs
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
using Avalonia.Media;
|
||||||
|
using DynamicData;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Ncm;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats.Png;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Color = Avalonia.Media.Color;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
|
{
|
||||||
|
internal class AvatarProfileViewModel : BaseModel, IDisposable
|
||||||
|
{
|
||||||
|
private const int MaxImageTasks = 4;
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, byte[]> _avatarStore = new();
|
||||||
|
private static bool _isPreloading;
|
||||||
|
private static Action _loadCompleteAction;
|
||||||
|
|
||||||
|
private ObservableCollection<ProfileImageModel> _images;
|
||||||
|
private Color _backgroundColor = Colors.White;
|
||||||
|
|
||||||
|
private int _selectedIndex;
|
||||||
|
private int _imagesLoaded;
|
||||||
|
private bool _isActive;
|
||||||
|
private byte[] _selectedImage;
|
||||||
|
private bool _isIndeterminate = true;
|
||||||
|
|
||||||
|
public bool IsActive
|
||||||
|
{
|
||||||
|
get => _isActive;
|
||||||
|
set => _isActive = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvatarProfileViewModel()
|
||||||
|
{
|
||||||
|
_images = new ObservableCollection<ProfileImageModel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvatarProfileViewModel(Action loadCompleteAction)
|
||||||
|
{
|
||||||
|
_images = new ObservableCollection<ProfileImageModel>();
|
||||||
|
|
||||||
|
if (_isPreloading)
|
||||||
|
{
|
||||||
|
_loadCompleteAction = loadCompleteAction;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ReloadImages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color BackgroundColor
|
||||||
|
{
|
||||||
|
get => _backgroundColor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_backgroundColor = value;
|
||||||
|
|
||||||
|
IsActive = false;
|
||||||
|
|
||||||
|
ReloadImages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<ProfileImageModel> Images
|
||||||
|
{
|
||||||
|
get => _images;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_images = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsIndeterminate
|
||||||
|
{
|
||||||
|
get => _isIndeterminate;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isIndeterminate = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ImageCount => _avatarStore.Count;
|
||||||
|
|
||||||
|
public int ImagesLoaded
|
||||||
|
{
|
||||||
|
get => _imagesLoaded;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_imagesLoaded = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SelectedIndex
|
||||||
|
{
|
||||||
|
get => _selectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_selectedIndex = value;
|
||||||
|
|
||||||
|
if (_selectedIndex == -1)
|
||||||
|
{
|
||||||
|
SelectedImage = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedImage = _images[_selectedIndex].Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] SelectedImage
|
||||||
|
{
|
||||||
|
get => _selectedImage;
|
||||||
|
private set => _selectedImage = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReloadImages()
|
||||||
|
{
|
||||||
|
if (_isPreloading)
|
||||||
|
{
|
||||||
|
IsIndeterminate = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
IsActive = true;
|
||||||
|
|
||||||
|
Images.Clear();
|
||||||
|
int selectedIndex = _selectedIndex;
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
ImagesLoaded = 0;
|
||||||
|
IsIndeterminate = false;
|
||||||
|
|
||||||
|
var keys = _avatarStore.Keys.ToList();
|
||||||
|
|
||||||
|
var newImages = new List<ProfileImageModel>();
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
|
for (int i = 0; i < MaxImageTasks; i++)
|
||||||
|
{
|
||||||
|
var start = i;
|
||||||
|
tasks.Add(Task.Run(() => ImageTask(start)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.WaitAll(tasks.ToArray());
|
||||||
|
|
||||||
|
Images.AddRange(newImages);
|
||||||
|
|
||||||
|
void ImageTask(int start)
|
||||||
|
{
|
||||||
|
for (int i = start; i < keys.Count; i += MaxImageTasks)
|
||||||
|
{
|
||||||
|
if (!IsActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = keys[i];
|
||||||
|
var image = _avatarStore[keys[i]];
|
||||||
|
|
||||||
|
var data = ProcessImage(image);
|
||||||
|
newImages.Add(new ProfileImageModel(key, data));
|
||||||
|
if (index++ == selectedIndex)
|
||||||
|
{
|
||||||
|
SelectedImage = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _imagesLoaded);
|
||||||
|
OnPropertyChanged(nameof(ImagesLoaded));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ProcessImage(byte[] data)
|
||||||
|
{
|
||||||
|
using (MemoryStream streamJpg = new())
|
||||||
|
{
|
||||||
|
Image avatarImage = Image.Load(data, new PngDecoder());
|
||||||
|
|
||||||
|
avatarImage.Mutate(x => x.BackgroundColor(new Rgba32(BackgroundColor.R,
|
||||||
|
BackgroundColor.G,
|
||||||
|
BackgroundColor.B,
|
||||||
|
BackgroundColor.A)));
|
||||||
|
avatarImage.SaveAsJpeg(streamJpg);
|
||||||
|
|
||||||
|
return streamJpg.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_avatarStore.Count > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isPreloading = true;
|
||||||
|
|
||||||
|
string contentPath =
|
||||||
|
contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem,
|
||||||
|
NcaContentType.Data);
|
||||||
|
string avatarPath = virtualFileSystem.SwitchPathToSystemPath(contentPath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(avatarPath))
|
||||||
|
{
|
||||||
|
using (IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open))
|
||||||
|
{
|
||||||
|
Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
|
||||||
|
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
|
||||||
|
{
|
||||||
|
// TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
|
||||||
|
if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") &&
|
||||||
|
item.FullPath.Contains("szs"))
|
||||||
|
{
|
||||||
|
using var file = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
romfs.OpenFile(ref file.Ref(), ("/" + item.FullPath).ToU8Span(), OpenMode.Read)
|
||||||
|
.ThrowIfFailure();
|
||||||
|
|
||||||
|
using (MemoryStream stream = new())
|
||||||
|
using (MemoryStream streamPng = new())
|
||||||
|
{
|
||||||
|
file.Get.AsStream().CopyTo(stream);
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
|
||||||
|
Image avatarImage = Image.LoadPixelData<Rgba32>(DecompressYaz0(stream), 256, 256);
|
||||||
|
|
||||||
|
avatarImage.SaveAsPng(streamPng);
|
||||||
|
|
||||||
|
_avatarStore.Add(item.FullPath, streamPng.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isPreloading = false;
|
||||||
|
_loadCompleteAction?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] DecompressYaz0(Stream stream)
|
||||||
|
{
|
||||||
|
using (BinaryReader reader = new(stream))
|
||||||
|
{
|
||||||
|
reader.ReadInt32(); // Magic
|
||||||
|
|
||||||
|
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
|
||||||
|
|
||||||
|
reader.ReadInt64(); // Padding
|
||||||
|
|
||||||
|
byte[] input = new byte[stream.Length - stream.Position];
|
||||||
|
stream.Read(input, 0, input.Length);
|
||||||
|
|
||||||
|
uint inputOffset = 0;
|
||||||
|
|
||||||
|
byte[] output = new byte[decodedLength];
|
||||||
|
uint outputOffset = 0;
|
||||||
|
|
||||||
|
ushort mask = 0;
|
||||||
|
byte header = 0;
|
||||||
|
|
||||||
|
while (outputOffset < decodedLength)
|
||||||
|
{
|
||||||
|
if ((mask >>= 1) == 0)
|
||||||
|
{
|
||||||
|
header = input[inputOffset++];
|
||||||
|
mask = 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((header & mask) != 0)
|
||||||
|
{
|
||||||
|
if (outputOffset == output.Length)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
output[outputOffset++] = input[inputOffset++];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
byte byte1 = input[inputOffset++];
|
||||||
|
byte byte2 = input[inputOffset++];
|
||||||
|
|
||||||
|
uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
|
||||||
|
uint position = outputOffset - (dist + 1);
|
||||||
|
|
||||||
|
uint length = (uint)byte1 >> 4;
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
length = (uint)input[inputOffset++] + 0x12;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
length += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint gap = outputOffset - position;
|
||||||
|
uint nonOverlappingLength = length;
|
||||||
|
|
||||||
|
if (nonOverlappingLength > gap)
|
||||||
|
{
|
||||||
|
nonOverlappingLength = gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
|
||||||
|
outputOffset += nonOverlappingLength;
|
||||||
|
position += nonOverlappingLength;
|
||||||
|
length -= nonOverlappingLength;
|
||||||
|
|
||||||
|
while (length-- > 0)
|
||||||
|
{
|
||||||
|
output[outputOffset++] = output[position++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_loadCompleteAction = null;
|
||||||
|
IsActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -67,6 +67,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
private bool _isPaused;
|
private bool _isPaused;
|
||||||
private bool _showContent = true;
|
private bool _showContent = true;
|
||||||
private bool _isLoadingIndeterminate = true;
|
private bool _isLoadingIndeterminate = true;
|
||||||
|
private bool _showAll;
|
||||||
|
private string _lastScannedAmiiboId;
|
||||||
private ReadOnlyObservableCollection<ApplicationData> _appsObservableList;
|
private ReadOnlyObservableCollection<ApplicationData> _appsObservableList;
|
||||||
|
|
||||||
public string TitleName { get; internal set; }
|
public string TitleName { get; internal set; }
|
||||||
@ -695,15 +697,28 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenAmiiboWindow()
|
public async void OpenAmiiboWindow()
|
||||||
{
|
{
|
||||||
if (!_isAmiiboRequested)
|
if (!_isAmiiboRequested)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_owner.AppHost.Device.System.SearchingForAmiibo(out int deviceId))
|
||||||
|
{
|
||||||
|
string titleId = _owner.AppHost.Device.Application.TitleIdText.ToUpper();
|
||||||
|
AmiiboWindow window = new(_showAll, _lastScannedAmiiboId, titleId);
|
||||||
|
|
||||||
// TODO : Implement Amiibo window
|
await window.ShowDialog(_owner);
|
||||||
ContentDialogHelper.ShowNotAvailableMessage(_owner);
|
|
||||||
|
if (window.IsScanned)
|
||||||
|
{
|
||||||
|
_showAll = window.ViewModel.ShowAllAmiibo;
|
||||||
|
_lastScannedAmiiboId = window.ScannedAmiibo.GetId();
|
||||||
|
|
||||||
|
_owner.AppHost.Device.System.ScanAmiibo(deviceId, _lastScannedAmiiboId, window.ViewModel.UseRandomUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleShaderProgress(Switch emulationContext)
|
public void HandleShaderProgress(Switch emulationContext)
|
||||||
@ -953,10 +968,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
LoadConfigurableHotKeys();
|
LoadConfigurableHotKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ManageProfiles()
|
public async void ManageProfiles()
|
||||||
{
|
{
|
||||||
// TODO : Implement Profiles window
|
UserProfileWindow window = new(_owner.AccountManager, _owner.ContentManager, _owner.VirtualFileSystem);
|
||||||
ContentDialogHelper.ShowNotAvailableMessage(_owner);
|
|
||||||
|
await window.ShowDialog(_owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OpenAboutWindow()
|
public async void OpenAboutWindow()
|
||||||
@ -1227,33 +1243,60 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenTitleUpdateManager()
|
public async void OpenTitleUpdateManager()
|
||||||
{
|
{
|
||||||
// TODO : Implement Update window
|
var selection = SelectedApplication;
|
||||||
ContentDialogHelper.ShowNotAvailableMessage(_owner);
|
|
||||||
|
if (selection != null)
|
||||||
|
{
|
||||||
|
TitleUpdateWindow titleUpdateManager =
|
||||||
|
new(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName);
|
||||||
|
|
||||||
|
await titleUpdateManager.ShowDialog(_owner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenDlcManager()
|
public async void OpenDlcManager()
|
||||||
{
|
{
|
||||||
// TODO : Implement Dlc window
|
var selection = SelectedApplication;
|
||||||
ContentDialogHelper.ShowNotAvailableMessage(_owner);
|
|
||||||
|
if (selection != null)
|
||||||
|
{
|
||||||
|
DlcManagerWindow dlcManager = new(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName);
|
||||||
|
|
||||||
|
await dlcManager.ShowDialog(_owner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenCheatManager()
|
public async void OpenCheatManager()
|
||||||
{
|
{
|
||||||
// TODO : Implement cheat window
|
var selection = SelectedApplication;
|
||||||
ContentDialogHelper.ShowNotAvailableMessage(_owner);
|
|
||||||
|
if (selection != null)
|
||||||
|
{
|
||||||
|
CheatWindow cheatManager = new(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName);
|
||||||
|
|
||||||
|
await cheatManager.ShowDialog(_owner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenCheatManagerForCurrentApp()
|
public async void OpenCheatManagerForCurrentApp()
|
||||||
{
|
{
|
||||||
if (!IsGameRunning)
|
if (!IsGameRunning)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO : Implement cheat window
|
var application = _owner.AppHost.Device.Application;
|
||||||
ContentDialogHelper.ShowNotAvailableMessage(_owner);
|
|
||||||
|
if (application != null)
|
||||||
|
{
|
||||||
|
CheatWindow cheatManager = new(_owner.VirtualFileSystem, application.TitleIdText, application.TitleName);
|
||||||
|
|
||||||
|
await cheatManager.ShowDialog(_owner);
|
||||||
|
|
||||||
|
_owner.AppHost.Device.EnableCheats();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenDeviceSaveDirectory()
|
public void OpenDeviceSaveDirectory()
|
||||||
|
166
Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs
Normal file
166
Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
|
using Ryujinx.Ava.Ui.Windows;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.ViewModels
|
||||||
|
{
|
||||||
|
public class UserProfileViewModel : BaseModel, IDisposable
|
||||||
|
{
|
||||||
|
private const uint MaxProfileNameLength = 0x20;
|
||||||
|
|
||||||
|
private readonly UserProfileWindow _owner;
|
||||||
|
|
||||||
|
private UserProfile _selectedProfile;
|
||||||
|
private string _tempUserName;
|
||||||
|
|
||||||
|
public UserProfileViewModel()
|
||||||
|
{
|
||||||
|
Profiles = new ObservableCollection<UserProfile>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserProfileViewModel(UserProfileWindow owner) : this()
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
|
||||||
|
LoadProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<UserProfile> Profiles { get; set; }
|
||||||
|
|
||||||
|
public UserProfile SelectedProfile
|
||||||
|
{
|
||||||
|
get => _selectedProfile;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_selectedProfile = value;
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(SelectedProfile));
|
||||||
|
OnPropertyChanged(nameof(IsSelectedProfileDeletable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSelectedProfileDeletable =>
|
||||||
|
_selectedProfile != null && _selectedProfile.UserId != AccountManager.DefaultUserId;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadProfiles()
|
||||||
|
{
|
||||||
|
Profiles.Clear();
|
||||||
|
|
||||||
|
var profiles = _owner.AccountManager.GetAllUsers()
|
||||||
|
.OrderByDescending(x => x.AccountState == AccountState.Open);
|
||||||
|
|
||||||
|
foreach (var profile in profiles)
|
||||||
|
{
|
||||||
|
Profiles.Add(new UserProfile(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId);
|
||||||
|
|
||||||
|
if (SelectedProfile == null)
|
||||||
|
{
|
||||||
|
SelectedProfile = Profiles.First();
|
||||||
|
|
||||||
|
if (SelectedProfile != null)
|
||||||
|
{
|
||||||
|
_owner.AccountManager.OpenUser(_selectedProfile.UserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void ChooseProfileImage()
|
||||||
|
{
|
||||||
|
await SelectProfileImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SelectProfileImage(bool isNewUser = false)
|
||||||
|
{
|
||||||
|
ProfileImageSelectionDialog selectionDialog = new(_owner.ContentManager);
|
||||||
|
|
||||||
|
await selectionDialog.ShowDialog(_owner);
|
||||||
|
|
||||||
|
if (selectionDialog.BufferImageProfile != null)
|
||||||
|
{
|
||||||
|
if (isNewUser)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(_tempUserName))
|
||||||
|
{
|
||||||
|
_owner.AccountManager.AddUser(_tempUserName, selectionDialog.BufferImageProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (SelectedProfile != null)
|
||||||
|
{
|
||||||
|
_owner.AccountManager.SetUserImage(SelectedProfile.UserId, selectionDialog.BufferImageProfile);
|
||||||
|
SelectedProfile.Image = selectionDialog.BufferImageProfile;
|
||||||
|
|
||||||
|
SelectedProfile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadProfiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void AddUser()
|
||||||
|
{
|
||||||
|
var dlgTitle = LocaleManager.Instance["InputDialogAddNewProfileTitle"];
|
||||||
|
var dlgMainText = LocaleManager.Instance["InputDialogAddNewProfileHeader"];
|
||||||
|
var dlgSubText = string.Format(LocaleManager.Instance["InputDialogAddNewProfileSubtext"],
|
||||||
|
MaxProfileNameLength);
|
||||||
|
|
||||||
|
_tempUserName =
|
||||||
|
await ContentDialogHelper.CreateInputDialog(dlgTitle, dlgMainText, dlgSubText, _owner,
|
||||||
|
MaxProfileNameLength);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_tempUserName))
|
||||||
|
{
|
||||||
|
await SelectProfileImage(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tempUserName = String.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void DeleteUser()
|
||||||
|
{
|
||||||
|
if (_selectedProfile != null)
|
||||||
|
{
|
||||||
|
var lastUserId = _owner.AccountManager.LastOpenedUser.UserId;
|
||||||
|
|
||||||
|
if (_selectedProfile.UserId == lastUserId)
|
||||||
|
{
|
||||||
|
// If we are deleting the currently open profile, then we must open something else before deleting.
|
||||||
|
var profile = Profiles.FirstOrDefault(x => x.UserId != lastUserId);
|
||||||
|
|
||||||
|
if (profile == null)
|
||||||
|
{
|
||||||
|
ContentDialogHelper.CreateErrorDialog(_owner,
|
||||||
|
LocaleManager.Instance["DialogUserProfileDeletionWarningMessage"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_owner.AccountManager.OpenUser(profile.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result =
|
||||||
|
await ContentDialogHelper.CreateConfirmationDialog(_owner,
|
||||||
|
LocaleManager.Instance["DialogUserProfileDeletionConfirmMessage"], "",
|
||||||
|
LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], "");
|
||||||
|
|
||||||
|
if (result == UserResult.Yes)
|
||||||
|
{
|
||||||
|
_owner.AccountManager.DeleteUser(_selectedProfile.UserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadProfiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml
Normal file
68
Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<window:StyleableWindow 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="400" d:DesignHeight="350"
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Windows.AmiiboWindow"
|
||||||
|
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
||||||
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||||
|
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Width="800" MinHeight="650" Height="650"
|
||||||
|
SizeToContent="Manual"
|
||||||
|
MinWidth="600">
|
||||||
|
<Design.DataContext>
|
||||||
|
<viewModels:AmiiboWindowViewModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
<Grid Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid Grid.Row="1" HorizontalAlignment="Stretch">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition />
|
||||||
|
<ColumnDefinition />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Spacing="10" Orientation="Horizontal" HorizontalAlignment="Left">
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboSeriesLabel}" />
|
||||||
|
<ComboBox SelectedIndex="{Binding SeriesSelectedIndex}" Items="{Binding AmiiboSeries}" MinWidth="100" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="10" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboCharacterLabel}" />
|
||||||
|
<ComboBox SelectedIndex="{Binding AmiiboSelectedIndex}" MinWidth="100" Items="{Binding AmiiboList}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel Margin="20" Grid.Row="2">
|
||||||
|
<Image Source="{Binding AmiiboImage}" Height="350" Width="350" HorizontalAlignment="Center" />
|
||||||
|
<ScrollViewer MaxHeight="120" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
|
||||||
|
Margin="20" VerticalAlignment="Top" HorizontalAlignment="Stretch">
|
||||||
|
<TextBlock TextWrapping="Wrap" Text="{Binding Usage}" HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</StackPanel>
|
||||||
|
<Grid Grid.Row="3">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<CheckBox Margin="10" Grid.Column="0" VerticalContentAlignment="Center" IsChecked="{Binding ShowAllAmiibo}"
|
||||||
|
Content="{locale:Locale AmiiboOptionsShowAllLabel}" />
|
||||||
|
<CheckBox Margin="10" VerticalContentAlignment="Center" Grid.Column="1" IsChecked="{Binding UseRandomUuid}"
|
||||||
|
Content="{locale:Locale AmiiboOptionsUsRandomTagLabel}" />
|
||||||
|
|
||||||
|
<Button Grid.Column="3" IsEnabled="{Binding EnableScanning}" Width="80"
|
||||||
|
Content="{locale:Locale AmiiboScanButtonLabel}" Name="ScanButton"
|
||||||
|
Click="ScanButton_Click" />
|
||||||
|
<Button Grid.Column="4" Margin="10,0" Width="80" Content="{locale:Locale InputDialogCancel}"
|
||||||
|
Name="CancelButton"
|
||||||
|
Click="CancelButton_Click" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</window:StyleableWindow>
|
70
Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs
Normal file
70
Ryujinx.Ava/Ui/Windows/AmiiboWindow.axaml.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
|
{
|
||||||
|
public class AmiiboWindow : StyleableWindow
|
||||||
|
{
|
||||||
|
public AmiiboWindow(bool showAll, string lastScannedAmiiboId, string titleId)
|
||||||
|
{
|
||||||
|
ViewModel = new AmiiboWindowViewModel(this, lastScannedAmiiboId, titleId);
|
||||||
|
|
||||||
|
ViewModel.ShowAllAmiibo = showAll;
|
||||||
|
|
||||||
|
DataContext = ViewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
#if DEBUG
|
||||||
|
this.AttachDevTools();
|
||||||
|
#endif
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmiiboWindow()
|
||||||
|
{
|
||||||
|
ViewModel = new AmiiboWindowViewModel(this, string.Empty, string.Empty);
|
||||||
|
|
||||||
|
DataContext = ViewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
#if DEBUG
|
||||||
|
this.AttachDevTools();
|
||||||
|
#endif
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsScanned { get; set; }
|
||||||
|
public Amiibo.AmiiboApi ScannedAmiibo { get; set; }
|
||||||
|
public AmiiboWindowViewModel ViewModel { get; set; }
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScanButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel.AmiiboSelectedIndex > -1)
|
||||||
|
{
|
||||||
|
Amiibo.AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
|
||||||
|
ScannedAmiibo = amiibo;
|
||||||
|
IsScanned = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
IsScanned = false;
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml
Normal file
53
Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Windows.AvatarWindow"
|
||||||
|
CanResize="False"
|
||||||
|
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||||
|
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
x:CompileBindings="True"
|
||||||
|
x:DataType="viewModels:AvatarProfileViewModel"
|
||||||
|
SizeToContent="WidthAndHeight">
|
||||||
|
<Design.DataContext>
|
||||||
|
<viewModels:AvatarProfileViewModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
<Window.Resources>
|
||||||
|
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||||
|
</Window.Resources>
|
||||||
|
<Grid Margin="5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<ListBox Grid.Row="1" BorderThickness="0" SelectedIndex="{Binding SelectedIndex}" Width="600" Height="500"
|
||||||
|
Items="{Binding Images}" HorizontalAlignment="Stretch" VerticalAlignment="Center">
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal" MaxWidth="600" Margin="0" HorizontalAlignment="Center" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Image Margin="5" Height="96" Width="96"
|
||||||
|
Source="{Binding Data, Converter={StaticResource ByteImage}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
<ProgressBar Grid.Row="2" IsIndeterminate="{Binding IsIndeterminate}" Value="{Binding ImagesLoaded}" HorizontalAlignment="Stretch" Margin="5"
|
||||||
|
Maximum="{Binding ImageCount}" Minimum="0" />
|
||||||
|
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="10" Margin="10" HorizontalAlignment="Center">
|
||||||
|
<Button Content="{Locale:Locale AvatarChoose}" Width="200" Name="ChooseButton" Click="ChooseButton_OnClick" />
|
||||||
|
<ui:ColorPickerButton Color="{Binding BackgroundColor, Mode=TwoWay}" Name="ColorButton" />
|
||||||
|
<Button HorizontalAlignment="Right" Content="{Locale:Locale AvatarClose}" Click="CloseButton_OnClick"
|
||||||
|
Name="CloseButton"
|
||||||
|
Width="200" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
71
Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs
Normal file
71
Ryujinx.Ava/Ui/Windows/AvatarWindow.axaml.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
|
{
|
||||||
|
public class AvatarWindow : StyleableWindow
|
||||||
|
{
|
||||||
|
public AvatarWindow(ContentManager contentManager)
|
||||||
|
{
|
||||||
|
ContentManager = contentManager;
|
||||||
|
ViewModel = new AvatarProfileViewModel(() => ViewModel.ReloadImages());
|
||||||
|
|
||||||
|
DataContext = ViewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
#if DEBUG
|
||||||
|
this.AttachDevTools();
|
||||||
|
#endif
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["AvatarWindowTitle"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvatarWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
#if DEBUG
|
||||||
|
this.AttachDevTools();
|
||||||
|
#endif
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["AvatarWindowTitle"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentManager ContentManager { get; }
|
||||||
|
|
||||||
|
public byte[] SelectedImage { get; set; }
|
||||||
|
|
||||||
|
internal AvatarProfileViewModel ViewModel { get; set; }
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClosed(EventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel.Dispose();
|
||||||
|
base.OnClosed(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel.SelectedIndex > -1)
|
||||||
|
{
|
||||||
|
SelectedImage = ViewModel.SelectedImage;
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
Ryujinx.Ava/Ui/Windows/CheatWindow.axaml
Normal file
90
Ryujinx.Ava/Ui/Windows/CheatWindow.axaml
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<window:StyleableWindow x:Class="Ryujinx.Ava.Ui.Windows.CheatWindow"
|
||||||
|
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
xmlns:model="clr-namespace:Ryujinx.Ava.Ui.Models"
|
||||||
|
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
SizeToContent="Height"
|
||||||
|
Width="500" MinHeight="500" Height="500"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
MinWidth="500">
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="TreeViewItem">
|
||||||
|
<Setter Property="IsExpanded" Value="True" />
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
<Grid Name="DlcGrid" Margin="15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="20,15,20,20"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxWidth="500"
|
||||||
|
LineHeight="18"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="{Binding Heading}"
|
||||||
|
TextAlignment="Center" />
|
||||||
|
<Border
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
BorderBrush="Gray"
|
||||||
|
BorderThickness="1">
|
||||||
|
<TreeView Items="{Binding LoadedCheats}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Name="CheatsView"
|
||||||
|
MinHeight="300">
|
||||||
|
<TreeView.DataTemplates>
|
||||||
|
<TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}">
|
||||||
|
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||||
|
<CheckBox IsChecked="{Binding IsEnabled}" MinWidth="20" />
|
||||||
|
<TextBlock Width="150"
|
||||||
|
Text="{Binding BuildId}" />
|
||||||
|
<TextBlock
|
||||||
|
Text="{Binding Path}" />
|
||||||
|
</StackPanel>
|
||||||
|
</TreeDataTemplate>
|
||||||
|
<DataTemplate x:DataType="model:CheatModel">
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||||
|
<CheckBox IsChecked="{Binding IsEnabled}" MinWidth="20" />
|
||||||
|
<TextBlock Text="{Binding CleanName}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</TreeView.DataTemplates>
|
||||||
|
</TreeView>
|
||||||
|
</Border>
|
||||||
|
<DockPanel
|
||||||
|
Grid.Row="3"
|
||||||
|
Margin="0"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<DockPanel Margin="0" HorizontalAlignment="Right">
|
||||||
|
<Button
|
||||||
|
Name="SaveButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
IsVisible="{Binding !NoCheatsFound}"
|
||||||
|
Command="{Binding Save}">
|
||||||
|
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="CancelButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding Close}">
|
||||||
|
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
||||||
|
</Button>
|
||||||
|
</DockPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
</window:StyleableWindow>
|
137
Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs
Normal file
137
Ryujinx.Ava/Ui/Windows/CheatWindow.axaml.cs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.HOS;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
|
{
|
||||||
|
public class CheatWindow : StyleableWindow
|
||||||
|
{
|
||||||
|
private readonly string _enabledCheatsPath;
|
||||||
|
public bool NoCheatsFound { get; }
|
||||||
|
|
||||||
|
private AvaloniaList<CheatsList> LoadedCheats { get; }
|
||||||
|
|
||||||
|
public string Heading { get; }
|
||||||
|
|
||||||
|
public CheatWindow()
|
||||||
|
{
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
AttachDebugDevTools();
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
|
||||||
|
{
|
||||||
|
LoadedCheats = new AvaloniaList<CheatsList>();
|
||||||
|
|
||||||
|
Heading = string.Format(LocaleManager.Instance["CheatWindowHeading"], titleName, titleId.ToUpper());
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
#if DEBUG
|
||||||
|
this.AttachDevTools();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
|
||||||
|
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
|
||||||
|
ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber);
|
||||||
|
|
||||||
|
_enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt");
|
||||||
|
|
||||||
|
string[] enabled = { };
|
||||||
|
|
||||||
|
if (File.Exists(_enabledCheatsPath))
|
||||||
|
{
|
||||||
|
enabled = File.ReadAllLines(_enabledCheatsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cheatAdded = 0;
|
||||||
|
|
||||||
|
var mods = new ModLoader.ModCache();
|
||||||
|
|
||||||
|
ModLoader.QueryContentsDir(mods, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleIdValue);
|
||||||
|
|
||||||
|
string currentCheatFile = string.Empty;
|
||||||
|
string buildId = string.Empty;
|
||||||
|
string parentPath = string.Empty;
|
||||||
|
|
||||||
|
CheatsList currentGroup = null;
|
||||||
|
|
||||||
|
foreach (var cheat in mods.Cheats)
|
||||||
|
{
|
||||||
|
if (cheat.Path.FullName != currentCheatFile)
|
||||||
|
{
|
||||||
|
currentCheatFile = cheat.Path.FullName;
|
||||||
|
parentPath = currentCheatFile.Replace(titleModsPath, "");
|
||||||
|
|
||||||
|
buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper();
|
||||||
|
currentGroup = new CheatsList(buildId, parentPath);
|
||||||
|
|
||||||
|
LoadedCheats.Add(currentGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = new CheatModel(cheat.Name, buildId, enabled.Contains($"{buildId}-{cheat.Name}"));
|
||||||
|
currentGroup?.Add(model);
|
||||||
|
|
||||||
|
cheatAdded++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cheatAdded == 0)
|
||||||
|
{
|
||||||
|
NoCheatsFound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private void AttachDebugDevTools()
|
||||||
|
{
|
||||||
|
this.AttachDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
if (NoCheatsFound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> enabledCheats = new List<string>();
|
||||||
|
|
||||||
|
foreach (var cheats in LoadedCheats)
|
||||||
|
{
|
||||||
|
foreach (var cheat in cheats)
|
||||||
|
{
|
||||||
|
if (cheat.IsEnabled)
|
||||||
|
{
|
||||||
|
enabledCheats.Add(cheat.BuildIdKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(_enabledCheatsPath));
|
||||||
|
|
||||||
|
File.WriteAllLines(_enabledCheatsPath, enabledCheats);
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml
Normal file
132
Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<window:StyleableWindow
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Windows.DlcManagerWindow"
|
||||||
|
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
||||||
|
SizeToContent="Height"
|
||||||
|
Width="600" MinHeight="500" Height="500"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
MinWidth="600"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<Grid Name="DlcGrid" Margin="15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="20,15,20,20"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxWidth="500"
|
||||||
|
LineHeight="18"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="{Binding Heading}"
|
||||||
|
TextAlignment="Center" />
|
||||||
|
<Border
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
BorderBrush="Gray"
|
||||||
|
BorderThickness="1">
|
||||||
|
<DataGrid
|
||||||
|
MinHeight="200"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
Items="{Binding Dlcs}"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTemplateColumn Width="90">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<CheckBox
|
||||||
|
Width="50"
|
||||||
|
MinWidth="40"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
IsChecked="{Binding IsEnabled}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataGridTemplateColumn.Header>
|
||||||
|
<TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
|
||||||
|
</DataGridTemplateColumn.Header>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
<DataGridTextColumn
|
||||||
|
Width="190"
|
||||||
|
Binding="{Binding TitleId}"
|
||||||
|
CanUserResize="True">
|
||||||
|
<DataGridTextColumn.Header>
|
||||||
|
<TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
|
||||||
|
</DataGridTextColumn.Header>
|
||||||
|
</DataGridTextColumn>
|
||||||
|
<DataGridTextColumn
|
||||||
|
Width="*"
|
||||||
|
Binding="{Binding ContainerPath}"
|
||||||
|
CanUserResize="True">
|
||||||
|
<DataGridTextColumn.Header>
|
||||||
|
<TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
|
||||||
|
</DataGridTextColumn.Header>
|
||||||
|
</DataGridTextColumn>
|
||||||
|
<DataGridTextColumn
|
||||||
|
Width="*"
|
||||||
|
Binding="{Binding FullPath}"
|
||||||
|
CanUserResize="True">
|
||||||
|
<DataGridTextColumn.Header>
|
||||||
|
<TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
|
||||||
|
</DataGridTextColumn.Header>
|
||||||
|
</DataGridTextColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Border>
|
||||||
|
<DockPanel
|
||||||
|
Grid.Row="3"
|
||||||
|
Margin="0"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<DockPanel Margin="0" HorizontalAlignment="Left">
|
||||||
|
<Button
|
||||||
|
Name="AddButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding 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}">
|
||||||
|
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
|
||||||
|
</Button>
|
||||||
|
</DockPanel>
|
||||||
|
<DockPanel Margin="0" HorizontalAlignment="Right">
|
||||||
|
<Button
|
||||||
|
Name="SaveButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding Save}">
|
||||||
|
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="CancelButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding Close}">
|
||||||
|
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
||||||
|
</Button>
|
||||||
|
</DockPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
</window:StyleableWindow>
|
261
Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs
Normal file
261
Ryujinx.Ava/Ui/Windows/DlcManagerWindow.axaml.cs
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
|
{
|
||||||
|
public class DlcManagerWindow : StyleableWindow
|
||||||
|
{
|
||||||
|
private readonly List<DlcContainer> _dlcContainerList;
|
||||||
|
private readonly string _dlcJsonPath;
|
||||||
|
|
||||||
|
public VirtualFileSystem VirtualFileSystem { get; }
|
||||||
|
|
||||||
|
public AvaloniaList<DlcModel> Dlcs { get; set; }
|
||||||
|
public Grid DlcGrid { get; private set; }
|
||||||
|
public ulong TitleId { get; }
|
||||||
|
public string TitleName { get; }
|
||||||
|
|
||||||
|
public string Heading => string.Format(LocaleManager.Instance["DlcWindowHeading"], TitleName, TitleId.ToString("X16"));
|
||||||
|
|
||||||
|
public DlcManagerWindow()
|
||||||
|
{
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
AttachDebugDevTools();
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public DlcManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||||
|
{
|
||||||
|
VirtualFileSystem = virtualFileSystem;
|
||||||
|
TitleId = titleId;
|
||||||
|
TitleName = titleName;
|
||||||
|
|
||||||
|
_dlcJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(_dlcJsonPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_dlcContainerList = new List<DlcContainer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
AttachDebugDevTools();
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
||||||
|
|
||||||
|
LoadDlcs();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private void AttachDebugDevTools()
|
||||||
|
{
|
||||||
|
this.AttachDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
Dlcs = new AvaloniaList<DlcModel>();
|
||||||
|
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
|
DlcGrid = this.FindControl<Grid>("DlcGrid");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadDlcs()
|
||||||
|
{
|
||||||
|
foreach (DlcContainer dlcContainer in _dlcContainerList)
|
||||||
|
{
|
||||||
|
using FileStream containerFile = File.OpenRead(dlcContainer.Path);
|
||||||
|
|
||||||
|
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
||||||
|
|
||||||
|
VirtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
|
foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
|
||||||
|
{
|
||||||
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
pfs.OpenFile(ref ncaFile.Ref(), dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.Path);
|
||||||
|
|
||||||
|
if (nca != null)
|
||||||
|
{
|
||||||
|
Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), dlcContainer.Path, dlcNca.Path,
|
||||||
|
dlcNca.Enabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new Nca(VirtualFileSystem.KeySet, ncaStorage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ContentDialogHelper.CreateErrorDialog(this,
|
||||||
|
string.Format(LocaleManager.Instance[
|
||||||
|
"DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDlc(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path) || Dlcs.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (FileStream containerFile = File.OpenRead(path))
|
||||||
|
{
|
||||||
|
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
||||||
|
bool containsDlc = false;
|
||||||
|
|
||||||
|
VirtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||||
|
{
|
||||||
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
|
||||||
|
|
||||||
|
if (nca == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||||
|
{
|
||||||
|
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != TitleId)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
|
||||||
|
|
||||||
|
containsDlc = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!containsDlc)
|
||||||
|
{
|
||||||
|
ContentDialogHelper.CreateErrorDialog(this, LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDlcs(bool removeSelectedOnly = false)
|
||||||
|
{
|
||||||
|
if (removeSelectedOnly)
|
||||||
|
{
|
||||||
|
Dlcs.RemoveAll(Dlcs.Where(x => x.IsEnabled).ToList());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dlcs.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSelected()
|
||||||
|
{
|
||||||
|
RemoveDlcs(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAll()
|
||||||
|
{
|
||||||
|
RemoveDlcs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Add()
|
||||||
|
{
|
||||||
|
OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectDlcDialogTitle"], AllowMultiple = true };
|
||||||
|
|
||||||
|
dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
|
||||||
|
|
||||||
|
string[] files = await dialog.ShowAsync(this);
|
||||||
|
|
||||||
|
if (files != null)
|
||||||
|
{
|
||||||
|
foreach (string file in files)
|
||||||
|
{
|
||||||
|
AddDlc(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
_dlcContainerList.Clear();
|
||||||
|
|
||||||
|
DlcContainer container = default;
|
||||||
|
|
||||||
|
foreach (DlcModel dlc in Dlcs)
|
||||||
|
{
|
||||||
|
if (container.Path != dlc.ContainerPath)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(container.Path))
|
||||||
|
{
|
||||||
|
_dlcContainerList.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
container = new DlcContainer { Path = dlc.ContainerPath, DlcNcaList = new List<DlcNca>() };
|
||||||
|
}
|
||||||
|
|
||||||
|
container.DlcNcaList.Add(new DlcNca
|
||||||
|
{
|
||||||
|
Enabled = dlc.IsEnabled,
|
||||||
|
TitleId = Convert.ToUInt64(dlc.TitleId, 16),
|
||||||
|
Path = dlc.FullPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(container.Path))
|
||||||
|
{
|
||||||
|
_dlcContainerList.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough))
|
||||||
|
{
|
||||||
|
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml
Normal file
104
Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<window:StyleableWindow
|
||||||
|
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
||||||
|
SizeToContent="Height"
|
||||||
|
Width="600" MinHeight="500" Height="500"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
MinWidth="600"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<Grid Margin="15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="20,15,20,20"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxWidth="500"
|
||||||
|
LineHeight="18"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="{Binding Heading}"
|
||||||
|
TextAlignment="Center" />
|
||||||
|
<Border
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
BorderBrush="Gray"
|
||||||
|
BorderThickness="1">
|
||||||
|
<ScrollViewer
|
||||||
|
Width="550"
|
||||||
|
MinHeight="200"
|
||||||
|
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" VerticalAlignment="Center" Content="{Binding Label}" />
|
||||||
|
</RadioButton>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
<DockPanel
|
||||||
|
Grid.Row="3"
|
||||||
|
Margin="0"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<DockPanel Margin="0" HorizontalAlignment="Left">
|
||||||
|
<Button
|
||||||
|
Name="AddButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding 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}">
|
||||||
|
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
|
||||||
|
</Button>
|
||||||
|
</DockPanel>
|
||||||
|
<DockPanel Margin="0" HorizontalAlignment="Right">
|
||||||
|
<Button
|
||||||
|
Name="SaveButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding Save}">
|
||||||
|
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="CancelButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding Close}">
|
||||||
|
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
||||||
|
</Button>
|
||||||
|
</DockPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
</window:StyleableWindow>
|
265
Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs
Normal file
265
Ryujinx.Ava/Ui/Windows/TitleUpdateWindow.axaml.cs
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using LibHac.Ns;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.HOS;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
|
{
|
||||||
|
public class TitleUpdateWindow : StyleableWindow
|
||||||
|
{
|
||||||
|
private readonly string _updateJsonPath;
|
||||||
|
private TitleUpdateMetadata _titleUpdateWindowData;
|
||||||
|
|
||||||
|
public VirtualFileSystem VirtualFileSystem { get; }
|
||||||
|
|
||||||
|
internal AvaloniaList<TitleUpdateModel> TitleUpdates { get; set; }
|
||||||
|
public string TitleId { get; }
|
||||||
|
public string TitleName { get; }
|
||||||
|
|
||||||
|
public string Heading => string.Format(LocaleManager.Instance["GameUpdateWindowHeading"], TitleName, TitleId.ToUpper());
|
||||||
|
|
||||||
|
public TitleUpdateWindow()
|
||||||
|
{
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
AttachDebugDevTools();
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
|
||||||
|
{
|
||||||
|
VirtualFileSystem = virtualFileSystem;
|
||||||
|
TitleId = titleId;
|
||||||
|
TitleName = titleName;
|
||||||
|
|
||||||
|
_updateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_titleUpdateWindowData = new TitleUpdateMetadata {Selected = "", Paths = new List<string>()};
|
||||||
|
}
|
||||||
|
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
AttachDebugDevTools();
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
||||||
|
|
||||||
|
LoadUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private void AttachDebugDevTools()
|
||||||
|
{
|
||||||
|
this.AttachDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
TitleUpdates = new AvaloniaList<TitleUpdateModel>();
|
||||||
|
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadUpdates()
|
||||||
|
{
|
||||||
|
TitleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
|
||||||
|
|
||||||
|
foreach (string path in _titleUpdateWindowData.Paths)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(Nca patchNca, Nca controlNca) =
|
||||||
|
ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
|
||||||
|
|
||||||
|
if (controlNca != null && patchNca != null)
|
||||||
|
{
|
||||||
|
ApplicationControlProperty controlData = new ApplicationControlProperty();
|
||||||
|
|
||||||
|
using var nacpFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
ContentDialogHelper.CreateErrorDialog(this,
|
||||||
|
LocaleManager.Instance["DialogUpdateAddUpdateErrorMessage"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ContentDialogHelper.CreateErrorDialog(this,
|
||||||
|
string.Format(LocaleManager.Instance["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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSelected()
|
||||||
|
{
|
||||||
|
RemoveUpdates(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAll()
|
||||||
|
{
|
||||||
|
RemoveUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Add()
|
||||||
|
{
|
||||||
|
OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectUpdateDialogTitle"], AllowMultiple = true };
|
||||||
|
|
||||||
|
dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
|
||||||
|
|
||||||
|
string[] files = await dialog.ShowAsync(this);
|
||||||
|
|
||||||
|
if (files != null)
|
||||||
|
{
|
||||||
|
foreach (string file in files)
|
||||||
|
{
|
||||||
|
AddUpdate(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SortUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
TitleUpdates.Clear();
|
||||||
|
|
||||||
|
TitleUpdates.AddRange(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
_titleUpdateWindowData.Paths.Clear();
|
||||||
|
|
||||||
|
_titleUpdateWindowData.Selected = "";
|
||||||
|
|
||||||
|
foreach (TitleUpdateModel update in TitleUpdates)
|
||||||
|
{
|
||||||
|
_titleUpdateWindowData.Paths.Add(update.Path);
|
||||||
|
|
||||||
|
if (update.IsEnabled)
|
||||||
|
{
|
||||||
|
_titleUpdateWindowData.Selected = update.Path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough))
|
||||||
|
{
|
||||||
|
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Owner is MainWindow window)
|
||||||
|
{
|
||||||
|
window.ViewModel.LoadApplications();
|
||||||
|
}
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml
Normal file
107
Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<window:StyleableWindow xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Windows.UserProfileWindow"
|
||||||
|
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||||
|
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
||||||
|
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
||||||
|
CanResize="False"
|
||||||
|
Width="850" MinHeight="550" Height="550"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
SizeToContent="Manual"
|
||||||
|
MinWidth="600">
|
||||||
|
<Design.DataContext>
|
||||||
|
<viewModels:UserProfileViewModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
<Window.Resources>
|
||||||
|
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||||
|
</Window.Resources>
|
||||||
|
<Grid Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<ContentControl
|
||||||
|
Focusable="False"
|
||||||
|
IsVisible="False"
|
||||||
|
KeyboardNavigation.IsTabStop="False">
|
||||||
|
<ui:ContentDialog Name="ContentDialog"
|
||||||
|
IsPrimaryButtonEnabled="True"
|
||||||
|
IsSecondaryButtonEnabled="True"
|
||||||
|
IsVisible="False" />
|
||||||
|
</ContentControl>
|
||||||
|
<TextBlock Text="{Locale:Locale UserProfilesSelectedUserProfile}" />
|
||||||
|
<Grid Grid.Row="1" Margin="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Image Height="96" Width="96"
|
||||||
|
Source="{Binding SelectedProfile.Image, Converter={StaticResource ByteImage}}" />
|
||||||
|
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="1" Spacing="10"
|
||||||
|
Margin="5, 10">
|
||||||
|
<TextBox Name="NameBox" Text="{Binding SelectedProfile.Name, Mode=OneWay}"
|
||||||
|
HorizontalAlignment="Stretch" />
|
||||||
|
<TextBlock Text="{Binding SelectedProfile.UserId}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="2" Spacing="10"
|
||||||
|
Margin="5">
|
||||||
|
<Button Content="{Locale:Locale UserProfilesSaveProfileName}" Name="SetNameButton"
|
||||||
|
Click="SetNameButton_OnClick" />
|
||||||
|
<Button Name="SelectProfileImage" Command="{Binding ChooseProfileImage}"
|
||||||
|
Content="{Locale:Locale UserProfilesChangeProfileImage}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid Grid.Row="2">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Text="{Locale:Locale UserProfilesAvailableUserProfiles}" />
|
||||||
|
<ListBox Grid.Row="1" Margin="10" Name="ProfilesList" DoubleTapped="ProfilesList_DoubleTapped"
|
||||||
|
Items="{Binding Profiles}">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid HorizontalAlignment="Stretch">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid Grid.Column="0" Background="{DynamicResource ThemeAccentColorBrush}"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="5" MinWidth="5"
|
||||||
|
IsVisible="{Binding IsOpened}" />
|
||||||
|
<Image Grid.Column="0" Height="96" Width="96"
|
||||||
|
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||||
|
<StackPanel Margin="10" Orientation="Vertical" HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Center" Grid.Column="1">
|
||||||
|
<TextBlock Text="{Binding Name}" />
|
||||||
|
<TextBlock Text="{Binding UserId}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel Grid.Row="3" Orientation="Horizontal" Margin="10,0" Spacing="10" HorizontalAlignment="Stretch">
|
||||||
|
<Button Content="{Locale:Locale UserProfilesAddNewProfile}" Command="{Binding AddUser}" />
|
||||||
|
<Button IsEnabled="{Binding IsSelectedProfileDeletable}"
|
||||||
|
Content="{Locale:Locale UserProfilesDeleteSelectedProfile}" Command="{Binding DeleteUser}" />
|
||||||
|
<Button HorizontalAlignment="Right" Content="{Locale:Locale UserProfilesClose}" Click="CloseButton_OnClick"
|
||||||
|
Name="CloseButton" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</window:StyleableWindow>
|
102
Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs
Normal file
102
Ryujinx.Ava/Ui/Windows/UserProfileWindow.axaml.cs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
|
{
|
||||||
|
public class UserProfileWindow : StyleableWindow
|
||||||
|
{
|
||||||
|
private TextBox _nameBox;
|
||||||
|
|
||||||
|
public UserProfileWindow(AccountManager accountManager, ContentManager contentManager,
|
||||||
|
VirtualFileSystem virtualFileSystem)
|
||||||
|
{
|
||||||
|
AccountManager = accountManager;
|
||||||
|
ContentManager = contentManager;
|
||||||
|
ViewModel = new UserProfileViewModel(this);
|
||||||
|
|
||||||
|
DataContext = ViewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
#if DEBUG
|
||||||
|
this.AttachDevTools();
|
||||||
|
#endif
|
||||||
|
if (contentManager.GetCurrentFirmwareVersion() != null)
|
||||||
|
{
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UserProfileWindowTitle"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserProfileWindow()
|
||||||
|
{
|
||||||
|
ViewModel = new UserProfileViewModel();
|
||||||
|
|
||||||
|
DataContext = ViewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
#if DEBUG
|
||||||
|
this.AttachDevTools();
|
||||||
|
#endif
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UserProfileWindowTitle"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountManager AccountManager { get; }
|
||||||
|
public ContentManager ContentManager { get; }
|
||||||
|
|
||||||
|
public UserProfileViewModel ViewModel { get; set; }
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
_nameBox = this.FindControl<TextBox>("NameBox");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProfilesList_DoubleTapped(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is ListBox listBox)
|
||||||
|
{
|
||||||
|
int selectedIndex = listBox.SelectedIndex;
|
||||||
|
|
||||||
|
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
|
||||||
|
{
|
||||||
|
ViewModel.SelectedProfile = ViewModel.Profiles[selectedIndex];
|
||||||
|
|
||||||
|
AccountManager.OpenUser(ViewModel.SelectedProfile.UserId);
|
||||||
|
|
||||||
|
ViewModel.LoadProfiles();
|
||||||
|
|
||||||
|
foreach (UserProfile profile in ViewModel.Profiles)
|
||||||
|
{
|
||||||
|
profile.UpdateState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetNameButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(_nameBox.Text))
|
||||||
|
{
|
||||||
|
ViewModel.SelectedProfile.Name = _nameBox.Text;
|
||||||
|
AccountManager.SetUserName(ViewModel.SelectedProfile.UserId, _nameBox.Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user