aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAc_K <Acoustik666@gmail.com>2021-04-23 22:26:31 +0200
committerGitHub <noreply@github.com>2021-04-23 22:26:31 +0200
commitc46f6879ff9171a1e024965618242e8bad373b6b (patch)
treeb1ce4b4ce61831797377f39be2089aaeecc3c58b
parent3e61fb0268ea0f52a37c4513dde0ec1f5a6463a2 (diff)
account: add Custom User Profiles support (#2227)
* Initial Impl * Fix names * remove useless ContentManager * Support backgrounds and improve avatar loading * Fix firmware checks * Addresses gdkchan feedback
-rw-r--r--Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs172
-rw-r--r--Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs87
-rw-r--r--Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfile.cs73
-rw-r--r--Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs6
-rw-r--r--Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs16
-rw-r--r--Ryujinx/Program.cs7
-rw-r--r--Ryujinx/Ui/MainWindow.cs17
-rw-r--r--Ryujinx/Ui/MainWindow.glade10
-rw-r--r--Ryujinx/Ui/Widgets/GameTableContextMenu.cs2
-rw-r--r--Ryujinx/Ui/Widgets/GtkDialog.cs29
-rw-r--r--Ryujinx/Ui/Widgets/GtkInputDialog.cs37
-rw-r--r--Ryujinx/Ui/Windows/AvatarWindow.cs289
-rw-r--r--Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs255
-rw-r--r--Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs327
14 files changed, 1286 insertions, 41 deletions
diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs
index d36ea931..2cea57e9 100644
--- a/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs
+++ b/Ryujinx.HLE/HOS/Services/Account/Acc/AccountManager.cs
@@ -1,41 +1,85 @@
-using Ryujinx.Common;
+using LibHac;
+using LibHac.Fs;
+using LibHac.Fs.Shim;
+using Ryujinx.Common;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.FileSystem.Content;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
namespace Ryujinx.HLE.HOS.Services.Account.Acc
{
public class AccountManager
{
+ public static readonly UserId DefaultUserId = new UserId("00000000000000010000000000000000");
+
+ private readonly VirtualFileSystem _virtualFileSystem;
+ private readonly AccountSaveDataManager _accountSaveDataManager;
+
private ConcurrentDictionary<string, UserProfile> _profiles;
public UserProfile LastOpenedUser { get; private set; }
- public AccountManager()
+ public AccountManager(VirtualFileSystem virtualFileSystem)
{
+ _virtualFileSystem = virtualFileSystem;
+
_profiles = new ConcurrentDictionary<string, UserProfile>();
- UserId defaultUserId = new UserId("00000000000000010000000000000000");
- byte[] defaultUserImage = EmbeddedResources.Read("Ryujinx.HLE/HOS/Services/Account/Acc/DefaultUserImage.jpg");
+ _accountSaveDataManager = new AccountSaveDataManager(_profiles);
+
+ if (!_profiles.TryGetValue(DefaultUserId.ToString(), out _))
+ {
+ byte[] defaultUserImage = EmbeddedResources.Read("Ryujinx.HLE/HOS/Services/Account/Acc/DefaultUserImage.jpg");
- AddUser(defaultUserId, "Player", defaultUserImage);
-
- OpenUser(defaultUserId);
+ AddUser("RyuPlayer", defaultUserImage, DefaultUserId);
+
+ OpenUser(DefaultUserId);
+ }
+ else
+ {
+ OpenUser(_accountSaveDataManager.LastOpened);
+ }
}
- public void AddUser(UserId userId, string name, byte[] image)
+ public void AddUser(string name, byte[] image, UserId userId = new UserId())
{
+ if (userId.IsNull)
+ {
+ userId = new UserId(Guid.NewGuid().ToString().Replace("-", ""));
+ }
+
UserProfile profile = new UserProfile(userId, name, image);
_profiles.AddOrUpdate(userId.ToString(), profile, (key, old) => profile);
+
+ _accountSaveDataManager.Save(_profiles);
}
public void OpenUser(UserId userId)
{
if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile))
{
+ // TODO: Support multiple open users ?
+ foreach (UserProfile userProfile in GetAllUsers())
+ {
+ if (userProfile == LastOpenedUser)
+ {
+ userProfile.AccountState = AccountState.Closed;
+
+ break;
+ }
+ }
+
(LastOpenedUser = profile).AccountState = AccountState.Open;
+
+ _accountSaveDataManager.LastOpened = userId;
}
+
+ _accountSaveDataManager.Save(_profiles);
}
public void CloseUser(UserId userId)
@@ -44,9 +88,117 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
{
profile.AccountState = AccountState.Closed;
}
+
+ _accountSaveDataManager.Save(_profiles);
+ }
+
+ public void OpenUserOnlinePlay(UserId userId)
+ {
+ if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile))
+ {
+ // TODO: Support multiple open online users ?
+ foreach (UserProfile userProfile in GetAllUsers())
+ {
+ if (userProfile == LastOpenedUser)
+ {
+ userProfile.OnlinePlayState = AccountState.Closed;
+
+ break;
+ }
+ }
+
+ profile.OnlinePlayState = AccountState.Open;
+ }
+
+ _accountSaveDataManager.Save(_profiles);
+ }
+
+ public void CloseUserOnlinePlay(UserId userId)
+ {
+ if (_profiles.TryGetValue(userId.ToString(), out UserProfile profile))
+ {
+ profile.OnlinePlayState = AccountState.Closed;
+ }
+
+ _accountSaveDataManager.Save(_profiles);
+ }
+
+ public void SetUserImage(UserId userId, byte[] image)
+ {
+ foreach (UserProfile userProfile in GetAllUsers())
+ {
+ if (userProfile.UserId == userId)
+ {
+ userProfile.Image = image;
+
+ break;
+ }
+ }
+
+ _accountSaveDataManager.Save(_profiles);
+ }
+
+ public void SetUserName(UserId userId, string name)
+ {
+ foreach (UserProfile userProfile in GetAllUsers())
+ {
+ if (userProfile.UserId == userId)
+ {
+ userProfile.Name = name;
+
+ break;
+ }
+ }
+
+ _accountSaveDataManager.Save(_profiles);
+ }
+
+ public void DeleteUser(UserId userId)
+ {
+ DeleteSaveData(userId);
+
+ _profiles.Remove(userId.ToString(), out _);
+
+ OpenUser(DefaultUserId);
+
+ _accountSaveDataManager.Save(_profiles);
+ }
+
+ private void DeleteSaveData(UserId userId)
+ {
+ SaveDataFilter saveDataFilter = new SaveDataFilter();
+ saveDataFilter.SetUserId(new LibHac.Fs.UserId((ulong)userId.High, (ulong)userId.Low));
+
+ Result result = _virtualFileSystem.FsClient.OpenSaveDataIterator(out SaveDataIterator saveDataIterator, SaveDataSpaceId.User, ref saveDataFilter);
+ if (result.IsSuccess())
+ {
+ Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
+
+ while (true)
+ {
+ saveDataIterator.ReadSaveDataInfo(out long readCount, saveDataInfo);
+
+ if (readCount == 0)
+ {
+ break;
+ }
+
+ for (int i = 0; i < readCount; i++)
+ {
+ // TODO: We use Directory.Delete workaround because DeleteSaveData softlock without, due to a bug in LibHac 0.12.0.
+ string savePath = Path.Combine(_virtualFileSystem.GetNandPath(), $"user/save/{saveDataInfo[i].SaveDataId:x16}");
+ string saveMetaPath = Path.Combine(_virtualFileSystem.GetNandPath(), $"user/saveMeta/{saveDataInfo[i].SaveDataId:x16}");
+
+ Directory.Delete(savePath, true);
+ Directory.Delete(saveMetaPath, true);
+
+ _virtualFileSystem.FsClient.DeleteSaveData(SaveDataSpaceId.User, saveDataInfo[i].SaveDataId);
+ }
+ }
+ }
}
- public int GetUserCount()
+ internal int GetUserCount()
{
return _profiles.Count;
}
@@ -56,7 +208,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
return _profiles.TryGetValue(userId.ToString(), out profile);
}
- internal IEnumerable<UserProfile> GetAllUsers()
+ public IEnumerable<UserProfile> GetAllUsers()
{
return _profiles.Values;
}
diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs
new file mode 100644
index 00000000..44ef3f33
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs
@@ -0,0 +1,87 @@
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.HLE.HOS.Services.Account.Acc
+{
+ class AccountSaveDataManager
+ {
+ private readonly string _profilesJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "Profiles.json");
+
+ private struct ProfilesJson
+ {
+ [JsonPropertyName("profiles")]
+ public List<UserProfileJson> Profiles { get; set; }
+ [JsonPropertyName("last_opened")]
+ public string LastOpened { get; set; }
+ }
+
+ private struct UserProfileJson
+ {
+ [JsonPropertyName("user_id")]
+ public string UserId { get; set; }
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+ [JsonPropertyName("account_state")]
+ public AccountState AccountState { get; set; }
+ [JsonPropertyName("online_play_state")]
+ public AccountState OnlinePlayState { get; set; }
+ [JsonPropertyName("last_modified_timestamp")]
+ public long LastModifiedTimestamp { get; set; }
+ [JsonPropertyName("image")]
+ public byte[] Image { get; set; }
+ }
+
+ public UserId LastOpened { get; set; }
+
+ public AccountSaveDataManager(ConcurrentDictionary<string, UserProfile> profiles)
+ {
+ // TODO: Use 0x8000000000000010 system savedata instead of a JSON file if needed.
+
+ if (File.Exists(_profilesJsonPath))
+ {
+ ProfilesJson profilesJson = JsonHelper.DeserializeFromFile<ProfilesJson>(_profilesJsonPath);
+
+ foreach (var profile in profilesJson.Profiles)
+ {
+ UserProfile addedProfile = new UserProfile(new UserId(profile.UserId), profile.Name, profile.Image, profile.LastModifiedTimestamp);
+
+ profiles.AddOrUpdate(profile.UserId, addedProfile, (key, old) => addedProfile);
+ }
+
+ LastOpened = new UserId(profilesJson.LastOpened);
+ }
+ else
+ {
+ LastOpened = AccountManager.DefaultUserId;
+ }
+ }
+
+ public void Save(ConcurrentDictionary<string, UserProfile> profiles)
+ {
+ ProfilesJson profilesJson = new ProfilesJson()
+ {
+ Profiles = new List<UserProfileJson>(),
+ LastOpened = LastOpened.ToString()
+ };
+
+ foreach (var profile in profiles)
+ {
+ profilesJson.Profiles.Add(new UserProfileJson()
+ {
+ UserId = profile.Value.UserId.ToString(),
+ Name = profile.Value.Name,
+ AccountState = profile.Value.AccountState,
+ OnlinePlayState = profile.Value.OnlinePlayState,
+ LastModifiedTimestamp = profile.Value.LastModifiedTimestamp,
+ Image = profile.Value.Image,
+ });
+ }
+
+ File.WriteAllText(_profilesJsonPath, JsonHelper.Serialize(profilesJson, true));
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfile.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfile.cs
index a57796c9..ef0a1a64 100644
--- a/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfile.cs
+++ b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfile.cs
@@ -8,31 +8,80 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
public UserId UserId { get; }
- public string Name { get; }
+ public long LastModifiedTimestamp { get; set; }
- public byte[] Image { get; }
+ private string _name;
- public long LastModifiedTimestamp { get; private set; }
+ public string Name
+ {
+ get => _name;
+ set
+ {
+ _name = value;
+
+ UpdateLastModifiedTimestamp();
+ }
+ }
- public AccountState AccountState { get; set; }
- public AccountState OnlinePlayState { get; set; }
+ private byte[] _image;
- public UserProfile(UserId userId, string name, byte[] image)
+ public byte[] Image
{
- UserId = userId;
- Name = name;
+ get => _image;
+ set
+ {
+ _image = value;
- Image = image;
+ UpdateLastModifiedTimestamp();
+ }
+ }
+
+ private AccountState _accountState;
- LastModifiedTimestamp = 0;
+ public AccountState AccountState
+ {
+ get => _accountState;
+ set
+ {
+ _accountState = value;
+
+ UpdateLastModifiedTimestamp();
+ }
+ }
+
+ public AccountState _onlinePlayState;
+
+ public AccountState OnlinePlayState
+ {
+ get => _onlinePlayState;
+ set
+ {
+ _onlinePlayState = value;
+
+ UpdateLastModifiedTimestamp();
+ }
+ }
+
+ public UserProfile(UserId userId, string name, byte[] image, long lastModifiedTimestamp = 0)
+ {
+ UserId = userId;
+ Name = name;
+ Image = image;
AccountState = AccountState.Closed;
OnlinePlayState = AccountState.Closed;
- UpdateTimestamp();
+ if (lastModifiedTimestamp != 0)
+ {
+ LastModifiedTimestamp = lastModifiedTimestamp;
+ }
+ else
+ {
+ UpdateLastModifiedTimestamp();
+ }
}
- private void UpdateTimestamp()
+ private void UpdateLastModifiedTimestamp()
{
LastModifiedTimestamp = (long)(DateTime.Now - Epoch).TotalSeconds;
}
diff --git a/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs b/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs
index 37cc9bda..35781562 100644
--- a/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs
+++ b/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs
@@ -1,7 +1,6 @@
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Caps.Types;
using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.IO;
@@ -19,11 +18,6 @@ namespace Ryujinx.HLE.HOS.Services.Caps
public CaptureManager(Switch device)
{
_sdCardPath = device.FileSystem.GetSdCardPath();
-
- SixLabors.ImageSharp.Configuration.Default.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder()
- {
- Quality = 100
- });
}
public ResultCode SetShimLibraryVersion(ServiceCtx context)
diff --git a/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs b/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs
index 1ae5d487..83b81e00 100644
--- a/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs
+++ b/Ryujinx.HLE/HOS/Services/Friend/ServiceCreator/IFriendService.cs
@@ -150,12 +150,9 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
return ResultCode.InvalidArgument;
}
- if (context.Device.System.AccountManager.TryGetUser(userId, out UserProfile profile))
- {
- profile.OnlinePlayState = AccountState.Open;
- }
-
- Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString(), profile.OnlinePlayState });
+ context.Device.System.AccountManager.OpenUserOnlinePlay(userId);
+
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() });
return ResultCode.Success;
}
@@ -171,12 +168,9 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
return ResultCode.InvalidArgument;
}
- if (context.Device.System.AccountManager.TryGetUser(userId, out UserProfile profile))
- {
- profile.OnlinePlayState = AccountState.Closed;
- }
+ context.Device.System.AccountManager.CloseUserOnlinePlay(userId);
- Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString(), profile.OnlinePlayState });
+ Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() });
return ResultCode.Success;
}
diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs
index 4df82da6..16f3fd74 100644
--- a/Ryujinx/Program.cs
+++ b/Ryujinx/Program.cs
@@ -8,6 +8,7 @@ using Ryujinx.Configuration;
using Ryujinx.Modules;
using Ryujinx.Ui;
using Ryujinx.Ui.Widgets;
+using SixLabors.ImageSharp.Formats.Jpeg;
using System;
using System.IO;
using System.Reflection;
@@ -97,6 +98,12 @@ namespace Ryujinx
// Initialize Discord integration.
DiscordIntegrationModule.Initialize();
+ // Sets ImageSharp Jpeg Encoder Quality.
+ SixLabors.ImageSharp.Configuration.Default.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder()
+ {
+ Quality = 100
+ });
+
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json");
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, "Config.json");
diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs
index 433d23dc..08527ea3 100644
--- a/Ryujinx/Ui/MainWindow.cs
+++ b/Ryujinx/Ui/MainWindow.cs
@@ -78,6 +78,7 @@ namespace Ryujinx.Ui
[GUI] Box _footerBox;
[GUI] Box _statusBar;
[GUI] MenuItem _optionMenu;
+ [GUI] MenuItem _manageUserProfiles;
[GUI] MenuItem _actionMenu;
[GUI] MenuItem _stopEmulation;
[GUI] MenuItem _simulateWakeUpMessage;
@@ -140,7 +141,7 @@ namespace Ryujinx.Ui
// Instanciate HLE objects.
_virtualFileSystem = VirtualFileSystem.CreateInstance();
_contentManager = new ContentManager(_virtualFileSystem);
- _accountManager = new AccountManager();
+ _accountManager = new AccountManager(_virtualFileSystem);
_userChannelPersistence = new UserChannelPersistence();
// Instanciate GUI objects.
@@ -155,6 +156,7 @@ namespace Ryujinx.Ui
_applicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated;
_actionMenu.StateChanged += ActionMenu_StateChanged;
+ _optionMenu.StateChanged += OptionMenu_StateChanged;
_gameTable.ButtonReleaseEvent += Row_Clicked;
_fullScreen.Activated += FullScreen_Toggled;
@@ -1192,6 +1194,11 @@ namespace Ryujinx.Ui
SaveConfig();
}
+ private void OptionMenu_StateChanged(object o, StateChangedArgs args)
+ {
+ _manageUserProfiles.Sensitive = _emulationContext == null;
+ }
+
private void Settings_Pressed(object sender, EventArgs args)
{
SettingsWindow settingsWindow = new SettingsWindow(this, _virtualFileSystem, _contentManager);
@@ -1200,6 +1207,14 @@ namespace Ryujinx.Ui
settingsWindow.Show();
}
+ private void ManageUserProfiles_Pressed(object sender, EventArgs args)
+ {
+ UserProfilesManagerWindow userProfilesManagerWindow = new UserProfilesManagerWindow(_accountManager, _contentManager, _virtualFileSystem);
+
+ userProfilesManagerWindow.SetSizeRequest((int)(userProfilesManagerWindow.DefaultWidth * Program.WindowScaleFactor), (int)(userProfilesManagerWindow.DefaultHeight * Program.WindowScaleFactor));
+ userProfilesManagerWindow.Show();
+ }
+
private void Simulate_WakeUp_Message_Pressed(object sender, EventArgs args)
{
if (_emulationContext != null)
diff --git a/Ryujinx/Ui/MainWindow.glade b/Ryujinx/Ui/MainWindow.glade
index beeed265..129b768e 100644
--- a/Ryujinx/Ui/MainWindow.glade
+++ b/Ryujinx/Ui/MainWindow.glade
@@ -248,6 +248,16 @@
<signal name="activate" handler="Settings_Pressed" swapped="no"/>
</object>
</child>
+ <child>
+ <object class="GtkMenuItem" id="_manageUserProfiles">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">Open User Profiles Manager window</property>
+ <property name="label" translatable="yes">Manage User Profiles</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="ManageUserProfiles_Pressed" swapped="no"/>
+ </object>
+ </child>
</object>
</child>
</object>
diff --git a/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
index 4fdc666a..eb3150ce 100644
--- a/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
+++ b/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
@@ -115,7 +115,7 @@ namespace Ryujinx.Ui.Widgets
Logger.Warning?.Print(LogClass.Application, "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
}
- Uid user = new Uid(1, 0); // TODO: Remove Hardcoded value.
+ Uid user = new Uid((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
result = EnsureApplicationSaveData(_virtualFileSystem.FsClient, out _, new LibHac.Ncm.ApplicationId(titleId), ref control, ref user);
diff --git a/Ryujinx/Ui/Widgets/GtkDialog.cs b/Ryujinx/Ui/Widgets/GtkDialog.cs
index 3d19724d..83f6bb2b 100644
--- a/Ryujinx/Ui/Widgets/GtkDialog.cs
+++ b/Ryujinx/Ui/Widgets/GtkDialog.cs
@@ -1,6 +1,7 @@
using Gtk;
using System.Reflection;
using Ryujinx.Common.Logging;
+using System.Collections.Generic;
namespace Ryujinx.Ui.Widgets
{
@@ -76,6 +77,34 @@ namespace Ryujinx.Ui.Widgets
return response == ResponseType.Yes;
}
+ internal static ResponseType CreateCustomDialog(string title, string mainText, string secondaryText, Dictionary<int, string> buttons, MessageType messageType = MessageType.Other)
+ {
+ GtkDialog gtkDialog = new GtkDialog(title, mainText, secondaryText, messageType, ButtonsType.None);
+
+ foreach (var button in buttons)
+ {
+ gtkDialog.AddButton(button.Value, button.Key);
+ }
+
+ return (ResponseType)gtkDialog.Run();
+ }
+
+ internal static string CreateInputDialog(Window parent, string title, string mainText, uint inputMax)
+ {
+ GtkInputDialog gtkDialog = new GtkInputDialog(parent, title, mainText, inputMax);
+ ResponseType response = (ResponseType)gtkDialog.Run();
+ string responseText = gtkDialog.InputEntry.Text.TrimEnd();
+
+ gtkDialog.Dispose();
+
+ if (response == ResponseType.Ok)
+ {
+ return responseText;
+ }
+
+ return "";
+ }
+
internal static bool CreateExitDialog()
{
return CreateChoiceDialog("Ryujinx - Exit", "Are you sure you want to close Ryujinx?", "All unsaved data will be lost!");
diff --git a/Ryujinx/Ui/Widgets/GtkInputDialog.cs b/Ryujinx/Ui/Widgets/GtkInputDialog.cs
new file mode 100644
index 00000000..21b34937
--- /dev/null
+++ b/Ryujinx/Ui/Widgets/GtkInputDialog.cs
@@ -0,0 +1,37 @@
+using Gtk;
+
+namespace Ryujinx.Ui.Widgets
+{
+ public class GtkInputDialog : MessageDialog
+ {
+ public Entry InputEntry { get; }
+
+ public GtkInputDialog(Window parent, string title, string mainText, uint inputMax) : base(parent, DialogFlags.Modal | DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.OkCancel, null)
+ {
+ SetDefaultSize(300, 0);
+
+ Title = title;
+
+ Label mainTextLabel = new Label
+ {
+ Text = mainText
+ };
+
+ InputEntry = new Entry
+ {
+ MaxLength = (int)inputMax
+ };
+
+ Label inputMaxTextLabel = new Label
+ {
+ Text = $"(Max length: {inputMax})"
+ };
+
+ ((Box)MessageArea).PackStart(mainTextLabel, true, true, 0);
+ ((Box)MessageArea).PackStart(InputEntry, true, true, 5);
+ ((Box)MessageArea).PackStart(inputMaxTextLabel, true, true, 0);
+
+ ShowAll();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx/Ui/Windows/AvatarWindow.cs b/Ryujinx/Ui/Windows/AvatarWindow.cs
new file mode 100644
index 00000000..52e03d30
--- /dev/null
+++ b/Ryujinx/Ui/Windows/AvatarWindow.cs
@@ -0,0 +1,289 @@
+using Gtk;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.FsSystem.NcaUtils;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.FileSystem.Content;
+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.IO;
+using System.Reflection;
+
+using Image = SixLabors.ImageSharp.Image;
+
+namespace Ryujinx.Ui.Windows
+{
+ public class AvatarWindow : Window
+ {
+ public byte[] SelectedProfileImage;
+ public bool NewUser;
+
+ private static Dictionary<string, byte[]> _avatarDict = new Dictionary<string, byte[]>();
+
+ private ListStore _listStore;
+ private IconView _iconView;
+ private Button _setBackgroungColorButton;
+ private Gdk.RGBA _backgroundColor;
+
+ public AvatarWindow() : base($"Ryujinx {Program.Version} - Manage Accounts - Avatar")
+ {
+ Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Ryujinx.png");
+
+ CanFocus = false;
+ Resizable = false;
+ Modal = true;
+ TypeHint = Gdk.WindowTypeHint.Dialog;
+
+ SetDefaultSize(740, 400);
+ SetPosition(WindowPosition.Center);
+
+ VBox vbox = new VBox(false, 0);
+ Add(vbox);
+
+ ScrolledWindow scrolledWindow = new ScrolledWindow
+ {
+ ShadowType = ShadowType.EtchedIn
+ };
+ scrolledWindow.SetPolicy(PolicyType.Automatic, PolicyType.Automatic);
+
+ HBox hbox = new HBox(false, 0);
+
+ Button chooseButton = new Button()
+ {
+ Label = "Choose",
+ CanFocus = true,
+ ReceivesDefault = true
+ };
+ chooseButton.Clicked += ChooseButton_Pressed;
+
+ _setBackgroungColorButton = new Button()
+ {
+ Label = "Set Background Color",
+ CanFocus = true
+ };
+ _setBackgroungColorButton.Clicked += SetBackgroungColorButton_Pressed;
+
+ _backgroundColor.Red = 1;
+ _backgroundColor.Green = 1;
+ _backgroundColor.Blue = 1;
+ _backgroundColor.Alpha = 1;
+
+ Button closeButton = new Button()
+ {
+ Label = "Close",
+ CanFocus = true
+ };
+ closeButton.Clicked += CloseButton_Pressed;
+
+ vbox.PackStart(scrolledWindow, true, true, 0);
+ hbox.PackStart(chooseButton, true, true, 0);
+ hbox.PackStart(_setBackgroungColorButton, true, true, 0);
+ hbox.PackStart(closeButton, true, true, 0);
+ vbox.PackStart(hbox, false, false, 0);
+
+ _listStore = new ListStore(typeof(string), typeof(Gdk.Pixbuf));
+ _listStore.SetSortColumnId(0, SortType.Ascending);
+
+ _iconView = new IconView(_listStore);
+ _iconView.ItemWidth = 64;
+ _iconView.ItemPadding = 10;
+ _iconView.PixbufColumn = 1;
+
+ _iconView.SelectionChanged += IconView_SelectionChanged;
+
+ scrolledWindow.Add(_iconView);
+
+ _iconView.GrabFocus();
+
+ ProcessAvatars();
+
+ ShowAll();
+ }
+
+ public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
+ {
+ if (_avatarDict.Count > 0)
+ {
+ return;
+ }
+
+ string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.NandSystem, NcaContentType.Data);
+ string avatarPath = virtualFileSystem.SwitchPathToSystemPath(contentPath);
+
+ if (!string.IsNullOrWhiteSpace(avatarPath))
+ {
+ using (IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open))
+ {
+ Nca nca = new Nca(virtualFileSystem.KeySet, ncaFileStream);
+ IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
+
+ foreach (var 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"))
+ {
+ romfs.OpenFile(out IFile file, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ using (MemoryStream stream = new MemoryStream())
+ using (MemoryStream streamPng = new MemoryStream())
+ {
+ file.AsStream().CopyTo(stream);
+
+ stream.Position = 0;
+
+ Image avatarImage = Image.LoadPixelData<Rgba32>(DecompressYaz0(stream), 256, 256);
+
+ avatarImage.SaveAsPng(streamPng);
+
+ _avatarDict.Add(item.FullPath, streamPng.ToArray());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void ProcessAvatars()
+ {
+ _listStore.Clear();
+
+ foreach (var avatar in _avatarDict)
+ {
+ _listStore.AppendValues(avatar.Key, new Gdk.Pixbuf(ProcessImage(avatar.Value), 96, 96));
+ }
+
+ _iconView.SelectPath(new TreePath(new int[] { 0 }));
+ }
+
+ private byte[] ProcessImage(byte[] data)
+ {
+ using (MemoryStream streamJpg = new MemoryStream())
+ {
+ Image avatarImage = Image.Load(data, new PngDecoder());
+
+ avatarImage.Mutate(x => x.BackgroundColor(new Rgba32((byte)(_backgroundColor.Red * 255),
+ (byte)(_backgroundColor.Green * 255),
+ (byte)(_backgroundColor.Blue * 255),
+ (byte)(_backgroundColor.Alpha * 255))));
+ avatarImage.SaveAsJpeg(streamJpg);
+
+ return streamJpg.ToArray();
+ }
+ }
+
+ private void CloseButton_Pressed(object sender, EventArgs e)
+ {
+ SelectedProfileImage = null;
+
+ Close();
+ }
+
+ private void IconView_SelectionChanged(object sender, EventArgs e)
+ {
+ if (_iconView.SelectedItems.Length > 0)
+ {
+ _listStore.GetIter(out TreeIter iter, _iconView.SelectedItems[0]);
+
+ SelectedProfileImage = ProcessImage(_avatarDict[(string)_listStore.GetValue(iter, 0)]);
+ }
+ }
+
+ private void SetBackgroungColorButton_Pressed(object sender, EventArgs e)
+ {
+ using (ColorChooserDialog colorChooserDialog = new ColorChooserDialog("Set Background Color", this))
+ {
+ colorChooserDialog.UseAlpha = false;
+ colorChooserDialog.Rgba = _backgroundColor;
+
+ if (colorChooserDialog.Run() == (int)ResponseType.Ok)
+ {
+ _backgroundColor = colorChooserDialog.Rgba;
+
+ ProcessAvatars();
+ }
+
+ colorChooserDialog.Hide();
+ }
+ }
+
+ private void ChooseButton_Pressed(object sender, EventArgs e)
+ {
+ Close();
+ }
+
+ private static byte[] DecompressYaz0(Stream stream)
+ {
+ using (BinaryReader reader = new BinaryReader(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);
+
+ long inputOffset = 0;
+
+ byte[] output = new byte[decodedLength];
+ long 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++];
+
+ int dist = ((byte1 & 0xF) << 8) | byte2;
+ int position = (int)outputOffset - (dist + 1);
+
+ int length = byte1 >> 4;
+ if (length == 0)
+ {
+ length = input[inputOffset++] + 0x12;
+ }
+ else
+ {
+ length += 2;
+ }
+
+ while (length-- > 0)
+ {
+ output[outputOffset++] = output[position++];
+ }
+ }
+ }
+
+ return output;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs b/Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs
new file mode 100644
index 00000000..70291290
--- /dev/null
+++ b/Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs
@@ -0,0 +1,255 @@
+using Gtk;
+using Pango;
+
+namespace Ryujinx.Ui.Windows
+{
+ public partial class UserProfilesManagerWindow : Window
+ {
+ private Box _mainBox;
+ private Label _selectedLabel;
+ private Box _selectedUserBox;
+ private Image _selectedUserImage;
+ private VBox _selectedUserInfoBox;
+ private Entry _selectedUserNameEntry;
+ private Label _selectedUserIdLabel;
+ private VBox _selectedUserButtonsBox;
+ private Button _saveProfileNameButton;
+ private Button _changeProfileImageButton;
+ private Box _usersTreeViewBox;
+ private Label _availableUsersLabel;
+ private ScrolledWindow _usersTreeViewWindow;
+ private ListStore _tableStore;
+ private TreeView _usersTreeView;
+ private Box _bottomBox;
+ private Button _addButton;
+ private Button _deleteButton;
+ private Button _closeButton;
+
+ private void InitializeComponent()
+ {
+
+#pragma warning disable CS0612
+
+ //
+ // UserProfilesManagerWindow
+ //
+ CanFocus = false;
+ Resizable = false;
+ Modal = true;
+ WindowPosition = WindowPosition.Center;
+ DefaultWidth = 620;
+ DefaultHeight = 548;
+ TypeHint = Gdk.WindowTypeHint.Dialog;
+
+ //
+ // _mainBox
+ //
+ _mainBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _selectedLabel
+ //
+ _selectedLabel = new Label("Selected User Profile:")
+ {
+ Margin = 15,
+ Attributes = new AttrList()
+ };
+ _selectedLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold));
+
+ //
+ // _viewBox
+ //
+ _usersTreeViewBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _SelectedUserBox
+ //
+ _selectedUserBox = new Box(Orientation.Horizontal, 0)
+ {
+ MarginLeft = 30
+ };
+
+ //
+ // _selectedUserImage
+ //
+ _selectedUserImage = new Image();
+
+ //
+ // _selectedUserInfoBox
+ //
+ _selectedUserInfoBox = new VBox(true, 0);
+
+ //
+ // _selectedUserNameEntry
+ //
+ _selectedUserNameEntry = new Entry("")
+ {
+ MarginLeft = 15,
+ MaxLength = (int)MaxProfileNameLength
+ };
+ _selectedUserNameEntry.KeyReleaseEvent += SelectedUserNameEntry_KeyReleaseEvent;
+
+ //
+ // _selectedUserIdLabel
+ //
+ _selectedUserIdLabel = new Label("")
+ {
+ MarginTop = 15,
+ MarginLeft = 15
+ };
+
+ //
+ // _selectedUserButtonsBox
+ //
+ _selectedUserButtonsBox = new VBox()
+ {
+ MarginRight = 30
+ };
+
+ //
+ // _saveProfileNameButton
+ //
+ _saveProfileNameButton = new Button()
+ {
+ Label = "Save Profile Name",
+ CanFocus = true,
+ ReceivesDefault = true,
+ Sensitive = false
+ };
+ _saveProfileNameButton.Clicked += EditProfileNameButton_Pressed;
+
+ //
+ // _changeProfileImageButton
+ //
+ _changeProfileImageButton = new Button()
+ {
+ Label = "Change Profile Image",
+ CanFocus = true,
+ ReceivesDefault = true,
+ MarginTop = 10
+ };
+ _changeProfileImageButton.Clicked += ChangeProfileImageButton_Pressed;
+
+ //
+ // _availableUsersLabel
+ //
+ _availableUsersLabel = new Label("Available User Profiles:")
+ {
+ Margin = 15,
+ Attributes = new AttrList()
+ };
+ _availableUsersLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold));
+
+ //
+ // _usersTreeViewWindow
+ //
+ _usersTreeViewWindow = new ScrolledWindow()
+ {
+ ShadowType = ShadowType.In,
+ CanFocus = true,
+ Expand = true,
+ MarginLeft = 30,
+ MarginRight = 30,
+ MarginBottom = 15
+ };
+
+ //
+ // _tableStore
+ //
+ _tableStore = new ListStore(typeof(bool), typeof(Gdk.Pixbuf), typeof(string), typeof(Gdk.RGBA));
+
+ //
+ // _usersTreeView
+ //
+ _usersTreeView = new TreeView(_tableStore)
+ {
+ HoverSelection = true,
+ HeadersVisible = false,
+ };
+ _usersTreeView.RowActivated += UsersTreeView_Activated;
+
+ //
+ // _bottomBox
+ //
+ _bottomBox = new Box(Orientation.Horizontal, 0)
+ {
+ MarginLeft = 30,
+ MarginRight = 30,
+ MarginBottom = 15
+ };
+
+ //
+ // _addButton
+ //
+ _addButton = new Button()
+ {
+ Label = "Add New Profile",
+ CanFocus = true,
+ ReceivesDefault = true,
+ HeightRequest = 35
+ };
+ _addButton.Clicked += AddButton_Pressed;
+
+ //
+ // _deleteButton
+ //
+ _deleteButton = new Button()
+ {
+ Label = "Delete Selected Profile",
+ CanFocus = true,
+ ReceivesDefault = true,
+ HeightRequest = 35,
+ MarginLeft = 10
+ };
+ _deleteButton.Clicked += DeleteButton_Pressed;
+
+ //
+ // _closeButton
+ //
+ _closeButton = new Button()
+ {
+ Label = "Close",
+ CanFocus = true,
+ ReceivesDefault = true,
+ HeightRequest = 35,
+ WidthRequest = 80
+ };
+ _closeButton.Clicked += CloseButton_Pressed;
+
+#pragma warning restore CS0612
+
+ ShowComponent();
+ }
+
+ private void ShowComponent()
+ {
+ _usersTreeViewWindow.Add(_usersTreeView);
+
+ _usersTreeViewBox.Add(_usersTreeViewWindow);
+
+ _bottomBox.PackStart(new Gtk.Alignment(-1, 0, 0, 0) { _addButton }, false, false, 0);
+ _bottomBox.PackStart(new Gtk.Alignment(-1, 0, 0, 0) { _deleteButton }, false, false, 0);
+ _bottomBox.PackEnd(new Gtk.Alignment(1, 0, 0, 0) { _closeButton }, false, false, 0);
+
+ _selectedUserInfoBox.Add(_selectedUserNameEntry);
+ _selectedUserInfoBox.Add(_selectedUserIdLabel);
+
+ _selectedUserButtonsBox.Add(_saveProfileNameButton);
+ _selectedUserButtonsBox.Add(_changeProfileImageButton);
+
+ _selectedUserBox.Add(_selectedUserImage);
+ _selectedUserBox.PackStart(new Gtk.Alignment(-1, 0, 0, 0) { _selectedUserInfoBox }, true, true, 0);
+ _selectedUserBox.Add(_selectedUserButtonsBox);
+
+ _mainBox.PackStart(new Gtk.Alignment(-1, 0, 0, 0) { _selectedLabel }, false, false, 0);
+ _mainBox.PackStart(_selectedUserBox, false, true, 0);
+ _mainBox.PackStart(new Gtk.Alignment(-1, 0, 0, 0) { _availableUsersLabel }, false, false, 0);
+ _mainBox.Add(_usersTreeViewBox);
+ _mainBox.Add(_bottomBox);
+
+ Add(_mainBox);
+
+ ShowAll();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs b/Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs
new file mode 100644
index 00000000..6a4788b1
--- /dev/null
+++ b/Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs
@@ -0,0 +1,327 @@
+using Gtk;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.FileSystem.Content;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.Ui.Widgets;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Image = SixLabors.ImageSharp.Image;
+using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId;
+
+namespace Ryujinx.Ui.Windows
+{
+ public partial class UserProfilesManagerWindow : Window
+ {
+ private const uint MaxProfileNameLength = 0x20;
+
+ private readonly AccountManager _accountManager;
+ private readonly ContentManager _contentManager;
+
+ private byte[] _bufferImageProfile;
+ private string _tempNewProfileName;
+
+ private Gdk.RGBA _selectedColor;
+
+ private ManualResetEvent _avatarsPreloadingEvent = new ManualResetEvent(false);
+
+ public UserProfilesManagerWindow(AccountManager accountManager, ContentManager contentManager, VirtualFileSystem virtualFileSystem) : base($"Ryujinx {Program.Version} - Manage User Profiles")
+ {
+ Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Ryujinx.png");
+
+ InitializeComponent();
+
+ _selectedColor.Red = 0.212;
+ _selectedColor.Green = 0.843;
+ _selectedColor.Blue = 0.718;
+ _selectedColor.Alpha = 1;
+
+ _accountManager = accountManager;
+ _contentManager = contentManager;
+
+ CellRendererToggle userSelectedToggle = new CellRendererToggle();
+ userSelectedToggle.Toggled += UserSelectedToggle_Toggled;
+
+ // NOTE: Uncomment following line when multiple selection of user profiles is supported.
+ //_usersTreeView.AppendColumn("Selected", userSelectedToggle, "active", 0);
+ _usersTreeView.AppendColumn("User Icon", new CellRendererPixbuf(), "pixbuf", 1);
+ _usersTreeView.AppendColumn("User Info", new CellRendererText(), "text", 2, "background-rgba", 3);
+
+ _tableStore.SetSortColumnId(0, SortType.Descending);
+
+ RefreshList();
+
+ if (_contentManager.GetCurrentFirmwareVersion() != null)
+ {
+ Task.Run(() =>
+ {
+ AvatarWindow.PreloadAvatars(contentManager, virtualFileSystem);
+ _avatarsPreloadingEvent.Set();
+ });
+ }
+ }
+
+ public void RefreshList()
+ {
+ _tableStore.Clear();
+
+ foreach (UserProfile userProfile in _accountManager.GetAllUsers())
+ {
+ _tableStore.AppendValues(userProfile.AccountState == AccountState.Open, new Gdk.Pixbuf(userProfile.Image, 96, 96), $"{userProfile.Name}\n{userProfile.UserId}", Gdk.RGBA.Zero);
+
+ if (userProfile.AccountState == AccountState.Open)
+ {
+ _selectedUserImage.Pixbuf = new Gdk.Pixbuf(userProfile.Image, 96, 96);
+ _selectedUserIdLabel.Text = userProfile.UserId.ToString();
+ _selectedUserNameEntry.Text = userProfile.Name;
+
+ _deleteButton.Sensitive = userProfile.UserId != AccountManager.DefaultUserId;
+
+ _usersTreeView.Model.GetIterFirst(out TreeIter firstIter);
+ _tableStore.SetValue(firstIter, 3, _selectedColor);
+ }
+ }
+ }
+
+ //
+ // Events
+ //
+
+ private void UsersTreeView_Activated(object o, RowActivatedArgs args)
+ {
+ SelectUserTreeView();
+ }
+
+ private void UserSelectedToggle_Toggled(object o, ToggledArgs args)
+ {
+ SelectUserTreeView();
+ }
+
+ private void SelectUserTreeView()
+ {
+ // Get selected item informations.
+ _usersTreeView.Selection.GetSelected(out TreeIter selectedIter);
+
+ Gdk.Pixbuf userPicture = (Gdk.Pixbuf)_tableStore.GetValue(selectedIter, 1);
+
+ string userName = _tableStore.GetValue(selectedIter, 2).ToString().Split("\n")[0];
+ string userId = _tableStore.GetValue(selectedIter, 2).ToString().Split("\n")[1];
+
+ // Unselect the first user.
+ _usersTreeView.Model.GetIterFirst(out TreeIter firstIter);
+ _tableStore.SetValue(firstIter, 0, false);
+ _tableStore.SetValue(firstIter, 3, Gdk.RGBA.Zero);
+
+ // Set new informations.
+ _tableStore.SetValue(selectedIter, 0, true);
+
+ _selectedUserImage.Pixbuf = userPicture;
+ _selectedUserNameEntry.Text = userName;
+ _selectedUserIdLabel.Text = userId;
+ _saveProfileNameButton.Sensitive = false;
+
+ // Open the selected one.
+ _accountManager.OpenUser(new UserId(userId));
+
+ _deleteButton.Sensitive = userId != AccountManager.DefaultUserId.ToString();
+
+ _tableStore.SetValue(selectedIter, 3, _selectedColor);
+ }
+
+ private void SelectedUserNameEntry_KeyReleaseEvent(object o, KeyReleaseEventArgs args)
+ {
+ if (_saveProfileNameButton.Sensitive == false)
+ {
+ _saveProfileNameButton.Sensitive = true;
+ }
+ }
+
+ private void AddButton_Pressed(object sender, EventArgs e)
+ {
+ _tempNewProfileName = GtkDialog.CreateInputDialog(this, "Choose the Profile Name", "Please Enter a Profile Name", MaxProfileNameLength);
+
+ if (_tempNewProfileName != "")
+ {
+ SelectProfileImage(true);
+
+ if (_bufferImageProfile != null)
+ {
+ AddUser();
+ }
+ }
+ }
+
+ private void DeleteButton_Pressed(object sender, EventArgs e)
+ {
+ if (GtkDialog.CreateChoiceDialog("Delete User Profile", "Are you sure you want to delete the profile ?", "Deleting this profile will also delete all associated save data."))
+ {
+ _accountManager.DeleteUser(GetSelectedUserId());
+
+ RefreshList();
+ }
+ }
+
+ private void EditProfileNameButton_Pressed(object sender, EventArgs e)
+ {
+ _saveProfileNameButton.Sensitive = false;
+
+ _accountManager.SetUserName(GetSelectedUserId(), _selectedUserNameEntry.Text);
+
+ RefreshList();
+ }
+
+ private void ProcessProfileImage(byte[] buffer)
+ {
+ using (Image image = Image.Load(buffer))
+ {
+ image.Mutate(x => x.Resize(256, 256));
+
+ using (MemoryStream streamJpg = new MemoryStream())
+ {
+ image.SaveAsJpeg(streamJpg);
+
+ _bufferImageProfile = streamJpg.ToArray();
+ }
+ }
+ }
+
+ private void ProfileImageFileChooser()
+ {
+ FileChooserDialog fileChooser = new FileChooserDialog("Import Custom Profile Image", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Import", ResponseType.Accept)
+ {
+ SelectMultiple = false,
+ Filter = new FileFilter()
+ };
+
+ fileChooser.SetPosition(WindowPosition.Center);
+ fileChooser.Filter.AddPattern("*.jpg");
+ fileChooser.Filter.AddPattern("*.jpeg");
+ fileChooser.Filter.AddPattern("*.png");
+ fileChooser.Filter.AddPattern("*.bmp");
+
+ if (fileChooser.Run() == (int)ResponseType.Accept)
+ {
+ ProcessProfileImage(File.ReadAllBytes(fileChooser.Filename));
+ }
+
+ fileChooser.Dispose();
+ }
+
+ private void SelectProfileImage(bool newUser = false)
+ {
+ if (_contentManager.GetCurrentFirmwareVersion() == null)
+ {
+ ProfileImageFileChooser();
+ }
+ else
+ {
+ Dictionary<int, string> buttons = new Dictionary<int, string>()
+ {
+ { 0, "Import Image File" },
+ { 1, "Select Firmware Avatar" }
+ };
+
+ ResponseType responseDialog = GtkDialog.CreateCustomDialog("Profile Image Selection",
+ "Choose a Profile Image",
+ "You may import a custom profile image, or select an avatar from the system firmware.",
+ buttons, MessageType.Question);
+
+ if (responseDialog == 0)
+ {
+ ProfileImageFileChooser();
+ }
+ else if (responseDialog == (ResponseType)1)
+ {
+ AvatarWindow avatarWindow = new AvatarWindow()
+ {
+ NewUser = newUser
+ };
+
+ avatarWindow.DeleteEvent += AvatarWindow_DeleteEvent;
+
+ avatarWindow.SetSizeRequest((int)(avatarWindow.DefaultWidth * Program.WindowScaleFactor), (int)(avatarWindow.DefaultHeight * Program.WindowScaleFactor));
+ avatarWindow.Show();
+ }
+ }
+ }
+
+ private void ChangeProfileImageButton_Pressed(object sender, EventArgs e)
+ {
+ if (_contentManager.GetCurrentFirmwareVersion() != null)
+ {
+ _avatarsPreloadingEvent.WaitOne();
+ }
+
+ SelectProfileImage();
+
+ if (_bufferImageProfile != null)
+ {
+ SetUserImage();
+ }
+ }
+
+ private void AvatarWindow_DeleteEvent(object sender, DeleteEventArgs args)
+ {
+ _bufferImageProfile = ((AvatarWindow)sender).SelectedProfileImage;
+
+ if (_bufferImageProfile != null)
+ {
+ if (((AvatarWindow)sender).NewUser)
+ {
+ AddUser();
+ }
+ else
+ {
+ SetUserImage();
+ }
+ }
+ }
+
+ private void AddUser()
+ {
+ _accountManager.AddUser(_tempNewProfileName, _bufferImageProfile);
+
+ _bufferImageProfile = null;
+ _tempNewProfileName = "";
+
+ RefreshList();
+ }
+
+ private void SetUserImage()
+ {
+ _accountManager.SetUserImage(GetSelectedUserId(), _bufferImageProfile);
+
+ _bufferImageProfile = null;
+
+ RefreshList();
+ }
+
+ private UserId GetSelectedUserId()
+ {
+ if (_usersTreeView.Model.GetIterFirst(out TreeIter iter))
+ {
+ do
+ {
+ if ((bool)_tableStore.GetValue(iter, 0))
+ {
+ break;
+ }
+ }
+ while (_usersTreeView.Model.IterNext(ref iter));
+ }
+
+ return new UserId(_tableStore.GetValue(iter, 2).ToString().Split("\n")[1]);
+ }
+
+ private void CloseButton_Pressed(object sender, EventArgs e)
+ {
+ Close();
+ }
+ }
+} \ No newline at end of file