Skip to content

Commit 04c131f

Browse files
Added profile manager
1 parent da0b8b4 commit 04c131f

9 files changed

Lines changed: 354 additions & 7 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<local:WindowModal xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:c="using:HedgeModManager.UI.Controls"
4+
xmlns:cb="using:HedgeModManager.UI.Controls.Basic"
5+
xmlns:cp="using:HedgeModManager.UI.Controls.Primitives"
6+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
7+
xmlns:html="using:TheArtOfDev.HtmlRenderer.Avalonia"
8+
xmlns:local="using:HedgeModManager.UI.Controls.Modals"
9+
xmlns:m="using:HedgeModManager.UI.Models"
10+
xmlns:materialIcons="using:Material.Icons.Avalonia"
11+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
12+
xmlns:vm="using:HedgeModManager.UI.ViewModels"
13+
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="540"
14+
x:Class="HedgeModManager.UI.Controls.Modals.ProfileManagerModal"
15+
x:DataType="vm:MainWindowViewModel"
16+
Loaded="OnLoaded" Unloaded="OnUnloaded"
17+
MinWidth="400" MinHeight="200" Title="Modal.Title.ProfileManager"
18+
UseTitlePadding="True" LargeWindow="True">
19+
<local:WindowModal.Buttons>
20+
<Grid>
21+
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" Spacing="10">
22+
<cb:Button Text="Modal.Button.DuplicateProfile" Click="OnDuplicateClick" IsEnabled="{Binding ViewModel.CanDuplicate, RelativeSource={RelativeSource AncestorType=local:ProfileManagerModal}}" />
23+
<cb:Button Text="Modal.Button.DeleteProfile" Click="OnDeleteClick" IsEnabled="{Binding ViewModel.CanDelete, RelativeSource={RelativeSource AncestorType=local:ProfileManagerModal}}" />
24+
<cb:Button Text="Modal.Button.RenameProfile" Click="OnRenameClick" IsEnabled="{Binding ViewModel.CanRename, RelativeSource={RelativeSource AncestorType=local:ProfileManagerModal}}" />
25+
<cb:Button Text="Modal.Button.SelectProfile" Click="OnSelectClick" IsEnabled="{Binding ViewModel.CanSelect, RelativeSource={RelativeSource AncestorType=local:ProfileManagerModal}}" />
26+
</StackPanel>
27+
<StackPanel HorizontalAlignment="Right">
28+
<cb:Button Text="Common.Button.Close" Click="OnCloseClick" />
29+
</StackPanel>
30+
</Grid>
31+
</local:WindowModal.Buttons>
32+
<local:WindowModal.Data>
33+
<Border CornerRadius="10" ClipToBounds="True" Margin="16">
34+
<ListBox x:Name="ProfileListBox"
35+
ItemsSource="{Binding Profiles}"
36+
SelectedItem="{Binding ViewModel.SelectedProfile, RelativeSource={RelativeSource AncestorType=local:ProfileManagerModal}}"
37+
HorizontalAlignment="Stretch"
38+
VerticalAlignment="Stretch"
39+
Background="{DynamicResource BackgroundL0Brush}"
40+
SelectionChanged="OnSelectionChanged">
41+
<ListBox.ItemTemplate>
42+
<DataTemplate>
43+
<StackPanel Orientation="Vertical" Spacing="4">
44+
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource Text.NormalBrush}" />
45+
<TextBlock Text="{Binding FileName}" Foreground="{DynamicResource Text.SubBrush}"/>
46+
</StackPanel>
47+
</DataTemplate>
48+
</ListBox.ItemTemplate>
49+
</ListBox>
50+
</Border>
51+
</local:WindowModal.Data>
52+
</local:WindowModal>
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
using Avalonia;
2+
using Avalonia.Controls;
3+
using Avalonia.Data;
4+
using Avalonia.Interactivity;
5+
using Avalonia.Markup.Xaml;
6+
using Avalonia.Threading;
7+
using HedgeModManager.Foundation;
8+
using HedgeModManager.UI.ViewModels;
9+
using System.ComponentModel;
10+
using static HedgeModManager.UI.Languages.Language;
11+
12+
namespace HedgeModManager.UI.Controls.Modals;
13+
14+
public partial class ProfileManagerModal : WindowModal
15+
{
16+
public ProfileManagerViewModel ViewModel { get; set; }
17+
18+
public ProfileManagerModal()
19+
{
20+
ViewModel = new();
21+
AvaloniaXamlLoader.Load(this);
22+
}
23+
24+
public string GenerateProfileName(string baseName)
25+
{
26+
if (ViewModel.MainWindowViewModel == null)
27+
return string.Empty;
28+
29+
var profiles = ViewModel.MainWindowViewModel.Profiles;
30+
string name = baseName;
31+
while (profiles.Any(x => x.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
32+
name += " - Copy";
33+
return name;
34+
}
35+
36+
private void OnLoaded(object? sender, RoutedEventArgs e)
37+
{
38+
ViewModel.MainWindowViewModel = DataContext as MainWindowViewModel;
39+
if (ViewModel.MainWindowViewModel != null)
40+
{
41+
ViewModel.SelectedProfile = ViewModel.MainWindowViewModel.SelectedProfile;
42+
ViewModel.MainWindowViewModel.PropertyChanged += OnMainViewModelPropertyChanged;
43+
}
44+
ViewModel.Update();
45+
}
46+
47+
private void OnUnloaded(object? sender, RoutedEventArgs e)
48+
{
49+
if (ViewModel.MainWindowViewModel != null)
50+
ViewModel.MainWindowViewModel.PropertyChanged -= OnMainViewModelPropertyChanged;
51+
}
52+
53+
private void OnCloseClick(object? sender, RoutedEventArgs e)
54+
{
55+
Close();
56+
}
57+
58+
private void OnDuplicateClick(object? sender, RoutedEventArgs e)
59+
{
60+
if (ViewModel.MainWindowViewModel == null || ViewModel.SelectedProfile == null)
61+
return;
62+
63+
if (ViewModel.MainWindowViewModel.SelectedGame?.Game is not ModdableGameGeneric game)
64+
return;
65+
66+
var baseProfile = ViewModel.SelectedProfile;
67+
var profile = new ModProfile(GenerateProfileName(baseProfile.Name), baseProfile.ModDBPath, baseProfile.FileName);
68+
profile.FileName = profile.GenerateFileNameFromName();
69+
70+
// Copy profile
71+
string modsPath = game.ModDatabase.Root;
72+
string baseProfilePath = Path.Combine(modsPath, baseProfile.FileName);
73+
string newProfilePath = Path.Combine(modsPath, profile.FileName);
74+
if (File.Exists(baseProfilePath))
75+
{
76+
try
77+
{
78+
File.Copy(baseProfilePath, newProfilePath, true);
79+
}
80+
catch (Exception ex)
81+
{
82+
Logger.Error($"Failed to copy profile: {baseProfilePath} -> {newProfilePath}");
83+
Logger.Error(ex);
84+
}
85+
}
86+
87+
ViewModel.MainWindowViewModel.Profiles.Add(profile);
88+
ViewModel.MainWindowViewModel.SelectedProfile = ViewModel.SelectedProfile = profile;
89+
Dispatcher.UIThread.Invoke(async () => await ViewModel.MainWindowViewModel.SaveProfilesAsync(game));
90+
ViewModel.Update();
91+
}
92+
93+
private void OnDeleteClick(object? sender, RoutedEventArgs e)
94+
{
95+
if (ViewModel.MainWindowViewModel == null || ViewModel.SelectedProfile == null)
96+
return;
97+
98+
var messageBox = new MessageBoxModal("Modal.Title.DeleteProfile", Localize("Modal.Message.DeleteProfile", ViewModel.SelectedProfile.Name));
99+
messageBox.AddButton("Common.Button.No", (s, e) => messageBox.Close());
100+
messageBox.AddButton("Common.Button.Yes", (s, e) =>
101+
{
102+
if (ViewModel.MainWindowViewModel.SelectedGame?.Game is not ModdableGameGeneric game)
103+
return;
104+
105+
var profile = ViewModel.SelectedProfile;
106+
string profilePath = Path.Combine(game.ModDatabase.Root, profile.FileName);
107+
if (File.Exists(profilePath))
108+
{
109+
try
110+
{
111+
File.Delete(profilePath);
112+
}
113+
catch (Exception ex)
114+
{
115+
Logger.Error($"Failed to delete profile: {profilePath}");
116+
Logger.Error(ex);
117+
}
118+
}
119+
120+
int pos = ViewModel.MainWindowViewModel.Profiles.IndexOf(ViewModel.SelectedProfile);
121+
if (pos == ViewModel.MainWindowViewModel.Profiles.Count - 1)
122+
pos -= 1;
123+
ViewModel.MainWindowViewModel?.Profiles.Remove(ViewModel.SelectedProfile);
124+
ViewModel.SelectedProfile = ViewModel.MainWindowViewModel?.Profiles[pos];
125+
ViewModel.Update();
126+
messageBox.Close();
127+
});
128+
messageBox.SetDanger();
129+
messageBox.Open(ViewModel.MainWindowViewModel);
130+
}
131+
132+
private void OnRenameClick(object? sender, RoutedEventArgs e)
133+
{
134+
if (ViewModel.MainWindowViewModel == null || ViewModel.SelectedProfile == null)
135+
return;
136+
137+
var modal = new ProfileRenameModal(ViewModel.SelectedProfile);
138+
modal.OnProfileRenamed = () =>
139+
{
140+
ProfileListBox.Bind(ListBox.ItemsSourceProperty, new Binding("Profiles"));
141+
};
142+
modal.Open(ViewModel.MainWindowViewModel);
143+
ViewModel.Update();
144+
}
145+
146+
private void OnSelectClick(object? sender, RoutedEventArgs e)
147+
{
148+
if (ViewModel.MainWindowViewModel == null || ViewModel.SelectedProfile == null)
149+
return;
150+
151+
ViewModel.MainWindowViewModel.SelectedProfile = ViewModel.SelectedProfile;
152+
ViewModel.Update();
153+
}
154+
155+
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
156+
{
157+
ViewModel.Update();
158+
}
159+
160+
private void OnMainViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
161+
{
162+
if (e.PropertyName == nameof(MainWindowViewModel.IsBusy))
163+
ViewModel?.Update();
164+
}
165+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<local:WindowModal xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:c="using:HedgeModManager.UI.Controls"
4+
xmlns:cb="using:HedgeModManager.UI.Controls.Basic"
5+
xmlns:cp="using:HedgeModManager.UI.Controls.Primitives"
6+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
7+
xmlns:html="using:TheArtOfDev.HtmlRenderer.Avalonia"
8+
xmlns:local="using:HedgeModManager.UI.Controls.Modals"
9+
xmlns:m="using:HedgeModManager.UI.Models"
10+
xmlns:materialIcons="using:Material.Icons.Avalonia"
11+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
12+
xmlns:vm="using:HedgeModManager.UI.ViewModels"
13+
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="200"
14+
x:Class="HedgeModManager.UI.Controls.Modals.ProfileRenameModal"
15+
x:DataType="vm:MainWindowViewModel"
16+
MinWidth="500" MinHeight="200" Title="Modal.Title.RenameProfile"
17+
UseTitlePadding="True">
18+
<local:WindowModal.Buttons>
19+
<Grid>
20+
<StackPanel HorizontalAlignment="Right">
21+
<cb:Button Text="Common.Button.OK" Click="OnOkClick" />
22+
</StackPanel>
23+
</Grid>
24+
</local:WindowModal.Buttons>
25+
<local:WindowModal.Data>
26+
<StackPanel Margin="20,0,20,0"
27+
Orientation="Vertical"
28+
HorizontalAlignment="Stretch"
29+
VerticalAlignment="Center">
30+
<TextBlock Text="{DynamicResource Modal.Text.NewProfileName}"
31+
VerticalAlignment="Center" />
32+
<TextBox Text="{Binding NewName, RelativeSource={RelativeSource AncestorType=local:ProfileRenameModal}}"
33+
Watermark="{Binding SelectedProfile.Name, RelativeSource={RelativeSource AncestorType=local:ProfileRenameModal}}"
34+
HorizontalAlignment="Stretch" VerticalAlignment="Top" />
35+
</StackPanel>
36+
</local:WindowModal.Data>
37+
</local:WindowModal>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Avalonia.Interactivity;
2+
using Avalonia.Markup.Xaml;
3+
4+
namespace HedgeModManager.UI.Controls.Modals;
5+
6+
public partial class ProfileRenameModal : WindowModal
7+
{
8+
public ModProfile SelectedProfile { get; set; }
9+
public string NewName { get; set; } = string.Empty;
10+
public Action? OnProfileRenamed { get; set; }
11+
12+
// Preview
13+
public ProfileRenameModal() : this(new ModProfile("No Profile")) { }
14+
15+
public ProfileRenameModal(ModProfile modProfile)
16+
{
17+
SelectedProfile = modProfile;
18+
AvaloniaXamlLoader.Load(this);
19+
}
20+
21+
private void OnOkClick(object? sender, RoutedEventArgs e)
22+
{
23+
NewName = NewName?.Trim() ?? string.Empty;
24+
if (string.IsNullOrEmpty(NewName))
25+
{
26+
Close();
27+
return;
28+
}
29+
SelectedProfile.Name = NewName;
30+
OnProfileRenamed?.Invoke();
31+
Close();
32+
}
33+
}

Source/HedgeModManager.UI/Controls/Settings/Settings.axaml

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@
2525
Title="{DynamicResource Settings.Title.Profile}"
2626
Description="{DynamicResource Settings.Description.Profile}">
2727
<cs:SettingsEntry.Data>
28-
<ComboBox MinWidth="240"
29-
ItemsSource="{Binding Profiles}"
30-
SelectedItem="{Binding ViewModel.SelectedProfile, RelativeSource={RelativeSource AncestorType=cs:Settings}}"
31-
SelectionChanged="OnProfileSelectionChanged"
28+
<StackPanel Orientation="Horizontal" Spacing="10">
29+
<cb:Button MinWidth="115"
30+
Text="{DynamicResource Settings.Button.ManageProfiles}"
3231
VerticalContentAlignment="Center"
33-
HorizontalAlignment="Stretch" />
32+
Click="OnManageProfilesClick" />
33+
<ComboBox MinWidth="240" MinHeight="36"
34+
ItemsSource="{Binding Profiles}"
35+
SelectedItem="{Binding ViewModel.SelectedProfile, RelativeSource={RelativeSource AncestorType=cs:Settings}}"
36+
SelectionChanged="OnProfileSelectionChanged"
37+
VerticalContentAlignment="Center"
38+
HorizontalAlignment="Stretch" />
39+
</StackPanel>
3440
</cs:SettingsEntry.Data>
3541
</cs:SettingsEntry>
3642
<cs:SettingsEntry Icon="FolderOpen"
@@ -172,7 +178,7 @@
172178
<cs:SettingsEntry Icon="Palette"
173179
Title="{DynamicResource Settings.Title.Theme}">
174180
<cs:SettingsEntry.Data>
175-
<ComboBox MinWidth="240"
181+
<ComboBox MinWidth="240" MinHeight="36"
176182
ItemsSource="{Binding ViewModel.ThemeCollection, RelativeSource={RelativeSource AncestorType=cs:Settings}}"
177183
SelectedValue="{Binding ViewModel.SelectedTheme, RelativeSource={RelativeSource AncestorType=cs:Settings}}"
178184
SelectionChanged="OnThemeSelectionChanged"
@@ -189,7 +195,7 @@
189195
<cs:SettingsEntry Icon="Translate"
190196
Title="{DynamicResource Settings.Title.Language}">
191197
<cs:SettingsEntry.Data>
192-
<ComboBox MinWidth="240"
198+
<ComboBox MinWidth="240" MinHeight="36"
193199
ItemsSource="{Binding Languages}"
194200
SelectedItem="{Binding ViewModel.SelectedLanguage, RelativeSource={RelativeSource AncestorType=cs:Settings}}"
195201
SelectionChanged="OnLanguageSelectionChanged"

Source/HedgeModManager.UI/Controls/Settings/Settings.axaml.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,15 @@ private async void OnCheckCodeUpdatesClick(object? sender, RoutedEventArgs e)
302302
ViewModel.CheckCodeUpdatesText = "Settings.Button.CheckUpdates";
303303
}
304304

305+
private void OnManageProfilesClick(object? sender, RoutedEventArgs e)
306+
{
307+
if (DataContext is not MainWindowViewModel mainViewModel)
308+
return;
309+
310+
var profileManager = new ProfileManagerModal();
311+
profileManager.Open(mainViewModel);
312+
}
313+
305314
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
306315
{
307316
if (e.Property == GameProperty)

0 commit comments

Comments
 (0)