diff --git a/ADB.cs b/ADB.cs index 9a3f752..3352889 100644 --- a/ADB.cs +++ b/ADB.cs @@ -680,78 +680,6 @@ namespace AndroidSideloader return $"Total space: {string.Format("{0:0.00}", (double)totalSize / 1000)}GB\nUsed space: {string.Format("{0:0.00}", (double)usedSize / 1000)}GB\nFree space: {string.Format("{0:0.00}", (double)freeSize / 1000)}GB"; } - - public static ProcessOutput Sideload(string path, string packagename = "") - { - ProcessOutput ret = new ProcessOutput(); - ret += RunAdbCommandToString($"install -g \"{path}\""); - string out2 = ret.Output + ret.Error; - - if (out2.Contains("failed")) - { - _ = Logger.Log(out2); - - if (out2.Contains("offline") && !settings.NodeviceMode) - { - DialogResult dialogResult2 = FlexibleMessageBox.Show(Program.form, "Device is offline. Press Yes to reconnect, or if you don't wish to connect and just want to download the game (requires unchecking \"Delete games after install\" from settings menu) then press No.", "Device offline.", MessageBoxButtons.YesNoCancel); - } - - if (out2.Contains($"signatures do not match previously") || out2.Contains("INSTALL_FAILED_VERSION_DOWNGRADE") || out2.Contains("signatures do not match") || out2.Contains("failed to install")) - { - ret.Error = string.Empty; - ret.Output = string.Empty; - - bool cancelClicked = false; - - if (!settings.AutoReinstall) - { - Program.form.Invoke((MethodInvoker)(() => - { - DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, "In place upgrade has failed. Rookie can attempt to backup your save data and reinstall the game automatically, however some games do not store their saves in an accessible location (less than 5%). Continue with reinstall?", "In place upgrade failed.", MessageBoxButtons.OKCancel); - if (dialogResult1 == DialogResult.Cancel) - cancelClicked = true; - })); - } - - if (cancelClicked) - return ret; - - Program.form.changeTitle("Performing reinstall, please wait..."); - _ = RunAdbCommandToString("kill-server"); - _ = RunAdbCommandToString("devices"); - _ = RunAdbCommandToString($"pull \"/sdcard/Android/data/{MainForm.CurrPCKG}\" \"{Environment.CurrentDirectory}\""); - Program.form.changeTitle("Uninstalling game..."); - _ = Sideloader.UninstallGame(MainForm.CurrPCKG); - Program.form.changeTitle("Reinstalling game..."); - ret += RunAdbCommandToString($"install -g \"{path}\""); - _ = RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{MainForm.CurrPCKG}\" /sdcard/Android/data/"); - - string directoryToDelete = Path.Combine(Environment.CurrentDirectory, MainForm.CurrPCKG); - if (Directory.Exists(directoryToDelete)) - { - if (directoryToDelete != Environment.CurrentDirectory) - { - FileSystemUtilities.TryDeleteDirectory(directoryToDelete); - } - } - - Program.form.changeTitle(""); - return ret; - } - } - - Program.form.changeTitle(""); - return ret; - } - - public static ProcessOutput CopyOBB(string path) - { - string folder = Path.GetFileName(path); - string lastFolder = Path.GetFileName(path); - return folder.Contains(".") - ? RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{lastFolder}\" && mkdir \"/sdcard/Android/obb/{lastFolder}\"") + RunAdbCommandToString($"push \"{path}\" \"/sdcard/Android/obb\"") - : new ProcessOutput("No OBB Folder found"); - } } internal class EtaEstimator diff --git a/ChangelogHistory.txt b/ChangelogHistory.txt index c017c77..0cfce89 100644 --- a/ChangelogHistory.txt +++ b/ChangelogHistory.txt @@ -1,4 +1,21 @@ -RSL 2.34 +RSL 3.0 + +Major Rookie overhaul with modernized UI, significant performance improvements and upgraded UX. + +- Added high-performance Gallery View with search, filters, sorting, favorites, hover animations, smooth scrolling and uninstall buttons +- Toggle seamlessly between List and Gallery views with your preference remembered across launches +- Complete UI redesign with new dark theme, modernized components and subtle animations throughout +- Refined navigation, layouts, sizing and color consistency across the entire application +- Added uninstall buttons directly in List and Gallery views for quicker app management +- Improved startup performance through overhaul of initialization logic, removal of splash screen, parallelized async loading, batched version retrieval, optimized metadata extraction and game list initialization +- Instant list filtering through caching and streamlined filter logic (INSTALLED / UPDATE AVAILABLE / NEWER THAN LIST) +- Improved search speed and responsiveness +- Fixed and improved trailer handling with faster trailer loading +- Fixed multiple startup issues including connection errors and zombie ADB instances +- Added local blacklist support allowing users to permanently suppress donation prompts for specific apps +- Reduced application size by removal of now unused assets + +RSL 2.34 - Feature: Allow users to favorite games (right click on game) - Fix: Release Notes not showing with trailers enabled diff --git a/GalleryView.cs b/GalleryView.cs index bbe7dd6..424c847 100644 --- a/GalleryView.cs +++ b/GalleryView.cs @@ -21,6 +21,27 @@ public class FastGalleryPanel : Control private readonly int _tileHeight; private readonly int _spacing; + // Grouping + private Dictionary> _groupedByPackage; + private List _displayTiles; + private int _expandedTileIndex = -1; + private float _expandOverlayOpacity = 0f; + private float _targetExpandOverlayOpacity = 0f; + private int _overlayHoveredVersion = -1; + private Rectangle _overlayRect; + private List _versionRects; + private int _overlayScrollOffset = 0; + private int _overlayMaxScroll = 0; + + private class GroupedTile + { + public string PackageName; + public string GameName; + public string BaseGameName; // Common name across all versions + public List Versions; + public ListViewItem Primary => Versions[0]; + } + // Sorting private SortField _currentSortField = SortField.Name; private SortDirection _currentSortDirection = SortDirection.Ascending; @@ -55,11 +76,13 @@ public class FastGalleryPanel : Control // Interaction private int _hoveredIndex = -1; public int _selectedIndex = -1; + private ListViewItem _selectedItem = null; private bool _isHoveringDeleteButton = false; // Context Menu & Favorites private ContextMenuStrip _contextMenu; private int _rightClickedIndex = -1; + private int _rightClickedVersionIndex = -1; private HashSet _favoritesCache; // Rendering @@ -73,6 +96,9 @@ public class FastGalleryPanel : Control private const float SCROLL_SMOOTHING = 0.3f; private const int DELETE_BUTTON_SIZE = 26; private const int DELETE_BUTTON_MARGIN = 6; + private const int VERSION_ROW_HEIGHT = 44; + private const int OVERLAY_PADDING = 10; + private const int OVERLAY_MAX_HEIGHT = 320; // Theme colors private static readonly Color TileBorderHover = Color.FromArgb(93, 203, 173); @@ -86,11 +112,15 @@ public class FastGalleryPanel : Control private static readonly Color SortButtonBg = Color.FromArgb(40, 42, 48); private static readonly Color SortButtonActiveBg = Color.FromArgb(93, 203, 173); private static readonly Color SortButtonHoverBg = Color.FromArgb(55, 58, 65); + private static readonly Color OverlayBgColor = Color.FromArgb(250, 28, 30, 36); + private static readonly Color VersionRowHoverBg = Color.FromArgb(255, 45, 48, 56); + private static readonly Color VersionBadgeColor = Color.FromArgb(200, 93, 203, 173); public event EventHandler TileClicked; public event EventHandler TileDoubleClicked; public event EventHandler TileDeleteClicked; public event EventHandler TileRightClicked; + public event EventHandler TileHovered; // Update release notes for hovered grouped sub-item public event EventHandler SortChanged; [DllImport("dwmapi.dll")] @@ -102,15 +132,13 @@ public class FastGalleryPanel : Control private void ApplyModernScrollbars() { if (_scrollBar == null || !_scrollBar.IsHandleCreated) return; - int dark = 1; int hr = DwmSetWindowAttribute(_scrollBar.Handle, 20, ref dark, sizeof(int)); - if (hr != 0) - DwmSetWindowAttribute(_scrollBar.Handle, 19, ref dark, sizeof(int)); - + if (hr != 0) DwmSetWindowAttribute(_scrollBar.Handle, 19, ref dark, sizeof(int)); if (SetWindowTheme(_scrollBar.Handle, "DarkMode_Explorer", null) != 0) SetWindowTheme(_scrollBar.Handle, "Explorer", null); } + private class TileAnimationState { public float Scale = 1.0f; @@ -127,12 +155,17 @@ public class FastGalleryPanel : Control public float TargetDeleteButtonOpacity = 0f; public float FavoriteOpacity = 0f; public float TargetFavoriteOpacity = 0f; + public float GroupBadgeOpacity = 0f; + public float TargetGroupBadgeOpacity = 0f; } public FastGalleryPanel(List items, int tileWidth, int tileHeight, int spacing, int initialWidth, int initialHeight) { _originalItems = items ?? new List(); _items = new List(_originalItems); + _displayTiles = new List(); + _groupedByPackage = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _versionRects = new List(); _tileWidth = tileWidth; _tileHeight = tileHeight; _spacing = spacing; @@ -156,10 +189,6 @@ public class FastGalleryPanel : Control BackColor = Color.FromArgb(24, 26, 30); - // Initialize animation states - for (int i = 0; i < _items.Count; i++) - _tileStates[i] = new TileAnimationState(); - // Create context menu CreateContextMenu(); @@ -176,7 +205,6 @@ public class FastGalleryPanel : Control _isScrolling = false; Invalidate(); }; - _scrollBar.HandleCreated += (s, e) => ApplyModernScrollbars(); Controls.Add(_scrollBar); @@ -190,6 +218,95 @@ public class FastGalleryPanel : Control RecalculateLayout(); } + private string GetBaseGameName(List versions) + { + if (versions == null || versions.Count == 0) return ""; + + // If only one version, use actual name + if (versions.Count == 1) return versions[0].Text; + + // Strip parentheses (...) from all names - except (MR-Fix), then use the shortest result + var strippedNames = versions.Select(v => + { + string name = v.Text; + bool hasMrFix = name.IndexOf("(MR-Fix)", StringComparison.OrdinalIgnoreCase) >= 0; + name = System.Text.RegularExpressions.Regex.Replace(name, @"\s*\([^)]*\)", "").Trim(); + return hasMrFix ? name + " (MR-Fix)" : name; + }); + + return strippedNames.OrderBy(n => n.Length).First(); + } + + private void BuildGroupedTiles() + { + _groupedByPackage.Clear(); + _displayTiles.Clear(); + + foreach (var item in _items) + { + string packageName = item.SubItems.Count > 2 ? item.SubItems[2].Text : ""; + if (string.IsNullOrEmpty(packageName)) packageName = item.Text; + + if (!_groupedByPackage.ContainsKey(packageName)) + _groupedByPackage[packageName] = new List(); + _groupedByPackage[packageName].Add(item); + } + + foreach (var kvp in _groupedByPackage) + { + kvp.Value.Sort((a, b) => + { + var dateA = ParseDate(a.SubItems.Count > 4 ? a.SubItems[4].Text : ""); + var dateB = ParseDate(b.SubItems.Count > 4 ? b.SubItems[4].Text : ""); + return dateB.CompareTo(dateA); + }); + + _displayTiles.Add(new GroupedTile + { + PackageName = kvp.Key, + GameName = kvp.Value[0].Text, + BaseGameName = GetBaseGameName(kvp.Value), + Versions = kvp.Value + }); + } + + SortDisplayTiles(); + + _tileStates.Clear(); + for (int i = 0; i < _displayTiles.Count; i++) + _tileStates[i] = new TileAnimationState(); + } + + private void SortDisplayTiles() + { + switch (_currentSortField) + { + case SortField.Name: + _displayTiles = _currentSortDirection == SortDirection.Ascending + ? _displayTiles.OrderBy(t => t.BaseGameName, new GameNameComparer()).ToList() + : _displayTiles.OrderByDescending(t => t.BaseGameName, new GameNameComparer()).ToList(); + break; + case SortField.LastUpdated: + _displayTiles = _currentSortDirection == SortDirection.Ascending + ? _displayTiles.OrderBy(t => t.Versions.Max(v => ParseDate(v.SubItems.Count > 4 ? v.SubItems[4].Text : ""))).ToList() + : _displayTiles.OrderByDescending(t => t.Versions.Max(v => ParseDate(v.SubItems.Count > 4 ? v.SubItems[4].Text : ""))).ToList(); + break; + case SortField.Size: + _displayTiles = _currentSortDirection == SortDirection.Ascending + ? _displayTiles.OrderBy(t => t.Versions.Max(v => ParseSize(v.SubItems.Count > 5 ? v.SubItems[5].Text : "0"))).ToList() + : _displayTiles.OrderByDescending(t => t.Versions.Max(v => ParseSize(v.SubItems.Count > 5 ? v.SubItems[5].Text : "0"))).ToList(); + break; + case SortField.Popularity: + if (_currentSortDirection == SortDirection.Ascending) + _displayTiles = _displayTiles.OrderByDescending(t => ParsePopularity(t.Primary.SubItems.Count > 6 ? t.Primary.SubItems[6].Text : "-")) + .ThenBy(t => t.BaseGameName, new GameNameComparer()).ToList(); + else + _displayTiles = _displayTiles.OrderBy(t => ParsePopularity(t.Primary.SubItems.Count > 6 ? t.Primary.SubItems[6].Text : "-")) + .ThenBy(t => t.BaseGameName, new GameNameComparer()).ToList(); + break; + } + } + private Panel CreateSortPanel() { var panel = new Panel @@ -211,7 +328,6 @@ public class FastGalleryPanel : Control panel.Controls.Add(label); int buttonX = 70; - SortField[] fields = { SortField.Name, SortField.LastUpdated, SortField.Size, SortField.Popularity }; string[] texts = { "Name", "Updated", "Size", "Popularity" }; @@ -223,7 +339,6 @@ public class FastGalleryPanel : Control buttonX += btn.Width + 6; } - // Add sort status label to the right of buttons _sortStatusLabel = new Label { Text = GetSortStatusText(), @@ -242,16 +357,11 @@ public class FastGalleryPanel : Control { switch (_currentSortField) { - case SortField.Name: - return _currentSortDirection == SortDirection.Ascending ? "A → Z" : "Z → A"; - case SortField.LastUpdated: - return _currentSortDirection == SortDirection.Ascending ? "Oldest → Newest" : "Newest → Oldest"; - case SortField.Size: - return _currentSortDirection == SortDirection.Ascending ? "Smallest → Largest" : "Largest → Smallest"; - case SortField.Popularity: - return _currentSortDirection == SortDirection.Ascending ? "Least → Most Popular" : "Most → Least Popular"; - default: - return ""; + case SortField.Name: return _currentSortDirection == SortDirection.Ascending ? "A → Z" : "Z → A"; + case SortField.LastUpdated: return _currentSortDirection == SortDirection.Ascending ? "Oldest → Newest" : "Newest → Oldest"; + case SortField.Size: return _currentSortDirection == SortDirection.Ascending ? "Smallest → Largest" : "Largest → Smallest"; + case SortField.Popularity: return _currentSortDirection == SortDirection.Ascending ? "Least → Most Popular" : "Most → Least Popular"; + default: return ""; } } @@ -269,41 +379,30 @@ public class FastGalleryPanel : Control Location = new Point(x, 5), Cursor = Cursors.Hand }; - btn.FlatAppearance.BorderSize = 0; - // Hover colors will be set dynamically in UpdateSortButtonStyles btn.FlatAppearance.MouseOverBackColor = SortButtonHoverBg; btn.FlatAppearance.MouseDownBackColor = SortButtonActiveBg; - btn.Click += (s, e) => OnSortButtonClick(field); return btn; } private string GetSortButtonText(string baseText) { - string arrow = _currentSortDirection == SortDirection.Ascending ? " ▲" : " ▼"; - return baseText + arrow; + return baseText + (_currentSortDirection == SortDirection.Ascending ? " ▲" : " ▼"); } private void OnSortButtonClick(SortField field) { if (_currentSortField == field) - { // Toggle direction - _currentSortDirection = _currentSortDirection == SortDirection.Ascending - ? SortDirection.Descending - : SortDirection.Ascending; - } + _currentSortDirection = _currentSortDirection == SortDirection.Ascending ? SortDirection.Descending : SortDirection.Ascending; else { _currentSortField = field; // Popularity, LastUpdated, Size default to descending (most popular/newest/largest first) // Name defaults to ascending (A-Z) - _currentSortDirection = (field == SortField.Name) - ? SortDirection.Ascending - : SortDirection.Descending; + _currentSortDirection = field == SortField.Name ? SortDirection.Ascending : SortDirection.Descending; } - UpdateSortButtonStyles(); ApplySort(); SortChanged?.Invoke(this, field); @@ -315,83 +414,32 @@ public class FastGalleryPanel : Control { var field = (SortField)btn.Tag; bool isActive = field == _currentSortField; - - string baseText = field == SortField.Name ? "Name" : - field == SortField.LastUpdated ? "Updated" : - field == SortField.Size ? "Size" : "Popularity"; + string baseText = field == SortField.Name ? "Name" : field == SortField.LastUpdated ? "Updated" : field == SortField.Size ? "Size" : "Popularity"; btn.Text = isActive ? GetSortButtonText(baseText) : baseText; + // Set appropriate hover color based on active state btn.BackColor = isActive ? SortButtonActiveBg : SortButtonBg; btn.ForeColor = isActive ? Color.FromArgb(24, 26, 30) : Color.White; - - // Set appropriate hover color based on active state - if (isActive) - { - // Active button: use a slightly lighter teal on hover - btn.FlatAppearance.MouseOverBackColor = Color.FromArgb(110, 215, 190); - btn.FlatAppearance.MouseDownBackColor = Color.FromArgb(80, 180, 155); - } - else - { - // Inactive button: use grey hover - btn.FlatAppearance.MouseOverBackColor = SortButtonHoverBg; - btn.FlatAppearance.MouseDownBackColor = SortButtonActiveBg; - } + btn.FlatAppearance.MouseOverBackColor = isActive ? Color.FromArgb(110, 215, 190) : SortButtonHoverBg; + btn.FlatAppearance.MouseDownBackColor = isActive ? Color.FromArgb(80, 180, 155) : SortButtonActiveBg; } // Update the sort status label - if (_sortStatusLabel != null) - { - _sortStatusLabel.Text = GetSortStatusText(); - } + if (_sortStatusLabel != null) _sortStatusLabel.Text = GetSortStatusText(); } private void ApplySort() { - // Reset to original order first + // Reset original order _items = new List(_originalItems); - // Apply sorting - switch (_currentSortField) - { - case SortField.Name: - // Custom sort to match list sort behaviour: '_' before digits, digits before letters (case-insensitive) - if (_currentSortDirection == SortDirection.Ascending) - _items = _items.OrderBy(i => i.Text, new GameNameComparer()).ToList(); - else - _items = _items.OrderByDescending(i => i.Text, new GameNameComparer()).ToList(); - break; - - case SortField.LastUpdated: - _items = _currentSortDirection == SortDirection.Ascending - ? _items.OrderBy(i => ParseDate(i.SubItems.Count > 4 ? i.SubItems[4].Text : "")).ToList() - : _items.OrderByDescending(i => ParseDate(i.SubItems.Count > 4 ? i.SubItems[4].Text : "")).ToList(); - break; - - case SortField.Size: - _items = _currentSortDirection == SortDirection.Ascending - ? _items.OrderBy(i => ParseSize(i.SubItems.Count > 5 ? i.SubItems[5].Text : "0")).ToList() - : _items.OrderByDescending(i => ParseSize(i.SubItems.Count > 5 ? i.SubItems[5].Text : "0")).ToList(); - break; - - case SortField.Popularity: - if (_currentSortDirection == SortDirection.Ascending) - _items = _items.OrderByDescending(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "-")) - .ThenBy(i => i.Text, new GameNameComparer()).ToList(); - else - _items = _items.OrderBy(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "-")) - .ThenBy(i => i.Text, new GameNameComparer()).ToList(); - break; - } - // Reset selection and hover _hoveredIndex = -1; _selectedIndex = -1; + _selectedItem = null; - // Rebuild animation states - _tileStates.Clear(); - for (int i = 0; i < _items.Count; i++) - _tileStates[i] = new TileAnimationState(); + CloseOverlay(); + BuildGroupedTiles(); // Reset scroll position _scrollY = 0; @@ -401,6 +449,15 @@ public class FastGalleryPanel : Control Invalidate(); } + private void CloseOverlay() + { + _expandedTileIndex = -1; + _targetExpandOverlayOpacity = 0f; + _overlayHoveredVersion = -1; + _overlayScrollOffset = 0; + _rightClickedVersionIndex = -1; + } + public void SetSortState(SortField field, SortDirection direction) { _currentSortField = field; @@ -411,32 +468,11 @@ public class FastGalleryPanel : Control private int ParsePopularity(string popStr) { - if (string.IsNullOrEmpty(popStr)) - return int.MaxValue; // Unranked goes to end - + if (string.IsNullOrEmpty(popStr)) return int.MaxValue; // Unranked goes to end popStr = popStr.Trim(); - - // Handle new format: "#123" or "-" - if (popStr == "-") - { - return int.MaxValue; // Unranked items sort to the end - } - - if (popStr.StartsWith("#")) - { - string numPart = popStr.Substring(1); - if (int.TryParse(numPart, out int rank)) - { - return rank; - } - } - - // Fallback: try parsing as raw number - if (int.TryParse(popStr, out int rawNum)) - { - return rawNum; - } - + if (popStr == "-") return int.MaxValue; // Unranked goes to end + if (popStr.StartsWith("#") && int.TryParse(popStr.Substring(1), out int rank)) return rank; + if (int.TryParse(popStr, out int rawNum)) return rawNum; // Fallback: try parsing as raw number return int.MaxValue; // Unparseable goes to end } @@ -452,23 +488,16 @@ public class FastGalleryPanel : Control int minLen = Math.Min(x.Length, y.Length); for (int i = 0; i < minLen; i++) { - char cx = x[i]; - char cy = y[i]; - - int orderX = GetCharOrder(cx); - int orderY = GetCharOrder(cy); - - if (orderX != orderY) - return orderX.CompareTo(orderY); + int orderX = GetCharOrder(x[i]); + int orderY = GetCharOrder(y[i]); + if (orderX != orderY) return orderX.CompareTo(orderY); // Same category, compare case-insensitively - int cmp = char.ToLowerInvariant(cx).CompareTo(char.ToLowerInvariant(cy)); - if (cmp != 0) - return cmp; + int cmp = char.ToLowerInvariant(x[i]).CompareTo(char.ToLowerInvariant(y[i])); + if (cmp != 0) return cmp; } - // Shorter string comes first - return x.Length.CompareTo(y.Length); + return x.Length.CompareTo(y.Length); // Shorter string comes first } private static int GetCharOrder(char c) @@ -484,53 +513,32 @@ public class FastGalleryPanel : Control private DateTime ParseDate(string dateStr) { if (string.IsNullOrEmpty(dateStr)) return DateTime.MinValue; - string[] formats = { "yyyy-MM-dd HH:mm 'UTC'", "yyyy-MM-dd HH:mm" }; - - return DateTime.TryParseExact( - dateStr, - formats, - System.Globalization.CultureInfo.InvariantCulture, + return DateTime.TryParseExact(dateStr, formats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal, - out DateTime date) - ? date - : DateTime.MinValue; + out DateTime date) ? date : DateTime.MinValue; } private double ParseSize(string sizeStr) { - if (string.IsNullOrEmpty(sizeStr)) - return 0; - - // Remove whitespace - sizeStr = sizeStr.Trim(); + if (string.IsNullOrEmpty(sizeStr)) return 0; + sizeStr = sizeStr.Trim(); // Remove whitespace // Handle new format: "1.23 GB" or "123 MB" if (sizeStr.EndsWith(" GB", StringComparison.OrdinalIgnoreCase)) { - string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim(); - if (double.TryParse(numPart, System.Globalization.NumberStyles.Any, - System.Globalization.CultureInfo.InvariantCulture, out double gb)) - { - return gb * 1024.0; // Convert GB to MB for consistent sorting - } + if (double.TryParse(sizeStr.Substring(0, sizeStr.Length - 3).Trim(), System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out double gb)) return gb * 1024.0; // Convert GB to MB for consistent sorting } else if (sizeStr.EndsWith(" MB", StringComparison.OrdinalIgnoreCase)) { - string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim(); - if (double.TryParse(numPart, System.Globalization.NumberStyles.Any, - System.Globalization.CultureInfo.InvariantCulture, out double mb)) - { - return mb; - } + if (double.TryParse(sizeStr.Substring(0, sizeStr.Length - 3).Trim(), System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out double mb)) return mb; } // Fallback: try parsing as raw number if (double.TryParse(sizeStr, System.Globalization.NumberStyles.Any, - System.Globalization.CultureInfo.InvariantCulture, out double rawMb)) - { - return rawMb; - } + System.Globalization.CultureInfo.InvariantCulture, out double raw)) return raw; return 0; } @@ -538,19 +546,16 @@ public class FastGalleryPanel : Control public void UpdateItems(List newItems) { if (newItems == null) newItems = new List(); - _originalItems = new List(newItems); _items = new List(newItems); // Reset selection and hover states _hoveredIndex = -1; _selectedIndex = -1; + _selectedItem = null; _isHoveringDeleteButton = false; - // Rebuild animation states for new item count - _tileStates.Clear(); - for (int i = 0; i < _items.Count; i++) - _tileStates[i] = new TileAnimationState(); + CloseOverlay(); // Reset scroll position for new results _scrollY = 0; @@ -564,24 +569,28 @@ public class FastGalleryPanel : Control public ListViewItem GetItemAtIndex(int index) { - if (index >= 0 && index < _items.Count) - return _items[index]; + if (_selectedItem != null) return _selectedItem; + if (index >= 0 && index < _displayTiles.Count) + return _displayTiles[index].Primary; return null; } private bool IsItemInstalled(ListViewItem item) { if (item == null) return false; - return item.ForeColor.ToArgb() == MainForm.ColorInstalled.ToArgb() || item.ForeColor.ToArgb() == MainForm.ColorUpdateAvailable.ToArgb() || item.ForeColor.ToArgb() == MainForm.ColorDonateGame.ToArgb(); } + private bool IsAnyVersionInstalled(GroupedTile tile) + { + return tile.Versions.Any(v => IsItemInstalled(v)); + } + private Rectangle GetDeleteButtonRect(int index, int row, int col, int scrollY) { - if (!_tileStates.TryGetValue(index, out var state)) - state = new TileAnimationState(); + if (!_tileStates.TryGetValue(index, out var state)) state = new TileAnimationState(); int baseX = _leftPadding + col * (_tileWidth + _spacing); int baseY = _spacing + SORT_PANEL_HEIGHT + row * (_tileHeight + _spacing) - scrollY; @@ -592,14 +601,9 @@ public class FastGalleryPanel : Control int x = baseX - (scaledW - _tileWidth) / 2; int y = baseY - (scaledH - _tileHeight) / 2; - // Calculate thumbnail area - int thumbPadding = 2; - int thumbHeight = scaledH - (thumbPadding * 2); - // Position delete button in bottom-right corner of thumbnail - int btnX = x + scaledW - DELETE_BUTTON_SIZE - thumbPadding - DELETE_BUTTON_MARGIN; - int btnY = y + thumbPadding + thumbHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN - 20; - + int btnX = x + scaledW - DELETE_BUTTON_SIZE - 2 - DELETE_BUTTON_MARGIN; + int btnY = y + 2 + scaledH - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN - 20; return new Rectangle(btnX, btnY, DELETE_BUTTON_SIZE, DELETE_BUTTON_SIZE); } @@ -619,10 +623,33 @@ public class FastGalleryPanel : Control _scrollBar.Value = Math.Max(_scrollBar.Minimum, Math.Min(_scrollBar.Maximum - _scrollBar.LargeChange + 1, (int)_scrollY)); needsRedraw = true; } - else + else { _scrollY = _targetScrollY; _isScrolling = false; } + } + + if (Math.Abs(_expandOverlayOpacity - _targetExpandOverlayOpacity) > 0.01f) + { + _expandOverlayOpacity += (_targetExpandOverlayOpacity - _expandOverlayOpacity) * 0.4f; + needsRedraw = true; + } + else _expandOverlayOpacity = _targetExpandOverlayOpacity; + + // Update overlay hover state based on current mouse position + if (_expandedTileIndex >= 0 && _expandOverlayOpacity > 0.5f && _versionRects.Count > 0) + { + var mousePos = PointToClient(Cursor.Position); + int newHover = GetOverlayVersionAtPoint(mousePos.X, mousePos.Y); + if (newHover != _overlayHoveredVersion) { - _scrollY = _targetScrollY; - _isScrolling = false; + _overlayHoveredVersion = newHover; + needsRedraw = true; + + // Update release notes when hovering over a version + if (newHover >= 0 && newHover < _displayTiles[_expandedTileIndex].Versions.Count) + { + var hoveredVersion = _displayTiles[_expandedTileIndex].Versions[newHover]; + string releaseName = hoveredVersion.SubItems.Count > 1 ? hoveredVersion.SubItems[1].Text : ""; + TileHovered?.Invoke(this, releaseName); + } } } @@ -636,7 +663,7 @@ public class FastGalleryPanel : Control for (int col = 0; col < _columns; col++) { int index = row * _columns + col; - if (index >= _items.Count) break; + if (index >= _displayTiles.Count) break; if (!_tileStates.TryGetValue(index, out var state)) { @@ -644,71 +671,47 @@ public class FastGalleryPanel : Control _tileStates[index] = state; } - bool isHovered = index == _hoveredIndex; + var tile = _displayTiles[index]; + bool isHovered = index == _hoveredIndex && _expandedTileIndex < 0; bool isSelected = index == _selectedIndex; - bool isInstalled = IsItemInstalled(_items[index]); - string pkgName = _items[index].SubItems.Count > 2 ? _items[index].SubItems[1].Text : ""; + bool isInstalled = IsAnyVersionInstalled(tile); + string pkgName = tile.Primary.SubItems.Count > 1 ? tile.Primary.SubItems[1].Text : ""; bool isFavorite = _favoritesCache.Contains(pkgName); - state.TargetFavoriteOpacity = isFavorite ? 1.0f : 0f; + state.TargetFavoriteOpacity = isFavorite ? 1.0f : 0f; state.TargetScale = isHovered ? HOVER_SCALE : 1.0f; state.TargetBorderOpacity = isHovered ? 1.0f : 0f; state.TargetBackgroundBrightness = isHovered ? 45f : (isSelected ? 38f : 30f); state.TargetSelectionOpacity = isSelected ? 1.0f : 0f; state.TargetTooltipOpacity = isHovered ? 1.0f : 0f; state.TargetDeleteButtonOpacity = (isHovered && isInstalled) ? 1.0f : 0f; + state.TargetGroupBadgeOpacity = tile.Versions.Count > 1 ? 1.0f : 0f; - if (Math.Abs(state.Scale - state.TargetScale) > 0.001f) - { - state.Scale += (state.TargetScale - state.Scale) * ANIMATION_SPEED; - needsRedraw = true; - } - else state.Scale = state.TargetScale; - - if (Math.Abs(state.BorderOpacity - state.TargetBorderOpacity) > 0.01f) - { - state.BorderOpacity += (state.TargetBorderOpacity - state.BorderOpacity) * ANIMATION_SPEED; - needsRedraw = true; - } - else state.BorderOpacity = state.TargetBorderOpacity; - - if (Math.Abs(state.BackgroundBrightness - state.TargetBackgroundBrightness) > 0.5f) - { - state.BackgroundBrightness += (state.TargetBackgroundBrightness - state.BackgroundBrightness) * ANIMATION_SPEED; - needsRedraw = true; - } - else state.BackgroundBrightness = state.TargetBackgroundBrightness; - - if (Math.Abs(state.SelectionOpacity - state.TargetSelectionOpacity) > 0.01f) - { - state.SelectionOpacity += (state.TargetSelectionOpacity - state.SelectionOpacity) * ANIMATION_SPEED; - needsRedraw = true; - } - else state.SelectionOpacity = state.TargetSelectionOpacity; - - if (Math.Abs(state.TooltipOpacity - state.TargetTooltipOpacity) > 0.01f) - { - state.TooltipOpacity += (state.TargetTooltipOpacity - state.TooltipOpacity) * 0.35f; - needsRedraw = true; - } - else state.TooltipOpacity = state.TargetTooltipOpacity; - - if (Math.Abs(state.DeleteButtonOpacity - state.TargetDeleteButtonOpacity) > 0.01f) - { - state.DeleteButtonOpacity += (state.TargetDeleteButtonOpacity - state.DeleteButtonOpacity) * 0.35f; - needsRedraw = true; - } - else state.DeleteButtonOpacity = state.TargetDeleteButtonOpacity; - - if (Math.Abs(state.FavoriteOpacity - state.TargetFavoriteOpacity) > 0.01f) - { state.FavoriteOpacity += (state.TargetFavoriteOpacity - state.FavoriteOpacity) * 0.35f; needsRedraw = true; } - else state.FavoriteOpacity = state.TargetFavoriteOpacity; + needsRedraw |= AnimateValue(ref state.Scale, state.TargetScale, ANIMATION_SPEED, 0.001f); + needsRedraw |= AnimateValue(ref state.BorderOpacity, state.TargetBorderOpacity, ANIMATION_SPEED, 0.01f); + needsRedraw |= AnimateValue(ref state.BackgroundBrightness, state.TargetBackgroundBrightness, ANIMATION_SPEED, 0.5f); + needsRedraw |= AnimateValue(ref state.SelectionOpacity, state.TargetSelectionOpacity, ANIMATION_SPEED, 0.01f); + needsRedraw |= AnimateValue(ref state.TooltipOpacity, state.TargetTooltipOpacity, 0.35f, 0.01f); + needsRedraw |= AnimateValue(ref state.DeleteButtonOpacity, state.TargetDeleteButtonOpacity, 0.35f, 0.01f); + needsRedraw |= AnimateValue(ref state.FavoriteOpacity, state.TargetFavoriteOpacity, 0.35f, 0.01f); + needsRedraw |= AnimateValue(ref state.GroupBadgeOpacity, state.TargetGroupBadgeOpacity, 0.35f, 0.01f); } } if (needsRedraw) Invalidate(); } + private bool AnimateValue(ref float current, float target, float speed, float threshold) + { + if (Math.Abs(current - target) > threshold) + { + current += (target - current) * speed; + return true; + } + current = target; + return false; + } + protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) { if (height <= 0 && Height > 0) height = Height; @@ -719,31 +722,25 @@ public class FastGalleryPanel : Control protected override void OnResize(EventArgs e) { base.OnResize(e); - if (Width > 0 && Height > 0 && _scrollBar != null) - { - RecalculateLayout(); - Refresh(); - } + if (Width > 0 && Height > 0 && _scrollBar != null) { RecalculateLayout(); Refresh(); } } protected override void OnParentChanged(EventArgs e) { base.OnParentChanged(e); - if (Parent != null && !IsDisposed && !Disposing) - RecalculateLayout(); + if (Parent != null && !IsDisposed && !Disposing) RecalculateLayout(); } private void RecalculateLayout() { - if (IsDisposed || Disposing || _scrollBar == null || Width <= 0 || Height <= 0) - return; + if (IsDisposed || Disposing || _scrollBar == null || Width <= 0 || Height <= 0) return; int availableHeight = Height - SORT_PANEL_HEIGHT; _scrollBar.SetBounds(Width - _scrollBar.Width, SORT_PANEL_HEIGHT, _scrollBar.Width, availableHeight); int availableWidth = Width - _scrollBar.Width - _spacing * 2; _columns = Math.Max(1, (availableWidth + _spacing) / (_tileWidth + _spacing)); - _rows = (int)Math.Ceiling((double)_items.Count / _columns); + _rows = (int)Math.Ceiling((double)_displayTiles.Count / _columns); _contentHeight = _rows * (_tileHeight + _spacing) + _spacing + 20; int usedWidth = _columns * (_tileWidth + _spacing) - _spacing; @@ -775,16 +772,8 @@ public class FastGalleryPanel : Control // Fill sort panel area g.FillRectangle(new SolidBrush(Color.FromArgb(28, 30, 34)), 0, 0, Width, SORT_PANEL_HEIGHT); - if (_isScrolling) - { - g.SmoothingMode = SmoothingMode.HighSpeed; - g.InterpolationMode = InterpolationMode.Low; - } - else - { - g.SmoothingMode = SmoothingMode.AntiAlias; - g.InterpolationMode = InterpolationMode.Bilinear; - } + g.SmoothingMode = _isScrolling ? SmoothingMode.HighSpeed : SmoothingMode.AntiAlias; + g.InterpolationMode = _isScrolling ? InterpolationMode.Low : InterpolationMode.Bilinear; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; // Clip to tile area (below sort panel) @@ -800,14 +789,14 @@ public class FastGalleryPanel : Control for (int col = 0; col < _columns; col++) { int index = row * _columns + col; - if (index >= _items.Count) break; + if (index >= _displayTiles.Count) break; if (index != _hoveredIndex && index != _selectedIndex) DrawTile(g, index, row, col, scrollYInt); } } // Draw selected tile - if (_selectedIndex >= 0 && _selectedIndex < _items.Count && _selectedIndex != _hoveredIndex) + if (_selectedIndex >= 0 && _selectedIndex < _displayTiles.Count && _selectedIndex != _hoveredIndex) { int selectedRow = _selectedIndex / _columns; int selectedCol = _selectedIndex % _columns; @@ -816,7 +805,7 @@ public class FastGalleryPanel : Control } // Draw hovered tile last (on top) - if (_hoveredIndex >= 0 && _hoveredIndex < _items.Count) + if (_hoveredIndex >= 0 && _hoveredIndex < _displayTiles.Count) { int hoveredRow = _hoveredIndex / _columns; int hoveredCol = _hoveredIndex % _columns; @@ -824,11 +813,283 @@ public class FastGalleryPanel : Control DrawTile(g, _hoveredIndex, hoveredRow, hoveredCol, scrollYInt); } + if (_expandedTileIndex >= 0 && _expandOverlayOpacity > 0.01f) + DrawGroupOverlay(g, scrollYInt); + g.ResetClip(); } e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0); } + private ulong ParseCloudVersionCode(ListViewItem item) + { + if (item == null || item.SubItems.Count <= 3) return 0; + string versionText = item.SubItems[3].Text; + + int startIdx = versionText.IndexOf('v'); + if (startIdx < 0) return 0; + + int endIdx = versionText.IndexOf(' ', startIdx); + string cloudPart = endIdx > 0 + ? versionText.Substring(startIdx + 1, endIdx - startIdx - 1) + : versionText.Substring(startIdx + 1); + + ulong.TryParse(StringUtilities.KeepOnlyNumbers(cloudPart), out ulong result); + return result; + } + + private ulong ParseInstalledVersionCode(ListViewItem item) + { + if (item == null || item.SubItems.Count <= 3) return 0; + string versionText = item.SubItems[3].Text; + + int dividerIdx = versionText.IndexOf(" / v"); + if (dividerIdx < 0) return 0; + + string installedPart = versionText.Substring(dividerIdx + 4); + ulong.TryParse(StringUtilities.KeepOnlyNumbers(installedPart), out ulong result); + return result; + } + + private void DrawGroupOverlay(Graphics g, int scrollY) + { + if (_expandedTileIndex < 0 || _expandedTileIndex >= _displayTiles.Count) return; + + var tile = _displayTiles[_expandedTileIndex]; + if (tile.Versions.Count <= 1) return; + + // Find installed version code from any version of this package + ulong installedVersionCode = 0; + foreach (var v in tile.Versions) + { + ulong installed = ParseInstalledVersionCode(v); + if (installed > 0) { installedVersionCode = installed; break; } + } + + int row = _expandedTileIndex / _columns; + int col = _expandedTileIndex % _columns; + int baseX = _leftPadding + col * (_tileWidth + _spacing); + int baseY = _spacing + SORT_PANEL_HEIGHT + row * (_tileHeight + _spacing) - scrollY; + int tileCenterX = baseX + _tileWidth / 2; + int tileCenterY = baseY + _tileHeight / 2; + + int overlayWidth = Math.Max(300, _tileWidth + 60); + int headerHeight = 28; + int contentHeight = tile.Versions.Count * VERSION_ROW_HEIGHT; + + int maxContentArea = OVERLAY_MAX_HEIGHT - headerHeight - OVERLAY_PADDING; + bool needsScroll = contentHeight > maxContentArea; + int visibleContentHeight = needsScroll ? maxContentArea : contentHeight; + int overlayHeight = headerHeight + visibleContentHeight + OVERLAY_PADDING; + + _overlayMaxScroll = needsScroll ? contentHeight - visibleContentHeight : 0; + _overlayScrollOffset = Math.Max(0, Math.Min(_overlayScrollOffset, _overlayMaxScroll)); + + int overlayX = tileCenterX - overlayWidth / 2; + int overlayY = tileCenterY - overlayHeight / 2; + + overlayX = Math.Max(10, Math.Min(overlayX, Width - overlayWidth - _scrollBar.Width - 10)); + overlayY = Math.Max(SORT_PANEL_HEIGHT + 10, Math.Min(overlayY, Height - overlayHeight - 10)); + + _overlayRect = new Rectangle(overlayX, overlayY, overlayWidth, overlayHeight); + + int alpha = (int)(255 * _expandOverlayOpacity); + + // Dim background + using (var dimBrush = new SolidBrush(Color.FromArgb((int)(120 * _expandOverlayOpacity), 0, 0, 0))) + g.FillRectangle(dimBrush, 0, SORT_PANEL_HEIGHT, Width, Height - SORT_PANEL_HEIGHT); + + // Shadow + using (var shadowPath = CreateRoundedRectangle(new Rectangle(overlayX + 4, overlayY + 4, overlayWidth, overlayHeight), 12)) + using (var shadowBrush = new SolidBrush(Color.FromArgb((int)(80 * _expandOverlayOpacity), 0, 0, 0))) + g.FillPath(shadowBrush, shadowPath); + + // Main overlay + using (var overlayPath = CreateRoundedRectangle(_overlayRect, 12)) + using (var bgBrush = new SolidBrush(Color.FromArgb(alpha, OverlayBgColor.R, OverlayBgColor.G, OverlayBgColor.B))) + using (var borderPen = new Pen(Color.FromArgb(alpha, TileBorderHover), 2f)) + { + g.FillPath(bgBrush, overlayPath); + g.DrawPath(borderPen, overlayPath); + } + + // Header + using (var headerFont = new Font("Segoe UI", 9f)) + using (var headerBrush = new SolidBrush(Color.FromArgb((int)(alpha * 0.7), 200, 200, 200))) + { + g.DrawString($"Select a version ({tile.Versions.Count} available)", headerFont, headerBrush, + overlayX + OVERLAY_PADDING, overlayY + OVERLAY_PADDING); + } + + // Clip for scrollable content + var contentRect = new Rectangle(overlayX, overlayY + headerHeight, overlayWidth, visibleContentHeight); + var oldClip = g.Clip; + g.SetClip(contentRect, CombineMode.Intersect); + + // Version rows + _versionRects.Clear(); + int yOffset = overlayY + headerHeight - _overlayScrollOffset; + + using (var nameFont = new Font("Segoe UI Semibold", 9f)) + using (var nameFontBold = new Font("Segoe UI", 9f, FontStyle.Bold)) + using (var detailFont = new Font("Segoe UI", 8.5f)) + using (var detailFontBold = new Font("Segoe UI", 8.5f, FontStyle.Bold)) + { + for (int i = 0; i < tile.Versions.Count; i++) + { + var version = tile.Versions[i]; + int rowWidth = needsScroll ? overlayWidth - 20 : overlayWidth - 12; + var rowRect = new Rectangle(overlayX + 6, yOffset, rowWidth, VERSION_ROW_HEIGHT - 4); + + var clickRect = new Rectangle(rowRect.X, Math.Max(rowRect.Y, contentRect.Y), + rowRect.Width, Math.Min(rowRect.Bottom, contentRect.Bottom) - Math.Max(rowRect.Y, contentRect.Y)); + _versionRects.Add(clickRect.Height > 0 ? clickRect : Rectangle.Empty); + + bool isHovered = i == _overlayHoveredVersion; + + // Determine version status by comparing version codes + ulong thisVersionCode = ParseCloudVersionCode(version); + bool isExactInstalled = installedVersionCode > 0 && thisVersionCode == installedVersionCode; + bool isNewerThanInstalled = installedVersionCode > 0 && thisVersionCode > installedVersionCode; + bool isOlderThanInstalled = installedVersionCode > 0 && thisVersionCode < installedVersionCode; + + if (isHovered && rowRect.IntersectsWith(contentRect)) + { + using (var hoverPath = CreateRoundedRectangle(rowRect, 6)) + using (var hoverBrush = new SolidBrush(Color.FromArgb(alpha, VersionRowHoverBg))) + g.FillPath(hoverBrush, hoverPath); + } + + int textX = rowRect.X + 8; + int textY = rowRect.Y + 4; + + // Check if this version is a favorite + string versionPkgName = version.SubItems.Count > 1 ? version.SubItems[1].Text : ""; + bool isFavorite = !string.IsNullOrEmpty(versionPkgName) && _favoritesCache.Contains(versionPkgName); + + // Draw favorite star badge on the left + if (isFavorite) + { + int starSize = 8; + int starX = textX - 5 + starSize / 2; + int starY = rowRect.Y + rowRect.Height / 2; + + using (var starPath = CreateStarPath(starX, starY, starSize / 2, starSize / 4, 5)) + using (var starBrush = new SolidBrush(Color.FromArgb(alpha, TileBorderFavorite))) + g.FillPath(starBrush, starPath); + + textX += 6; // Shift text right to accommodate star + } + + // Determine badge width for name rect + int badgeWidth = isExactInstalled ? 68 : (isNewerThanInstalled ? 58 : (isOlderThanInstalled ? 52 : 8)); + + // Game name - bold font when hovered + using (var nameBrush = new SolidBrush(Color.FromArgb(alpha, 255, 255, 255))) + { + int extraPadding = isFavorite ? 3 : 0; + var nameRect = new Rectangle(textX, textY, rowRect.Width - badgeWidth - 8 - extraPadding, 18); + var sf = new StringFormat { Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }; + g.DrawString(version.Text, isHovered ? nameFontBold : nameFont, nameBrush, nameRect, sf); + } + + // Size, date, version - bold font when hovered + string size = version.SubItems.Count > 5 ? version.SubItems[5].Text : ""; + string date = FormatLastUpdated(version.SubItems.Count > 4 ? version.SubItems[4].Text : ""); + ulong versionNum = ParseCloudVersionCode(version); + string versionCode = versionNum > 0 ? "v" + versionNum : ""; + string details = string.Join(" • ", new[] { size, date, versionCode }.Where(s => !string.IsNullOrEmpty(s))); + + Color detailColor = Color.FromArgb(180, 180, 180); + using (var detailBrush = new SolidBrush(Color.FromArgb((int)(alpha * (isHovered ? 1.0 : 0.6)), detailColor))) + g.DrawString(details, isHovered ? detailFontBold : detailFont, detailBrush, textX, textY + 18); + + // Status badge + if (isExactInstalled) + { + var badgeRect = new Rectangle(rowRect.Right - 68, rowRect.Y + (rowRect.Height - 18) / 2, 60, 18); + using (var badgePath = CreateRoundedRectangle(badgeRect, 4)) + using (var badgeBrush = new SolidBrush(Color.FromArgb(alpha, BadgeInstalledBg))) + { + g.FillPath(badgeBrush, badgePath); + var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; + using (var textBrush = new SolidBrush(Color.FromArgb(alpha, 255, 255, 255))) + g.DrawString("INSTALLED", new Font("Segoe UI", 6.5f, FontStyle.Bold), textBrush, badgeRect, sf); + } + } + else if (isNewerThanInstalled) + { + var badgeRect = new Rectangle(rowRect.Right - 58, rowRect.Y + (rowRect.Height - 18) / 2, 50, 18); + using (var badgePath = CreateRoundedRectangle(badgeRect, 4)) + using (var badgeBrush = new SolidBrush(Color.FromArgb(alpha, MainForm.ColorUpdateAvailable))) + { + g.FillPath(badgeBrush, badgePath); + var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; + using (var textBrush = new SolidBrush(Color.FromArgb(alpha, 255, 255, 255))) + g.DrawString("NEWER", new Font("Segoe UI", 6.5f, FontStyle.Bold), textBrush, badgeRect, sf); + } + } + else if (isOlderThanInstalled) + { + var badgeRect = new Rectangle(rowRect.Right - 52, rowRect.Y + (rowRect.Height - 18) / 2, 44, 18); + using (var badgePath = CreateRoundedRectangle(badgeRect, 4)) + using (var badgeBrush = new SolidBrush(Color.FromArgb((int)(alpha * 0.5), 100, 100, 100))) + { + g.FillPath(badgeBrush, badgePath); + var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; + using (var textBrush = new SolidBrush(Color.FromArgb((int)(alpha * 0.7), 180, 180, 180))) + g.DrawString("OLDER", new Font("Segoe UI", 6.5f, FontStyle.Bold), textBrush, badgeRect, sf); + } + } + + yOffset += VERSION_ROW_HEIGHT; + } + } + + g.Clip = oldClip; + + // Scroll indicator + if (needsScroll) + { + int scrollBarX = overlayX + overlayWidth - 10; + int scrollTrackY = overlayY + headerHeight + 4; + int scrollTrackHeight = visibleContentHeight - 8; + + using (var trackBrush = new SolidBrush(Color.FromArgb((int)(40 * _expandOverlayOpacity), 255, 255, 255))) + g.FillRectangle(trackBrush, scrollBarX, scrollTrackY, 4, scrollTrackHeight); + + float scrollRatio = (float)_overlayScrollOffset / _overlayMaxScroll; + float thumbRatio = (float)visibleContentHeight / contentHeight; + int thumbHeight = Math.Max(20, (int)(scrollTrackHeight * thumbRatio)); + int thumbY = scrollTrackY + (int)((scrollTrackHeight - thumbHeight) * scrollRatio); + + using (var thumbPath = CreateRoundedRectangle(new Rectangle(scrollBarX, thumbY, 4, thumbHeight), 2)) + using (var thumbBrush = new SolidBrush(Color.FromArgb((int)(120 * _expandOverlayOpacity), TileBorderHover))) + g.FillPath(thumbBrush, thumbPath); + } + } + + private GraphicsPath CreateStarPath(float cx, float cy, float outerRadius, float innerRadius, int points) + { + var path = new GraphicsPath(); + var starPoints = new PointF[points * 2]; + double angleStep = Math.PI / points; + double startAngle = -Math.PI / 2; // Start from top + + for (int i = 0; i < points * 2; i++) + { + double angle = startAngle + i * angleStep; + float radius = (i % 2 == 0) ? outerRadius : innerRadius; + starPoints[i] = new PointF( + cx + (float)(radius * Math.Cos(angle)), + cy + (float)(radius * Math.Sin(angle)) + ); + } + + path.AddPolygon(starPoints); + return path; + } + private GraphicsPath CreateRoundedRectangle(Rectangle rect, int radius) { var path = new GraphicsPath(); @@ -843,9 +1104,10 @@ public class FastGalleryPanel : Control private void DrawTile(Graphics g, int index, int row, int col, int scrollY) { - var item = _items[index]; + var tile = _displayTiles[index]; + var item = tile.Primary; var state = _tileStates.ContainsKey(index) ? _tileStates[index] : new TileAnimationState(); - bool isHovered = index == _hoveredIndex; + bool isHovered = index == _hoveredIndex && _expandedTileIndex < 0; int baseX = _leftPadding + col * (_tileWidth + _spacing); int baseY = _spacing + SORT_PANEL_HEIGHT + row * (_tileHeight + _spacing) - scrollY; @@ -857,9 +1119,7 @@ public class FastGalleryPanel : Control int y = baseY - (scaledH - _tileHeight) / 2; var tileRect = new Rectangle(x, y, scaledW, scaledH); - - string packageName = item.SubItems.Count > 2 ? item.SubItems[2].Text : ""; - var thumbnail = GetCachedImage(packageName); + var thumbnail = GetCachedImage(tile.PackageName); using (var tilePath = CreateRoundedRectangle(tileRect, THUMB_CORNER_RADIUS)) { @@ -868,83 +1128,61 @@ public class FastGalleryPanel : Control if (thumbnail != null) { - InterpolationMode previousMode = g.InterpolationMode; - if (isHovered) - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - + if (isHovered) g.InterpolationMode = InterpolationMode.HighQualityBicubic; float imgRatio = (float)thumbnail.Width / thumbnail.Height; float rectRatio = (float)tileRect.Width / tileRect.Height; Rectangle drawRect = imgRatio > rectRatio ? new Rectangle(x - ((int)(scaledH * imgRatio) - scaledW) / 2, y, (int)(scaledH * imgRatio), scaledH) : new Rectangle(x, y - ((int)(scaledW / imgRatio) - scaledH) / 2, scaledW, (int)(scaledW / imgRatio)); g.DrawImage(thumbnail, drawRect); - - if (isHovered) - g.InterpolationMode = previousMode; + if (isHovered) g.InterpolationMode = InterpolationMode.Bilinear; } else { using (var brush = new SolidBrush(Color.FromArgb(35, 35, 40))) g.FillPath(brush, tilePath); - - var nameRect = new Rectangle(x + 10, y, scaledW - 20, scaledH); using (var font = new Font("Segoe UI", 10f, FontStyle.Bold)) + using (var text = new SolidBrush(Color.FromArgb(110, 110, 120))) { - var sfName = new StringFormat - { - Alignment = StringAlignment.Center, - LineAlignment = StringAlignment.Center, - Trimming = StringTrimming.EllipsisCharacter - }; - using (var text = new SolidBrush(Color.FromArgb(110, 110, 120))) - g.DrawString(item.Text, font, text, nameRect, sfName); + var sfName = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter }; + g.DrawString(tile.BaseGameName, font, text, new Rectangle(x + 10, y, scaledW - 20, scaledH), sfName); } } - g.Clip = oldClip; } - // Status badges (left side) + // Left-side badges int badgeY = y + 4; + if (state.FavoriteOpacity > 0.5f) { DrawBadge(g, "★", x + 4, badgeY, BadgeFavoriteBg); badgeY += 18; } - if (state.FavoriteOpacity > 0.5f) - { - DrawBadge(g, "★", x + 4, badgeY, BadgeFavoriteBg); - badgeY += 18; - } + bool hasUpdate = tile.Versions.Any(v => v.ForeColor.ToArgb() == MainForm.ColorUpdateAvailable.ToArgb()); + bool installed = IsAnyVersionInstalled(tile); + bool canDonate = tile.Versions.Any(v => v.ForeColor.ToArgb() == MainForm.ColorDonateGame.ToArgb()); - bool hasUpdate = item.ForeColor.ToArgb() == MainForm.ColorUpdateAvailable.ToArgb(); - bool installed = item.ForeColor.ToArgb() == MainForm.ColorInstalled.ToArgb(); - bool canDonate = item.ForeColor.ToArgb() == MainForm.ColorDonateGame.ToArgb(); - - if (hasUpdate) - { - DrawBadge(g, "UPDATE AVAILABLE", x + 4, badgeY, Color.FromArgb(180, MainForm.ColorUpdateAvailable.R, MainForm.ColorUpdateAvailable.G, MainForm.ColorUpdateAvailable.B)); - badgeY += 18; - } - - if (canDonate) - { - DrawBadge(g, "NEWER THAN LIST", x + 4, badgeY, Color.FromArgb(180, MainForm.ColorDonateGame.R, MainForm.ColorDonateGame.G, MainForm.ColorDonateGame.B)); - badgeY += 18; - } - - if (installed || hasUpdate || canDonate) - DrawBadge(g, "INSTALLED", x + 4, badgeY, BadgeInstalledBg); + if (hasUpdate) { DrawBadge(g, "UPDATE AVAILABLE", x + 4, badgeY, Color.FromArgb(180, MainForm.ColorUpdateAvailable)); badgeY += 18; } + if (canDonate) { DrawBadge(g, "NEWER THAN LIST", x + 4, badgeY, Color.FromArgb(180, MainForm.ColorDonateGame)); badgeY += 18; } + if (installed) DrawBadge(g, "INSTALLED", x + 4, badgeY, BadgeInstalledBg); // Right-side badges int rightBadgeY = y + 4; - if (item.SubItems.Count > 5) + // Version count badge + if (tile.Versions.Count > 1 && state.GroupBadgeOpacity > 0.01f) { - string sizeText = item.SubItems[5].Text; - if (!string.IsNullOrEmpty(sizeText)) - { - DrawRightAlignedBadge(g, sizeText, x + scaledW - 4, rightBadgeY, 1.0f); - rightBadgeY += 18; - } + string countText = tile.Versions.Count + " VERSIONS"; + DrawRightAlignedBadge(g, countText, x + scaledW - 4, rightBadgeY, state.GroupBadgeOpacity); + rightBadgeY += 18; } + // Size badge + string sizeText = item.SubItems.Count > 5 ? item.SubItems[5].Text : ""; + if (!string.IsNullOrEmpty(sizeText)) + { + DrawRightAlignedBadge(g, sizeText, x + scaledW - 4, rightBadgeY, 1.0f); + rightBadgeY += 18; + } + + // Date badge if (state.TooltipOpacity > 0.01f && item.SubItems.Count > 4) { string formattedDate = FormatLastUpdated(item.SubItems[4].Text); @@ -954,35 +1192,27 @@ public class FastGalleryPanel : Control // Delete button if (state.DeleteButtonOpacity > 0.01f) - DrawDeleteButton(g, x, y, scaledW, scaledH, 0, state.DeleteButtonOpacity, _isHoveringDeleteButton && index == _hoveredIndex); + DrawDeleteButton(g, x, y, scaledW, scaledH, state.DeleteButtonOpacity, _isHoveringDeleteButton && index == _hoveredIndex); - // Game name + // Game name overlay - use BaseGameName if (state.TooltipOpacity > 0.01f) { int overlayH = 20; var overlayRect = new Rectangle(x, y + scaledH - overlayH, scaledW, overlayH); - - Region oldClip = g.Clip; using (var clipPath = CreateRoundedRectangle(tileRect, THUMB_CORNER_RADIUS)) { + Region oldClip = g.Clip; g.SetClip(clipPath, CombineMode.Intersect); - var fillRect = new Rectangle(overlayRect.X - 1, overlayRect.Y, overlayRect.Width + 2, overlayRect.Height + 1); using (var overlayBrush = new SolidBrush(Color.FromArgb((int)(180 * state.TooltipOpacity), 0, 0, 0))) - g.FillRectangle(overlayBrush, fillRect); + g.FillRectangle(overlayBrush, overlayRect.X - 1, overlayRect.Y, overlayRect.Width + 2, overlayRect.Height + 1); g.Clip = oldClip; } using (var font = new Font("Segoe UI", 8f, FontStyle.Bold)) - using (var brush = new SolidBrush(Color.FromArgb((int)(TextColor.A * state.TooltipOpacity), TextColor.R, TextColor.G, TextColor.B))) + using (var brush = new SolidBrush(Color.FromArgb((int)(TextColor.A * state.TooltipOpacity), TextColor))) { - var sf = new StringFormat - { - Alignment = StringAlignment.Center, - LineAlignment = StringAlignment.Center, - Trimming = StringTrimming.EllipsisCharacter, - FormatFlags = StringFormatFlags.NoWrap - }; - g.DrawString(item.Text, font, brush, new Rectangle(overlayRect.X, overlayRect.Y + 1, overlayRect.Width, overlayRect.Height), sf); + var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }; + g.DrawString(tile.BaseGameName, font, brush, new Rectangle(overlayRect.X, overlayRect.Y + 1, overlayRect.Width, overlayRect.Height), sf); } } @@ -1003,21 +1233,17 @@ public class FastGalleryPanel : Control } } - private void DrawDeleteButton(Graphics g, int tileX, int tileY, int tileWidth, int thumbHeight, int thumbPadding, float opacity, bool isHovering) + private void DrawDeleteButton(Graphics g, int tileX, int tileY, int tileWidth, int tileHeight, float opacity, bool isHovering) { // Position in bottom-right corner of thumbnail - int btnX = tileX + tileWidth - DELETE_BUTTON_SIZE - thumbPadding - DELETE_BUTTON_MARGIN; - int btnY = tileY + thumbPadding + thumbHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN - 20; + int btnX = tileX + tileWidth - DELETE_BUTTON_SIZE - 2 - DELETE_BUTTON_MARGIN; + int btnY = tileY + 2 + tileHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN - 20; var btnRect = new Rectangle(btnX, btnY, DELETE_BUTTON_SIZE, DELETE_BUTTON_SIZE); - int bgAlpha = (int)(opacity * 255); Color bgColor = isHovering ? DeleteButtonHoverBg : DeleteButtonBg; - using (var path = CreateRoundedRectangle(btnRect, 6)) - using (var bgBrush = new SolidBrush(Color.FromArgb(bgAlpha, bgColor.R, bgColor.G, bgColor.B))) - { + using (var bgBrush = new SolidBrush(Color.FromArgb((int)(opacity * 255), bgColor))) g.FillPath(bgBrush, path); - } // Draw trash icon int iconPadding = 5; @@ -1061,46 +1287,32 @@ public class FastGalleryPanel : Control } } - private void DrawRightAlignedBadge(Graphics g, string text, int rightX, int y, float opacity = 1.0f) + private void DrawRightAlignedBadge(Graphics g, string text, int rightX, int y, float opacity) { using (var font = new Font("Segoe UI", 7f, FontStyle.Bold)) { var sz = g.MeasureString(text, font); - int badgeWidth = (int)sz.Width + 8; - var rect = new Rectangle(rightX - badgeWidth, y, badgeWidth, 14); - - int bgAlpha = (int)(180 * opacity); - int textAlpha = (int)(255 * opacity); - + var rect = new Rectangle(rightX - (int)sz.Width - 8, y, (int)sz.Width + 8, 14); using (var path = CreateRoundedRectangle(rect, 4)) - using (var bgBrush = new SolidBrush(Color.FromArgb(bgAlpha, 0, 0, 0))) + using (var bgBrush = new SolidBrush(Color.FromArgb((int)(180 * opacity), 0, 0, 0))) { g.FillPath(bgBrush, path); var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; - using (var textBrush = new SolidBrush(Color.FromArgb(textAlpha, 255, 255, 255))) - { + using (var textBrush = new SolidBrush(Color.FromArgb((int)(255 * opacity), 255, 255, 255))) g.DrawString(text, font, textBrush, rect, sf); - } } } } private string FormatLastUpdated(string dateStr) { - if (string.IsNullOrEmpty(dateStr)) - return ""; + if (string.IsNullOrEmpty(dateStr)) return ""; // Extract just the date part before space - string datePart = dateStr.Split(' ')[0]; - - if (DateTime.TryParse(datePart, out DateTime date)) - { - // Format as "29 JUL 2025" + if (DateTime.TryParse(dateStr.Split(' ')[0], out DateTime date)) return date.ToString("dd MMM yyyy", System.Globalization.CultureInfo.InvariantCulture).ToUpperInvariant(); - } - // Fallback: return original if parsing fails - return dateStr; + return dateStr; // Fallback: return original if parsing fails } private void DrawBadge(Graphics g, string text, int x, int y, Color bgColor) @@ -1135,7 +1347,6 @@ public class FastGalleryPanel : Control string oldKey = _cacheOrder.Dequeue(); if (_imageCache.TryGetValue(oldKey, out var oldImg)) { oldImg.Dispose(); _imageCache.Remove(oldKey); } } - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { var img = Image.FromStream(stream); @@ -1147,44 +1358,68 @@ public class FastGalleryPanel : Control catch { return null; } } - private int GetIndexAtPoint(int x, int y) + private int GetTileIndexAtPoint(int x, int y) { // Account for sort panel offset if (y < SORT_PANEL_HEIGHT) return -1; - int adjustedY = (y - SORT_PANEL_HEIGHT) + (int)_scrollY; int col = (x - _leftPadding) / (_tileWidth + _spacing); int row = (adjustedY - _spacing) / (_tileHeight + _spacing); - if (col < 0 || col >= _columns || row < 0) return -1; int tileX = _leftPadding + col * (_tileWidth + _spacing); int tileY = _spacing + row * (_tileHeight + _spacing); - if (x >= tileX && x < tileX + _tileWidth && adjustedY >= tileY && adjustedY < tileY + _tileHeight) { int index = row * _columns + col; - return index < _items.Count ? index : -1; + return index < _displayTiles.Count ? index : -1; } return -1; } private bool IsPointOnDeleteButton(int x, int y, int index) { - if (index < 0 || index >= _items.Count) return false; - if (!IsItemInstalled(_items[index])) return false; - + if (index < 0 || index >= _displayTiles.Count) return false; + if (!IsAnyVersionInstalled(_displayTiles[index])) return false; int row = index / _columns; int col = index % _columns; - var btnRect = GetDeleteButtonRect(index, row, col, (int)_scrollY); + return GetDeleteButtonRect(index, row, col, (int)_scrollY).Contains(x, y); + } - return btnRect.Contains(x, y); + private int GetOverlayVersionAtPoint(int x, int y) + { + if (_expandOverlayOpacity < 0.5f || _expandedTileIndex < 0) return -1; + for (int i = 0; i < _versionRects.Count; i++) + if (!_versionRects[i].IsEmpty && _versionRects[i].Contains(x, y)) return i; + return -1; } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); - int newHover = GetIndexAtPoint(e.X, e.Y); + + if (_expandedTileIndex >= 0 && _expandOverlayOpacity > 0.5f) + { + int versionIdx = GetOverlayVersionAtPoint(e.X, e.Y); + if (versionIdx != _overlayHoveredVersion) + { + _overlayHoveredVersion = versionIdx; + + // Update release notes when hovering over a version + if (versionIdx >= 0 && versionIdx < _displayTiles[_expandedTileIndex].Versions.Count) + { + var hoveredVersion = _displayTiles[_expandedTileIndex].Versions[versionIdx]; + string releaseName = hoveredVersion.SubItems.Count > 1 ? hoveredVersion.SubItems[1].Text : ""; + TileHovered?.Invoke(this, releaseName); + } + + Invalidate(); + } + Cursor = versionIdx >= 0 ? Cursors.Hand : Cursors.Default; + return; + } + + int newHover = GetTileIndexAtPoint(e.X, e.Y); bool wasHoveringDelete = _isHoveringDeleteButton; if (newHover != _hoveredIndex) @@ -1193,41 +1428,20 @@ public class FastGalleryPanel : Control _isHoveringDeleteButton = false; } - // Check if hovering over delete button if (_hoveredIndex >= 0) - { _isHoveringDeleteButton = IsPointOnDeleteButton(e.X, e.Y, _hoveredIndex); - } else - { _isHoveringDeleteButton = false; - } // Update cursor - if (_isHoveringDeleteButton) - { - Cursor = Cursors.Hand; - } - else if (_hoveredIndex >= 0) - { - Cursor = Cursors.Hand; - } - else - { - Cursor = Cursors.Default; - } - - // Redraw if delete button hover state changed - if (wasHoveringDelete != _isHoveringDeleteButton) - { - Invalidate(); - } + Cursor = _isHoveringDeleteButton || _hoveredIndex >= 0 ? Cursors.Hand : Cursors.Default; + if (wasHoveringDelete != _isHoveringDeleteButton) Invalidate(); // Redraw if delete button hover state changed } protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); - if (_hoveredIndex >= 0) _hoveredIndex = -1; + _hoveredIndex = -1; _isHoveringDeleteButton = false; } @@ -1236,44 +1450,120 @@ public class FastGalleryPanel : Control base.OnMouseClick(e); // Take focus to unfocus any other control (like search text box) - if (!Focused) - { - Focus(); - } + if (!Focused) Focus(); if (e.Button == MouseButtons.Left) { - int i = GetIndexAtPoint(e.X, e.Y); + if (_expandedTileIndex >= 0 && _expandOverlayOpacity > 0.5f) + { + int versionIdx = GetOverlayVersionAtPoint(e.X, e.Y); + if (versionIdx >= 0) + { + var tile = _displayTiles[_expandedTileIndex]; + var selectedVersion = tile.Versions[versionIdx]; + _selectedItem = selectedVersion; + int actualIndex = _items.IndexOf(selectedVersion); + CloseOverlay(); + TileClicked?.Invoke(this, actualIndex); + TileDoubleClicked?.Invoke(this, actualIndex); + Invalidate(); + return; + } + + if (!_overlayRect.Contains(e.X, e.Y)) + { + CloseOverlay(); + Invalidate(); + return; + } + return; + } + + int i = GetTileIndexAtPoint(e.X, e.Y); if (i >= 0) { + var tile = _displayTiles[i]; + // Check if clicking on delete button if (IsPointOnDeleteButton(e.X, e.Y, i)) { // Select this item so the uninstall knows which app to remove _selectedIndex = i; - TileClicked?.Invoke(this, i); + _selectedItem = tile.Primary; + int actualIndex = _items.IndexOf(tile.Primary); + TileClicked?.Invoke(this, actualIndex); Invalidate(); // Then trigger delete - TileDeleteClicked?.Invoke(this, i); + TileDeleteClicked?.Invoke(this, actualIndex); + return; + } + + if (tile.Versions.Count > 1) + { + _expandedTileIndex = i; + _targetExpandOverlayOpacity = 1.0f; + _overlayHoveredVersion = -1; + _overlayScrollOffset = 0; + + // Pre-select the shortest-named version (base game) to show thumbnail/trailer + _selectedIndex = i; + var baseVersion = tile.Versions.OrderBy(v => v.Text.Length).First(); + _selectedItem = baseVersion; + int baseIdx = _items.IndexOf(baseVersion); + TileClicked?.Invoke(this, baseIdx); + + Invalidate(); return; } _selectedIndex = i; + _selectedItem = tile.Primary; + int idx = _items.IndexOf(tile.Primary); Invalidate(); - TileClicked?.Invoke(this, i); + TileClicked?.Invoke(this, idx); } } else if (e.Button == MouseButtons.Right) { - int i = GetIndexAtPoint(e.X, e.Y); + // Right-click in overlay - context menu for specific version + if (_expandedTileIndex >= 0 && _expandOverlayOpacity > 0.5f) + { + int versionIdx = GetOverlayVersionAtPoint(e.X, e.Y); + if (versionIdx >= 0) + { + var tile = _displayTiles[_expandedTileIndex]; + var version = tile.Versions[versionIdx]; + _selectedItem = version; + _rightClickedIndex = _expandedTileIndex; + _rightClickedVersionIndex = versionIdx; + int actualIdx = _items.IndexOf(version); + TileClicked?.Invoke(this, actualIdx); + TileRightClicked?.Invoke(this, actualIdx); + _contextMenu.Show(this, e.Location); + Invalidate(); + return; + } + + if (!_overlayRect.Contains(e.X, e.Y)) + { + CloseOverlay(); + Invalidate(); + } + return; + } + + int i = GetTileIndexAtPoint(e.X, e.Y); if (i >= 0) { _rightClickedIndex = i; + _rightClickedVersionIndex = -1; _selectedIndex = i; + _selectedItem = _displayTiles[i].Primary; + int actualIdx = _items.IndexOf(_displayTiles[i].Primary); Invalidate(); - TileClicked?.Invoke(this, i); - TileRightClicked?.Invoke(this, i); + TileClicked?.Invoke(this, actualIdx); + TileRightClicked?.Invoke(this, actualIdx); _contextMenu.Show(this, e.Location); } } @@ -1282,16 +1572,18 @@ public class FastGalleryPanel : Control protected override void OnMouseDoubleClick(MouseEventArgs e) { base.OnMouseDoubleClick(e); - if (e.Button == MouseButtons.Left) - { - int i = GetIndexAtPoint(e.X, e.Y); - if (i >= 0) - { - // Don't trigger double-click if on delete button - if (IsPointOnDeleteButton(e.X, e.Y, i)) - return; + if (e.Button != MouseButtons.Left || _expandedTileIndex >= 0) return; - TileDoubleClicked?.Invoke(this, i); + int i = GetTileIndexAtPoint(e.X, e.Y); + + // Don't trigger double-click if on delete button + if (i >= 0 && !IsPointOnDeleteButton(e.X, e.Y, i)) + { + var tile = _displayTiles[i]; + if (tile.Versions.Count == 1) + { + int idx = _items.IndexOf(tile.Primary); + TileDoubleClicked?.Invoke(this, idx); } } } @@ -1299,6 +1591,18 @@ public class FastGalleryPanel : Control protected override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); + + // Scroll overlay if open and has overflow + if (_expandedTileIndex >= 0 && _overlayMaxScroll > 0 && _overlayRect.Contains(e.X, e.Y)) + { + _overlayScrollOffset -= e.Delta / 3; + _overlayScrollOffset = Math.Max(0, Math.Min(_overlayScrollOffset, _overlayMaxScroll)); + Invalidate(); + return; + } + + if (_expandedTileIndex >= 0) return; + float scrollAmount = e.Delta * 1.2f; int maxScroll = Math.Max(0, _contentHeight - (Height - SORT_PANEL_HEIGHT)); _targetScrollY = Math.Max(0, Math.Min(maxScroll, _targetScrollY - scrollAmount)); @@ -1321,9 +1625,18 @@ public class FastGalleryPanel : Control private void ContextMenu_Opening(object sender, System.ComponentModel.CancelEventArgs e) { - if (_rightClickedIndex < 0 || _rightClickedIndex >= _items.Count) { e.Cancel = true; return; } - var item = _items[_rightClickedIndex]; - string packageName = item.SubItems.Count > 2 ? item.SubItems[1].Text : ""; + if (_rightClickedIndex < 0 || _rightClickedIndex >= _displayTiles.Count) { e.Cancel = true; return; } + + var tile = _displayTiles[_rightClickedIndex]; + ListViewItem targetItem; + + // If right-clicked on a specific version in overlay, use that + if (_rightClickedVersionIndex >= 0 && _rightClickedVersionIndex < tile.Versions.Count) + targetItem = tile.Versions[_rightClickedVersionIndex]; + else + targetItem = tile.Primary; + + string packageName = targetItem.SubItems.Count > 1 ? targetItem.SubItems[1].Text : ""; if (string.IsNullOrEmpty(packageName)) { e.Cancel = true; return; } bool isFavorite = _favoritesCache.Contains(packageName); @@ -1332,9 +1645,18 @@ public class FastGalleryPanel : Control private void ContextMenu_FavoriteClick(object sender, EventArgs e) { - if (_rightClickedIndex < 0 || _rightClickedIndex >= _items.Count) return; - var item = _items[_rightClickedIndex]; - string packageName = item.SubItems.Count > 1 ? item.SubItems[1].Text : ""; + if (_rightClickedIndex < 0 || _rightClickedIndex >= _displayTiles.Count) return; + + var tile = _displayTiles[_rightClickedIndex]; + ListViewItem targetItem; + + // If right-clicked on a specific version in overlay, use that + if (_rightClickedVersionIndex >= 0 && _rightClickedVersionIndex < tile.Versions.Count) + targetItem = tile.Versions[_rightClickedVersionIndex]; + else + targetItem = tile.Primary; + + string packageName = targetItem.SubItems.Count > 1 ? targetItem.SubItems[1].Text : ""; if (string.IsNullOrEmpty(packageName)) return; var settings = SettingsManager.Instance; @@ -1342,15 +1664,55 @@ public class FastGalleryPanel : Control { settings.RemoveFavoriteGame(packageName); _favoritesCache.Remove(packageName); + + // Check if in favorites-only view (= this item is now the only non-favorite) + bool isFavoritesView = _items.All(item => + item == targetItem || _favoritesCache.Contains(item.SubItems.Count > 1 ? item.SubItems[1].Text : "")); + + if (isFavoritesView) + RemoveVersionFromDisplay(tile, targetItem, _rightClickedIndex); } else { settings.AddFavoriteGame(packageName); _favoritesCache.Add(packageName); } + Invalidate(); } + private void RemoveVersionFromDisplay(GroupedTile tile, ListViewItem item, int tileIndex) + { + _items.Remove(item); + _originalItems.Remove(item); + tile.Versions.Remove(item); + + if (tile.Versions.Count == 0) + { + _displayTiles.RemoveAt(tileIndex); + CloseOverlay(); + if (_selectedIndex == tileIndex) { _selectedIndex = -1; _selectedItem = null; } + else if (_selectedIndex > tileIndex) _selectedIndex--; + } + else + { + tile.BaseGameName = GetBaseGameName(tile.Versions); + tile.GameName = tile.Versions[0].Text; + + if (tile.Versions.Count == 1) + CloseOverlay(); + else if (_overlayHoveredVersion >= tile.Versions.Count) + _overlayHoveredVersion = tile.Versions.Count - 1; + } + + // Rebuild tile states + _tileStates.Clear(); + for (int i = 0; i < _displayTiles.Count; i++) + _tileStates[i] = new TileAnimationState(); + + RecalculateLayout(); + } + public void RefreshFavoritesCache() { _favoritesCache = new HashSet(SettingsManager.Instance.FavoritedGames, StringComparer.OrdinalIgnoreCase); @@ -1358,22 +1720,20 @@ public class FastGalleryPanel : Control public void ScrollToPackage(string releaseName) { - if (string.IsNullOrEmpty(releaseName) || _items == null || _items.Count == 0) - return; + if (string.IsNullOrEmpty(releaseName) || _displayTiles == null || _displayTiles.Count == 0) return; // Find the index of the item with the matching release name - for (int i = 0; i < _items.Count; i++) + for (int i = 0; i < _displayTiles.Count; i++) { - var item = _items[i]; - if (item.SubItems.Count > 1 && - item.SubItems[1].Text.Equals(releaseName, StringComparison.OrdinalIgnoreCase)) + var tile = _displayTiles[i]; + if (tile.Primary.SubItems.Count > 1 && tile.Primary.SubItems[1].Text.Equals(releaseName, StringComparison.OrdinalIgnoreCase)) { // Calculate the row this item is in int row = i / _columns; // Calculate the Y position to scroll to (center the row in view if possible) int targetY = _spacing + SORT_PANEL_HEIGHT + row * (_tileHeight + _spacing); - int viewportHeight = this.Height - SORT_PANEL_HEIGHT; + int viewportHeight = Height - SORT_PANEL_HEIGHT; int centeredY = targetY - (viewportHeight / 2) + (_tileHeight / 2); // Clamp to valid scroll range @@ -1383,9 +1743,7 @@ public class FastGalleryPanel : Control // Update scrollbar and redraw if (_scrollBar.Visible) - { _scrollBar.Value = Math.Max(_scrollBar.Minimum, Math.Min(_scrollBar.Maximum - _scrollBar.LargeChange + 1, (int)_scrollY)); - } // Also select this item visually _selectedIndex = i; @@ -1403,7 +1761,6 @@ public class FastGalleryPanel : Control _animationTimer?.Stop(); _animationTimer?.Dispose(); _contextMenu?.Dispose(); - foreach (var img in _imageCache.Values) { try { img?.Dispose(); } catch { } } _imageCache.Clear(); _cacheOrder.Clear(); diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs index e31c208..68d4d31 100644 --- a/MainForm.Designer.cs +++ b/MainForm.Designer.cs @@ -90,7 +90,6 @@ namespace AndroidSideloader this.speedLabel_Tooltip = new System.Windows.Forms.ToolTip(this.components); this.etaLabel_Tooltip = new System.Windows.Forms.ToolTip(this.components); this.progressDLbtnContainer = new System.Windows.Forms.Panel(); - this.progressBar = new AndroidSideloader.ModernProgressBar(); this.diskLabel = new System.Windows.Forms.Label(); this.questStorageProgressBar = new System.Windows.Forms.Panel(); this.batteryLevImg = new System.Windows.Forms.PictureBox(); @@ -119,9 +118,14 @@ namespace AndroidSideloader this.deviceIdLabel = new System.Windows.Forms.Label(); this.rookieStatusLabel = new System.Windows.Forms.Label(); this.sidebarMediaPanel = new System.Windows.Forms.Panel(); - this.downloadInstallGameButton = new AndroidSideloader.RoundButton(); this.selectedGameLabel = new System.Windows.Forms.Label(); this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.webView21 = new Microsoft.Web.WebView2.WinForms.WebView2(); + this.favoriteGame = new System.Windows.Forms.ContextMenuStrip(this.components); + this.favoriteButton = new System.Windows.Forms.ToolStripMenuItem(); + this.gamesGalleryView = new System.Windows.Forms.FlowLayoutPanel(); + this.btnViewToggle_Tooltip = new System.Windows.Forms.ToolTip(this.components); + this.webViewPlaceholderPanel = new System.Windows.Forms.Panel(); this.searchPanel = new AndroidSideloader.RoundButton(); this.searchIconPictureBox = new System.Windows.Forms.PictureBox(); this.searchTextBox = new System.Windows.Forms.TextBox(); @@ -130,12 +134,8 @@ namespace AndroidSideloader this.btnInstalled = new AndroidSideloader.RoundButton(); this.btnUpdateAvailable = new AndroidSideloader.RoundButton(); this.btnNewerThanList = new AndroidSideloader.RoundButton(); - this.webView21 = new Microsoft.Web.WebView2.WinForms.WebView2(); - this.favoriteGame = new System.Windows.Forms.ContextMenuStrip(this.components); - this.favoriteButton = new System.Windows.Forms.ToolStripMenuItem(); - this.gamesGalleryView = new System.Windows.Forms.FlowLayoutPanel(); - this.btnViewToggle_Tooltip = new System.Windows.Forms.ToolTip(this.components); - this.webViewPlaceholderPanel = new System.Windows.Forms.Panel(); + this.progressBar = new AndroidSideloader.ModernProgressBar(); + this.downloadInstallGameButton = new AndroidSideloader.RoundButton(); ((System.ComponentModel.ISupportInitialize)(this.gamesPictureBox)).BeginInit(); this.gamesPictureBox.SuspendLayout(); this.progressDLbtnContainer.SuspendLayout(); @@ -151,10 +151,10 @@ namespace AndroidSideloader this.statusInfoPanel.SuspendLayout(); this.sidebarMediaPanel.SuspendLayout(); this.tableLayoutPanel1.SuspendLayout(); - this.searchPanel.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.webView21)).BeginInit(); this.favoriteGame.SuspendLayout(); + this.searchPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).BeginInit(); this.SuspendLayout(); // // m_combo @@ -165,7 +165,7 @@ namespace AndroidSideloader this.m_combo.ForeColor = global::AndroidSideloader.Properties.Settings.Default.FontColor; this.m_combo.Location = new System.Drawing.Point(253, 9); this.m_combo.Name = "m_combo"; - this.m_combo.Size = new System.Drawing.Size(374, 25); + this.m_combo.Size = new System.Drawing.Size(374, 24); this.m_combo.TabIndex = 0; this.m_combo.Text = "Select an Installed App..."; this.m_combo.Visible = false; @@ -201,7 +201,6 @@ 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; @@ -226,7 +225,7 @@ namespace AndroidSideloader this.devicesComboBox.Location = new System.Drawing.Point(253, 39); this.devicesComboBox.Margin = new System.Windows.Forms.Padding(2); this.devicesComboBox.Name = "devicesComboBox"; - this.devicesComboBox.Size = new System.Drawing.Size(164, 25); + this.devicesComboBox.Size = new System.Drawing.Size(164, 24); this.devicesComboBox.TabIndex = 1; this.devicesComboBox.Text = "Select your device"; this.devicesComboBox.Visible = false; @@ -242,7 +241,7 @@ namespace AndroidSideloader this.remotesList.Location = new System.Drawing.Point(567, 40); this.remotesList.Margin = new System.Windows.Forms.Padding(2); this.remotesList.Name = "remotesList"; - this.remotesList.Size = new System.Drawing.Size(67, 25); + this.remotesList.Size = new System.Drawing.Size(67, 24); this.remotesList.TabIndex = 3; this.remotesList.Visible = false; this.remotesList.SelectedIndexChanged += new System.EventHandler(this.remotesList_SelectedIndexChanged); @@ -336,7 +335,7 @@ namespace AndroidSideloader // // notesRichTextBox // - this.notesRichTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + this.notesRichTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.notesRichTextBox.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); this.notesRichTextBox.BorderStyle = System.Windows.Forms.BorderStyle.None; @@ -347,11 +346,12 @@ namespace AndroidSideloader this.notesRichTextBox.Name = "notesRichTextBox"; this.notesRichTextBox.ReadOnly = true; this.notesRichTextBox.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.None; - this.notesRichTextBox.SelectionAlignment = System.Windows.Forms.HorizontalAlignment.Center; this.notesRichTextBox.ShowSelectionMargin = true; this.notesRichTextBox.Size = new System.Drawing.Size(265, 192); this.notesRichTextBox.TabIndex = 10; - this.notesRichTextBox.Text = "\n\n\n\n\nTip: Press F1 to see all shortcuts\n\nDrag and drop APKs or folders to install"; + this.notesRichTextBox.SelectionAlignment = System.Windows.Forms.HorizontalAlignment.Center; + this.notesRichTextBox.Text = "\n\n\n\n\nTip: Press F1 to see all shortcuts\n\nDrag and drop APKs or folders to install" + + ""; this.notesRichTextBox.LinkClicked += new System.Windows.Forms.LinkClickedEventHandler(this.notesRichTextBox_LinkClicked); // // lblNotes @@ -458,7 +458,7 @@ namespace AndroidSideloader this.backupadbbutton.Padding = new System.Windows.Forms.Padding(30, 0, 0, 0); this.backupadbbutton.Size = new System.Drawing.Size(233, 28); this.backupadbbutton.TabIndex = 1; - this.backupadbbutton.Text = "BACKUP WITH ADB"; + this.backupadbbutton.Text = "BACKUP GAMESAVE WITH ADB"; this.backupadbbutton.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; this.backupadbbutton_Tooltip.SetToolTip(this.backupadbbutton, "Save game data via ADB-Backup"); this.backupadbbutton.UseVisualStyleBackColor = false; @@ -478,7 +478,7 @@ namespace AndroidSideloader this.backupbutton.Padding = new System.Windows.Forms.Padding(30, 0, 0, 0); this.backupbutton.Size = new System.Drawing.Size(233, 28); this.backupbutton.TabIndex = 1; - this.backupbutton.Text = "BACKUP GAMESAVES"; + this.backupbutton.Text = "BACKUP ALL GAMESAVES"; this.backupbutton.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; this.backupbutton_Tooltip.SetToolTip(this.backupbutton, "Save game and apps data to the backup folder (Does not save APKs or OBBs)"); this.backupbutton.UseVisualStyleBackColor = false; @@ -500,7 +500,7 @@ namespace AndroidSideloader this.restorebutton.TabIndex = 0; this.restorebutton.Text = "RESTORE GAMESAVES"; this.restorebutton.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; - this.restorebutton_Tooltip.SetToolTip(this.restorebutton, "Restore game and apps data to the device (Use BACKUP GAMESAVES first)"); + this.restorebutton_Tooltip.SetToolTip(this.restorebutton, "Restore game and apps data to the device"); this.restorebutton.UseVisualStyleBackColor = false; this.restorebutton.Click += new System.EventHandler(this.restorebutton_Click); // @@ -759,31 +759,6 @@ namespace AndroidSideloader this.progressDLbtnContainer.Size = new System.Drawing.Size(984, 40); this.progressDLbtnContainer.TabIndex = 96; // - // progressBar - // - this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.progressBar.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(35)))), ((int)(((byte)(45))))); - this.progressBar.BackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(28)))), ((int)(((byte)(32)))), ((int)(((byte)(38))))); - this.progressBar.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); - this.progressBar.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.progressBar.IndeterminateColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.progressBar.IsIndeterminate = false; - this.progressBar.Location = new System.Drawing.Point(1, 23); - this.progressBar.Maximum = 100F; - this.progressBar.Minimum = 0F; - this.progressBar.MinimumSize = new System.Drawing.Size(200, 13); - this.progressBar.Name = "progressBar"; - this.progressBar.OperationType = ""; - this.progressBar.ProgressEndColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(160)))), ((int)(((byte)(130))))); - this.progressBar.ProgressStartColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(220)))), ((int)(((byte)(190))))); - this.progressBar.Radius = 6; - this.progressBar.Size = new System.Drawing.Size(983, 13); - this.progressBar.StatusText = ""; - this.progressBar.TabIndex = 7; - this.progressBar.TextColor = System.Drawing.Color.FromArgb(((int)(((byte)(230)))), ((int)(((byte)(230)))), ((int)(((byte)(230))))); - this.progressBar.Value = 0F; - // // diskLabel // this.diskLabel.BackColor = System.Drawing.Color.Transparent; @@ -1249,35 +1224,6 @@ namespace AndroidSideloader this.sidebarMediaPanel.Size = new System.Drawing.Size(233, 214); this.sidebarMediaPanel.TabIndex = 101; // - // downloadInstallGameButton - // - this.downloadInstallGameButton.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190))))); - this.downloadInstallGameButton.Active2 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190))))); - this.downloadInstallGameButton.BackColor = System.Drawing.Color.Transparent; - this.downloadInstallGameButton.Cursor = System.Windows.Forms.Cursors.Hand; - this.downloadInstallGameButton.DialogResult = System.Windows.Forms.DialogResult.OK; - this.downloadInstallGameButton.Disabled1 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22))))); - this.downloadInstallGameButton.Disabled2 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22))))); - this.downloadInstallGameButton.DisabledStrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(55)))), ((int)(((byte)(65))))); - this.downloadInstallGameButton.Enabled = false; - this.downloadInstallGameButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); - this.downloadInstallGameButton.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(59)))), ((int)(((byte)(67)))), ((int)(((byte)(82))))); - this.downloadInstallGameButton.Inactive1 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.downloadInstallGameButton.Inactive2 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.downloadInstallGameButton.Location = new System.Drawing.Point(6, 177); - this.downloadInstallGameButton.Margin = new System.Windows.Forms.Padding(0); - this.downloadInstallGameButton.Name = "downloadInstallGameButton"; - this.downloadInstallGameButton.Radius = 4; - this.downloadInstallGameButton.Size = new System.Drawing.Size(238, 30); - this.downloadInstallGameButton.Stroke = true; - this.downloadInstallGameButton.StrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); - this.downloadInstallGameButton.TabIndex = 94; - this.downloadInstallGameButton.Text = "DOWNLOAD AND INSTALL"; - this.downloadInstallGameButton.Transparency = false; - this.downloadInstallGameButton.Click += new System.EventHandler(this.downloadInstallGameButton_Click); - this.downloadInstallGameButton.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop); - this.downloadInstallGameButton.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter); - // // selectedGameLabel // this.selectedGameLabel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(24)))), ((int)(((byte)(29))))); @@ -1316,6 +1262,58 @@ namespace AndroidSideloader this.tableLayoutPanel1.Size = new System.Drawing.Size(984, 34); this.tableLayoutPanel1.TabIndex = 97; // + // webView21 + // + this.webView21.AllowExternalDrop = true; + this.webView21.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.webView21.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); + this.webView21.CreationProperties = null; + this.webView21.DefaultBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); + this.webView21.Location = new System.Drawing.Point(259, 496); + this.webView21.Name = "webView21"; + this.webView21.Size = new System.Drawing.Size(384, 216); + this.webView21.TabIndex = 98; + this.webView21.ZoomFactor = 1D; + // + // favoriteGame + // + this.favoriteGame.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48))))); + this.favoriteGame.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.favoriteButton}); + this.favoriteGame.Name = "favoriteGame"; + this.favoriteGame.ShowImageMargin = false; + this.favoriteGame.Size = new System.Drawing.Size(149, 26); + // + // favoriteButton + // + this.favoriteButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48))))); + this.favoriteButton.ForeColor = System.Drawing.Color.White; + this.favoriteButton.Name = "favoriteButton"; + this.favoriteButton.Size = new System.Drawing.Size(148, 22); + this.favoriteButton.Text = "★ Add to Favorites"; + this.favoriteButton.Click += new System.EventHandler(this.favoriteButton_Click); + // + // gamesGalleryView + // + this.gamesGalleryView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.gamesGalleryView.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15))))); + this.gamesGalleryView.Location = new System.Drawing.Point(258, 44); + this.gamesGalleryView.Name = "gamesGalleryView"; + this.gamesGalleryView.Size = new System.Drawing.Size(984, 409); + this.gamesGalleryView.TabIndex = 102; + // + // webViewPlaceholderPanel + // + this.webViewPlaceholderPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.webViewPlaceholderPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); + this.webViewPlaceholderPanel.Location = new System.Drawing.Point(259, 496); + this.webViewPlaceholderPanel.Name = "webViewPlaceholderPanel"; + this.webViewPlaceholderPanel.Size = new System.Drawing.Size(384, 217); + this.webViewPlaceholderPanel.TabIndex = 103; + this.webViewPlaceholderPanel.Paint += new System.Windows.Forms.PaintEventHandler(this.webViewPlaceholderPanel_Paint); + // // searchPanel // this.searchPanel.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(56)))), ((int)(((byte)(70))))); @@ -1499,57 +1497,59 @@ namespace AndroidSideloader this.btnNewerThanList.Transparency = false; this.btnNewerThanList.Click += new System.EventHandler(this.btnNewerThanList_Click); // - // webView21 + // progressBar // - this.webView21.AllowExternalDrop = true; - this.webView21.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.webView21.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); - this.webView21.CreationProperties = null; - this.webView21.DefaultBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); - this.webView21.Location = new System.Drawing.Point(259, 496); - this.webView21.Name = "webView21"; - this.webView21.Size = new System.Drawing.Size(384, 216); - this.webView21.TabIndex = 98; - this.webView21.ZoomFactor = 1D; - // - // favoriteGame - // - this.favoriteGame.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48))))); - this.favoriteGame.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.favoriteButton}); - this.favoriteGame.Name = "favoriteGame"; - this.favoriteGame.ShowImageMargin = false; - this.favoriteGame.Size = new System.Drawing.Size(149, 26); - // - // favoriteButton - // - this.favoriteButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48))))); - this.favoriteButton.ForeColor = System.Drawing.Color.White; - this.favoriteButton.Name = "favoriteButton"; - this.favoriteButton.Size = new System.Drawing.Size(148, 22); - this.favoriteButton.Text = "★ Add to Favorites"; - this.favoriteButton.Click += new System.EventHandler(this.favoriteButton_Click); - // - // gamesGalleryView - // - this.gamesGalleryView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) + this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.gamesGalleryView.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15))))); - this.gamesGalleryView.Location = new System.Drawing.Point(258, 44); - this.gamesGalleryView.Name = "gamesGalleryView"; - this.gamesGalleryView.Size = new System.Drawing.Size(984, 409); - this.gamesGalleryView.TabIndex = 102; + this.progressBar.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(35)))), ((int)(((byte)(45))))); + this.progressBar.BackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(28)))), ((int)(((byte)(32)))), ((int)(((byte)(38))))); + this.progressBar.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + this.progressBar.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.progressBar.IndeterminateColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.progressBar.IsIndeterminate = false; + this.progressBar.Location = new System.Drawing.Point(1, 23); + this.progressBar.Maximum = 100F; + this.progressBar.Minimum = 0F; + this.progressBar.MinimumSize = new System.Drawing.Size(200, 13); + this.progressBar.Name = "progressBar"; + this.progressBar.OperationType = ""; + this.progressBar.ProgressEndColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(160)))), ((int)(((byte)(130))))); + this.progressBar.ProgressStartColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(220)))), ((int)(((byte)(190))))); + this.progressBar.Radius = 6; + this.progressBar.Size = new System.Drawing.Size(983, 13); + this.progressBar.StatusText = ""; + this.progressBar.TabIndex = 7; + this.progressBar.TextColor = System.Drawing.Color.FromArgb(((int)(((byte)(230)))), ((int)(((byte)(230)))), ((int)(((byte)(230))))); + this.progressBar.Value = 0F; // - // webViewPlaceholderPanel + // downloadInstallGameButton // - this.webViewPlaceholderPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.webViewPlaceholderPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30))))); - this.webViewPlaceholderPanel.Location = new System.Drawing.Point(259, 496); - this.webViewPlaceholderPanel.Name = "webViewPlaceholderPanel"; - this.webViewPlaceholderPanel.Size = new System.Drawing.Size(384, 217); - this.webViewPlaceholderPanel.TabIndex = 103; - this.webViewPlaceholderPanel.Paint += new System.Windows.Forms.PaintEventHandler(this.webViewPlaceholderPanel_Paint); + this.downloadInstallGameButton.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190))))); + this.downloadInstallGameButton.Active2 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190))))); + this.downloadInstallGameButton.BackColor = System.Drawing.Color.Transparent; + this.downloadInstallGameButton.Cursor = System.Windows.Forms.Cursors.Hand; + this.downloadInstallGameButton.DialogResult = System.Windows.Forms.DialogResult.OK; + this.downloadInstallGameButton.Disabled1 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22))))); + this.downloadInstallGameButton.Disabled2 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22))))); + this.downloadInstallGameButton.DisabledStrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(55)))), ((int)(((byte)(65))))); + this.downloadInstallGameButton.Enabled = false; + this.downloadInstallGameButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + this.downloadInstallGameButton.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(59)))), ((int)(((byte)(67)))), ((int)(((byte)(82))))); + this.downloadInstallGameButton.Inactive1 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.downloadInstallGameButton.Inactive2 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.downloadInstallGameButton.Location = new System.Drawing.Point(6, 177); + this.downloadInstallGameButton.Margin = new System.Windows.Forms.Padding(0); + this.downloadInstallGameButton.Name = "downloadInstallGameButton"; + this.downloadInstallGameButton.Radius = 4; + this.downloadInstallGameButton.Size = new System.Drawing.Size(238, 30); + this.downloadInstallGameButton.Stroke = true; + this.downloadInstallGameButton.StrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173))))); + this.downloadInstallGameButton.TabIndex = 94; + this.downloadInstallGameButton.Text = "DOWNLOAD"; + this.downloadInstallGameButton.Transparency = false; + this.downloadInstallGameButton.Click += new System.EventHandler(this.downloadInstallGameButton_Click); + this.downloadInstallGameButton.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop); + this.downloadInstallGameButton.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter); // // MainForm // @@ -1600,11 +1600,11 @@ namespace AndroidSideloader this.statusInfoPanel.ResumeLayout(false); this.sidebarMediaPanel.ResumeLayout(false); this.tableLayoutPanel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.webView21)).EndInit(); + this.favoriteGame.ResumeLayout(false); this.searchPanel.ResumeLayout(false); this.searchPanel.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.webView21)).EndInit(); - this.favoriteGame.ResumeLayout(false); this.ResumeLayout(false); this.PerformLayout(); diff --git a/MainForm.cs b/MainForm.cs index fa16f40..a2ddc51 100755 --- a/MainForm.cs +++ b/MainForm.cs @@ -74,6 +74,7 @@ namespace AndroidSideloader private static readonly Color ColorError = ColorTranslator.FromHtml("#f52f57"); private Panel _listViewUninstallButton; private bool _listViewUninstallButtonHovered = false; + private ListViewItem _hoveredItemForDeleteBtn; private bool isGalleryView; // Will be set from settings in constructor private List _galleryDataSource; private FastGalleryPanel _fastGallery; @@ -91,6 +92,7 @@ namespace AndroidSideloader public static bool noRcloneUpdating; public static bool noAppCheck = false; public static bool hasPublicConfig = false; + public static bool hasUploadConfig = false; public static bool UsingPublicConfig = false; public static bool enviromentCreated = false; public static PublicConfig PublicConfigFile; @@ -171,12 +173,10 @@ namespace AndroidSideloader if (_listViewUninstallButton == null) return; - // Check if we have a tagged item to track - if (!(_listViewUninstallButton.Tag is ListViewItem item)) - return; + var item = _hoveredItemForDeleteBtn; - // Verify item is still valid and selected - if (!gamesListView.Items.Contains(item) || !item.Selected) + // Hide if no item is hovered + if (item == null || !gamesListView.Items.Contains(item)) { _listViewUninstallButton.Visible = false; return; @@ -213,6 +213,7 @@ namespace AndroidSideloader if (isVisible) { _listViewUninstallButton.Location = new Point(buttonX, buttonY); + _listViewUninstallButton.Tag = item; // Store reference for click handler if (!_listViewUninstallButton.Visible) { _listViewUninstallButton.Visible = true; @@ -225,12 +226,19 @@ namespace AndroidSideloader }; uninstallButtonTimer.Start(); - // Hide button when selection changes - gamesListView.ItemSelectionChanged += (s, ev) => + gamesListView.MouseMove += (s, ev) => { - if (!ev.IsSelected && _listViewUninstallButton != null) + var hitTest = gamesListView.HitTest(ev.Location); + _hoveredItemForDeleteBtn = hitTest.Item; + }; + + gamesListView.MouseLeave += (s, ev) => + { + // Clear hover if mouse left the ListView bounds + Point clientPoint = gamesListView.PointToClient(Control.MousePosition); + if (!gamesListView.ClientRectangle.Contains(clientPoint)) { - _listViewUninstallButton.Visible = false; + _hoveredItemForDeleteBtn = null; } }; @@ -522,13 +530,15 @@ namespace AndroidSideloader // Upload in background _ = Task.Run(() => { - _ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.CurrentCrashPath}\" RSL-gameuploads:CrashLogs"); - this.Invoke(() => - { - _ = FlexibleMessageBox.Show(Program.form, - $"Your CrashLog has been copied to the server.\nPlease mention your CrashLogID ({settings.CurrentCrashName}) to the Mods.\nIt has been automatically copied to your clipboard."); - Clipboard.SetText(settings.CurrentCrashName); - }); + if (hasUploadConfig) { + _ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.CurrentCrashPath}\" RSL-gameuploads:CrashLogs"); + this.Invoke(() => + { + _ = FlexibleMessageBox.Show(Program.form, + $"Your CrashLog has been copied to the server.\nPlease mention your CrashLogID ({settings.CurrentCrashName}) to the Mods.\nIt has been automatically copied to your clipboard."); + Clipboard.SetText(settings.CurrentCrashName); + }); + } }); } } @@ -567,7 +577,7 @@ namespace AndroidSideloader string configFilePath = Path.Combine(Environment.CurrentDirectory, "vrp-public.json"); // Public config check - if (File.Exists(configFilePath)) + if (File.Exists(configFilePath) && settings.AutoUpdateConfig) { await GetPublicConfigAsync(); if (!hasPublicConfig) @@ -577,28 +587,17 @@ namespace AndroidSideloader "Config Update Failed", MessageBoxButtons.OK); } } - else if (settings.AutoUpdateConfig && settings.CreatePubMirrorFile) + else if (settings.AutoUpdateConfig) { - DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, - "Rookie has detected that you are missing the public config file, would you like to create it?", - "Public Config Missing", MessageBoxButtons.YesNo); - - if (dialogResult == DialogResult.Yes) + // Auto-create the public config file if it doesn't exist + Logger.Log("Public config file missing, creating automatically..."); + File.Create(configFilePath).Close(); + await GetPublicConfigAsync(); + if (!hasPublicConfig) { - File.Create(configFilePath).Close(); - await GetPublicConfigAsync(); - if (!hasPublicConfig) - { - _ = FlexibleMessageBox.Show(Program.form, - "Failed to fetch public mirror config, and the current one is unreadable.\r\nPlease ensure you can access https://vrpirates.wiki/ in your browser.", - "Config Update Failed", MessageBoxButtons.OK); - } - } - else - { - settings.CreatePubMirrorFile = false; - settings.AutoUpdateConfig = false; - settings.Save(); + _ = FlexibleMessageBox.Show(Program.form, + "Failed to fetch public mirror config, and the current one is unreadable.\r\nPlease ensure you can access https://vrpirates.wiki/ in your browser.", + "Config Update Failed", MessageBoxButtons.OK); } } @@ -643,6 +642,12 @@ namespace AndroidSideloader changeTitle("Getting Upload Config..."); await Task.Run(() => SideloaderRCLONE.updateUploadConfig()); + string uploadConfigPath = Path.Combine(Environment.CurrentDirectory, "rclone", "vrp.upload.config"); + if (File.Exists(uploadConfigPath)) + { + hasUploadConfig = true; + } + _ = Logger.Log("Initializing Servers"); changeTitle("Initializing Servers..."); @@ -876,22 +881,52 @@ namespace AndroidSideloader } ADB.DeviceID = GetDeviceID(); - Thread t1 = new Thread(() => - { - output += ADB.Sideload(path); - }) - { - IsBackground = true - }; - t1.Start(); + string filename = Path.GetFileName(path); + changeTitle($"Installing {filename}..."); + progressBar.IsIndeterminate = false; + progressBar.OperationType = "Installing"; + progressBar.Value = 0; + progressBar.StatusText = "Preparing..."; - while (t1.IsAlive) - { - await Task.Delay(100); - } + output = await ADB.SideloadWithProgressAsync( + path, + (percent, eta) => this.Invoke(() => + { + if (percent == 0) + { + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Installing"; + } + else + { + progressBar.IsIndeterminate = false; + progressBar.Value = percent; + } + UpdateProgressStatus("Installing", percent: (int)Math.Round(percent), eta: eta); + progressBar.StatusText = $"Installing · {percent:0.0}%"; + }), + status => this.Invoke(() => + { + if (!string.IsNullOrEmpty(status)) + { + if (status.Contains("Completing Installation")) + { + speedLabel.Text = status; + } + progressBar.StatusText = status; + } + }), + "", + filename); + + // Reset UI on completion + progressBar.Value = 0; + progressBar.StatusText = ""; + progressBar.IsIndeterminate = false; + speedLabel.Text = ""; + changeTitle(""); showAvailableSpace(); - ShowPrcOutput(output); } @@ -984,31 +1019,45 @@ namespace AndroidSideloader progressBar.IsIndeterminate = false; progressBar.Value = 0; progressBar.OperationType = "Copying OBB"; + progressBar.StatusText = "Preparing..."; + + string currentStatusBase = string.Empty; output = await ADB.CopyOBBWithProgressAsync( path, - (progress, eta) => this.Invoke(() => + (percent, eta) => this.Invoke(() => { - progressBar.Value = progress; - string etaStr = eta.HasValue && eta.Value.TotalSeconds > 0 - ? $" · ETA: {eta.Value:mm\\:ss}" - : ""; - speedLabel.Text = $"Progress: {progress}%{etaStr}"; + progressBar.Value = percent; + UpdateProgressStatus("Copying OBB", percent: (int)Math.Round(percent), eta: eta); + + if (!string.IsNullOrEmpty(currentStatusBase)) + { + progressBar.StatusText = $"{currentStatusBase} · {percent:0.0}%"; + } + else + { + progressBar.StatusText = $"Copying · {percent:0.0}%"; + } }), status => this.Invoke(() => { - progressBar.StatusText = status; + currentStatusBase = status ?? string.Empty; + if (!string.IsNullOrEmpty(status)) + { + progressBar.StatusText = status; + } }), folderName); - progressBar.Value = 100; + // Reset UI on completion + progressBar.Value = 0; progressBar.StatusText = ""; - changeTitle("Done."); - showAvailableSpace(); - - ShowPrcOutput(output); - changeTitle(""); + progressBar.IsIndeterminate = false; speedLabel.Text = ""; + changeTitle(""); + + showAvailableSpace(); + ShowPrcOutput(output); } } @@ -1041,6 +1090,9 @@ namespace AndroidSideloader Text = "No Device Connected"; if (!settings.NodeviceMode) { + // Explicitly update sideloading UI when the dialog is shown + UpdateSideloadingUI(true); + DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "No device found. Please ensure the following:\n\n - Developer mode is enabled\n - ADB drivers are installed\n - ADB connection is enabled on your device (this can reset)\n - Your device is plugged in\n\nThen press \"Retry\"", "No device found.", MessageBoxButtons.RetryCancel); if (dialogResult == DialogResult.Retry) { @@ -1184,8 +1236,8 @@ namespace AndroidSideloader string CurrBackups = Path.Combine(backupFolder, date_str); DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, - $"Do you want to backup all gamesaves to:\n{CurrBackups}\\", - "Backup Gamesaves", + $"Do you want to attempt to backup all gamesaves to:\n{CurrBackups}\\", + "Backup All Gamesaves", MessageBoxButtons.YesNo); if (dialogResult1 == DialogResult.No || dialogResult1 == DialogResult.Cancel) return; @@ -1275,7 +1327,10 @@ namespace AndroidSideloader summary.AppendLine($" • {failed}"); } - FlexibleMessageBox.Show(Program.form, summary.ToString(), "Backup Complete"); + summary.AppendLine("\nNote: Some games may not support bulk backup as they require special permissions."); + summary.AppendLine("In this case, use 'BACKUP GAMESAVE WITH ADB' to backup the game individually."); + + FlexibleMessageBox.Show(Program.form, summary.ToString(), "Bulk Backup Complete"); } private async void restorebutton_Click(object sender, EventArgs e) @@ -1715,23 +1770,49 @@ namespace AndroidSideloader }; if (dialog.Show(Handle)) { - Thread t1 = new Thread(() => - { - Sideloader.RecursiveOutput = new ProcessOutput(String.Empty, String.Empty); - Sideloader.RecursiveCopyOBB(dialog.FileName); - }) - { - IsBackground = true - }; - t1.Start(); + changeTitle("Copying OBB folders to device..."); + progressBar.IsIndeterminate = false; + progressBar.Value = 0; + progressBar.OperationType = "Copying OBB"; + progressBar.StatusText = "Preparing..."; + + Sideloader.RecursiveOutput = new ProcessOutput(String.Empty, String.Empty); + + string currentStatusBase = string.Empty; + + await Sideloader.RecursiveCopyOBBAsync( + dialog.FileName, + (percent, eta) => this.Invoke(() => + { + progressBar.Value = percent; + UpdateProgressStatus("Copying OBB", percent: (int)Math.Round(percent), eta: eta); + + if (!string.IsNullOrEmpty(currentStatusBase)) + { + progressBar.StatusText = $"{currentStatusBase} · {percent:0.0}%"; + } + else + { + progressBar.StatusText = $"Copying · {percent:0.0}%"; + } + }), + status => this.Invoke(() => + { + currentStatusBase = status ?? string.Empty; + if (!string.IsNullOrEmpty(status)) + { + changeTitle($"Copying: {status}"); + } + })); + + // Reset UI on completion + progressBar.Value = 0; + progressBar.StatusText = ""; + progressBar.IsIndeterminate = false; + speedLabel.Text = ""; + changeTitle(""); showAvailableSpace(); - - while (t1.IsAlive) - { - await Task.Delay(100); - } - ShowPrcOutput(Sideloader.RecursiveOutput); } } @@ -1776,23 +1857,45 @@ namespace AndroidSideloader if (!data.Contains("+") && !data.Contains("_") && data.Contains(".")) { _ = Logger.Log($"Copying {data} to device"); - changeTitle($"Copying {data} to device..."); + string folderName = Path.GetFileName(data); + changeTitle($"Copying {folderName} to device..."); - Thread t2 = new Thread(() => + progressBar.IsIndeterminate = false; + progressBar.Value = 0; + progressBar.OperationType = "Copying OBB"; + progressBar.StatusText = "Preparing..."; - { - output += ADB.CopyOBB(data); - }) - { - IsBackground = true - }; - t2.Start(); + string currentStatusBase = string.Empty; - while (t2.IsAlive) - { - await Task.Delay(100); - } + output += await ADB.CopyOBBWithProgressAsync( + data, + (percent, eta) => this.Invoke(() => + { + progressBar.Value = percent; + UpdateProgressStatus("Copying OBB", percent: (int)Math.Round(percent), eta: eta); + if (!string.IsNullOrEmpty(currentStatusBase)) + { + progressBar.StatusText = $"{currentStatusBase} · {percent:0.0}%"; + } + else + { + progressBar.StatusText = $"Copying · {percent:0.0}%"; + } + }), + status => this.Invoke(() => + { + currentStatusBase = status ?? string.Empty; + if (!string.IsNullOrEmpty(status)) + { + changeTitle($"Copying: {status}"); + } + }), + folderName); + + // Reset UI after this operation + progressBar.StatusText = ""; + speedLabel.Text = ""; changeTitle(""); settings.CurrPckg = dir; settings.Save(); @@ -1827,42 +1930,88 @@ namespace AndroidSideloader }; t3.Tick += timer_Tick4; t3.Start(); - changeTitle($"Sideloading APK ({filename})"); - Thread t2 = new Thread(() => - { - output += ADB.Sideload(file2); - }) - { - IsBackground = true - }; - t2.Start(); - while (t2.IsAlive) - { - await Task.Delay(100); - } + changeTitle($"Sideloading APK ({filename})"); + progressBar.IsIndeterminate = false; + progressBar.Value = 0; + progressBar.OperationType = "Installing"; + progressBar.StatusText = "Preparing..."; + + output += await ADB.SideloadWithProgressAsync( + file2, + (percent, eta) => this.Invoke(() => + { + if (percent == 0) + { + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Installing"; + } + else + { + progressBar.IsIndeterminate = false; + progressBar.Value = percent; + } + UpdateProgressStatus("Installing", percent: (int)Math.Round(percent), eta: eta); + progressBar.StatusText = $"Installing · {percent:0.0}%"; + }), + status => this.Invoke(() => + { + if (!string.IsNullOrEmpty(status)) + { + if (status.Contains("Completing Installation")) + { + speedLabel.Text = status; + } + progressBar.StatusText = status; + } + }), + cmdout, + filename); t3.Stop(); + + // Reset after APK install + progressBar.StatusText = ""; + speedLabel.Text = ""; + if (Directory.Exists($"{pathname}\\{cmdout}")) { _ = Logger.Log($"Copying OBB folder to device- {cmdout}"); changeTitle($"Copying OBB folder to device..."); - Thread t1 = new Thread(() => - { - if (!string.IsNullOrEmpty(cmdout)) + + progressBar.IsIndeterminate = false; + progressBar.Value = 0; + progressBar.OperationType = "Copying OBB"; + progressBar.StatusText = "Preparing..."; + + string obbStatusBase = string.Empty; + + output += await ADB.CopyOBBWithProgressAsync( + $"{pathname}\\{cmdout}", + (percent, eta) => this.Invoke(() => { - _ = ADB.RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{cmdout}\" && mkdir \"/sdcard/Android/obb/{cmdout}\""); - } - _ = ADB.RunAdbCommandToString($"push \"{pathname}\\{cmdout}\" /sdcard/Android/obb/"); - }) - { - IsBackground = true - }; - t1.Start(); - while (t1.IsAlive) - { - await Task.Delay(100); - } + progressBar.Value = percent; + UpdateProgressStatus("Copying OBB", percent: (int)Math.Round(percent), eta: eta); + + if (!string.IsNullOrEmpty(obbStatusBase)) + { + progressBar.StatusText = $"{obbStatusBase} · {percent:0.0}%"; + } + else + { + progressBar.StatusText = $"Copying · {percent:0.0}%"; + } + }), + status => this.Invoke(() => + { + obbStatusBase = status ?? string.Empty; + }), + cmdout); + + // Reset after OBB copy + progressBar.StatusText = ""; + speedLabel.Text = ""; + changeTitle(""); } } @@ -1898,24 +2047,46 @@ namespace AndroidSideloader string[] folders = Directory.GetDirectories(data); foreach (string folder in folders) { + string folderName = Path.GetFileName(folder); _ = Logger.Log($"Copying {folder} to device"); - changeTitle($"Copying {folder} to device..."); + changeTitle($"Copying {folderName} to device..."); - Thread t2 = new Thread(() => + progressBar.IsIndeterminate = false; + progressBar.Value = 0; + progressBar.OperationType = "Copying OBB"; + progressBar.StatusText = "Preparing..."; - { - output += ADB.CopyOBB(folder); - }) - { - IsBackground = true - }; - t2.Start(); + string folderStatusBase = string.Empty; - while (t2.IsAlive) - { - await Task.Delay(100); - } + output += await ADB.CopyOBBWithProgressAsync( + folder, + (percent, eta) => this.Invoke(() => + { + progressBar.Value = percent; + UpdateProgressStatus("Copying OBB", percent: (int)Math.Round(percent), eta: eta); + if (!string.IsNullOrEmpty(folderStatusBase)) + { + progressBar.StatusText = $"{folderStatusBase} · {percent:0.0}%"; + } + else + { + progressBar.StatusText = $"Copying · {percent:0.0}%"; + } + }), + status => this.Invoke(() => + { + folderStatusBase = status ?? string.Empty; + if (!string.IsNullOrEmpty(status)) + { + changeTitle($"Copying: {status}"); + } + }), + folderName); + + // Reset after folder copy + progressBar.StatusText = ""; + speedLabel.Text = ""; changeTitle(""); settings.CurrPckg = dir; settings.Save(); @@ -1979,43 +2150,85 @@ namespace AndroidSideloader timer.Start(); changeTitle($"Installing {dataname}..."); + progressBar.IsIndeterminate = false; + progressBar.Value = 0; + progressBar.OperationType = "Installing"; + progressBar.StatusText = "Preparing..."; - Thread t1 = new Thread(() => - { - output += ADB.Sideload(data); - }) - { - IsBackground = true - }; - t1.Start(); - while (t1.IsAlive) - { - await Task.Delay(100); - } + output += await ADB.SideloadWithProgressAsync( + data, + (percent, eta) => this.Invoke(() => + { + if (percent == 0) + { + progressBar.IsIndeterminate = true; + progressBar.OperationType = "Installing"; + } + else + { + progressBar.IsIndeterminate = false; + progressBar.Value = percent; + } + UpdateProgressStatus("Installing", percent: (int)Math.Round(percent), eta: eta); + progressBar.StatusText = $"Installing · {percent:0.0}%"; + }), + status => this.Invoke(() => + { + if (!string.IsNullOrEmpty(status)) + { + if (status.Contains("Completing Installation")) + { + speedLabel.Text = status; + } + progressBar.StatusText = status; + } + }), + cmdout, + dataname); timer.Stop(); + // Reset after APK install + progressBar.StatusText = ""; + speedLabel.Text = ""; + if (Directory.Exists($"{pathname}\\{cmdout}")) { _ = Logger.Log($"Copying OBB folder to device- {cmdout}"); changeTitle($"Copying OBB folder to device..."); - Thread t2 = new Thread(() => - { - if (!string.IsNullOrEmpty(cmdout)) - { - _ = ADB.RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{cmdout}\" && mkdir \"/sdcard/Android/obb/{cmdout}\""); - } - _ = ADB.RunAdbCommandToString($"push \"{pathname}\\{cmdout}\" /sdcard/Android/obb/"); - }) - { - IsBackground = true - }; - t2.Start(); - while (t2.IsAlive) - { - await Task.Delay(100); - } + progressBar.IsIndeterminate = false; + progressBar.Value = 0; + progressBar.OperationType = "Copying OBB"; + progressBar.StatusText = "Preparing..."; + + string obbStatusBase = string.Empty; + + output += await ADB.CopyOBBWithProgressAsync( + $"{pathname}\\{cmdout}", + (percent, eta) => this.Invoke(() => + { + progressBar.Value = percent; + UpdateProgressStatus("Copying OBB", percent: (int)Math.Round(percent), eta: eta); + + if (!string.IsNullOrEmpty(obbStatusBase)) + { + progressBar.StatusText = $"{obbStatusBase} · {percent:0.0}%"; + } + else + { + progressBar.StatusText = $"Copying · {percent:0.0}%"; + } + }), + status => this.Invoke(() => + { + obbStatusBase = status ?? string.Empty; + }), + cmdout); + + // Reset after OBB copy + progressBar.StatusText = ""; + speedLabel.Text = ""; changeTitle(""); } } @@ -2032,21 +2245,41 @@ namespace AndroidSideloader File.Copy(data, Path.Combine(foldername, filename)); path = foldername; - Thread t1 = new Thread(() => - { - output += ADB.CopyOBB(path); - }) - { - IsBackground = true - }; _ = Logger.Log($"Copying OBB folder to device- {path}"); changeTitle($"Copying OBB folder to device ({filename})"); - t1.Start(); - while (t1.IsAlive) - { - await Task.Delay(100); - } + progressBar.IsIndeterminate = false; + progressBar.Value = 0; + progressBar.OperationType = "Copying OBB"; + progressBar.StatusText = "Preparing..."; + + string obbStatusBase = string.Empty; + + output += await ADB.CopyOBBWithProgressAsync( + path, + (percent, eta) => this.Invoke(() => + { + progressBar.Value = percent; + UpdateProgressStatus("Copying OBB", percent: (int)Math.Round(percent), eta: eta); + + if (!string.IsNullOrEmpty(obbStatusBase)) + { + progressBar.StatusText = $"{obbStatusBase} · {percent:0.0}%"; + } + else + { + progressBar.StatusText = $"Copying · {percent:0.0}%"; + } + }), + status => this.Invoke(() => + { + obbStatusBase = status ?? string.Empty; + }), + filename); + + // Reset after OBB copy + progressBar.StatusText = ""; + speedLabel.Text = ""; FileSystemUtilities.TryDeleteDirectory(foldername); changeTitle(""); @@ -2104,7 +2337,11 @@ namespace AndroidSideloader } } + // Final reset of all UI elements + progressBar.Value = 0; + progressBar.StatusText = ""; progressBar.IsIndeterminate = false; + speedLabel.Text = ""; showAvailableSpace(); ShowPrcOutput(output); @@ -2321,7 +2558,7 @@ namespace AndroidSideloader string packagename = release[SideloaderRCLONE.PackageNameIndex]; // Parse popularity score from column 6 - if (release.Length > 6 && double.TryParse(release[6], out double score)) + if (release.Length > 6 && StringUtilities.TryParseDouble(release[6], out double score)) { // Track the highest score per package if (popularityScores.TryGetValue(packagename, out var existing)) @@ -2387,7 +2624,7 @@ namespace AndroidSideloader // Check if this is a 0 MB entry that should be excluded bool shouldSkip = false; - if (release.Length > 5 && double.TryParse(release[5], out double sizeInMB)) + if (release.Length > 5 && StringUtilities.TryParseDouble(release[5], out double sizeInMB)) { // If size is 0 MB and this is not already an MR-Fix version if (sizeInMB == 0 && gameName.IndexOf("(MR-Fix)", StringComparison.OrdinalIgnoreCase) < 0) @@ -2470,7 +2707,7 @@ namespace AndroidSideloader item.SubItems[4].Text = item.SubItems[4].Text.Replace(" UTC", ""); // Convert size to GB or MB - if (double.TryParse(item.SubItems[5].Text, out double itemSizeInMB)) + if (StringUtilities.TryParseDouble(item.SubItems[5].Text, out double itemSizeInMB)) { if (itemSizeInMB >= 1024) { @@ -2543,7 +2780,7 @@ namespace AndroidSideloader progressBar.IsIndeterminate = false; - if (either && !updatesNotified && !noAppCheck) + if (either && !updatesNotified && !noAppCheck && hasUploadConfig) { changeTitle(""); DonorsListViewForm donorForm = new DonorsListViewForm(); @@ -2663,7 +2900,7 @@ namespace AndroidSideloader string RlsName = Sideloader.PackageNametoGameName(newGamesToUpload); Logger.Log($"Release name obtained: {RlsName}", LogLevel.INFO); - if (!updatesNotified && !onapplist && newint < 6) + if (!updatesNotified && !onapplist && newint < 6 && hasUploadConfig) { changeTitle("Unrecognized App found. Downloading APK to take a closer look. (This may take a minute)"); @@ -3302,13 +3539,9 @@ Additional Thanks & Resources if (quotaTries > remotesList.Items.Count) { ShowError_QuotaExceeded(); - - if (Application.MessageLoop) - { - isOffline = true; - success = false; - return success; - } + isOffline = true; + success = false; + return success; } return success; @@ -3326,7 +3559,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord FlexibleMessageBox.Show(Program.form, errorMessage, "Unable to connect to remote server"); // Close application after showing the message - Application.Exit(); + // Application.Exit(); } public async void cleanupActiveDownloadStatus() @@ -3443,7 +3676,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord { double sizeMB = 0; if (gameData.Length > 5) - double.TryParse(gameData[5], out sizeMB); + StringUtilities.TryParseDouble(gameData[5], out sizeMB); _queueEffectiveSizes[releaseName] = sizeMB; _effectiveQueueSizeMB += sizeMB; _totalQueueSizeMB += sizeMB; @@ -3483,7 +3716,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord if (gameData.Length > SideloaderRCLONE.ReleaseNameIndex && gameData[SideloaderRCLONE.ReleaseNameIndex].Equals(queuedGame, StringComparison.OrdinalIgnoreCase)) { - if (gameData.Length > 5 && double.TryParse(gameData[5], out double sizeMB)) + if (gameData.Length > 5 && StringUtilities.TryParseDouble(gameData[5], out double sizeMB)) maxQueuedGameSizeMB = Math.Max(maxQueuedGameSizeMB, sizeMB); break; } @@ -3510,7 +3743,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord gameData[SideloaderRCLONE.ReleaseNameIndex].Equals(releaseName, StringComparison.OrdinalIgnoreCase)) { if (gameData.Length > 5) - double.TryParse(gameData[5], out gameSizeMB); + StringUtilities.TryParseDouble(gameData[5], out gameSizeMB); if (gameData.Length > SideloaderRCLONE.PackageNameIndex) packagename = gameData[SideloaderRCLONE.PackageNameIndex]; break; @@ -3704,7 +3937,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord gameData[SideloaderRCLONE.ReleaseNameIndex].Equals(gameName, StringComparison.OrdinalIgnoreCase)) { if (gameData.Length > 5) - double.TryParse(gameData[5], out currentGameSizeMB); + StringUtilities.TryParseDouble(gameData[5], out currentGameSizeMB); break; } } @@ -5846,18 +6079,17 @@ function onYouTubeIframeAPIReady() { return simpleMatch.Success ? simpleMatch.Groups[1].Value : string.Empty; } - // Prepare game name words for matching - string lowerGameName = cleanedGameName.ToLowerInvariant(); + // Normalize: remove apostrophes and convert to lowercase + string lowerGameName = cleanedGameName.ToLowerInvariant().Replace("'", ""); var gameWords = lowerGameName .Split(new[] { ' ', '-', ':', '&' }, StringSplitOptions.RemoveEmptyEntries) .ToList(); - int requiredMatches = Math.Max(1, gameWords.Count / 2); + int requiredMatches = gameWords.Count; string bestVideoId = null; int bestScore = 0; int position = 0; - // Score each match foreach (Match match in videoMatches) { string videoId = match.Groups[1].Value; @@ -5866,18 +6098,20 @@ function onYouTubeIframeAPIReady() { title = UnicodeEscapeRegex.Replace(title, m => ((char)Convert.ToInt32(m.Groups[1].Value, 16)).ToString()); - // Entry must match at least half the game name - int matchedWords = gameWords.Count(w => title.Contains(w)); - if (matchedWords < requiredMatches) + // Normalize title: remove apostrophes for matching + string normalizedTitle = title.Replace("'", ""); + + // All game words must be present + if (!gameWords.All(w => normalizedTitle.Contains(w))) continue; position++; - // Only process first 5 matches - if (position > 5) + // Only process first 10 matches + if (position > 10) break; - int score = matchedWords * 10; + int score = 0; // Position bonus if (position == 1) score += 30; @@ -5885,9 +6119,10 @@ function onYouTubeIframeAPIReady() { else if (position == 3) score += 10; // Word bonus - if (title.Contains("trailer")) score += 20; - if (title.Contains("official") || title.Contains("launch") || title.Contains("release")) score += 15; - if (title.Contains("announce")) score += 12; // also includes "announcement" + if (title.Contains("trailer") || title.Contains("teaser")) score += 25; + if (title.Contains("official")) score += 20; + if (title.Contains("launch") || title.Contains("release")) score += 15; + if (title.Contains("announce") || title.Contains("reveal")) score += 12; // also includes "announcement" if (title.Contains("gameplay") || title.Contains("vr")) score += 5; // Noise penalty for extra words @@ -5900,6 +6135,9 @@ function onYouTubeIframeAPIReady() { if (title.Contains("review") || title.Contains("tutorial") || title.Contains("how to") || + title.Contains("install") || + title.Contains("guide") || + title.Contains("setup") || title.Contains("reaction")) score -= 30; @@ -5941,27 +6179,6 @@ function onYouTubeIframeAPIReady() { // Update the selected game label in the sidebar selectedGameLabel.Text = CurrentGameName; - // Show uninstall button only for installed games - bool isInstalled = selectedItem.ForeColor.ToArgb() == ColorInstalled.ToArgb() || - selectedItem.ForeColor.ToArgb() == ColorUpdateAvailable.ToArgb() || - selectedItem.ForeColor.ToArgb() == ColorDonateGame.ToArgb(); - - if (isInstalled && _listViewUninstallButton != null) - { - // Position the button at the right side of the selected item - Rectangle itemBounds = selectedItem.Bounds; - int buttonX = gamesListView.ClientSize.Width - _listViewUninstallButton.Width - 5; - int buttonY = itemBounds.Top + (itemBounds.Height - _listViewUninstallButton.Height) / 2; - - // Ensure the button stays within visible bounds - if (buttonY >= 0 && buttonY + _listViewUninstallButton.Height <= gamesListView.ClientSize.Height) - { - _listViewUninstallButton.Location = new Point(buttonX, buttonY); - _listViewUninstallButton.Tag = selectedItem; // Store reference to the item - _listViewUninstallButton.Visible = true; - } - } - // Thumbnail if (!keyheld) { @@ -6014,7 +6231,7 @@ function onYouTubeIframeAPIReady() { var videoId = await ResolveVideoIdAsync(CurrentGameName); if (string.IsNullOrEmpty(videoId)) { - changeTitle("No Trailer found"); + changeTitle("No Trailer found", true); ShowVideoPlaceholder(); } else @@ -6547,6 +6764,29 @@ function onYouTubeIframeAPIReady() { settings.AddFavoriteGame(packageName); UpdateFavoriteMenuItemText(); + + // If currently viewing favorites, refresh the list to reflect the change + bool isViewingFavorites = favoriteSwitcher.Text == "ALL"; + if (isViewingFavorites) + { + var favSet = new HashSet(settings.FavoritedGames, StringComparer.OrdinalIgnoreCase); + + var favoriteItems = _allItems + .Where(item => item.SubItems.Count > 1 && favSet.Contains(item.SubItems[1].Text)) + .ToList(); + + gamesListView.BeginUpdate(); + gamesListView.Items.Clear(); + gamesListView.Items.AddRange(favoriteItems.ToArray()); + gamesListView.EndUpdate(); + + _galleryDataSource = favoriteItems; + if (isGalleryView && _fastGallery != null) + { + _fastGallery.RefreshFavoritesCache(); + _fastGallery.UpdateItems(favoriteItems); + } + } } private void UpdateFavoriteMenuItemText() @@ -7055,6 +7295,7 @@ function onYouTubeIframeAPIReady() { _fastGallery.TileDoubleClicked += FastGallery_TileDoubleClicked; _fastGallery.TileDeleteClicked += FastGallery_TileDeleteClicked; _fastGallery.SortChanged += FastGallery_SortChanged; + _fastGallery.TileHovered += FastGallery_TileHovered; // Apply current shared sort state to gallery _fastGallery.SetSortState(_sharedSortField, _sharedSortDirection); @@ -7088,6 +7329,14 @@ function onYouTubeIframeAPIReady() { SaveWindowState(); } + private void FastGallery_TileHovered(object sender, string releaseName) + { + if (string.IsNullOrEmpty(releaseName)) return; + + string notePath = Path.Combine(SideloaderRCLONE.NotesFolder, $"{releaseName}.txt"); + UpdateReleaseNotes(notePath); + } + private void GamesGalleryView_Resize(object sender, EventArgs e) { if (_fastGallery != null && !_fastGallery.IsDisposed) @@ -7979,7 +8228,7 @@ function onYouTubeIframeAPIReady() { if (game.Length > SideloaderRCLONE.ReleaseNameIndex && game[SideloaderRCLONE.ReleaseNameIndex].Equals(releaseName, StringComparison.OrdinalIgnoreCase)) { - if (game.Length > 5 && double.TryParse(game[5], out double sizeMB)) + if (game.Length > 5 && StringUtilities.TryParseDouble(game[5], out double sizeMB)) { _totalQueueSizeMB += sizeMB; } @@ -8500,7 +8749,7 @@ function onYouTubeIframeAPIReady() { UpdateSideloadingUI(); } - public void UpdateSideloadingUI() + public void UpdateSideloadingUI(bool isNoDeviceDialogShown = false) { // Update the sideload button text if (settings.NodeviceMode) @@ -8517,11 +8766,19 @@ function onYouTubeIframeAPIReady() { { sideloadingStatusLabel.Text = "Sideloading: Disabled"; sideloadingStatusLabel.ForeColor = Color.FromArgb(255, 100, 100); // Red-ish for disabled + downloadInstallGameButton.Text = "DOWNLOAD"; + } + else if (isNoDeviceDialogShown || (!DeviceConnected && !isLoading)) + { + sideloadingStatusLabel.Text = "Sideloading: No Device Connected"; + sideloadingStatusLabel.ForeColor = Color.FromArgb(240, 150, 50); // Orange for no device + downloadInstallGameButton.Text = "DOWNLOAD"; } else { sideloadingStatusLabel.Text = "Sideloading: Enabled"; sideloadingStatusLabel.ForeColor = Color.FromArgb(93, 203, 173); // Accent green for enabled + downloadInstallGameButton.Text = "DOWNLOAD AND INSTALL"; } } diff --git a/Properties/Settings.settings b/Properties/Settings.settings index 8f67ef7..c77e48f 100644 --- a/Properties/Settings.settings +++ b/Properties/Settings.settings @@ -193,5 +193,6 @@ + \ No newline at end of file diff --git a/Sideloader.cs b/Sideloader.cs index 5c5f0c1..09538c2 100644 --- a/Sideloader.cs +++ b/Sideloader.cs @@ -1,13 +1,14 @@ -using JR.Utils.GUI.Forms; +using AndroidSideloader.Utilities; +using JR.Utils.GUI.Forms; using System; using System.Diagnostics; +using System.Drawing; using System.IO; using System.Management; using System.Net; using System.Text.RegularExpressions; -using System.Drawing; +using System.Threading.Tasks; using System.Windows.Forms; -using AndroidSideloader.Utilities; namespace AndroidSideloader { @@ -84,9 +85,12 @@ namespace AndroidSideloader return output; } - //Recursive sideload any apk fileD + //Recursive sideload any apk file public static ProcessOutput RecursiveOutput = new ProcessOutput(); - public static void RecursiveSideload(string FolderPath) + public static async Task RecursiveSideloadAsync( + string FolderPath, + Action progressCallback = null, + Action statusCallback = null) { try { @@ -94,31 +98,40 @@ namespace AndroidSideloader { if (Path.GetExtension(f) == ".apk") { - RecursiveOutput += ADB.Sideload(f); + string gameName = Path.GetFileNameWithoutExtension(f); + statusCallback?.Invoke(gameName); + RecursiveOutput += await ADB.SideloadWithProgressAsync(f, progressCallback, statusCallback, "", gameName); } } foreach (string d in Directory.GetDirectories(FolderPath)) { - RecursiveSideload(d); + await RecursiveSideloadAsync(d, progressCallback, statusCallback); } } catch (Exception ex) { _ = Logger.Log(ex.Message, LogLevel.ERROR); } } //Recursive copy any obb folder - public static void RecursiveCopyOBB(string FolderPath) + public static async Task RecursiveCopyOBBAsync( + string FolderPath, + Action progressCallback = null, + Action statusCallback = null) { try { - foreach (string f in Directory.GetFiles(FolderPath)) - { - RecursiveOutput += ADB.CopyOBB(f); - } - foreach (string d in Directory.GetDirectories(FolderPath)) { - RecursiveCopyOBB(d); + string folderName = Path.GetFileName(d); + if (folderName.Contains(".")) + { + statusCallback?.Invoke(folderName); + RecursiveOutput += await ADB.CopyOBBWithProgressAsync(d, progressCallback, statusCallback, folderName); + } + else + { + await RecursiveCopyOBBAsync(d, progressCallback, statusCallback); + } } } catch (Exception ex) { _ = Logger.Log(ex.Message, LogLevel.ERROR); } diff --git a/Updater.cs b/Updater.cs index be2025a..8a93186 100644 --- a/Updater.cs +++ b/Updater.cs @@ -13,7 +13,7 @@ namespace AndroidSideloader private static readonly string RawGitHubUrl = "https://raw.githubusercontent.com/VRPirates/rookie"; public static readonly string GitHubUrl = "https://github.com/VRPirates/rookie"; - public static readonly string LocalVersion = "3.0"; + public static readonly string LocalVersion = "3.0.1"; public static string currentVersion = string.Empty; public static string changelog = string.Empty; diff --git a/Utilities/StringUtilities.cs b/Utilities/StringUtilities.cs index a8fcaad..b70a239 100644 --- a/Utilities/StringUtilities.cs +++ b/Utilities/StringUtilities.cs @@ -61,5 +61,13 @@ namespace AndroidSideloader.Utilities return s; } + + public static bool TryParseDouble(string value, out double result) + { + return double.TryParse(value, + System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, + out result); + } } } diff --git a/changelog.txt b/changelog.txt index 92bec70..1e1342e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,16 +1,12 @@ -RSL 3.0 +RSL 3.0.1 -Major Rookie overhaul with modernized UI, significant performance improvements and upgraded UX. - -- Added high-performance Gallery View with search, filters, sorting, favorites, hover animations, smooth scrolling and uninstall buttons -- Toggle seamlessly between List and Gallery views with your preference remembered across launches -- Complete UI redesign with new dark theme, modernized components and subtle animations throughout -- Refined navigation, layouts, sizing and color consistency across the entire application -- Added uninstall buttons directly in List and Gallery views for quicker app management -- Improved startup performance through overhaul of initialization logic, removal of splash screen, parallelized async loading, batched version retrieval, optimized metadata extraction and game list initialization -- Instant list filtering through caching and streamlined filter logic (INSTALLED / UPDATE AVAILABLE / NEWER THAN LIST) -- Improved search speed and responsiveness -- Fixed and improved trailer handling with faster trailer loading -- Fixed multiple startup issues including connection errors and zombie ADB instances -- Added local blacklist support allowing users to permanently suppress donation prompts for specific apps -- Reduced application size by removal of now unused assets \ No newline at end of file +- Fixed popularity ranking not working on some systems +- Fixed favorites not updating immediately when removing items +- Improved YouTube trailer matching accuracy +- Implemented real-time progress updates for drag and drop operations +- Refined backup button labels and dialogs +- Gallery View: Added grouped tiles for games with multiple versions (e.g. Beat Saber) +- ListView: Uninstall button now shows on hover instead of click +- Public config file is now created automatically without prompt +- Sideloading status label now shows device connection state +- Download button text now reflects sideloading status \ No newline at end of file diff --git a/version b/version index f398a20..13d683c 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.0 \ No newline at end of file +3.0.1 \ No newline at end of file