diff --git a/AndroidSideloader.csproj b/AndroidSideloader.csproj
index 0fe2ebb..3185b2c 100644
--- a/AndroidSideloader.csproj
+++ b/AndroidSideloader.csproj
@@ -194,6 +194,9 @@
Component
+
+ Component
+
True
True
diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs
index 8ec8f9c..17012c2 100644
--- a/MainForm.Designer.cs
+++ b/MainForm.Designer.cs
@@ -202,6 +202,7 @@ namespace AndroidSideloader
//
// gamesQueListBox
//
+ this.gamesQueListBox.AllowDrop = false;
this.gamesQueListBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.gamesQueListBox.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
this.gamesQueListBox.BorderStyle = System.Windows.Forms.BorderStyle.None;
@@ -215,10 +216,6 @@ namespace AndroidSideloader
this.gamesQueListBox.Name = "gamesQueListBox";
this.gamesQueListBox.Size = new System.Drawing.Size(266, 192);
this.gamesQueListBox.TabIndex = 9;
- this.gamesQueListBox.MouseClick += new System.Windows.Forms.MouseEventHandler(this.gamesQueListBox_MouseClick);
- this.gamesQueListBox.DrawItem += new System.Windows.Forms.DrawItemEventHandler(this.gamesQueListBox_DrawItem);
- this.gamesQueListBox.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop);
- this.gamesQueListBox.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter);
//
// devicesComboBox
//
diff --git a/MainForm.cs b/MainForm.cs
index b3af3ce..0ebd11c 100755
--- a/MainForm.cs
+++ b/MainForm.cs
@@ -127,7 +127,7 @@ namespace AndroidSideloader
_debounceTimer = new System.Windows.Forms.Timer { Interval = 100, Enabled = false };
_debounceTimer.Tick += async (sender, e) => await RunSearch();
- gamesQueListBox.DataSource = gamesQueueList;
+
SetCurrentLogPath();
StartTimers();
@@ -514,6 +514,9 @@ namespace AndroidSideloader
}
}
+ // Ensure bottom panels are properly laid out
+ LayoutBottomPanels();
+
webView21.Visible = settings.TrailersEnabled;
// Continue with Form1_Shown
@@ -3091,6 +3094,7 @@ Additional Thanks & Resources
public static bool updatedConfig = false;
public static int steps = 0;
public static bool gamesAreDownloading = false;
+ private ModernQueuePanel _queuePanel;
private readonly BindingList gamesQueueList = new BindingList();
public static int quotaTries = 0;
public static bool timerticked = false;
@@ -3164,7 +3168,9 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
{
speedLabel.Text = String.Empty;
progressBar.Value = 0;
- gamesQueueList.RemoveAt(0);
+ if (gamesQueueList.Count > 0)
+ gamesQueueList.RemoveAt(0);
+ _queuePanel?.Invalidate();
}
public void SetProgress(float progress)
@@ -3239,7 +3245,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
}
gamesAreDownloading = true;
-
+ if (_queuePanel != null) _queuePanel.IsDownloading = true;
//Do user json on firsttime
if (settings.UserJsonOnGameInstall)
@@ -3467,33 +3473,44 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
if (removedownloading)
{
+ removedownloading = false;
+
+ // Store game info before removing from queue
+ string cancelledGame = gameName;
+ string cancelledHash = gameNameHash;
+
+ // Remove the cancelled item from queue
+ if (gamesQueueList.Count > 0)
+ {
+ gamesQueueList.RemoveAt(0);
+ }
+
+ // Reset progress UI
+ speedLabel.Text = String.Empty;
+ progressBar.Value = 0;
+
+ // Ask about keeping files
changeTitle("Keep game files?");
try
{
- cleanupActiveDownloadStatus();
-
DialogResult res = FlexibleMessageBox.Show(
- $"{gameName} exists in destination directory, do you want to delete it?\n\nClick NO to keep the files if you wish to resume your download later.",
+ $"{cancelledGame} download was cancelled. Do you want to delete the partial files?\n\nClick NO to keep the files if you wish to resume your download later.",
"Delete Temporary Files?", MessageBoxButtons.YesNo);
if (res == DialogResult.Yes)
{
- changeTitle("Deleting game files");
+ changeTitle("Deleting game files...");
if (UsingPublicConfig)
{
- if (Directory.Exists($"{settings.DownloadDir}\\{gameNameHash}"))
- {
- Directory.Delete($"{settings.DownloadDir}\\{gameNameHash}", true);
- }
-
- if (Directory.Exists($"{settings.DownloadDir}\\{gameName}"))
- {
- Directory.Delete($"{settings.DownloadDir}\\{gameName}", true);
- }
+ if (Directory.Exists($"{settings.DownloadDir}\\{cancelledHash}"))
+ Directory.Delete($"{settings.DownloadDir}\\{cancelledHash}", true);
+ if (Directory.Exists($"{settings.DownloadDir}\\{cancelledGame}"))
+ Directory.Delete($"{settings.DownloadDir}\\{cancelledGame}", true);
}
else
{
- Directory.Delete(settings.DownloadDir + "\\" + gameName, true);
+ if (Directory.Exists($"{settings.DownloadDir}\\{cancelledGame}"))
+ Directory.Delete($"{settings.DownloadDir}\\{cancelledGame}", true);
}
}
}
@@ -3501,8 +3518,9 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
{
_ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}");
}
+
changeTitle("");
- break;
+ continue; // Continue to next item in queue
}
{
//Quota Errors
@@ -3774,13 +3792,6 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
}
}
}
- if (removedownloading)
- {
- removedownloading = false;
- gamesAreDownloading = false;
- isinstalling = false;
- return;
- }
if (!obbsMismatch)
{
changeTitle("Refreshing games list, please wait...\n");
@@ -3788,12 +3799,15 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
await RefreshGameListAsync();
- if (settings.EnableMessageBoxes)
+ // Only show output if there's content
+ if (settings.EnableMessageBoxes && !string.IsNullOrWhiteSpace(output.Output + output.Error))
{
ShowPrcOutput(output);
}
+
progressBar.IsIndeterminate = false;
gamesAreDownloading = false;
+ if (_queuePanel != null) _queuePanel.IsDownloading = false;
isinstalling = false;
changeTitle("");
@@ -3909,6 +3923,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
}
progressBar.IsIndeterminate = false;
gamesAreDownloading = false;
+ if (_queuePanel != null) _queuePanel.IsDownloading = false;
isinstalling = false;
changeTitle("");
@@ -4615,20 +4630,6 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
ADBWirelessToggle.Text = isWirelessEnabled ? "WIRELESS ADB" : "ENABLE WIRELESS ADB";
}
- private void gamesQueListBox_MouseClick(object sender, MouseEventArgs e)
- {
- if (gamesQueListBox.SelectedIndex == 0 && gamesQueueList.Count == 1)
- {
- removedownloading = true;
- RCLONE.killRclone();
- }
- if (gamesQueListBox.SelectedIndex != -1 && gamesQueListBox.SelectedIndex != 0)
- {
- _ = gamesQueueList.Remove(gamesQueListBox.SelectedItem.ToString());
- }
-
- }
-
private void devicesComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
showAvailableSpace();
@@ -5450,21 +5451,6 @@ function onYouTubeIframeAPIReady() {
btnUpdateAvailable.Click += btnUpdateAvailable_Click;
btnNewerThanList.Click += btnNewerThanList_Click;
}
-
- private void gamesQueListBox_MouseDown(object sender, MouseEventArgs e)
- {
- if (gamesQueListBox.SelectedItem == null)
- {
- return;
- }
-
- _ = gamesQueListBox.DoDragDrop(gamesQueListBox.SelectedItem, DragDropEffects.Move);
- }
-
- private void gamesQueListBox_DragOver(object sender, DragEventArgs e)
- {
- e.Effect = DragDropEffects.Move;
- }
private async void pullAppToDesktopBtn_Click(object sender, EventArgs e)
{
@@ -7104,17 +7090,113 @@ function onYouTubeIframeAPIReady() {
{
Color panelColor = Color.FromArgb(24, 26, 30);
- // Create rounded panel for notesRichTextBox
+ // Initialize modern queue panel
+ _queuePanel = new ModernQueuePanel
+ {
+ Size = new Size(250, 150), // Placeholder; resized by LayoutBottomPanels
+ BackColor = panelColor
+ };
+ _queuePanel.ItemRemoved += QueuePanel_ItemRemoved;
+ _queuePanel.ItemReordered += QueuePanel_ItemReordered;
+
+ // Sync with binding list
+ gamesQueueList.ListChanged += (s, e) => SyncQueuePanel();
+
+ // Notes panel
notesPanel = CreateRoundedPanel(notesRichTextBox, panelColor, 8, true);
- // Create rounded panel for gamesQueListBox
- queuePanel = CreateRoundedPanel(gamesQueListBox, panelColor, 8, false);
+ // Queue panel
+ queuePanel = CreateQueuePanel(panelColor, 8);
- // Bring labels to front so they appear above the panels
gamesQueueLabel.BringToFront();
lblNotes.BringToFront();
+
+ // Trigger initial layout
+ LayoutBottomPanels();
}
-
+
+ private Panel CreateQueuePanel(Color panelColor, int radius)
+ {
+ var panel = new Panel
+ {
+ Location = gamesQueListBox.Location,
+ Size = new Size(
+ gamesQueListBox.Width + ChildHorizontalPadding + ChildRightMargin,
+ gamesQueListBox.Height + ReservedLabelHeight
+ ),
+ BackColor = Color.Transparent,
+ Padding = new Padding(ChildHorizontalPadding, ChildTopMargin, ChildRightMargin, ChildTopMargin)
+ };
+
+ // Double buffering
+ typeof(Panel).InvokeMember("DoubleBuffered",
+ System.Reflection.BindingFlags.SetProperty |
+ System.Reflection.BindingFlags.Instance |
+ System.Reflection.BindingFlags.NonPublic,
+ null, panel, new object[] { true });
+
+ panel.Paint += (s, e) =>
+ {
+ var p = (Panel)s;
+ e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
+ var rect = new Rectangle(0, 0, p.Width - 1, p.Height - 1);
+
+ using (var path = CreateRoundedRectPath(rect, radius))
+ using (var brush = new SolidBrush(panelColor))
+ {
+ e.Graphics.FillPath(brush, path);
+ }
+
+ using (var regionPath = CreateRoundedRectPath(new Rectangle(0, 0, p.Width, p.Height), radius))
+ {
+ p.Region = new Region(regionPath);
+ }
+ };
+
+ var parent = gamesQueListBox.Parent;
+ parent.Controls.Remove(gamesQueListBox);
+ gamesQueListBox.Dispose();
+
+ _queuePanel.Location = new Point(ChildHorizontalPadding, ChildTopMargin);
+ _queuePanel.Anchor = AnchorStyles.None;
+ panel.Controls.Add(_queuePanel);
+ parent.Controls.Add(panel);
+ panel.BringToFront();
+
+ return panel;
+ }
+
+ private void SyncQueuePanel()
+ {
+ if (_queuePanel == null) return;
+ _queuePanel.SetItems(gamesQueueList);
+ _queuePanel.IsDownloading = gamesAreDownloading && gamesQueueList.Count > 0;
+ }
+
+ private void QueuePanel_ItemRemoved(object sender, int index)
+ {
+ if (index == 0 && gamesQueueList.Count >= 1)
+ {
+ removedownloading = true;
+ RCLONE.killRclone();
+ }
+ else if (index > 0 && index < gamesQueueList.Count)
+ {
+ gamesQueueList.RemoveAt(index);
+ }
+ }
+
+ private void QueuePanel_ItemReordered(object sender, ReorderEventArgs e)
+ {
+ if (e.FromIndex <= 0 || e.FromIndex >= gamesQueueList.Count) return;
+
+ var item = gamesQueueList[e.FromIndex];
+ gamesQueueList.RemoveAt(e.FromIndex);
+
+ int insertAt = Math.Max(1, Math.Min(e.ToIndex, gamesQueueList.Count));
+ gamesQueueList.Insert(insertAt, item);
+ }
+
private void notesRichTextBox_LinkClicked(object sender, LinkClickedEventArgs e)
{
try
@@ -7127,69 +7209,6 @@ function onYouTubeIframeAPIReady() {
}
}
- private void gamesQueListBox_DrawItem(object sender, DrawItemEventArgs e)
- {
- if (e.Index < 0) return;
-
- // Determine colors based on selection state
- Color backColor = (e.State & DrawItemState.Selected) == DrawItemState.Selected
- ? Color.FromArgb(93, 203, 173) // Accent color for selected
- : gamesQueListBox.BackColor;
-
- Color foreColor = (e.State & DrawItemState.Selected) == DrawItemState.Selected
- ? Color.FromArgb(20, 20, 20) // Dark text on accent
- : gamesQueListBox.ForeColor;
-
- Font font = (e.State & DrawItemState.Selected) == DrawItemState.Selected
- ? new Font("Microsoft Sans Serif", 10F, FontStyle.Bold)
- : new Font("Microsoft Sans Serif", 10F, FontStyle.Regular);
-
- // Clear the item background first
- using (SolidBrush clearBrush = new SolidBrush(gamesQueListBox.BackColor))
- {
- e.Graphics.FillRectangle(clearBrush, e.Bounds);
- }
-
- // Draw rounded background
- e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
- e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
- int radius = 1;
- int margin = 4;
- Rectangle roundedRect = new Rectangle(
- e.Bounds.X,
- e.Bounds.Y,
- e.Bounds.Width - (margin * 2),
- e.Bounds.Height
- );
-
- using (GraphicsPath path = CreateRoundedRectPath(roundedRect, radius))
- using (SolidBrush backgroundBrush = new SolidBrush(backColor))
- {
- e.Graphics.FillPath(backgroundBrush, path);
- }
-
- // Draw text with padding
- string text = gamesQueListBox.Items[e.Index].ToString();
- Rectangle textRect = new Rectangle(
- roundedRect.X + 4,
- roundedRect.Y,
- roundedRect.Width - 8,
- roundedRect.Height
- );
-
- using (SolidBrush textBrush = new SolidBrush(foreColor))
- {
- var sf = new StringFormat
- {
- Alignment = StringAlignment.Near,
- LineAlignment = StringAlignment.Center,
- Trimming = StringTrimming.EllipsisCharacter,
- FormatFlags = StringFormatFlags.NoWrap
- };
- e.Graphics.DrawString(text, font, textBrush, textRect, sf);
- }
- }
-
private void ApplyWebViewRoundedCorners()
{
if (webView21 == null) return;
@@ -7305,7 +7324,7 @@ function onYouTubeIframeAPIReady() {
{
// Skip if panels aren't initialized yet
if (notesPanel == null || queuePanel == null) return;
- if (gamesQueListBox == null || notesRichTextBox == null) return;
+ if (notesRichTextBox == null) return;
// Panels start after webView21 (webView21 ends at 259 + 384 = 643, add spacing)
int panelsStartX = 654;
@@ -7323,13 +7342,22 @@ function onYouTubeIframeAPIReady() {
queuePanel.Location = new Point(panelsStartX, panelY);
queuePanel.Size = new Size(queueWidth, BottomPanelHeight);
+ // Layout queue panel child (_queuePanel)
+ if (_queuePanel != null)
+ {
+ _queuePanel.Location = new Point(ChildHorizontalPadding, ChildTopMargin);
+ _queuePanel.Size = new Size(
+ queuePanel.Width - ChildHorizontalPadding - ChildRightMargin,
+ queuePanel.Height - ChildTopMargin * 2 - ReservedLabelHeight
+ );
+ }
+
// Notes panel
int notesX = panelsStartX + queueWidth + PanelSpacing;
notesPanel.Location = new Point(notesX, panelY);
notesPanel.Size = new Size(this.ClientSize.Width - notesX - RightMargin, BottomPanelHeight);
- // Layout children using the same helper as CreateRoundedPanel
- LayoutChildInPanel(queuePanel, gamesQueListBox, isNotesPanel: false);
+ // Layout notes child
LayoutChildInPanel(notesPanel, notesRichTextBox, isNotesPanel: true);
// Position labels at bottom of their panels
diff --git a/ModernQueuePanel.cs b/ModernQueuePanel.cs
new file mode 100644
index 0000000..5ee6b14
--- /dev/null
+++ b/ModernQueuePanel.cs
@@ -0,0 +1,511 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Windows.Forms;
+
+namespace AndroidSideloader
+{
+ // Modern download queue panel with drag-reorder, cancel buttons
+ // and custom scrollbar with auto-scrolling during drag
+ public sealed class ModernQueuePanel : Control
+ {
+ // Layout constants
+ private const int ItemHeight = 28, ItemMargin = 4, ItemRadius = 5;
+ private const int XButtonSize = 18, DragHandleWidth = 20, TextPadding = 6;
+ private const int ScrollbarWidth = 6, ScrollbarWidthHover = 8, ScrollbarMargin = 2;
+ private const int ScrollbarRadius = 3, MinThumbHeight = 20;
+ private const int AutoScrollZoneHeight = 30, AutoScrollSpeed = 3;
+
+ // Color palette
+ private static readonly Color BgColor = Color.FromArgb(24, 26, 30);
+ private static readonly Color ItemBg = Color.FromArgb(32, 36, 44);
+ private static readonly Color ItemHoverBg = Color.FromArgb(42, 46, 54);
+ private static readonly Color ItemDragBg = Color.FromArgb(45, 55, 70);
+ private static readonly Color TextColor = Color.FromArgb(210, 210, 210);
+ private static readonly Color TextDimColor = Color.FromArgb(140, 140, 140);
+ private static readonly Color AccentColor = Color.FromArgb(93, 203, 173);
+ private static readonly Color XButtonBg = Color.FromArgb(55, 60, 70);
+ private static readonly Color XButtonHoverBg = Color.FromArgb(200, 60, 60);
+ private static readonly Color GripColor = Color.FromArgb(70, 75, 85);
+ private static readonly Color ItemDragBorder = Color.FromArgb(55, 65, 80);
+ private static readonly Color ScrollTrackColor = Color.FromArgb(35, 38, 45);
+ private static readonly Color ScrollThumbColor = Color.FromArgb(70, 75, 85);
+ private static readonly Color ScrollThumbHoverColor = Color.FromArgb(90, 95, 105);
+ private static readonly Color ScrollThumbDragColor = Color.FromArgb(110, 115, 125);
+
+ private readonly List _items = new List();
+ private readonly Timer _autoScrollTimer;
+
+ // State tracking
+ private int _hoveredIndex = -1, _dragIndex = -1, _dropIndex = -1, _scrollOffset;
+ private bool _hoveringX, _scrollbarHovered, _scrollbarDragging;
+ private int _scrollDragStartY, _scrollDragStartOffset, _autoScrollDirection;
+ private Rectangle _scrollThumbRect, _scrollTrackRect;
+
+ public event EventHandler ItemRemoved;
+ public event EventHandler ItemReordered;
+
+ public ModernQueuePanel()
+ {
+ SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
+ ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
+ BackColor = BgColor;
+ _autoScrollTimer = new Timer { Interval = 16 }; // ~60 FPS
+ _autoScrollTimer.Tick += (s, e) => HandleAutoScroll();
+ }
+
+ public bool IsDownloading { get; set; }
+ public int Count => _items.Count;
+ private int ContentHeight => _items.Count * (ItemHeight + ItemMargin) + ItemMargin;
+ private int MaxScroll => Math.Max(0, ContentHeight - Height);
+ private bool ScrollbarVisible => ContentHeight > Height;
+
+ public void SetItems(IEnumerable items)
+ {
+ _items.Clear();
+ _items.AddRange(items);
+ ResetState();
+ }
+
+ private void ResetState()
+ {
+ _hoveredIndex = _dragIndex = -1;
+ ClampScroll();
+ Invalidate();
+ }
+
+ private void ClampScroll() =>
+ _scrollOffset = Math.Max(0, Math.Min(MaxScroll, _scrollOffset));
+
+ // Auto-scroll when dragging near edges
+ private void HandleAutoScroll()
+ {
+ if (_dragIndex < 0 || _autoScrollDirection == 0)
+ {
+ _autoScrollTimer.Stop();
+ return;
+ }
+
+ int oldOffset = _scrollOffset;
+ _scrollOffset += _autoScrollDirection * AutoScrollSpeed;
+ ClampScroll();
+
+ if (_scrollOffset != oldOffset)
+ {
+ UpdateDropIndex(PointToClient(MousePosition).Y);
+ Invalidate();
+ }
+ }
+
+ private void UpdateAutoScroll(int mouseY)
+ {
+ if (_dragIndex < 0 || MaxScroll <= 0)
+ {
+ StopAutoScroll();
+ return;
+ }
+
+ _autoScrollDirection = mouseY < AutoScrollZoneHeight && _scrollOffset > 0 ? -1 :
+ mouseY > Height - AutoScrollZoneHeight && _scrollOffset < MaxScroll ? 1 : 0;
+
+ if (_autoScrollDirection != 0 && !_autoScrollTimer.Enabled)
+ _autoScrollTimer.Start();
+ else if (_autoScrollDirection == 0)
+ _autoScrollTimer.Stop();
+ }
+
+ private void StopAutoScroll()
+ {
+ _autoScrollDirection = 0;
+ _autoScrollTimer.Stop();
+ }
+
+ private void UpdateDropIndex(int mouseY) =>
+ _dropIndex = Math.Max(1, Math.Min(_items.Count, (mouseY + _scrollOffset + ItemHeight / 2) / (ItemHeight + ItemMargin)));
+
+ protected override void OnPaint(PaintEventArgs e)
+ {
+ var g = e.Graphics;
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+ g.Clear(BgColor);
+
+ if (_items.Count == 0)
+ {
+ DrawEmptyState(g);
+ return;
+ }
+
+ // Draw visible items
+ for (int i = 0; i < _items.Count; i++)
+ {
+ var rect = GetItemRect(i);
+ if (rect.Bottom >= 0 && rect.Top <= Height)
+ DrawItem(g, i, rect);
+ }
+
+ // Draw drop indicator and scrollbar
+ if (_dragIndex >= 0 && _dropIndex >= 0 && _dropIndex != _dragIndex)
+ DrawDropIndicator(g);
+ if (ScrollbarVisible)
+ DrawScrollbar(g);
+ }
+
+ private void DrawEmptyState(Graphics g)
+ {
+ using (var brush = new SolidBrush(TextDimColor))
+ using (var font = new Font("Segoe UI", 8.5f, FontStyle.Italic))
+ {
+ var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
+ g.DrawString("Queue is empty", font, brush, ClientRectangle, sf);
+ }
+ }
+
+ private void DrawItem(Graphics g, int index, Rectangle rect)
+ {
+ bool isFirst = index == 0;
+ bool isDragging = index == _dragIndex;
+ bool isHovered = !isDragging && index == _hoveredIndex;
+ Color bg = isDragging ? ItemDragBg : isHovered ? ItemHoverBg : ItemBg;
+
+ // Draw item background
+ using (var path = CreateRoundedRect(rect, ItemRadius))
+ using (var brush = new SolidBrush(bg))
+ g.FillPath(brush, path);
+
+ // Active download (first item) gets gradient accent and border
+ if (isFirst)
+ DrawFirstItemAccent(g, rect);
+ // Dragged items get subtle highlight border
+ else if (isDragging)
+ DrawBorder(g, rect, ItemDragBorder, 0.5f);
+
+ // Draw drag handle, text, and close button
+ if (!isFirst)
+ DrawDragHandle(g, rect);
+ DrawItemText(g, index, rect, isFirst);
+ DrawXButton(g, rect, isHovered && _hoveringX);
+ }
+
+ // Draw gradient accent and border for active download (first item)
+ private void DrawFirstItemAccent(Graphics g, Rectangle rect)
+ {
+ using (var path = CreateRoundedRect(rect, ItemRadius))
+ using (var gradBrush = new LinearGradientBrush(rect,
+ Color.FromArgb(60, AccentColor), Color.FromArgb(0, AccentColor), LinearGradientMode.Horizontal))
+ {
+ var oldClip = g.Clip;
+ g.SetClip(path);
+ g.FillRectangle(gradBrush, rect);
+ g.Clip = oldClip;
+ }
+ DrawBorder(g, rect, AccentColor, 1.5f);
+ }
+
+ private void DrawBorder(Graphics g, Rectangle rect, Color color, float width)
+ {
+ using (var path = CreateRoundedRect(rect, ItemRadius))
+ using (var pen = new Pen(color, width))
+ g.DrawPath(pen, path);
+ }
+
+ private void DrawDragHandle(Graphics g, Rectangle rect)
+ {
+ int cx = rect.X + 8, cy = rect.Y + rect.Height / 2;
+ using (var brush = new SolidBrush(GripColor))
+ {
+ for (int row = -1; row <= 1; row++)
+ for (int col = 0; col < 2; col++)
+ g.FillEllipse(brush, cx + col * 4, cy + row * 4 - 1, 2, 2);
+ }
+ }
+
+ private void DrawItemText(Graphics g, int index, Rectangle rect, bool isFirst)
+ {
+ int textLeft = isFirst ? rect.X + TextPadding : rect.X + DragHandleWidth;
+ int rightPad = ScrollbarVisible ? ScrollbarWidthHover + ScrollbarMargin * 2 : 0;
+ var textRect = new Rectangle(textLeft, rect.Y, rect.Right - XButtonSize - 6 - textLeft - rightPad, rect.Height);
+
+ using (var brush = new SolidBrush(TextColor))
+ using (var font = new Font("Segoe UI", isFirst ? 8.5f : 8f, isFirst ? FontStyle.Bold : FontStyle.Regular))
+ {
+ var sf = new StringFormat
+ {
+ Alignment = StringAlignment.Near,
+ LineAlignment = StringAlignment.Center,
+ Trimming = StringTrimming.EllipsisCharacter,
+ FormatFlags = StringFormatFlags.NoWrap
+ };
+ g.DrawString(_items[index], font, brush, textRect, sf);
+ }
+ }
+
+ private void DrawXButton(Graphics g, Rectangle itemRect, bool hovered)
+ {
+ var xRect = GetXButtonRect(itemRect);
+ using (var path = CreateRoundedRect(xRect, 3))
+ using (var brush = new SolidBrush(hovered ? XButtonHoverBg : XButtonBg))
+ g.FillPath(brush, path);
+
+ using (var pen = new Pen(Color.White, 1.4f) { StartCap = LineCap.Round, EndCap = LineCap.Round })
+ {
+ int p = 4;
+ g.DrawLine(pen, xRect.X + p, xRect.Y + p, xRect.Right - p, xRect.Bottom - p);
+ g.DrawLine(pen, xRect.Right - p, xRect.Y + p, xRect.X + p, xRect.Bottom - p);
+ }
+ }
+
+ private void DrawDropIndicator(Graphics g)
+ {
+ int y = (_dropIndex >= _items.Count ? _items.Count : _dropIndex) * (ItemHeight + ItemMargin) + ItemMargin / 2 - _scrollOffset;
+ int left = ItemMargin + 2;
+ int right = Width - ItemMargin - 2 - (ScrollbarVisible ? ScrollbarWidthHover + ScrollbarMargin : 0);
+
+ using (var pen = new Pen(AccentColor, 2.5f) { StartCap = LineCap.Round, EndCap = LineCap.Round })
+ g.DrawLine(pen, left, y, right, y);
+ }
+
+ // Draw custom scrollbar with hover expansion
+ private void DrawScrollbar(Graphics g)
+ {
+ if (MaxScroll <= 0) return;
+
+ bool expanded = _scrollbarHovered || _scrollbarDragging;
+ int sbWidth = expanded ? ScrollbarWidthHover : ScrollbarWidth;
+ int trackX = Width - ScrollbarWidth - ScrollbarMargin - (expanded ? (ScrollbarWidthHover - ScrollbarWidth) / 2 : 0);
+
+ _scrollTrackRect = new Rectangle(trackX, ScrollbarMargin, sbWidth, Height - ScrollbarMargin * 2);
+
+ using (var trackBrush = new SolidBrush(Color.FromArgb(40, ScrollTrackColor)))
+ using (var trackPath = CreateRoundedRect(_scrollTrackRect, ScrollbarRadius))
+ g.FillPath(trackBrush, trackPath);
+
+ // Calculate thumb position and size
+ int trackHeight = _scrollTrackRect.Height;
+ int thumbHeight = Math.Max(MinThumbHeight, (int)(trackHeight * ((float)Height / ContentHeight)));
+ float scrollRatio = MaxScroll > 0 ? (float)_scrollOffset / MaxScroll : 0;
+ int thumbY = ScrollbarMargin + (int)((trackHeight - thumbHeight) * scrollRatio);
+
+ _scrollThumbRect = new Rectangle(trackX, thumbY, sbWidth, thumbHeight);
+ Color thumbColor = _scrollbarDragging ? ScrollThumbDragColor : _scrollbarHovered ? ScrollThumbHoverColor : ScrollThumbColor;
+
+ using (var thumbBrush = new SolidBrush(thumbColor))
+ using (var thumbPath = CreateRoundedRect(_scrollThumbRect, ScrollbarRadius))
+ g.FillPath(thumbBrush, thumbPath);
+ }
+
+ private Rectangle GetItemRect(int index)
+ {
+ int y = index * (ItemHeight + ItemMargin) + ItemMargin - _scrollOffset;
+ int w = Width - ItemMargin * 2 - (ScrollbarVisible ? ScrollbarWidthHover + ScrollbarMargin + 2 : 0);
+ return new Rectangle(ItemMargin, y, w, ItemHeight);
+ }
+
+ private Rectangle GetXButtonRect(Rectangle itemRect) =>
+ new Rectangle(itemRect.Right - XButtonSize - 3, itemRect.Y + (itemRect.Height - XButtonSize) / 2, XButtonSize, XButtonSize);
+
+ private int HitTest(Point pt)
+ {
+ for (int i = 0; i < _items.Count; i++)
+ if (GetItemRect(i).Contains(pt)) return i;
+ return -1;
+ }
+
+ private bool HitTestScrollbar(Point pt) =>
+ ScrollbarVisible && new Rectangle(_scrollTrackRect.X - 4, _scrollTrackRect.Y, _scrollTrackRect.Width + 8, _scrollTrackRect.Height).Contains(pt);
+
+ protected override void OnMouseMove(MouseEventArgs e)
+ {
+ base.OnMouseMove(e);
+
+ if (_scrollbarDragging)
+ {
+ HandleScrollbarDrag(e.Y);
+ return;
+ }
+
+ // Update scrollbar hover state
+ bool wasHovered = _scrollbarHovered;
+ _scrollbarHovered = HitTestScrollbar(e.Location);
+ if (_scrollbarHovered != wasHovered) Invalidate();
+
+ if (_scrollbarHovered)
+ {
+ Cursor = Cursors.Default;
+ _hoveredIndex = -1;
+ _hoveringX = false;
+ return;
+ }
+
+ // Handle drag operation
+ if (_dragIndex >= 0)
+ {
+ UpdateAutoScroll(e.Y);
+ int newDrop = Math.Max(1, Math.Min(_items.Count, (e.Y + _scrollOffset + ItemHeight / 2) / (ItemHeight + ItemMargin)));
+ if (newDrop != _dropIndex) { _dropIndex = newDrop; Invalidate(); }
+ return;
+ }
+
+ // Update hover state
+ int hit = HitTest(e.Location);
+ bool overX = hit >= 0 && GetXButtonRect(GetItemRect(hit)).Contains(e.Location);
+
+ if (hit != _hoveredIndex || overX != _hoveringX)
+ {
+ _hoveredIndex = hit;
+ _hoveringX = overX;
+ Cursor = overX ? Cursors.Hand : hit > 0 ? Cursors.SizeNS : Cursors.Default;
+ Invalidate();
+ }
+ }
+
+ private void HandleScrollbarDrag(int mouseY)
+ {
+ int trackHeight = Height - ScrollbarMargin * 2;
+ int thumbHeight = Math.Max(MinThumbHeight, (int)(trackHeight * ((float)Height / ContentHeight)));
+ int scrollableHeight = trackHeight - thumbHeight;
+
+ if (scrollableHeight > 0)
+ {
+ float scrollRatio = (float)(mouseY - _scrollDragStartY) / scrollableHeight;
+ _scrollOffset = _scrollDragStartOffset + (int)(scrollRatio * MaxScroll);
+ ClampScroll();
+ Invalidate();
+ }
+ }
+
+ protected override void OnMouseDown(MouseEventArgs e)
+ {
+ base.OnMouseDown(e);
+ if (e.Button != MouseButtons.Left) return;
+
+ // Handle scrollbar thumb drag
+ if (ScrollbarVisible && _scrollThumbRect.Contains(e.Location))
+ {
+ _scrollbarDragging = true;
+ _scrollDragStartY = e.Y;
+ _scrollDragStartOffset = _scrollOffset;
+ Capture = true;
+ return;
+ }
+
+ // Handle scrollbar track click
+ if (ScrollbarVisible && HitTestScrollbar(e.Location))
+ {
+ _scrollOffset += e.Y < _scrollThumbRect.Top ? -Height : Height;
+ ClampScroll();
+ Invalidate();
+ return;
+ }
+
+ int hit = HitTest(e.Location);
+ if (hit < 0) return;
+
+ // Handle close button click
+ if (GetXButtonRect(GetItemRect(hit)).Contains(e.Location))
+ {
+ ItemRemoved?.Invoke(this, hit);
+ return;
+ }
+
+ // Start drag operation (only for non-first items)
+ if (hit > 0)
+ {
+ _dragIndex = _dropIndex = hit;
+ Capture = true;
+ Invalidate();
+ }
+ }
+
+ protected override void OnMouseUp(MouseEventArgs e)
+ {
+ base.OnMouseUp(e);
+ StopAutoScroll();
+
+ if (_scrollbarDragging)
+ {
+ _scrollbarDragging = false;
+ Capture = false;
+ Invalidate();
+ return;
+ }
+
+ // Complete drag reorder operation
+ if (_dragIndex > 0 && _dropIndex > 0 && _dropIndex != _dragIndex)
+ {
+ int from = _dragIndex;
+ int to = Math.Max(1, _dropIndex > _dragIndex ? _dropIndex - 1 : _dropIndex);
+ var item = _items[from];
+ _items.RemoveAt(from);
+ _items.Insert(to, item);
+ ItemReordered?.Invoke(this, new ReorderEventArgs(from, to));
+ }
+
+ _dragIndex = _dropIndex = -1;
+ Capture = false;
+ Cursor = Cursors.Default;
+ Invalidate();
+ }
+
+ protected override void OnMouseLeave(EventArgs e)
+ {
+ base.OnMouseLeave(e);
+ if (_dragIndex < 0 && !_scrollbarDragging)
+ {
+ _hoveredIndex = -1;
+ _hoveringX = _scrollbarHovered = false;
+ Invalidate();
+ }
+ }
+
+ protected override void OnMouseWheel(MouseEventArgs e)
+ {
+ base.OnMouseWheel(e);
+ if (MaxScroll <= 0) return;
+ _scrollOffset -= e.Delta / 4;
+ ClampScroll();
+ Invalidate();
+ }
+
+ protected override void OnResize(EventArgs e)
+ {
+ base.OnResize(e);
+ ClampScroll();
+ Invalidate();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _autoScrollTimer?.Stop();
+ _autoScrollTimer?.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ private static GraphicsPath CreateRoundedRect(Rectangle rect, int radius)
+ {
+ var path = new GraphicsPath();
+ if (radius <= 0 || rect.Width <= 0 || rect.Height <= 0)
+ {
+ path.AddRectangle(rect);
+ return path;
+ }
+ int d = Math.Min(radius * 2, Math.Min(rect.Width, rect.Height));
+ path.AddArc(rect.X, rect.Y, d, d, 180, 90);
+ path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
+ path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90);
+ path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
+ path.CloseFigure();
+ return path;
+ }
+ }
+
+ public class ReorderEventArgs : EventArgs
+ {
+ public int FromIndex { get; }
+ public int ToIndex { get; }
+ public ReorderEventArgs(int from, int to) { FromIndex = from; ToIndex = to; }
+ }
+}
\ No newline at end of file