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
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue