Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions WheelWizard/Services/ModManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ private set
}

private bool _isProcessing;
private bool _isBatchUpdating;

private ModManager()
{
Expand Down Expand Up @@ -103,6 +104,9 @@ public void RemoveMod(Mod mod)

private void Mod_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_isBatchUpdating)
return;

if (
e.PropertyName != nameof(Mod.IsEnabled)
&& e.PropertyName != nameof(Mod.Title)
Expand Down Expand Up @@ -490,4 +494,48 @@ public void IncreasePriority(Mod mod)
public int GetLowestActivePriority() => Mods.Min(m => m.Priority);

public int GetHighestActivePriority() => Mods.Max(m => m.Priority);

/// <summary>
/// Moves a mod to a new position in the list using gap-based indexing.
/// Gap 0 = before first item, gap Count = after last item.
/// </summary>
public void MoveModToIndex(Mod mod, int gapIndex)
{
var sortedMods = Mods.OrderBy(m => m.Priority).ToList();
var currentIndex = sortedMods.IndexOf(mod);

if (currentIndex == -1)
return;

// Convert gap index to target index after removal
int targetIndex;
if (gapIndex <= currentIndex)
targetIndex = gapIndex;
else if (gapIndex > currentIndex + 1)
targetIndex = gapIndex - 1;
else
return; // No change needed (dropped in same position)

targetIndex = Math.Clamp(targetIndex, 0, sortedMods.Count - 1);

if (currentIndex == targetIndex)
return;

_isBatchUpdating = true;
try
{
sortedMods.RemoveAt(currentIndex);
sortedMods.Insert(targetIndex, mod);

for (var i = 0; i < sortedMods.Count; i++)
sortedMods[i].Priority = i;
}
finally
{
_isBatchUpdating = false;
}

SortModsByPriority();
SaveModsAsync();
}
}
91 changes: 91 additions & 0 deletions WheelWizard/Views/Components/WhWzLibrary/GridModPanel.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:components="clr-namespace:WheelWizard.Views.Components"
xmlns:pages="clr-namespace:WheelWizard.Views.Pages"
xmlns:lang="clr-namespace:WheelWizard.Resources.Languages"
x:Class="WheelWizard.Views.Components.GridModPanel"
x:DataType="pages:ModListItem">

<Border CornerRadius="8" Background="{StaticResource Neutral900}"
ClipToBounds="True" Margin="4" Height="160">
<Grid>
<!-- Image area -->
<Border Background="{StaticResource Neutral700}" ClipToBounds="True">
<Panel>
<Image x:Name="ModImage" Stretch="UniformToFill" />
<!-- Placeholder icon when no image is loaded -->
<PathIcon x:Name="PlaceholderIcon"
Data="{StaticResource CubesStacked}"
Width="36" Height="36"
Foreground="{StaticResource Neutral500}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Panel>
</Border>

<!-- Gradient overlay for the bottom area -->
<Border VerticalAlignment="Bottom" Height="100" IsHitTestVisible="False">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Color="Transparent" Offset="0.0" />
<GradientStop Color="{StaticResource Neutral900}" Offset="0.7" />
<GradientStop Color="{StaticResource Neutral900}" Offset="1.0" />
</LinearGradientBrush>
</Border.Background>
</Border>

<!-- Content -->
<Grid RowDefinitions="*, Auto, Auto">
<!-- Title -->
<TextBlock Grid.Row="1" Text="{Binding Mod.Title}" FontSize="13" FontWeight="SemiBold"
Foreground="{StaticResource Neutral100}"
TextTrimming="CharacterEllipsis"
Margin="10,0,10,4" />

<!-- Bottom controls bar: Enabled switch + Priority -->
<Border Grid.Row="2" Padding="8,0,8,8">
<Grid ColumnDefinitions="Auto, *, Auto">
<CheckBox Grid.Column="0"
IsChecked="{Binding Mod.IsEnabled, Mode=TwoWay}"
Classes="SwitchDark"
VerticalAlignment="Center" />

<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4"
VerticalAlignment="Center">
<TextBlock Text="#" FontSize="13"
Foreground="{StaticResource Neutral500}"
VerticalAlignment="Center" />
<TextBox x:Name="PriorityTextBox"
Text="{Binding Mod.Priority, Mode=OneWay}"
Classes="dark"
MinWidth="30" MaxWidth="50"
MinHeight="28" Height="28"
VerticalAlignment="Center"
Padding="4,2"
HorizontalContentAlignment="Center"
TextChanged="PriorityText_OnTextChanged"
LostFocus="PriorityText_OnLostFocus"
KeyDown="PriorityText_OnKeyDown">
<TextBox.Styles>
<Style Selector="TextBox /template/ DockPanel#PART_InnerDockPanel">
<Setter Property="Margin" Value="0" />
</Style>
<Style Selector="TextBox /template/ ScrollViewer#PART_ScrollViewer">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalScrollBarVisibility" Value="Hidden" />
</Style>
</TextBox.Styles>
</TextBox>
</StackPanel>
</Grid>
</Border>
</Grid>
</Grid>

<!-- Hover effect -->
<Border.Styles>
<Style Selector="Border:pointerover">
<Setter Property="Background" Value="{StaticResource Neutral700}" />
</Style>
</Border.Styles>
</Border>
</UserControl>
93 changes: 93 additions & 0 deletions WheelWizard/Views/Components/WhWzLibrary/GridModPanel.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media.Imaging;
using Microsoft.Extensions.DependencyInjection;
using WheelWizard.GameBanana;
using WheelWizard.Views.Pages;

namespace WheelWizard.Views.Components;

public partial class GridModPanel : UserControl
{
public GridModPanel()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}

private void OnDataContextChanged(object? sender, EventArgs e)
{
// Reset image state when DataContext changes
ModImage.Source = null;
PlaceholderIcon.IsVisible = true;
LoadModImageAsync();
}

private async void LoadModImageAsync()
{
if (DataContext is not ModListItem item || item.Mod.ModID <= 0)
return;

try
{
var gameBananaService = App.Services.GetService<IGameBananaSingletonService>();
if (gameBananaService == null)
return;

var result = await gameBananaService.GetModDetails(item.Mod.ModID);
if (!result.IsSuccess || result.Value.PreviewMedia?.Images == null || result.Value.PreviewMedia.Images.Count == 0)
return;

var image = result.Value.PreviewMedia.Images[0];
// Prefer smaller 220px thumbnail for grid cards, fall back to full size
var imageUrl = image.File220 != null ? $"{image.BaseUrl}/{image.File220}" : $"{image.BaseUrl}/{image.File}";

using var httpClient = new HttpClient();
var response = await httpClient.GetAsync(imageUrl);
response.EnsureSuccessStatusCode();

await using var stream = await response.Content.ReadAsStreamAsync();
var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
memoryStream.Position = 0;
ModImage.Source = new Bitmap(memoryStream);
PlaceholderIcon.IsVisible = false;
}
catch
{
// Ignore - just show placeholder icon
}
}

private void PriorityText_OnLostFocus(object? sender, RoutedEventArgs e)
{
if (DataContext is not ModListItem item || e.Source is not TextBox textBox)
return;

textBox.Classes.Remove("error");
if (int.TryParse(textBox.Text, out var newPriority))
item.Mod.Priority = newPriority;
else
textBox.Text = item.Mod.Priority.ToString();
}

private void PriorityText_OnTextChanged(object? sender, TextChangedEventArgs e)
{
if (e.Source is not TextBox textBox)
return;

if (int.TryParse(textBox.Text, out _))
textBox.Classes.Remove("error");
else if (!textBox.Classes.Contains("error"))
textBox.Classes.Add("error");
}

private void PriorityText_OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key != Key.Enter || sender is not TextBox)
return;

this.Focus();
}
}
48 changes: 44 additions & 4 deletions WheelWizard/Views/Pages/ModsPage.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,28 +93,36 @@
VerticalAlignment="Bottom" />
</Grid>

<!-- Mods List -->
<!-- Mods List (Blocks / arrow mode) -->
<ListBox Grid.Row="1" x:Name="ModsListBox" ItemsSource="{Binding Mods}" Margin="10,5">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Classes="BoxItemBackground" Height="50">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="50" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<CheckBox Grid.Column="0" IsChecked="{Binding Mod.IsEnabled, Mode=TwoWay}"
<!-- Drag handle -->
<Panel Grid.Column="0" Background="Transparent" Cursor="Hand"
PointerPressed="DragHandle_PointerPressed">
<PathIcon Data="{StaticResource Grip}" Width="12" Height="12"
Foreground="{StaticResource Neutral500}" />
</Panel>

<CheckBox Grid.Column="1" IsChecked="{Binding Mod.IsEnabled, Mode=TwoWay}"
Classes="SwitchDark" Margin="0,5"
HorizontalAlignment="Center" VerticalAlignment="Center" />
<TextBlock Text="{Binding Mod.Title}" FontSize="14"
Foreground="{StaticResource Neutral100}"
Grid.Column="1" VerticalAlignment="Center" Margin="10,0,0,0" />
Grid.Column="2" VerticalAlignment="Center" Margin="10,0,0,0" />

<!-- Yes i know that it already works without any change event if we remove Mode=OneWay -->
<!-- Removing that will make it so editing that will also edit the model -->
<!-- however, it works really weird if you do that -->
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right"
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right"
VerticalAlignment="Center">
<TextBlock Text="{x:Static lang:Common.Attribute_Priority}" FontSize="14"
Foreground="{StaticResource Neutral100}"
Expand Down Expand Up @@ -187,6 +195,38 @@
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>

<!-- Mods Grid (Rows / priority text mode) -->
<ScrollViewer Grid.Row="1" x:Name="ModsGridView" IsVisible="False" Margin="10,5"
HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Mods}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border>
<components:GridModPanel />
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="{x:Static lang:Common.Action_Rename}" Click="RenameMod_Click" />
<MenuItem Header="{x:Static lang:Common.Action_Delete}" Click="DeleteMod_Click" />
<Separator />
<MenuItem Header="{x:Static lang:Common.Action_ViewMod}" Click="ViewMod_Click" />
<MenuItem Header="{x:Static lang:Common.Action_OpenFolder}" Click="OpenFolder_Click" />
</ContextMenu>
</Border.ContextMenu>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>

<!-- Drag-and-drop overlay canvas -->
<Canvas x:Name="DragCanvas" IsVisible="False" IsHitTestVisible="False"
Grid.RowSpan="2" ZIndex="1000" />
</Grid>
</UserControl>
Loading
Loading