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
This commit is contained in:
parent
3e61fb0268
commit
c46f6879ff
14 changed files with 1286 additions and 41 deletions
|
@ -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);
|
||||
|
||||
AddUser(defaultUserId, "Player", defaultUserImage);
|
||||
|
||||
OpenUser(defaultUserId);
|
||||
if (!_profiles.TryGetValue(DefaultUserId.ToString(), out _))
|
||||
{
|
||||
byte[] defaultUserImage = EmbeddedResources.Read("Ryujinx.HLE/HOS/Services/Account/Acc/DefaultUserImage.jpg");
|
||||
|
||||
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 int GetUserCount()
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
public AccountState AccountState { get; set; }
|
||||
public AccountState OnlinePlayState { get; set; }
|
||||
UpdateLastModifiedTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
public UserProfile(UserId userId, string name, byte[] image)
|
||||
private byte[] _image;
|
||||
|
||||
public byte[] Image
|
||||
{
|
||||
get => _image;
|
||||
set
|
||||
{
|
||||
_image = value;
|
||||
|
||||
UpdateLastModifiedTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
private AccountState _accountState;
|
||||
|
||||
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;
|
||||
|
||||
LastModifiedTimestamp = 0;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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!");
|
||||
|
|
37
Ryujinx/Ui/Widgets/GtkInputDialog.cs
Normal file
37
Ryujinx/Ui/Widgets/GtkInputDialog.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
289
Ryujinx/Ui/Windows/AvatarWindow.cs
Normal file
289
Ryujinx/Ui/Windows/AvatarWindow.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
255
Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs
generated
Normal file
255
Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs
generated
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
327
Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs
Normal file
327
Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue