diff --git a/MainForm.cs b/MainForm.cs index 0ebd11c..b960177 100755 --- a/MainForm.cs +++ b/MainForm.cs @@ -239,6 +239,9 @@ namespace AndroidSideloader // Subscribe to click events to unfocus search text box this.Click += UnfocusSearchTextBox; + + // Load saved window state + LoadWindowState(); } private void CheckCommandLineArguments() @@ -411,8 +414,11 @@ namespace AndroidSideloader } }); - // Basic UI setup - CenterToScreen(); + // Basic UI setup - only center if no saved position + if (this.StartPosition != FormStartPosition.Manual) + { + CenterToScreen(); + } gamesListView.View = View.Details; gamesListView.FullRowSelect = true; gamesListView.GridLines = false; @@ -2464,6 +2470,12 @@ namespace AndroidSideloader loaded = true; Logger.Log($"initListView total completed in {sw.ElapsedMilliseconds}ms"); + // Show header now that loading is complete + if (_listViewRenderer != null) + { + _listViewRenderer.SuppressHeader = false; + } + // Now that ListView is fully populated and _allItems is initialized, // switch to the user's preferred view this.Invoke(() => @@ -4020,6 +4032,9 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord private async void Form1_FormClosing(object sender, FormClosingEventArgs e) { + // Save window state before closing + SaveWindowState(); + // Cleanup DNS helper (stops proxy) DnsHelper.Cleanup(); @@ -4697,6 +4712,9 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord // Invalidate header to update sort indicators gamesListView.Invalidate(new Rectangle(0, 0, gamesListView.ClientSize.Width, gamesListView.Font.Height + 8)); + + // Save sort state + SaveWindowState(); } private void CheckEnter(object sender, System.Windows.Forms.KeyPressEventArgs e) @@ -6153,6 +6171,15 @@ function onYouTubeIframeAPIReady() { selectedReleaseName = selectedItem.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text; } } + else if (isGalleryView && _fastGallery != null && _fastGallery._selectedIndex >= 0) + { + // Capture selection from gallery view + var galleryItem = _fastGallery.GetItemAtIndex(_fastGallery._selectedIndex); + if (galleryItem != null && galleryItem.SubItems.Count > 1) + { + selectedReleaseName = galleryItem.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text; + } + } // Capture current sort state before switching if (isGalleryView && _fastGallery != null) @@ -6223,6 +6250,22 @@ function onYouTubeIframeAPIReady() { // Apply shared sort state to list view ApplySortToListView(); + + // Scroll to the previously selected item in list view + if (!string.IsNullOrEmpty(selectedReleaseName)) + { + foreach (ListViewItem item in gamesListView.Items) + { + if (item.SubItems.Count > 1 && + item.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text.Equals(selectedReleaseName, StringComparison.OrdinalIgnoreCase)) + { + item.Selected = true; + item.Focused = true; + item.EnsureVisible(); + break; + } + } + } } } @@ -6336,6 +6379,9 @@ function onYouTubeIframeAPIReady() { _sharedSortField = _fastGallery.CurrentSortField; _sharedSortDirection = _fastGallery.CurrentSortDirection; } + + // Save sort state + SaveWindowState(); } private void GamesGalleryView_Resize(object sender, EventArgs e) @@ -7710,6 +7756,143 @@ function onYouTubeIframeAPIReady() { speedLabel.Text = ""; } + + private void SaveWindowState() + { + try + { + // Save maximized state separately + settings.WindowMaximized = this.WindowState == FormWindowState.Maximized; + + // Save normal bounds (not maximized bounds) + if (this.WindowState == FormWindowState.Normal) + { + settings.WindowX = this.Location.X; + settings.WindowY = this.Location.Y; + settings.WindowWidth = this.Size.Width; + settings.WindowHeight = this.Size.Height; + } + else if (this.WindowState == FormWindowState.Maximized) + { + settings.WindowX = this.RestoreBounds.X; + settings.WindowY = this.RestoreBounds.Y; + settings.WindowWidth = this.RestoreBounds.Width; + settings.WindowHeight = this.RestoreBounds.Height; + } + + // Capture current sort state from active view before saving + if (isGalleryView && _fastGallery != null) + { + _sharedSortField = _fastGallery.CurrentSortField; + _sharedSortDirection = _fastGallery.CurrentSortDirection; + } + else if (!isGalleryView && lvwColumnSorter != null) + { + _sharedSortField = ColumnIndexToSortField(lvwColumnSorter.SortColumn); + SortDirection listDirection = lvwColumnSorter.Order == SortOrder.Ascending + ? SortDirection.Ascending + : SortDirection.Descending; + + // Flip popularity when capturing from list view + if (_sharedSortField == SortField.Popularity) + { + _sharedSortDirection = listDirection == SortDirection.Ascending + ? SortDirection.Descending + : SortDirection.Ascending; + } + else + { + _sharedSortDirection = listDirection; + } + } + + // Save sort state + settings.SortColumn = SortFieldToColumnIndex(_sharedSortField); + settings.SortAscending = _sharedSortDirection == SortDirection.Ascending; + + settings.Save(); + } + catch (Exception ex) + { + Logger.Log($"Failed to save window state: {ex.Message}", LogLevel.WARNING); + } + } + + private void LoadWindowState() + { + try + { + // Load window position and size + if (settings.WindowWidth > 0 && settings.WindowHeight > 0) + { + // Validate that the saved position is on a visible screen + Rectangle savedBounds = new Rectangle( + settings.WindowX, + settings.WindowY, + settings.WindowWidth, + settings.WindowHeight); + + bool isOnScreen = false; + foreach (Screen screen in Screen.AllScreens) + { + if (screen.WorkingArea.IntersectsWith(savedBounds)) + { + isOnScreen = true; + break; + } + } + + if (isOnScreen) + { + this.StartPosition = FormStartPosition.Manual; + this.Location = new Point(settings.WindowX, settings.WindowY); + this.Size = new Size(settings.WindowWidth, settings.WindowHeight); + + if (settings.WindowMaximized) + { + this.WindowState = FormWindowState.Maximized; + } + } + else + { + // Saved position is off-screen, use defaults + this.StartPosition = FormStartPosition.CenterScreen; + } + } + + // Load sort state + _sharedSortField = ColumnIndexToSortField(settings.SortColumn); + _sharedSortDirection = settings.SortAscending ? SortDirection.Ascending : SortDirection.Descending; + + // Apply to list view sorter (with popularity flip) + if (settings.SortColumn >= 0 && settings.SortColumn < gamesListView.Columns.Count) + { + lvwColumnSorter.SortColumn = settings.SortColumn; + + // For popularity, flip direction for list view + SortDirection effectiveDirection = _sharedSortDirection; + if (_sharedSortField == SortField.Popularity) + { + effectiveDirection = _sharedSortDirection == SortDirection.Ascending + ? SortDirection.Descending + : SortDirection.Ascending; + } + + lvwColumnSorter.Order = effectiveDirection == SortDirection.Ascending + ? SortOrder.Ascending + : SortOrder.Descending; + } + } + catch (Exception ex) + { + Logger.Log($"Failed to load window state: {ex.Message}", LogLevel.WARNING); + this.StartPosition = FormStartPosition.CenterScreen; + lvwColumnSorter.SortColumn = 0; + lvwColumnSorter.Order = SortOrder.Ascending; + _sharedSortField = SortField.Name; + _sharedSortDirection = SortDirection.Ascending; + } + } } public static class ControlExtensions diff --git a/ModernListView.cs b/ModernListView.cs index 24024c7..6ac3bd1 100644 --- a/ModernListView.cs +++ b/ModernListView.cs @@ -122,9 +122,31 @@ namespace AndroidSideloader [DllImport("user32.dll")] private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); + [DllImport("user32.dll")] + private static extern bool InvalidateRect(IntPtr hWnd, IntPtr lpRect, bool bErase); + [DllImport("gdi32.dll")] private static extern bool DeleteObject(IntPtr hObject); + private bool _suppressHeader = true; + + public bool SuppressHeader + { + get => _suppressHeader; + set + { + if (_suppressHeader == value) return; + _suppressHeader = value; + + // Invalidate ListView and header control + _listView.Invalidate(); + if (_headerCursor.Handle != IntPtr.Zero) + { + InvalidateRect(_headerCursor.Handle, IntPtr.Zero, true); + } + } + } + [StructLayout(LayoutKind.Sequential)] private struct HDHITTESTINFO { @@ -178,6 +200,57 @@ namespace AndroidSideloader protected override void WndProc(ref Message m) { + const int WM_LBUTTONDOWN = 0x0201; + const int WM_LBUTTONUP = 0x0202; + const int WM_LBUTTONDBLCLK = 0x0203; + const int WM_RBUTTONDOWN = 0x0204; + const int WM_RBUTTONUP = 0x0205; + + // Block mouse interaction when header is suppressed, but still handle layout + if (_owner._suppressHeader) + { + // Block mouse clicks + if (m.Msg == WM_LBUTTONDOWN || m.Msg == WM_LBUTTONUP || m.Msg == WM_LBUTTONDBLCLK || + m.Msg == WM_RBUTTONDOWN || m.Msg == WM_RBUTTONUP) + { + m.Result = IntPtr.Zero; + return; + } + + if (m.Msg == WM_SETCURSOR) + { + Cursor.Current = Cursors.Default; + m.Result = (IntPtr)1; + return; + } + + // Still handle HDM_LAYOUT to maintain custom header height + if (m.Msg == HDM_LAYOUT) + { + base.WndProc(ref m); + + try + { + HDLAYOUT hdl = Marshal.PtrToStructure(m.LParam); + WINDOWPOS wpos = Marshal.PtrToStructure(hdl.pwpos); + RECT rc = Marshal.PtrToStructure(hdl.prc); + + wpos.cy = CUSTOM_HEADER_HEIGHT; + rc.top = CUSTOM_HEADER_HEIGHT; + + Marshal.StructureToPtr(wpos, hdl.pwpos, false); + Marshal.StructureToPtr(rc, hdl.prc, false); + } + catch { } + + m.Result = (IntPtr)1; + return; + } + + base.WndProc(ref m); + return; + } + if (m.Msg == WM_ERASEBKGND) { m.Result = (IntPtr)1; @@ -610,6 +683,14 @@ namespace AndroidSideloader var g = e.Graphics; int listViewWidth = _listView.ClientSize.Width; + // During loading, just fill with background color + if (_suppressHeader) + { + g.FillRectangle(RowNormalBrush, new Rectangle(0, e.Bounds.Y, listViewWidth, e.Bounds.Height)); + e.DrawDefault = false; + return; + } + g.FillRectangle(HeaderBgBrush, e.Bounds); if (e.ColumnIndex == _listView.Columns.Count - 1) diff --git a/Utilities/SettingsManager.cs b/Utilities/SettingsManager.cs index e947d73..695fbd5 100644 --- a/Utilities/SettingsManager.cs +++ b/Utilities/SettingsManager.cs @@ -137,6 +137,18 @@ namespace AndroidSideloader.Utilities public string ProxyPort { get; set; } = string.Empty; public bool TrailersEnabled { get; set; } = true; public bool UseGalleryView { get; set; } = true; + + // Window state persistence + public int WindowX { get; set; } = -1; + public int WindowY { get; set; } = -1; + public int WindowWidth { get; set; } = -1; + public int WindowHeight { get; set; } = -1; + public bool WindowMaximized { get; set; } = false; + + // Sort state persistence + public int SortColumn { get; set; } = 0; + public bool SortAscending { get; set; } = true; + private SettingsManager() { Load(); @@ -261,6 +273,13 @@ namespace AndroidSideloader.Utilities ProxyPort = string.Empty; TrailersEnabled = true; UseGalleryView = true; + WindowX = -1; + WindowY = -1; + WindowWidth = -1; + WindowHeight = -1; + WindowMaximized = false; + SortColumn = 0; + SortAscending = true; Save(); Debug.WriteLine("Default settings created.");