using AndroidSideloader.Models; using AndroidSideloader.Utilities; using JR.Utils.GUI.Forms; using Microsoft.Web.WebView2.Core; using Newtonsoft.Json; using SergeUtils; using System; using System.Collections.Generic; using System.ComponentModel; using System.Configuration; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace AndroidSideloader { public partial class MainForm : Form { private readonly ListViewColumnSorter lvwColumnSorter; private static readonly SettingsManager settings = SettingsManager.Instance; #if DEBUG public static bool debugMode = true; public bool DeviceConnected; public bool keyheld; public bool keyheld2; public static string CurrAPK; public static string CurrPCKG; List gamesToUpload = new List(); public static string currremotesimple = String.Empty; #else public bool keyheld; public static string CurrAPK; public static string CurrPCKG; private readonly List gamesToUpload = new List(); public static bool debugMode = false; public bool DeviceConnected = false; public static string currremotesimple = ""; #endif // Shared sort state between Gallery and List views private SortField _sharedSortField = SortField.Name; private SortDirection _sharedSortDirection = SortDirection.Ascending; private const int BottomMargin = 8; private const int RightMargin = 12; private const int PanelSpacing = 10; private const int BottomPanelHeight = 217; private const int ChildTopMargin = 10; private const int ChildHorizontalPadding = 12; // default left/right private const int NotesLeftMargin = 6; // special left margin for notes private const int ChildRightMargin = 12; private const int LabelHeight = 20; private const int LabelBottomOffset = 4; // space from label bottom to panel bottom private const int ReservedLabelHeight = 25; private Task _adbInitTask; public static readonly Color ColorInstalled = ColorTranslator.FromHtml("#3c91e6"); public static readonly Color ColorUpdateAvailable = ColorTranslator.FromHtml("#4daa57"); public static readonly Color ColorDonateGame = ColorTranslator.FromHtml("#cb9cf2"); private static readonly Color ColorError = ColorTranslator.FromHtml("#f52f57"); private Panel _listViewUninstallButton; private bool _listViewUninstallButtonHovered = false; private bool isGalleryView; // Will be set from settings in constructor private List _galleryDataSource; private FastGalleryPanel _fastGallery; private const int TILE_WIDTH = 180; private const int TILE_HEIGHT = 125; private const int TILE_SPACING = 10; private string freeSpaceText = ""; private string freeSpaceTextDetailed = ""; private int _questStorageProgress = 0; private bool _trailerPlayerInitialized; // player.html created and loaded private bool _trailerHtmlLoaded; // initial navigation completed private static readonly Dictionary _videoIdCache = new Dictionary(StringComparer.OrdinalIgnoreCase); // per game cache private bool isLoading = true; public static bool isOffline = false; public static bool noRcloneUpdating; public static bool noAppCheck = false; public static bool hasPublicConfig = false; public static bool UsingPublicConfig = false; public static bool enviromentCreated = false; public static PublicConfig PublicConfigFile; public static string PublicMirrorExtraArgs = " --tpslimit 1.0 --tpslimit-burst 3"; public static string storedIpPath; public static string aaptPath; private System.Windows.Forms.Timer _debounceTimer; private CancellationTokenSource _cts; private List _allItems; private Dictionary> _searchIndex; public MainForm() { storedIpPath = Path.Combine(Environment.CurrentDirectory, "platform-tools", "StoredIP.txt"); aaptPath = Path.Combine(Environment.CurrentDirectory, "platform-tools", "aapt.exe"); InitializeComponent(); InitializeModernPanels(); // Initialize modern rounded panels for notes and queue Logger.Initialize(); InitializeTimeReferences(); CheckCommandLineArguments(); // Use same icon as the executable this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); // Load user's preferred view from settings isGalleryView = settings.UseGalleryView; // Always start with ListView visible so selections work properly // We'll switch to gallery view after initListView completes if needed gamesListView.Visible = true; gamesGalleryView.Visible = false; btnViewToggle.Text = isGalleryView ? "LIST" : "GALLERY"; favoriteGame.Renderer = new CenteredMenuRenderer(); // Set initial wireless ADB button text based on current state UpdateWirelessADBButtonText(); _debounceTimer = new System.Windows.Forms.Timer { Interval = 100, Enabled = false }; _debounceTimer.Tick += async (sender, e) => await RunSearch(); SetCurrentLogPath(); StartTimers(); lvwColumnSorter = new ListViewColumnSorter(); gamesListView.ListViewItemSorter = lvwColumnSorter; // Initialize modern ListView renderer _listViewRenderer = new ModernListView(gamesListView, lvwColumnSorter); // Set a larger item height for increased spacing between rows ImageList rowSpacingImageList = new ImageList(); rowSpacingImageList.ImageSize = new Size(1, 28); gamesListView.SmallImageList = rowSpacingImageList; SubscribeToHoverEvents(questInfoPanel); this.Resize += MainForm_Resize; // Create an uninstall button overlay for list view _listViewUninstallButton = new Panel { Size = new Size(22, 22), BackColor = Color.Transparent, Visible = false, Cursor = Cursors.Hand }; _listViewUninstallButton.Paint += ListViewUninstallButton_Paint; _listViewUninstallButton.MouseEnter += (s, ev) => { _listViewUninstallButtonHovered = true; _listViewUninstallButton.Invalidate(); }; _listViewUninstallButton.MouseLeave += (s, ev) => { _listViewUninstallButtonHovered = false; _listViewUninstallButton.Invalidate(); }; _listViewUninstallButton.Click += ListViewUninstallButton_Click; gamesListView.Controls.Add(_listViewUninstallButton); // Timer to keep button position synced with the selected item var uninstallButtonTimer = new System.Windows.Forms.Timer { Interval = 16 }; // ~60fps uninstallButtonTimer.Tick += (s, ev) => { if (_listViewUninstallButton == null) return; // Check if we have a tagged item to track if (!(_listViewUninstallButton.Tag is ListViewItem item)) return; // Verify item is still valid and selected if (!gamesListView.Items.Contains(item) || !item.Selected) { _listViewUninstallButton.Visible = false; return; } // Check if item is installed bool isInstalled = item.ForeColor.ToArgb() == ColorInstalled.ToArgb() || item.ForeColor.ToArgb() == ColorUpdateAvailable.ToArgb() || item.ForeColor.ToArgb() == ColorDonateGame.ToArgb(); if (!isInstalled) { _listViewUninstallButton.Visible = false; return; } // Calculate header height (items start below the header) int headerHeight = 0; if (gamesListView.View == View.Details && gamesListView.HeaderStyle != ColumnHeaderStyle.None) { headerHeight = gamesListView.Font.Height; } // Calculate button position based on item bounds Rectangle itemBounds = item.Bounds; int buttonX = gamesListView.ClientSize.Width - _listViewUninstallButton.Width - 5; int buttonY = itemBounds.Top + (itemBounds.Height - _listViewUninstallButton.Height) / 2; // Check if item is within visible bounds (below header and above bottom) bool isVisible = itemBounds.Top >= headerHeight && buttonY >= headerHeight && buttonY + _listViewUninstallButton.Height <= gamesListView.ClientSize.Height; if (isVisible) { _listViewUninstallButton.Location = new Point(buttonX, buttonY); if (!_listViewUninstallButton.Visible) { _listViewUninstallButton.Visible = true; } } else { _listViewUninstallButton.Visible = false; } }; uninstallButtonTimer.Start(); // Hide button when selection changes gamesListView.ItemSelectionChanged += (s, ev) => { if (!ev.IsSelected && _listViewUninstallButton != null) { _listViewUninstallButton.Visible = false; } }; // Set data that apparently can't be set in designer // We do it here so it doesn't get overwritten by designer batteryLevImg.Parent = questStorageProgressBar; batteryLabel.Parent = batteryLevImg; diskLabel.Parent = questStorageProgressBar; questInfoLabel.Parent = questStorageProgressBar; // Subscribe to click events to unfocus search text box this.Click += UnfocusSearchTextBox; } private void CheckCommandLineArguments() { string[] args = Environment.GetCommandLineArgs(); foreach (string arg in args) { if (arg == "--offline") { isOffline = true; } if (arg == "--no-rclone-update") { noRcloneUpdating = true; } if (arg == "--disable-app-check") { noAppCheck = true; } } } private void InitializeTimeReferences() { // Initialize time references TimeSpan newDayReference = new TimeSpan(96, 0, 0); // Time between asking for new apps if user clicks No. (DEFAULT: 96 hours) TimeSpan newDayReference2 = new TimeSpan(72, 0, 0); // Time between asking for updates after uploading. (DEFAULT: 72 hours) // Calculate time differences DateTime A = settings.LastLaunch; DateTime B = DateTime.Now; DateTime C = settings.LastLaunch2; TimeSpan comparison = B - A; TimeSpan comparison2 = B - C; // Reset properties if enough time has passed if (comparison > newDayReference) { ResetPropertiesAfterTimePassed(); } if (comparison2 > newDayReference2) { ResetProperties2AfterTimePassed(); } } private void ResetPropertiesAfterTimePassed() { settings.ListUpped = false; settings.NonAppPackages = String.Empty; settings.AppPackages = String.Empty; settings.LastLaunch = DateTime.Now; settings.Save(); } private void ResetProperties2AfterTimePassed() { settings.LastLaunch2 = DateTime.Now; settings.SubmittedUpdates = String.Empty; settings.Save(); } private void SetCurrentLogPath() { if (string.IsNullOrEmpty(settings.CurrentLogPath)) { settings.CurrentLogPath = Path.Combine(Environment.CurrentDirectory, "debuglog.txt"); } } private void StartTimers() { // Start timers System.Windows.Forms.Timer t = new System.Windows.Forms.Timer { Interval = 840000 // 14 mins between wakeup commands }; t.Tick += new EventHandler(timer_Tick); t.Start(); System.Windows.Forms.Timer t2 = new System.Windows.Forms.Timer { Interval = 300 // 300ms }; t2.Tick += new EventHandler(timer_Tick2); t2.Start(); // Device connection check timer, runs every second System.Windows.Forms.Timer deviceCheckTimer = new System.Windows.Forms.Timer { Interval = 1000 // 1 second }; deviceCheckTimer.Tick += new EventHandler(timer_DeviceCheck); deviceCheckTimer.Start(); } private async Task GetPublicConfigAsync() { await Task.Run(() => GetDependencies.updatePublicConfig()); try { string configFilePath = Path.Combine(Environment.CurrentDirectory, "vrp-public.json"); if (File.Exists(configFilePath)) { string configFileData = File.ReadAllText(configFilePath); PublicConfig config = JsonConvert.DeserializeObject(configFileData); if (config != null && !string.IsNullOrWhiteSpace(config.BaseUri) && !string.IsNullOrWhiteSpace(config.Password)) { PublicConfigFile = config; hasPublicConfig = true; // Test DNS for the public config hostname after it's been created/updated DnsHelper.TestPublicConfigDns(); } } } catch { hasPublicConfig = false; } } public static string donorApps = String.Empty; private string oldTitle = String.Empty; public static bool updatesNotified = false; public static string backupFolder; private static void KillAdbProcesses() { try { foreach (var p in Process.GetProcessesByName("adb")) { try { if (!p.HasExited) { p.Kill(); p.WaitForExit(3000); } } catch (Exception ex) { Logger.Log($"Failed to kill adb process (PID {p.Id}): {ex.Message}", LogLevel.WARNING); } } } catch (Exception ex) { Logger.Log($"Error enumerating adb processes: {ex.Message}", LogLevel.WARNING); } } private async void Form1_Load(object sender, EventArgs e) { _ = Logger.Log("Starting AndroidSideloader Application"); // Hard kill any lingering adb.exe instances to avoid port/handle conflicts KillAdbProcesses(); // ADB initialization in background _adbInitTask = Task.Run(() => { _ = Logger.Log("Attempting to Initialize ADB Server"); if (File.Exists(Path.Combine(Environment.CurrentDirectory, "platform-tools", "adb.exe"))) { _ = ADB.RunAdbCommandToString("start-server"); } }); // Basic UI setup CenterToScreen(); gamesListView.View = View.Details; gamesListView.FullRowSelect = true; gamesListView.GridLines = false; speedLabel.Text = String.Empty; diskLabel.Text = String.Empty; settings.MainDir = Environment.CurrentDirectory; settings.Save(); changeTitle(isOffline ? "Starting in Offline Mode..." : "Initializing..."); // Non-blocking background cleanup _ = Task.Run(() => { try { if (Directory.Exists(Sideloader.TempFolder)) { Directory.Delete(Sideloader.TempFolder, true); _ = Directory.CreateDirectory(Sideloader.TempFolder); } } catch (Exception ex) { Logger.Log($"Error cleaning temp folder: {ex.Message}", LogLevel.WARNING); } }); // Non-blocking log file cleanup _ = Task.Run(() => { try { string logFilePath = settings.CurrentLogPath; if (File.Exists(logFilePath)) { FileInfo fileInfo = new FileInfo(logFilePath); if (fileInfo.Length > 5 * 1024 * 1024) { File.Delete(logFilePath); } } } catch { } }); // Dependencies and RCLONE in background if (!isOffline) { await Task.Run(() => { changeTitle("Downloading Dependencies..."); GetDependencies.downloadFiles(); changeTitle("Initializing RCLONE..."); RCLONE.Init(); }); } // Crashlog handling if (File.Exists("crashlog.txt")) { if (File.Exists(settings.CurrentCrashPath)) { File.Delete(settings.CurrentCrashPath); } DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, $"Sideloader crashed during your last use.\nPress OK if you'd like to send us your crash log.\n\nNOTE: THIS CAN TAKE UP TO 30 SECONDS.", "Crash Detected", MessageBoxButtons.OKCancel); if (dialogResult == DialogResult.OK) { if (File.Exists(Path.Combine(Environment.CurrentDirectory, "crashlog.txt"))) { string UUID = SideloaderUtilities.UUID(); System.IO.File.Move("crashlog.txt", Path.Combine(Environment.CurrentDirectory, $"{UUID}.log")); settings.CurrentCrashPath = Path.Combine(Environment.CurrentDirectory, $"{UUID}.log"); settings.CurrentCrashName = UUID; settings.Save(); Clipboard.SetText(UUID); // 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); }); }); } } else { File.Delete(Path.Combine(Environment.CurrentDirectory, "crashlog.txt")); } } // Ensure bottom panels are properly laid out LayoutBottomPanels(); webView21.Visible = settings.TrailersEnabled; // Continue with Form1_Shown this.Form1_Shown(sender, e); } private async void Form1_Shown(object sender, EventArgs e) { //searchTextBox.Enabled = false; // Disclaimer thread new Thread(() => { Thread.Sleep(5000); freeDisclaimer.Invoke(() => { freeDisclaimer.Dispose(); freeDisclaimer.Enabled = false; }); }).Start(); if (!isOffline) { string configFilePath = Path.Combine(Environment.CurrentDirectory, "vrp-public.json"); // Public config check if (File.Exists(configFilePath)) { 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 if (settings.AutoUpdateConfig && settings.CreatePubMirrorFile) { 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) { 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(); } } // WebView cleanup in background _ = Task.Run(() => { try { string webViewDirectoryPath = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "RSL", "EBWebView"); if (Directory.Exists(webViewDirectoryPath)) { Directory.Delete(webViewDirectoryPath, true); } } catch { } }); // Pre-initialize trailer player in background try { await EnsureTrailerEnvironmentAsync(); } catch { /* swallow – prewarm should never crash startup */ } } // UI setup remotesList.Items.Clear(); if (hasPublicConfig) { UsingPublicConfig = true; _ = Logger.Log($"Using Public Mirror"); } if (isOffline) { remotesList.Size = System.Drawing.Size.Empty; _ = Logger.Log($"Using Offline Mode"); } if (settings.NodeviceMode) { btnNoDevice.Text = "ENABLE SIDELOADING"; } progressBar.IsIndeterminate = true; progressBar.OperationType = "Loading"; // Update check if (!debugMode && settings.CheckForUpdates && !isOffline) { Updater.AppName = "AndroidSideloader"; Updater.Repository = "VRPirates/rookie"; await Updater.Update(); } if (!isOffline) { changeTitle("Getting Upload Config..."); await Task.Run(() => SideloaderRCLONE.updateUploadConfig()); _ = Logger.Log("Initializing Servers"); changeTitle("Initializing Servers..."); await initMirrors(); if (!UsingPublicConfig) { changeTitle("Grabbing the Games List..."); await Task.Run(() => SideloaderRCLONE.initGames(currentRemote)); } } else { changeTitle("Offline mode enabled, no Rclone"); } // Device connection and Metadata can run simultaneously Task metadataTask = null; Task deviceConnectionTask = null; // Start device connection task deviceConnectionTask = Task.Run(() => { changeTitle("Connecting to device..."); if (!string.IsNullOrEmpty(settings.IPAddress)) { string path = Path.Combine(Environment.CurrentDirectory, "platform-tools", "adb.exe"); ProcessOutput wakeywakey = ADB.RunCommandToString($"\"{path}\" shell input keyevent KEYCODE_WAKEUP", path); if (wakeywakey.Output.Contains("more than one")) { settings.Wired = true; settings.Save(); } else if (wakeywakey.Output.Contains("found")) { settings.Wired = false; settings.Save(); } } if (File.Exists(storedIpPath) && !settings.Wired) { string IPcmndfromtxt = File.ReadAllText(storedIpPath); settings.IPAddress = IPcmndfromtxt; settings.Save(); ProcessOutput IPoutput = ADB.RunAdbCommandToString(IPcmndfromtxt); if (IPoutput.Output.Contains("attempt failed") || IPoutput.Output.Contains("refused")) { this.Invoke(() => { _ = FlexibleMessageBox.Show(Program.form, "Attempt to connect to saved IP has failed. This is usually due to rebooting the device or not having a STATIC IP set in your router.\nYou must enable Wireless ADB again!"); }); settings.IPAddress = ""; settings.Save(); try { File.Delete(storedIpPath); } catch (Exception ex) { Logger.Log($"Unable to delete StoredIP.txt due to {ex.Message}", LogLevel.ERROR); } } else { _ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_available 1"); _ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_enabled 1"); } } else if (!File.Exists(storedIpPath)) { settings.IPAddress = ""; settings.Save(); } }); // Start metadata task in parallel if (UsingPublicConfig) { metadataTask = Task.Run(() => { changeTitle("Updating Metadata..."); SideloaderRCLONE.UpdateMetadataFromPublic(); changeTitle("Processing Metadata..."); SideloaderRCLONE.ProcessMetadataFromPublic(); }); } else if (!isOffline) { metadataTask = Task.Run(() => { changeTitle("Updating Game Notes..."); SideloaderRCLONE.UpdateGameNotes(currentRemote); changeTitle("Updating Game Thumbnails..."); SideloaderRCLONE.UpdateGamePhotos(currentRemote); SideloaderRCLONE.UpdateNouns(currentRemote); if (!Directory.Exists(SideloaderRCLONE.ThumbnailsFolder) || !Directory.Exists(SideloaderRCLONE.NotesFolder)) { this.Invoke(() => { _ = FlexibleMessageBox.Show(Program.form, "It seems you are missing the thumbnails and/or notes database, the first start of the sideloader takes a bit more time, so dont worry if it looks stuck!"); }); } }); } // Wait for both tasks to complete var tasksToWait = new List(); if (deviceConnectionTask != null) tasksToWait.Add(deviceConnectionTask); if (metadataTask != null) tasksToWait.Add(metadataTask); if (tasksToWait.Count > 0) { await Task.WhenAll(tasksToWait); } progressBar.IsIndeterminate = true; progressBar.OperationType = "Loading"; changeTitle("Populating Game List..."); _ = await CheckForDevice(); if (ADB.DeviceID.Length < 5) { nodeviceonstart = true; } // Parallel execution await Task.WhenAll( Task.Run(() => listAppsBtn()) ); isLoading = false; // Initialize list view initListView(false); // Cleanup in background _ = Task.Run(() => { string[] files = Directory.GetFiles(Environment.CurrentDirectory); foreach (string file in files) { string fileName = Path.GetFileName(file); if (!fileName.Contains(settings.CurrentLogName) && !fileName.Contains(settings.CurrentCrashName) && !fileName.Contains("debuglog") && fileName.EndsWith(".txt")) { try { System.IO.File.Delete(file); } catch { } } } }); searchTextBox.Enabled = true; if (isOffline) { remotesList.Size = System.Drawing.Size.Empty; _ = Logger.Log($"Using Offline Mode"); } changeTitlebarToDevice(); UpdateStatusLabels(); } private void timer_Tick(object sender, EventArgs e) { _ = ADB.RunAdbCommandToString("shell input keyevent KEYCODE_WAKEUP"); } private void timer_Tick2(object sender, EventArgs e) { keyheld = false; } public async void changeTitle(string txt, bool reset = false) { try { string titleSuffix = string.IsNullOrWhiteSpace(txt) ? "" : " | " + txt; this.Invoke(() => { Text = "Rookie Sideloader " + Updater.LocalVersion + titleSuffix; rookieStatusLabel.Text = txt; }); if (!reset) { return; } await Task.Delay(TimeSpan.FromSeconds(5)); // Reset to base title without any status message this.Invoke(() => { Text = "Rookie Sideloader " + Updater.LocalVersion; rookieStatusLabel.Text = ""; }); } catch { } } private async void startsideloadbutton_Click(object sender, EventArgs e) { ProcessOutput output = new ProcessOutput("", ""); string path = string.Empty; using (OpenFileDialog openFileDialog = new OpenFileDialog()) { openFileDialog.Filter = "Android apps (*.apk)|*.apk"; openFileDialog.FilterIndex = 2; openFileDialog.RestoreDirectory = true; if (openFileDialog.ShowDialog() == DialogResult.OK) { path = openFileDialog.FileName; } else { return; } } ADB.DeviceID = GetDeviceID(); Thread t1 = new Thread(() => { output += ADB.Sideload(path); }) { IsBackground = true }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } showAvailableSpace(); ShowPrcOutput(output); } public void ShowPrcOutput(ProcessOutput prcout) { string message = $"{prcout.Output}"; if (prcout.Error.Length != 0) { message += $"\nError: {prcout.Error}"; } _ = FlexibleMessageBox.Show(Program.form, message); } public List Devices = new List(); public async Task CheckForDevice() { Devices.Clear(); ADB.DeviceID = GetDeviceID(); string output = await Task.Run(() => ADB.RunAdbCommandToString("devices").Output); // Run off UI thread string[] line = output.Split('\n'); int i = 0; devicesComboBox.Items.Clear(); _ = Logger.Log("Devices:"); foreach (string currLine in line) { if (i > 0 && currLine.Length > 0) { string deviceId = currLine.Split('\t')[0]; Devices.Add(deviceId); _ = devicesComboBox.Items.Add(deviceId); _ = Logger.Log(deviceId + "\n", LogLevel.INFO, false); } Debug.WriteLine(currLine); i++; } if (devicesComboBox.Items.Count > 0) { devicesComboBox.SelectedIndex = 0; string battery = await Task.Run(() => ADB.RunAdbCommandToString("shell dumpsys battery").Output); // Run off UI thread battery = Utilities.StringUtilities.RemoveEverythingBeforeFirst(battery, "level:"); battery = Utilities.StringUtilities.RemoveEverythingAfterFirst(battery, "\n"); battery = Utilities.StringUtilities.KeepOnlyNumbers(battery); batteryLabel.Text = battery; } UpdateQuestInfoPanel(); return devicesComboBox.SelectedIndex; } public async void devicesbutton_Click(object sender, EventArgs e) { _ = await CheckForDevice(); changeTitlebarToDevice(); showAvailableSpace(); } public static void notify(string message) { if (settings.EnableMessageBoxes) { _ = FlexibleMessageBox.Show(new Form { TopMost = true, StartPosition = FormStartPosition.CenterScreen }, message); } } private async void obbcopybutton_Click(object sender, EventArgs e) { ProcessOutput output = new ProcessOutput(String.Empty, String.Empty); FolderSelectDialog dialog = new FolderSelectDialog { Title = "Select OBB folder (must be direct OBB folder, E.G: com.Company.AppName)" }; if (dialog.Show(Handle)) { string path = dialog.FileName; string folderName = Path.GetFileName(path); changeTitle($"Copying {folderName} OBB to device..."); progressBar.IsIndeterminate = false; progressBar.Value = 0; progressBar.OperationType = "Copying OBB"; output = await ADB.CopyOBBWithProgressAsync( path, (progress, eta) => this.Invoke(() => { progressBar.Value = progress; string etaStr = eta.HasValue && eta.Value.TotalSeconds > 0 ? $" · ETA: {eta.Value:mm\\:ss}" : ""; speedLabel.Text = $"Progress: {progress}%{etaStr}"; }), status => this.Invoke(() => { progressBar.StatusText = status; }), folderName); progressBar.Value = 100; progressBar.StatusText = ""; changeTitle("Done."); showAvailableSpace(); ShowPrcOutput(output); changeTitle(""); speedLabel.Text = ""; } } public void changeTitlebarToDevice() { if (Devices.Contains("unauthorized")) { DeviceConnected = false; this.Invoke(() => { Text = "Rookie Sideloader " + Updater.LocalVersion + " | Device Not Authorized"; DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Please check inside your headset for ADB DEBUGGING prompt/notification, check the box \"Always allow from this computer.\" and hit OK.", "Not Authorized", MessageBoxButtons.RetryCancel); if (dialogResult == DialogResult.Retry) { devicesbutton.PerformClick(); } }); } else if (Devices.Count > 0 && Devices[0].Length > 1) // Check if Devices list is not empty and the first device has a valid length { this.Invoke(() => { Text = "Rookie Sideloader " + Updater.LocalVersion + " | Device Connected: " + Devices[0].Replace("device", String.Empty).Trim(); }); DeviceConnected = true; nodeviceonstart = false; // Device connected, clear the flag } else { this.Invoke(() => { DeviceConnected = false; Text = "No Device Connected"; if (!settings.NodeviceMode) { 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) { devicesbutton.PerformClick(); } else { return; } } nodeviceonstart = true; Text = "Rookie Sideloader " + Updater.LocalVersion + " | No Device (Download-Only Mode)"; }); } UpdateQuestInfoPanel(); UpdateStatusLabels(); } public async void showAvailableSpace() { string AvailableSpace = string.Empty; if (!settings.NodeviceMode || DeviceConnected) { try { ADB.DeviceID = GetDeviceID(); Thread t1 = new Thread(() => { AvailableSpace = ADB.GetAvailableSpace(); }); t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } diskLabel.Invoke(() => { diskLabel.Text = AvailableSpace; }); UpdateQuestInfoPanel(); } catch (Exception ex) { _ = Logger.Log($"Unable to get available space with the exception: {ex}", LogLevel.ERROR); } } } public string GetDeviceID() { string deviceId = string.Empty; int index = -1; int itemCount = 0; devicesComboBox.Invoke(() => { index = devicesComboBox.SelectedIndex; itemCount = devicesComboBox.Items.Count; }); if (index != -1) { devicesComboBox.Invoke(() => { deviceId = devicesComboBox.SelectedItem.ToString(); }); } else if (itemCount > 1) { // Multiple devices but none selected - prompt user deviceId = ShowDeviceSelector("Multiple devices detected - Select a device"); } else if (itemCount == 1) { // Only one device, select it automatically devicesComboBox.Invoke(() => { devicesComboBox.SelectedIndex = 0; deviceId = devicesComboBox.SelectedItem.ToString(); }); } return deviceId ?? string.Empty; } public static string taa = String.Empty; private async void backupadbbutton_Click(object sender, EventArgs e) { string selectedApp = ShowInstalledAppSelector("Select an app to backup with ADB"); if (selectedApp == null) { return; } if (!settings.CustomBackupDir) { backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups"); } else { backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups"); } if (!Directory.Exists(backupFolder)) { _ = Directory.CreateDirectory(backupFolder); } string output = String.Empty; string date_str = "ab." + DateTime.Today.ToString("yyyy.MM.dd"); string CurrBackups = Path.Combine(backupFolder, date_str); Program.form.Invoke(new Action(() => { FlexibleMessageBox.Show(Program.form, $"Backing up Game Data to {backupFolder}\\{date_str}"); })); _ = Directory.CreateDirectory(CurrBackups); string GameName = selectedApp; string packageName = Sideloader.gameNameToPackageName(GameName); string InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {packageName} | grep versionCode -F\"").Output; changeTitle("Running ADB Backup..."); _ = FlexibleMessageBox.Show(Program.form, "Click OK on this Message...\r\nThen on your Quest, Unlock your device and confirm the backup operation by clicking on 'Back Up My Data'"); output = ADB.RunAdbCommandToString($"adb backup -f \"{CurrBackups}\\{packageName}.ab\" {packageName}").Output; changeTitle(""); } private async void backupbutton_Click(object sender, EventArgs e) { if (!settings.CustomBackupDir) { backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups"); } else { backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups"); } if (!Directory.Exists(backupFolder)) { _ = Directory.CreateDirectory(backupFolder); } DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, $"Do you want to backup to {backupFolder}?", "Backup?", MessageBoxButtons.YesNo); if (dialogResult1 == DialogResult.No) { return; } ProcessOutput output = new ProcessOutput(String.Empty, String.Empty); Thread t1 = new Thread(() => { string date_str = DateTime.Today.ToString("yyyy.MM.dd"); string CurrBackups = Path.Combine(backupFolder, date_str); Program.form.Invoke(new Action(() => { FlexibleMessageBox.Show(Program.form, $"This may take up to a minute. Backing up gamesaves to {backupFolder}\\{date_str} (year.month.date)"); })); _ = Directory.CreateDirectory(CurrBackups); output = ADB.RunAdbCommandToString($"pull \"/sdcard/Android/data\" \"{CurrBackups}\""); changeTitle("Backing up Game Data in SD/Android/data..."); try { Directory.Move(ADB.adbFolderPath + "\\data", CurrBackups + "\\data"); } catch (Exception ex) { _ = Logger.Log($"Exception on backup: {ex}", LogLevel.ERROR); } }) { IsBackground = true }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); changeTitle("Backing up Game Data in SD/Android/data..."); } ShowPrcOutput(output); changeTitle(""); } private async void restorebutton_Click(object sender, EventArgs e) { ProcessOutput output = new ProcessOutput("", ""); string output_abRestore = string.Empty; if (!settings.CustomBackupDir) { backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups"); } else { backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups"); } FileDialog fileDialog = new OpenFileDialog(); fileDialog.Title = "Select a .ab Backup file or press Cancel to select a Folder"; fileDialog.CheckFileExists = true; fileDialog.CheckPathExists = true; fileDialog.ValidateNames = false; fileDialog.InitialDirectory = backupFolder; fileDialog.Filter = "Android Backup Files (*.ab)|*.ab|All Files (*.*)|*.*"; FolderBrowserDialog folderDialog = new FolderBrowserDialog(); folderDialog.Description = "Select Game Backup folder"; folderDialog.SelectedPath = backupFolder; folderDialog.ShowNewFolderButton = false; // To prevent creating new folders DialogResult fileDialogResult = fileDialog.ShowDialog(); DialogResult folderDialogResult = DialogResult.Cancel; if (fileDialogResult == DialogResult.OK) { string selectedPath = fileDialog.FileName; Logger.Log("Selected .ab file: " + selectedPath); _ = FlexibleMessageBox.Show(Program.form, "Click OK on this Message...\r\nThen on your Quest, Unlock your device and confirm the backup operation by clicking on 'Restore My Data'\r\nRookie will remain frozen until the process is completed."); output_abRestore = ADB.RunAdbCommandToString($"adb restore \"{selectedPath}\"").Output; } if (fileDialogResult != DialogResult.OK) { folderDialogResult = folderDialog.ShowDialog(); } if (folderDialogResult == DialogResult.OK) { string selectedFolder = folderDialog.SelectedPath; Logger.Log("Selected folder: " + selectedFolder); Thread t1 = new Thread(() => { if (selectedFolder.Contains("data")) { output += ADB.RunAdbCommandToString($"push \"{selectedFolder}\" /sdcard/Android/"); } else { output += ADB.RunAdbCommandToString($"push \"{selectedFolder}\" /sdcard/Android/data/"); } }) { IsBackground = true }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } } if (folderDialogResult == DialogResult.OK) { ShowPrcOutput(output); } else if (fileDialogResult == DialogResult.OK) { _ = FlexibleMessageBox.Show(Program.form, $"{output_abRestore}"); } } private string listApps() { ADB.DeviceID = GetDeviceID(); return ADB.RunAdbCommandToString("shell pm list packages -3").Output; } public void listAppsBtn() { m_combo.Invoke(() => { m_combo.Items.Clear(); }); string[] packages = listApps() .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) .Select(p => p.StartsWith("package:") ? p.Substring(8).Trim() : p.Trim()) .ToArray(); // Save the full string for settings settings.InstalledApps = string.Join("\n", packages); settings.Save(); List displayNames = new List(); foreach (string pkg in packages) { string name = pkg; foreach (string[] game in SideloaderRCLONE.games) { if (game.Length > 2 && game[2].Equals(pkg, StringComparison.OrdinalIgnoreCase)) { name = game[0]; // Friendly game name break; } } displayNames.Add(name); } // Sort and populate combo foreach (string name in displayNames.OrderBy(n => n)) { if (!string.IsNullOrWhiteSpace(name)) { m_combo.Invoke(() => { _ = m_combo.Items.Add(name); }); } } m_combo.Invoke(() => { m_combo.MatchingMethod = StringMatchingMethod.NoWildcards; }); } public static bool isuploading = false; public static bool isworking = false; private async void getApkButton_Click(object sender, EventArgs e) { if (isOffline) { notify("You are not connected to the Internet!"); return; } string selectedApp = ShowInstalledAppSelector("Select an app to share/upload"); if (selectedApp == null) { return; } DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, $"Do you want to upload {selectedApp} now?", "Upload app?", MessageBoxButtons.YesNo); if (dialogResult1 == DialogResult.No) { return; } string deviceCodeName = ADB.RunAdbCommandToString("shell getprop ro.product.device").Output.ToLower().Trim(); string codeNamesLink = "https://raw.githubusercontent.com/VRPirates/rookie/master/codenames"; bool codenameExists = false; try { codenameExists = HttpClient.GetStringAsync(codeNamesLink).Result.Contains(deviceCodeName); } catch { _ = Logger.Log("Unable to download Codenames file."); FlexibleMessageBox.Show(Program.form, $"Error downloading Codenames File from Github", "Verification Error", MessageBoxButtons.OK); } _ = Logger.Log($"Found Device Code Name: {deviceCodeName}"); _ = Logger.Log($"Identified as Meta Device: {codenameExists}"); if (codenameExists) { if (!isworking) { isworking = true; progressBar.IsIndeterminate = true; progressBar.OperationType = "Loading"; string HWID = SideloaderUtilities.UUID(); string GameName = selectedApp; string packageName = Sideloader.gameNameToPackageName(GameName); string InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {packageName} | grep versionCode -F\"").Output; InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingBeforeFirst(InstalledVersionCode, "versionCode="); InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingAfterFirst(InstalledVersionCode, " "); ulong VersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode)); string gameName = $"{GameName} v{VersionInt} {packageName} {HWID.Substring(0, 1)} {deviceCodeName}"; string gameZipName = $"{gameName}.zip"; // Delete both zip & txt if the files exist, most likely due to a failed upload. if (File.Exists($"{settings.MainDir}\\{gameZipName}")) { File.Delete($"{settings.MainDir}\\{gameZipName}"); } if (File.Exists($"{settings.MainDir}\\{gameName}.txt")) { File.Delete($"{settings.MainDir}\\{gameName}.txt"); } ProcessOutput output = new ProcessOutput("", ""); changeTitle("Extracting APK...."); _ = Directory.CreateDirectory($"{settings.MainDir}\\{packageName}"); Thread t1 = new Thread(() => { output = Sideloader.getApk(GameName); }) { IsBackground = true }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } changeTitle("Extracting OBB if it exists...."); Thread t2 = new Thread(() => { output += ADB.RunAdbCommandToString($"pull \"/sdcard/Android/obb/{packageName}\" \"{settings.MainDir}\\{packageName}\""); }) { IsBackground = true }; t2.Start(); while (t2.IsAlive) { await Task.Delay(100); } File.WriteAllText($"{settings.MainDir}\\{packageName}\\HWID.txt", HWID); File.WriteAllText($"{settings.MainDir}\\{packageName}\\uploadMethod.txt", "manual"); changeTitle("Zipping extracted application..."); string cmd = $"7z a -mx1 \"{gameZipName}\" .\\{packageName}\\*"; string path = $"{settings.MainDir}\\7z.exe"; progressBar.IsIndeterminate = false; Thread t4 = new Thread(() => { _ = ADB.RunCommandToString(cmd, path); }) { IsBackground = true }; t4.Start(); while (t4.IsAlive) { await Task.Delay(100); } changeTitle("Uploading to server, you can continue to use Rookie while it uploads."); ULLabel.Visible = true; isworking = false; isuploading = true; Thread t3 = new Thread(() => { string currentlyUploading = GameName; changeTitle("Uploading to server, you can continue to use Rookie while it uploads."); // Get size of pending zip upload and write to text file long zipSize = new FileInfo($"{settings.MainDir}\\{gameZipName}").Length; File.WriteAllText($"{settings.MainDir}\\{gameName}.txt", zipSize.ToString()); // Upload size file. _ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameName}.txt\" RSL-gameuploads:"); // Upload zip. _ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameZipName}\" RSL-gameuploads:"); // Delete files once uploaded. File.Delete($"{settings.MainDir}\\{gameName}.txt"); File.Delete($"{settings.MainDir}\\{gameZipName}"); this.Invoke(() => FlexibleMessageBox.Show(Program.form, $"Upload of {currentlyUploading} is complete! Thank you for your contribution!")); Directory.Delete($"{settings.MainDir}\\{packageName}", true); }) { IsBackground = true }; t3.Start(); isuploading = true; while (t3.IsAlive) { await Task.Delay(100); } changeTitle(""); isuploading = false; ULLabel.Visible = false; } else { _ = MessageBox.Show("You must wait until each app is finished uploading to start another."); } } else { FlexibleMessageBox.Show(Program.form, $"You are attempting to upload from an unknown device, please connect a Meta Quest device to upload", "Unknown Device", MessageBoxButtons.OK); } } private async void uninstallAppButton_Click(object sender, EventArgs e) { string selectedApp = ShowInstalledAppSelector("Select an app to uninstall"); if (selectedApp == null) { return; } if (!settings.CustomBackupDir) { backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups"); } else { backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups"); } string packagename; string GameName = selectedApp; DialogResult dialogresult = FlexibleMessageBox.Show($"Are you sure you want to uninstall {GameName}?", "Proceed with uninstall?", MessageBoxButtons.YesNo); if (dialogresult == DialogResult.No) { return; } DialogResult dialogresult2 = FlexibleMessageBox.Show($"Do you want to attempt to automatically backup any saves to {backupFolder}\\{DateTime.Today.ToString("yyyy.MM.dd")}\\", "Attempt Game Backup?", MessageBoxButtons.YesNo); packagename = !GameName.Contains(".") ? Sideloader.gameNameToPackageName(GameName) : GameName; if (dialogresult2 == DialogResult.Yes) { Sideloader.BackupGame(packagename); } ProcessOutput output = new ProcessOutput("", ""); progressBar.IsIndeterminate = true; progressBar.OperationType = "Loading"; Thread t1 = new Thread(() => { output += Sideloader.UninstallGame(packagename); }); t1.Start(); t1.IsBackground = true; while (t1.IsAlive) { await Task.Delay(100); } ShowPrcOutput(output); showAvailableSpace(); progressBar.IsIndeterminate = false; } private async void copyBulkObbButton_Click(object sender, EventArgs e) { FolderSelectDialog dialog = new FolderSelectDialog { Title = "Select your folder with OBBs" }; if (dialog.Show(Handle)) { Thread t1 = new Thread(() => { Sideloader.RecursiveOutput = new ProcessOutput(String.Empty, String.Empty); Sideloader.RecursiveCopyOBB(dialog.FileName); }) { IsBackground = true }; t1.Start(); showAvailableSpace(); while (t1.IsAlive) { await Task.Delay(100); } ShowPrcOutput(Sideloader.RecursiveOutput); } } private async void Form1_DragDrop(object sender, DragEventArgs e) { if (nodeviceonstart && !updatesNotified) { _ = await CheckForDevice(); changeTitlebarToDevice(); showAvailableSpace(); changeTitle("Device detected... refreshing update list."); listAppsBtn(); initListView(false); } changeTitle($"Processing dropped file. If Rookie freezes, please wait. Do not close Rookie!"); DragDropLbl.Visible = false; ProcessOutput output = new ProcessOutput(String.Empty, String.Empty); ADB.DeviceID = GetDeviceID(); progressBar.IsIndeterminate = true; progressBar.OperationType = "Loading"; CurrPCKG = String.Empty; string[] datas = (string[])e.Data.GetData(DataFormats.FileDrop); foreach (string data in datas) { string directory = Path.GetDirectoryName(data); //if is directory string dir = Path.GetDirectoryName(data); string path = $"{dir}\\Install.txt"; if (Directory.Exists(data)) { string installFilePath = Directory.GetFiles(data) .FirstOrDefault(f => string.Equals(Path.GetFileName(f), "install.txt", StringComparison.OrdinalIgnoreCase)); if (installFilePath != null) { // Run commands from install.txt output += Sideloader.RunADBCommandsFromFile(installFilePath); continue; // Skip further processing if install.txt is found } if (!data.Contains("+") && !data.Contains("_") && data.Contains(".")) { _ = Logger.Log($"Copying {data} to device"); changeTitle($"Copying {data} to device..."); Thread t2 = new Thread(() => { output += ADB.CopyOBB(data); }) { IsBackground = true }; t2.Start(); while (t2.IsAlive) { await Task.Delay(100); } changeTitle(""); settings.CurrPckg = dir; settings.Save(); } changeTitle(""); string extension = Path.GetExtension(data); string[] files = Directory.GetFiles(data); foreach (string file2 in files) { if (File.Exists(file2)) { if (file2.EndsWith(".apk")) { string pathname = Path.GetDirectoryName(file2); string filename = file2.Replace($"{pathname}\\", String.Empty); string cmd = $"\"{aaptPath}\" dump badging \"{file2}\" | findstr -i \"package: name\""; _ = Logger.Log($"Running adb command-{cmd}"); string cmdout = ADB.RunCommandToString(cmd, file2).Output; cmdout = Utilities.StringUtilities.RemoveEverythingBeforeFirst(cmdout, "="); cmdout = Utilities.StringUtilities.RemoveEverythingAfterFirst(cmdout, " "); cmdout = cmdout.Replace("'", String.Empty); cmdout = cmdout.Replace("=", String.Empty); CurrPCKG = cmdout; CurrAPK = file2; System.Windows.Forms.Timer t3 = new System.Windows.Forms.Timer { Interval = 150000 // 150 seconds to fail }; 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); } t3.Stop(); 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)) { _ = 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); } } } if (file2.EndsWith(".zip") && settings.BMBFChecked) { string datazip = file2; string zippath = Path.GetDirectoryName(data); datazip = datazip.Replace(zippath, ""); datazip = Utilities.StringUtilities.RemoveEverythingAfterFirst(datazip, "."); datazip = datazip.Replace(".", ""); string command2 = $"\"{settings.MainDir}\\7z.exe\" e \"{file2}\" -o\"{zippath}\\{datazip}\\\""; Thread t2 = new Thread(() => { _ = ADB.RunCommandToString(command2, file2); output += ADB.RunAdbCommandToString($"push \"{zippath}\\{datazip}\" \"/sdcard/ModData/com.beatgames.beatsaber/Mods/SongLoader/CustomLevels/\""); }) { IsBackground = true }; t2.Start(); while (t2.IsAlive) { await Task.Delay(100); } Directory.Delete($"{zippath}\\{datazip}", true); } } } string[] folders = Directory.GetDirectories(data); foreach (string folder in folders) { _ = Logger.Log($"Copying {folder} to device"); changeTitle($"Copying {folder} to device..."); Thread t2 = new Thread(() => { output += ADB.CopyOBB(folder); }) { IsBackground = true }; t2.Start(); while (t2.IsAlive) { await Task.Delay(100); } changeTitle(""); settings.CurrPckg = dir; settings.Save(); } } //if it's a file else if (File.Exists(data)) { string extension = Path.GetExtension(data); if (extension == ".apk") { if (File.Exists($"{dir}\\Install.txt")) { DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Special instructions have been found with this file, would you like to run them automatically?", "Special Instructions found!", MessageBoxButtons.OKCancel); if (dialogResult == DialogResult.Cancel) { return; } else { _ = Logger.Log($"Sideloading custom install.txt"); changeTitle("Sideloading custom install.txt automatically."); Thread t1 = new Thread(() => { output += Sideloader.RunADBCommandsFromFile(path); }) { IsBackground = true }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } changeTitle(""); } } else { string pathname = Path.GetDirectoryName(data); string dataname = data.Replace($"{pathname}\\", ""); string cmd = $"\"{aaptPath}\" dump badging \"{data}\" | findstr -i \"package: name\""; _ = Logger.Log($"Running adb command-{cmd}"); string cmdout = ADB.RunCommandToString(cmd, data).Output; cmdout = Utilities.StringUtilities.RemoveEverythingBeforeFirst(cmdout, "="); cmdout = Utilities.StringUtilities.RemoveEverythingAfterFirst(cmdout, " "); cmdout = cmdout.Replace("'", ""); cmdout = cmdout.Replace("=", ""); CurrPCKG = cmdout; CurrAPK = data; System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer { Interval = 150000 // 150 seconds to fail }; timer.Tick += timer_Tick4; timer.Start(); changeTitle($"Installing {dataname}..."); Thread t1 = new Thread(() => { output += ADB.Sideload(data); }) { IsBackground = true }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } timer.Stop(); 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); } changeTitle(""); } } } //If obb is dragged and dropped alone onto Rookie, Rookie will recreate its obb folder automatically with this code. else if (extension == ".obb") { string filename = Path.GetFileName(data); string foldername = filename.Substring(filename.IndexOf('.') + 1); foldername = foldername.Substring(foldername.IndexOf('.') + 1); foldername = foldername.Replace(".obb", ""); foldername = Path.Combine(Environment.CurrentDirectory, foldername); _ = Directory.CreateDirectory(foldername); 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); } Directory.Delete(foldername, true); changeTitle(""); } // BMBF Zip extraction then push to BMBF song folder on Quest. else if (extension == ".zip" && settings.BMBFChecked) { string datazip = data; string zippath = Path.GetDirectoryName(data); datazip = datazip.Replace(zippath, ""); datazip = Utilities.StringUtilities.RemoveEverythingAfterFirst(datazip, "."); datazip = datazip.Replace(".", ""); string command = $"\"{settings.MainDir}\\7z.exe\" e \"{data}\" -o\"{zippath}\\{datazip}\\\""; Thread t1 = new Thread(() => { _ = ADB.RunCommandToString(command, data); output += ADB.RunAdbCommandToString($"push \"{zippath}\\{datazip}\" \"/sdcard/ModData/com.beatgames.beatsaber/Mods/SongLoader/CustomLevels/\""); }) { IsBackground = true }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } Directory.Delete($"{zippath}\\{datazip}", true); } else if (extension == ".txt") { changeTitle("Sideloading custom install.txt automatically."); Thread t1 = new Thread(() => { output += Sideloader.RunADBCommandsFromFile(path); }) { IsBackground = true }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } changeTitle(""); } } } progressBar.IsIndeterminate = false; showAvailableSpace(); DragDropLbl.Visible = false; ShowPrcOutput(output); listAppsBtn(); } private void Form1_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { e.Effect = DragDropEffects.Copy; } DragDropLbl.Visible = true; DragDropLbl.Text = "Drag APK or OBB"; changeTitle(DragDropLbl.Text); } private void Form1_DragLeave(object sender, EventArgs e) { DragDropLbl.Visible = false; DragDropLbl.Text = String.Empty; changeTitle(""); } private List newGamesList = new List(); private List newGamesToUploadList = new List(); private readonly List gamesToAskForUpdate = new List(); public static bool loaded = false; public static string rookienamelist; public static bool updates = false; public static bool newapps = false; public static int newint = 0; public static int updint = 0; public static bool nodeviceonstart = false; public static bool either = false; private bool _allItemsInitialized = false; private async void initListView(bool favoriteView) { var sw = Stopwatch.StartNew(); Logger.Log("initListView started"); int upToDateCount = 0; int updateAvailableCount = 0; int newerThanListCount = 0; loaded = false; var rookienameSet = new HashSet(StringComparer.OrdinalIgnoreCase); var rookienameListBuilder = new StringBuilder(); string installedApps = settings.InstalledApps; string[] packageList = installedApps.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); if (packageList.Length == 0) { Logger.Log("No installed packages found, continuing in download-only mode"); nodeviceonstart = true; } string[] blacklist = new string[] { }; string[] whitelist = new string[] { }; // Load blacklists/whitelists concurrently await Task.WhenAll( Task.Run(() => { if (File.Exists($"{settings.MainDir}\\nouns\\blacklist.txt")) { blacklist = File.ReadAllLines($"{settings.MainDir}\\nouns\\blacklist.txt"); } string localBlacklistPath = Path.Combine(settings.MainDir, "blacklist.json"); if (File.Exists(localBlacklistPath)) { try { string jsonContent = File.ReadAllText(localBlacklistPath); string[] localBlacklist = JsonConvert.DeserializeObject(jsonContent); if (localBlacklist != null && localBlacklist.Length > 0) { var combined = new List(blacklist); combined.AddRange(localBlacklist); blacklist = combined.ToArray(); Logger.Log($"Loaded {localBlacklist.Length} entries from local blacklist"); } } catch (Exception ex) { Logger.Log($"Error loading local blacklist: {ex.Message}", LogLevel.WARNING); } } }), Task.Run(() => { if (File.Exists($"{settings.MainDir}\\nouns\\whitelist.txt")) { whitelist = File.ReadAllLines($"{settings.MainDir}\\nouns\\whitelist.txt"); } }) ); Logger.Log($"Blacklist/Whitelist loaded in {sw.ElapsedMilliseconds}ms"); int expectedGameCount = SideloaderRCLONE.games.Count > 0 ? SideloaderRCLONE.games.Count : 500; var GameList = new List(expectedGameCount); var rookieList = new List(expectedGameCount); var installedGamesSet = new HashSet(packageList, StringComparer.OrdinalIgnoreCase); var blacklistSet = new HashSet(blacklist, StringComparer.OrdinalIgnoreCase); var whitelistSet = new HashSet(whitelist, StringComparer.OrdinalIgnoreCase); newGamesToUploadList = whitelistSet.Intersect(installedGamesSet, StringComparer.OrdinalIgnoreCase).ToList(); if (SideloaderRCLONE.games.Count > 5) { progressBar.IsIndeterminate = true; progressBar.OperationType = ""; // Use full dumpsys to get all version codes at once Dictionary installedVersions = new Dictionary(packageList.Length, StringComparer.OrdinalIgnoreCase); await Task.Run(() => { Logger.Log("Fetching version codes via full dumpsys..."); var versionSw = Stopwatch.StartNew(); try { var dump = ADB.RunAdbCommandToString("shell dumpsys package").Output; Logger.Log($"Dumpsys returned {dump.Length} chars in {versionSw.ElapsedMilliseconds}ms"); versionSw.Restart(); string currentPkg = null; foreach (var rawLine in dump.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)) { var line = rawLine.TrimStart(); if (line.StartsWith("Package [", StringComparison.Ordinal)) { var start = line.IndexOf('['); var end = line.IndexOf(']'); if (start >= 0 && end > start) { currentPkg = line.Substring(start + 1, end - start - 1); } else { currentPkg = null; } continue; } if (currentPkg != null && line.StartsWith("versionCode=", StringComparison.Ordinal)) { var after = line.Substring(12); int spaceIdx = after.IndexOf(' '); var digits = spaceIdx > 0 ? after.Substring(0, spaceIdx) : after; if (ulong.TryParse(digits, out var v)) { // Only store if it's an installed package we care about if (installedGamesSet.Contains(currentPkg)) { installedVersions[currentPkg] = v; } } currentPkg = null; continue; } } Logger.Log($"Parsed {installedVersions.Count} version codes in {versionSw.ElapsedMilliseconds}ms"); } catch (Exception ex) { Logger.Log($"'dumpsys package' failed: {ex.Message}", LogLevel.ERROR); } Logger.Log($"Version fetch total: {versionSw.ElapsedMilliseconds}ms"); }); Logger.Log($"Version codes collected in {sw.ElapsedMilliseconds}ms"); sw.Restart(); // Precompute cloud max version per package var cloudMaxVersionByPackage = new Dictionary(SideloaderRCLONE.games.Count, StringComparer.OrdinalIgnoreCase); foreach (var release in SideloaderRCLONE.games) { string pkg = release[SideloaderRCLONE.PackageNameIndex]; ulong v = 0; try { v = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(release[SideloaderRCLONE.VersionCodeIndex])); } catch { v = 0; } if (cloudMaxVersionByPackage.TryGetValue(pkg, out var existing)) { if (v > existing) cloudMaxVersionByPackage[pkg] = v; } else { cloudMaxVersionByPackage[pkg] = v; } } Logger.Log($"Cloud versions precomputed in {sw.ElapsedMilliseconds}ms"); sw.Restart(); // Calculate popularity rankings var popularityScores = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (string[] release in SideloaderRCLONE.games) { string packagename = release[SideloaderRCLONE.PackageNameIndex]; // Parse popularity score from column 6 if (release.Length > 6 && double.TryParse(release[6], out double score)) { // Track the highest score per package if (popularityScores.TryGetValue(packagename, out var existing)) { if (score > existing) popularityScores[packagename] = score; } else { popularityScores[packagename] = score; } } } // Sort packages by popularity (descending) and assign rankings var rankedPackages = popularityScores .Where(kvp => kvp.Value > 0) // Exclude 0.00 scores .OrderByDescending(kvp => kvp.Value) .Select((kvp, index) => new { Package = kvp.Key, Rank = index + 1 }) .ToDictionary(x => x.Package, x => x.Rank, StringComparer.OrdinalIgnoreCase); Logger.Log($"Popularity rankings calculated for {rankedPackages.Count} games in {sw.ElapsedMilliseconds}ms"); sw.Restart(); // Build MR-Fix lookup. map base game name to whether an MR-Fix exists var mrFixGames = new HashSet(StringComparer.OrdinalIgnoreCase); const string MrFixTag = "(MR-Fix)"; foreach (string[] release in SideloaderRCLONE.games) { string gameName = release[SideloaderRCLONE.GameNameIndex]; // Check if game name contains "(MR-Fix)" using IndexOf for case-insensitive search int mrFixIndex = gameName.IndexOf(MrFixTag, StringComparison.OrdinalIgnoreCase); if (mrFixIndex >= 0) { string baseGameName = (gameName.Substring(0, mrFixIndex) + gameName.Substring(mrFixIndex + MrFixTag.Length)).Trim(); mrFixGames.Add(baseGameName); } } Logger.Log($"MR-Fix lookup built with {mrFixGames.Count} games in {sw.ElapsedMilliseconds}ms"); sw.Restart(); // Process games on background thread await Task.Run(() => { foreach (string[] release in SideloaderRCLONE.games) { string packagename = release[SideloaderRCLONE.PackageNameIndex]; rookieList.Add(packagename); string gameName = release[SideloaderRCLONE.GameNameIndex]; if (rookienameSet.Add(gameName)) { rookienameListBuilder.Append(gameName).Append('\n'); } var item = new ListViewItem(release); // 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 size is 0 MB and this is not already an MR-Fix version if (sizeInMB == 0 && gameName.IndexOf("(MR-Fix)", StringComparison.OrdinalIgnoreCase) < 0) { // Check if there's an MR-Fix version of this game if (mrFixGames.Contains(gameName)) { shouldSkip = true; } } } if (shouldSkip) { continue; // Skip this entry } // Show the installed version ulong installedVersion = 0; if (installedVersions.TryGetValue(packagename, out ulong installedVersionInt)) { item.ForeColor = ColorInstalled; installedVersion = installedVersionInt; try { cloudMaxVersionByPackage.TryGetValue(packagename, out ulong cloudVersionInt); if (installedVersionInt == cloudVersionInt) { upToDateCount++; } else if (installedVersionInt < cloudVersionInt) { item.ForeColor = ColorUpdateAvailable; updateAvailableCount++; } else if (installedVersionInt > cloudVersionInt) { bool dontget = blacklistSet.Contains(packagename); newerThanListCount++; item.ForeColor = ColorDonateGame; // Only prompt for upload if not blacklisted if (!dontget && !updatesNotified && !isworking && updint < 6 && !settings.SubmittedUpdates.Contains(packagename)) { either = true; updates = true; updint++; string RlsName = Sideloader.PackageNametoGameName(packagename); string GameName = Sideloader.gameNameToSimpleName(RlsName); var gameData = new UpdateGameData(GameName, packagename, installedVersionInt); gamesToAskForUpdate.Add(gameData); } } } catch (Exception ex) { item.ForeColor = ColorError; Logger.Log($"An error occurred while rendering game {release[SideloaderRCLONE.GameNameIndex]} in ListView", LogLevel.ERROR); Logger.Log($"ExMsg: {ex.Message}", LogLevel.ERROR); } } if (installedVersion != 0) { // Show the installed version and attach 'v' to both versions item.SubItems[3].Text = $"v{item.SubItems[3].Text} / v{installedVersion}"; } else { // Attach 'v' to remote version item.SubItems[3].Text = $"v{item.SubItems[3].Text}"; } // Remove ' UTC' from last updated 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 (itemSizeInMB >= 1024) { double sizeInGB = itemSizeInMB / 1024; item.SubItems[5].Text = $"{sizeInGB:F2} GB"; } else { item.SubItems[5].Text = $"{itemSizeInMB:F0} MB"; } } // Replace popularity score with ranking if (rankedPackages.TryGetValue(packagename, out int rank)) { item.SubItems[6].Text = $"#{rank}"; } else { // Unranked (0.00 popularity or not found) item.SubItems[6].Text = "-"; } if (favoriteView) { if (settings.FavoritedGames.Contains(item.SubItems[1].Text)) { GameList.Add(item); } } else { GameList.Add(item); } } }); rookienamelist = rookienameListBuilder.ToString(); Logger.Log($"Game processing completed in {sw.ElapsedMilliseconds}ms"); sw.Restart(); } else if (!isOffline) { SwitchMirrors(); if (!isOffline) { initListView(false); } return; } if (blacklistSet.Count == 0 && GameList.Count == 0 && !settings.NodeviceMode && !isOffline) { _ = FlexibleMessageBox.Show(Program.form, "Rookie seems to have failed to load all resources. Please try restarting Rookie a few times.\nIf error still persists please disable any VPN or firewalls (rookie uses direct download so a VPN is not needed)\nIf this error still persists try a system reboot, reinstalling the program, and lastly posting the problem on telegram.", "Error loading blacklist or game list!"); } var rookieSet = new HashSet(rookieList, StringComparer.OrdinalIgnoreCase); newGamesList = installedGamesSet .Except(rookieSet, StringComparer.OrdinalIgnoreCase) .Except(blacklistSet, StringComparer.OrdinalIgnoreCase) .ToList(); if (blacklistSet.Count > 100 && rookieList.Count > 100) { await ProcessNewApps(newGamesList, blacklistSet.ToList()); } progressBar.IsIndeterminate = false; if (either && !updatesNotified && !noAppCheck) { changeTitle(""); DonorsListViewForm donorForm = new DonorsListViewForm(); _ = donorForm.ShowDialog(this); _ = Focus(); } // Update UI with computed list this.Invoke(() => { changeTitle("Populating update list...\n\n"); int installedTotal = upToDateCount + updateAvailableCount; btnInstalled.Text = $"{installedTotal} INSTALLED"; btnInstalled.ForeColor = ColorInstalled; if (updateAvailableCount != 1) btnUpdateAvailable.Text = $"{updateAvailableCount} UPDATES AVAILABLE"; else btnUpdateAvailable.Text = $"{updateAvailableCount} UPDATE AVAILABLE"; btnUpdateAvailable.ForeColor = ColorUpdateAvailable; btnNewerThanList.Text = $"{newerThanListCount} NEWER THAN LIST"; btnNewerThanList.ForeColor = ColorDonateGame; ListViewItem[] arr = GameList.ToArray(); gamesListView.BeginUpdate(); gamesListView.Items.Clear(); gamesListView.Items.AddRange(arr); gamesListView.EndUpdate(); }); Logger.Log($"UI updated in {sw.ElapsedMilliseconds}ms"); sw.Restart(); changeTitle(""); if (!_allItemsInitialized) { _allItems = gamesListView.Items.Cast().ToList(); _searchIndex = new Dictionary>(_allItems.Count * 2, StringComparer.OrdinalIgnoreCase); foreach (var item in _allItems) { string gameNameKey = item.Text; if (!_searchIndex.TryGetValue(gameNameKey, out var list)) { list = new List(1); _searchIndex[gameNameKey] = list; } list.Add(item); if (item.SubItems.Count > 1) { string releaseName = item.SubItems[1].Text; if (!_searchIndex.TryGetValue(releaseName, out var releaseList)) { releaseList = new List(1); _searchIndex[releaseName] = releaseList; } releaseList.Add(item); } } _allItemsInitialized = true; } loaded = true; Logger.Log($"initListView total completed in {sw.ElapsedMilliseconds}ms"); // Now that ListView is fully populated and _allItems is initialized, // switch to the user's preferred view this.Invoke(() => { if (isGalleryView) { // Now it's safe to switch - ListView has been visible and populated gamesListView.Visible = false; gamesGalleryView.Visible = true; _galleryDataSource = null; PopulateGalleryView(); } }); } private async Task ProcessNewApps(List newGamesList, List blacklistItems) { await Task.Run(() => { foreach (UpdateGameData gameData in gamesToAskForUpdate) { if (!updatesNotified && !settings.SubmittedUpdates.Contains(gameData.Packagename)) { either = true; updates = true; donorApps += gameData.GameName + ";" + gameData.Packagename + ";" + gameData.InstalledVersionInt + ";" + "Update" + "\n"; } } string baseApkPath = Path.Combine(Environment.CurrentDirectory, "platform-tools", "base.apk"); if (blacklistItems.Count > 100 && !noAppCheck) { foreach (string newGamesToUpload in newGamesList) { try { bool onapplist = false; string NewApp = settings.NonAppPackages + "\n" + settings.AppPackages; if (NewApp.Contains(newGamesToUpload)) { onapplist = true; Logger.Log($"App '{newGamesToUpload}' found in app list.", LogLevel.INFO); } string RlsName = Sideloader.PackageNametoGameName(newGamesToUpload); Logger.Log($"Release name obtained: {RlsName}", LogLevel.INFO); if (!updatesNotified && !onapplist && newint < 6) { changeTitle("Unrecognized App found. Downloading APK to take a closer look. (This may take a minute)"); either = true; newapps = true; Logger.Log($"New app detected: {newGamesToUpload}, starting APK extraction and AAPT process.", LogLevel.INFO); string apppath = ADB.RunAdbCommandToString($"shell pm path {newGamesToUpload}").Output; Logger.Log($"ADB command 'pm path' executed. Path: {apppath}", LogLevel.INFO); apppath = Utilities.StringUtilities.RemoveEverythingBeforeFirst(apppath, "/"); apppath = Utilities.StringUtilities.RemoveEverythingAfterFirst(apppath, "\r\n"); if (File.Exists(baseApkPath)) { File.Delete(baseApkPath); Logger.Log("Old base.apk file deleted.", LogLevel.INFO); } Logger.Log($"Pulling APK from path: {apppath}", LogLevel.INFO); _ = ADB.RunAdbCommandToString($"pull \"{apppath}\""); string cmd = $"\"{aaptPath}\" dump badging \"{baseApkPath}\" | findstr -i \"application-label\""; Logger.Log($"Running AAPT command: {cmd}", LogLevel.INFO); string ReleaseName = ADB.RunCommandToString(cmd, aaptPath).Output; Logger.Log($"AAPT command output: {ReleaseName}", LogLevel.INFO); ReleaseName = Utilities.StringUtilities.RemoveEverythingBeforeFirst(ReleaseName, "'"); ReleaseName = Utilities.StringUtilities.RemoveEverythingAfterFirst(ReleaseName, "\r\n"); ReleaseName = ReleaseName.Replace("'", ""); File.Delete(baseApkPath); Logger.Log("Base.apk deleted after extracting release name.", LogLevel.INFO); if (ReleaseName.Contains("Microsoft Windows")) { ReleaseName = RlsName; Logger.Log("Release name fallback to RlsName due to Microsoft Windows detection.", LogLevel.INFO); } Logger.Log($"Final Release Name: {ReleaseName}", LogLevel.INFO); string GameName = Sideloader.gameNameToSimpleName(RlsName); Logger.Log($"Fetching version code for app: {newGamesToUpload}", LogLevel.INFO); string InstalledVersionCode; InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {newGamesToUpload} | grep versionCode -F\"").Output; Logger.Log($"Version code command output: {InstalledVersionCode}", LogLevel.INFO); InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingBeforeFirst(InstalledVersionCode, "versionCode="); InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingAfterFirst(InstalledVersionCode, " "); ulong installedVersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode)); Logger.Log($"Parsed installed version code: {installedVersionInt}", LogLevel.INFO); donorApps += ReleaseName + ";" + newGamesToUpload + ";" + installedVersionInt + ";" + "New App" + "\n"; Logger.Log($"Donor app info updated: {ReleaseName}; {newGamesToUpload}; {installedVersionInt}", LogLevel.INFO); newint++; } } catch (Exception ex) { Logger.Log($"Exception occured in ProcessNewApps: {ex.Message}", LogLevel.ERROR); } } } }); } private static readonly HttpClient HttpClient = new HttpClient(); public static async void doUpload() { Program.form.changeTitle("Uploading to server, you can continue to use Rookie while it uploads."); Program.form.ULLabel.Visible = true; isworking = true; string deviceCodeName = ADB.RunAdbCommandToString("shell getprop ro.product.device").Output.ToLower().Trim(); string codeNamesLink = "https://raw.githubusercontent.com/VRPirates/rookie/master/codenames"; bool codenameExists = false; try { codenameExists = HttpClient.GetStringAsync(codeNamesLink).Result.Contains(deviceCodeName); } catch { _ = Logger.Log("Unable to download Codenames file."); FlexibleMessageBox.Show(Program.form, $"Error downloading Codenames File from Github", "Verification Error", MessageBoxButtons.OK); } if (codenameExists) { foreach (UploadGame game in Program.form.gamesToUpload) { Thread t3 = new Thread(() => { string packagename = game.Pckgcommand; string gameName = $"{game.Uploadgamename} v{game.Uploadversion} {game.Pckgcommand} {SideloaderUtilities.UUID().Substring(0, 1)} {deviceCodeName}"; string gameZipName = $"{gameName}.zip"; // Delete both zip & txt if the files exist, most likely due to a failed upload. if (File.Exists($"{settings.MainDir}\\{gameZipName}")) { File.Delete($"{settings.MainDir}\\{gameZipName}"); } if (File.Exists($"{settings.MainDir}\\{gameName}.txt")) { File.Delete($"{settings.MainDir}\\{gameName}.txt"); } string path = $"{settings.MainDir}\\7z.exe"; string cmd = $"7z a -mx1 \"{settings.MainDir}\\{gameZipName}\" .\\{game.Pckgcommand}\\*"; Program.form.changeTitle("Zipping extracted application..."); _ = ADB.RunCommandToString(cmd, path); if (Directory.Exists($"{settings.MainDir}\\{game.Pckgcommand}")) { Directory.Delete($"{settings.MainDir}\\{game.Pckgcommand}", true); } Program.form.changeTitle("Uploading to server, you can continue to use Rookie while it uploads."); // Get size of pending zip upload and write to text file long zipSize = new FileInfo($"{settings.MainDir}\\{gameZipName}").Length; File.WriteAllText($"{settings.MainDir}\\{gameName}.txt", zipSize.ToString()); // Upload size file. _ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameName}.txt\" RSL-gameuploads:"); // Upload zip. _ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameZipName}\" RSL-gameuploads:"); if (game.isUpdate) { settings.SubmittedUpdates += game.Pckgcommand + "\n"; settings.Save(); } // Delete files once uploaded. if (File.Exists($"{settings.MainDir}\\{gameName}.txt")) { File.Delete($"{settings.MainDir}\\{gameName}.txt"); } if (File.Exists($"{settings.MainDir}\\{gameZipName}")) { File.Delete($"{settings.MainDir}\\{gameZipName}"); } }) { IsBackground = true }; t3.Start(); while (t3.IsAlive) { isuploading = true; await Task.Delay(100); } } Program.form.gamesToUpload.Clear(); isworking = false; isuploading = false; Program.form.ULLabel.Visible = false; Program.form.changeTitle(""); } else { FlexibleMessageBox.Show(Program.form, $"You are attempting to upload from an unknown device, please connect a Meta Quest device to upload", "Unknown Device", MessageBoxButtons.OK); } } public static async void newPackageUpload() { if (!string.IsNullOrEmpty(settings.NonAppPackages) && !settings.ListUpped) { Random r = new Random(); int x = r.Next(9999); int y = x; File.WriteAllText($"{settings.MainDir}\\FreeOrNonVR{y}.txt", settings.NonAppPackages); string path = $"{settings.MainDir}\\rclone\\rclone.exe"; Thread t1 = new Thread(() => { _ = ADB.RunCommandToString($"\"{settings.MainDir}\\rclone\\rclone.exe\" copy \"{settings.MainDir}\\FreeOrNonVR{y}.txt\" VRP-debuglogs:InstalledGamesList", path); File.Delete($"{settings.MainDir}\\FreeOrNonVR{y}.txt"); }) { IsBackground = true }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } settings.ListUpped = true; settings.Save(); } } public async Task extractAndPrepareGameToUploadAsync(string GameName, string packagename, ulong installedVersionInt, bool isupdate) { progressBar.IsIndeterminate = true; progressBar.OperationType = ""; Thread t1 = new Thread(() => { _ = Sideloader.getApk(packagename); }); changeTitle("Extracting APK file...."); t1.IsBackground = true; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } changeTitle("Extracting OBB if it exists...."); Thread t2 = new Thread(() => { _ = ADB.RunAdbCommandToString($"pull \"/sdcard/Android/obb/{packagename}\" \"{settings.MainDir}\\{packagename}\""); }) { IsBackground = true }; t2.Start(); while (t2.IsAlive) { await Task.Delay(100); } string HWID = SideloaderUtilities.UUID(); File.WriteAllText($"{settings.MainDir}\\{packagename}\\HWID.txt", HWID); progressBar.IsIndeterminate = false; UploadGame game = new UploadGame { isUpdate = isupdate, Pckgcommand = packagename, Uploadgamename = GameName, Uploadversion = installedVersionInt }; gamesToUpload.Add(game); } private async Task initMirrors() { _ = Logger.Log("Looking for Additional Mirrors..."); int index = 0; await Task.Run(() => remotesList.Invoke(() => { index = remotesList.SelectedIndex; remotesList.Items.Clear(); })); // Retry logic for RCLONE availability string[] mirrors = null; int maxRetries = 10; int retryCount = 0; while (retryCount < maxRetries) { try { mirrors = await Task.Run(() => RCLONE.runRcloneCommand_DownloadConfig("listremotes").Output.Split('\n')); break; // Success, exit retry loop } catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 2) // File not found { retryCount++; Logger.Log($"RCLONE not ready yet, attempt {retryCount}/{maxRetries}. Waiting...", LogLevel.WARNING); if (retryCount >= maxRetries) { Logger.Log("RCLONE failed to initialize after multiple attempts", LogLevel.ERROR); _ = FlexibleMessageBox.Show(Program.form, "RCLONE could not be initialized. Please check your internet connection and restart the application.\n\nIf the problem persists, try running as Administrator.", "RCLONE Initialization Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); // Fallback: Add only public mirror if available if (hasPublicConfig) { await Task.Run(() => remotesList.Invoke(() => { _ = remotesList.Items.Add("Public"); remotesList.SelectedIndex = 0; })); currentRemote = "Public"; UsingPublicConfig = true; } return; } // Wait before retry (exponential backoff: 500ms, 1s, 2s, 4s, ...) await Task.Delay(Math.Min(500 * (int)Math.Pow(2, retryCount - 1), 5000)); } catch (Exception ex) { Logger.Log($"Unexpected error initializing mirrors: {ex.Message}", LogLevel.ERROR); _ = FlexibleMessageBox.Show(Program.form, $"Error initializing mirrors: {ex.Message}", "Mirror Initialization Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } } if (mirrors == null) { Logger.Log("Failed to retrieve mirrors list", LogLevel.ERROR); return; } _ = Logger.Log("Loaded following mirrors: "); int itemsCount = 0; if (hasPublicConfig) { _ = remotesList.Items.Add("Public"); itemsCount++; } foreach (string mirror in mirrors) { if (mirror.Contains("mirror")) { _ = Logger.Log(mirror.Remove(mirror.Length - 1)); await Task.Run(() => remotesList.Invoke(() => { _ = remotesList.Items.Add(mirror.Remove(mirror.Length - 1).Replace("VRP-mirror", "")); })); itemsCount++; } } if (itemsCount > 0) { await Task.Run(() => remotesList.Invoke(() => { remotesList.SelectedIndex = 0; string selectedRemote = remotesList.SelectedItem.ToString(); currentRemote = ""; if (selectedRemote != "Public") { currentRemote = "VRP-mirror"; } currentRemote = string.Concat(currentRemote, selectedRemote); })); } } public static string processError = string.Empty; public static string currentRemote = string.Empty; private readonly string wrDelimiter = "-------"; private void deviceDropContainer_Click(object sender, EventArgs e) { ToggleContainer(deviceDropContainer, deviceDrop); } private void sideloadContainer_Click(object sender, EventArgs e) { ToggleContainer(sideloadContainer, sideloadDrop); } private void installedAppsMenuContainer_Click(object sender, EventArgs e) { ToggleContainer(installedAppsMenuContainer, installedAppsMenu); } private void backupDrop_Click(object sender, EventArgs e) { ToggleContainer(backupContainer, backupDrop); } private void otherDrop_Click(object sender, EventArgs e) { ToggleContainer(otherContainer, otherDrop); } private async void AnimateContainerHeight(Panel container, bool expand) { // Disable AutoSize during animation container.AutoSize = false; // Store the target height before any changes int targetHeight = expand ? container.PreferredSize.Height : 0; int startHeight = expand ? 0 : container.Height; // For collapsing: hide immediately if already at 0 if (!expand && container.Height == 0) { container.Visible = false; container.AutoSize = true; return; } // Set height before making visible to prevent flicker on expand container.Height = startHeight; // Only show if expanding (collapsing container is already visible) if (expand) { container.Visible = true; } // Suspend layout to prevent child controls from flickering container.SuspendLayout(); // Stopwatch for consistent timing, 1.5ms per pixel height int durationMs = (int)Math.Round(container.PreferredSize.Height * 1.5); var stopwatch = Stopwatch.StartNew(); while (stopwatch.ElapsedMilliseconds < durationMs) { float progress = (float)stopwatch.ElapsedMilliseconds / durationMs; progress = Math.Min(1f, progress); // Ease-out curve float easedProgress = 1f - (1f - progress) * (1f - progress); int newHeight = (int)(startHeight + (targetHeight - startHeight) * easedProgress); container.Height = Math.Max(0, newHeight); // Yield to UI thread, but don't rely on delay accuracy await Task.Delay(1); } // Ensure final state container.Height = targetHeight; // Resume layout before hiding to ensure clean state container.ResumeLayout(false); if (!expand) { container.Visible = false; } container.AutoSize = true; } private void CollapseAllContainersInstant(Panel exceptThis = null) { var containers = new[] { deviceDropContainer, sideloadContainer, installedAppsMenuContainer, backupContainer, otherContainer }; foreach (var container in containers) { if (container != exceptThis && container.Visible) { // Hide before any layout changes to prevent flicker container.Visible = false; container.SuspendLayout(); container.AutoSize = false; container.Height = 0; container.ResumeLayout(false); container.AutoSize = true; } } } private void ToggleContainer(Panel containerToToggle, Button dropButton) { // Collapse all other containers instantly CollapseAllContainersInstant(containerToToggle); // Check if we're collapsing (container is currently visible and has height) bool isExpanding = !containerToToggle.Visible || containerToToggle.Height == 0; if (isExpanding) { // Animate expansion AnimateContainerHeight(containerToToggle, true); } else { // Close instantly without animation - hide before any layout changes containerToToggle.Visible = false; containerToToggle.SuspendLayout(); containerToToggle.AutoSize = false; containerToToggle.Height = 0; containerToToggle.ResumeLayout(false); containerToToggle.AutoSize = true; } } private void settingsButton_Click(object sender, EventArgs e) { SettingsForm settingsForm = new SettingsForm(); settingsForm.Show(Program.form); } private void aboutBtn_Click(object sender, EventArgs e) { string about = $@"Version: {Updater.LocalVersion} This software is free. {Updater.GitHubUrl} Credits & Acknowledgements ----------------------------------------- • Software originally developed by: rookie.wtf • Special thanks to the VRP Mod Staff, Data Team, and all contributors • VRP Staff (past & present): fenopy, Maxine, JarJarBlinkz, pmow, SytheZN, Roma/Rookie, Flow, Ivan, Kaladin, HarryEffinPotter, John, Sam Hoque, JP Additional Thanks & Resources ----------------------------------------- • rclone - https://rclone.org • 7-Zip - https://www.7-zip.org • ErikE - https://stackoverflow.com/users/57611/erike • Serge Weinstock (SergeUtils) • Mike Gold - https://www.c-sharpcorner.com/members/mike-gold2 "; _ = FlexibleMessageBox.Show(Program.form, about); } private async void listApkButton_Click(object sender, EventArgs e) { string titleMessage = "Refreshing devices, apps and update list..."; changeTitle(titleMessage); if (isLoading) { return; } isLoading = true; progressBar.IsIndeterminate = true; progressBar.OperationType = "Refreshing"; devicesbutton_Click(sender, e); await initMirrors(); isLoading = false; await refreshCurrentMirror(titleMessage); } private async Task refreshCurrentMirror(string titleMessage) { changeTitle(titleMessage); if (isLoading) { return; } isLoading = true; progressBar.IsIndeterminate = true; progressBar.OperationType = "Refreshing"; Thread t1 = new Thread(() => { if (!UsingPublicConfig) { SideloaderRCLONE.initGames(currentRemote); } listAppsBtn(); }) { IsBackground = false }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } isLoading = false; // Use RefreshGameListAsync to preserve filter state await RefreshGameListAsync(); changeTitle(""); } private static readonly HttpClient client = new HttpClient(); public static bool reset = false; public static bool updatedConfig = false; public static int steps = 0; public static bool gamesAreDownloading = false; private ModernQueuePanel _queuePanel; private readonly BindingList gamesQueueList = new BindingList(); public static int quotaTries = 0; public static bool timerticked = false; public static bool skiponceafterremove = false; public bool SwitchMirrors() { bool success = true; try { quotaTries++; remotesList.Invoke((MethodInvoker)delegate { if (remotesList.SelectedIndex + 1 == remotesList.Items.Count) { reset = true; for (int i = 0; i < steps; i++) { remotesList.SelectedIndex--; } } if (reset) { remotesList.SelectedIndex--; } if (remotesList.Items.Count > remotesList.SelectedIndex && !reset) { remotesList.SelectedIndex++; steps++; } }); } catch { success = false; } // If we've tried all remotes and failed, show quota exceeded error if (quotaTries > remotesList.Items.Count) { ShowError_QuotaExceeded(); if (Application.MessageLoop) { isOffline = true; success = false; return success; } } return success; } private static void ShowError_QuotaExceeded() { string errorMessage = $@"Rookie cannot reach our servers. If this is your first time launching Rookie, please relaunch and try again. If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.gg/tBKMZy7QDA) for troubleshooting steps."; FlexibleMessageBox.Show(Program.form, errorMessage, "Unable to connect to remote server"); // Close application after showing the message Application.Exit(); } public async void cleanupActiveDownloadStatus() { speedLabel.Text = String.Empty; progressBar.Value = 0; if (gamesQueueList.Count > 0) gamesQueueList.RemoveAt(0); _queuePanel?.Invalidate(); } public void SetProgress(float progress) { if (progressBar.InvokeRequired) { progressBar.Invoke(new Action(() => progressBar.Value = progress)); } else { progressBar.Value = progress; } } public bool isinstalling = false; public static bool isInDownloadExtract = false; public static bool removedownloading = false; public async void downloadInstallGameButton_Click(object sender, EventArgs e) { { if (!settings.CustomDownloadDir) { settings.DownloadDir = Environment.CurrentDirectory.ToString(); } bool obbsMismatch = false; if (nodeviceonstart && !updatesNotified) { _ = await CheckForDevice(); changeTitlebarToDevice(); showAvailableSpace(); listAppsBtn(); } progressBar.IsIndeterminate = true; progressBar.OperationType = "Downloading"; if (gamesListView.SelectedItems.Count == 0) { progressBar.IsIndeterminate = false; changeTitle("You must select a game from the game list!"); return; } string namebox = gamesListView.SelectedItems[0].ToString(); string nameboxtranslated = Sideloader.gameNameToSimpleName(namebox); int count = 0; string[] gamesToDownload; if (gamesListView.SelectedItems.Count > 0) { count = gamesListView.SelectedItems.Count; gamesToDownload = new string[count]; for (int i = 0; i < count; i++) { gamesToDownload[i] = gamesListView.SelectedItems[i].SubItems[SideloaderRCLONE.ReleaseNameIndex].Text; } } else { return; } progressBar.Value = 0; progressBar.IsIndeterminate = false; string game = gamesToDownload.Length == 1 ? $"\"{gamesToDownload[0]}\"" : "the selected games"; isinstalling = true; //Add games to the queue for (int i = 0; i < gamesToDownload.Length; i++) { gamesQueueList.Add(gamesToDownload[i]); } if (gamesAreDownloading) { return; } gamesAreDownloading = true; if (_queuePanel != null) _queuePanel.IsDownloading = true; //Do user json on firsttime if (settings.UserJsonOnGameInstall) { Thread userJsonThread = new Thread(() => { changeTitle("Pushing user.json"); Sideloader.PushUserJsons(); }) { IsBackground = true }; userJsonThread.Start(); } ProcessOutput output = new ProcessOutput("", ""); string gameName = ""; while (gamesQueueList.Count > 0) { gameName = gamesQueueList.ToArray()[0]; string packagename = Sideloader.gameNameToPackageName(gameName); string versioncode = Sideloader.gameNameToVersionCode(gameName); string dir = Path.GetDirectoryName(gameName); string gameDirectory = Path.Combine(settings.DownloadDir, gameName); string downloadDirectory = Path.Combine(settings.DownloadDir, gameName); string path = gameDirectory; string gameNameHash = string.Empty; using (MD5 md5 = MD5.Create()) { byte[] bytes = Encoding.UTF8.GetBytes(gameName + "\n"); byte[] hash = md5.ComputeHash(bytes); StringBuilder sb = new StringBuilder(); foreach (byte b in hash) { _ = sb.Append(b.ToString("x2")); } gameNameHash = sb.ToString(); } ProcessOutput gameDownloadOutput = new ProcessOutput("", ""); _ = Logger.Log($"Starting Game Download"); Thread t1; string extraArgs = string.Empty; if (settings.SingleThreadMode) { extraArgs = "--transfers 1 --multi-thread-streams 0"; } string bandwidthLimit = string.Empty; if (settings.BandwidthLimit > 0) { bandwidthLimit = $"--bwlimit={settings.BandwidthLimit}M"; } if (UsingPublicConfig) { bool doDownload = true; bool skipRedownload = false; if (settings.UseDownloadedFiles == true) { skipRedownload = true; } if (Directory.Exists(gameDirectory)) { if (skipRedownload == true) { if (Directory.Exists($"{settings.DownloadDir}\\{gameName}")) { doDownload = false; } } else { DialogResult res = FlexibleMessageBox.Show(Program.form, $"{gameName} already exists in destination directory.\n\n" + "Yes = Overwrite and re-download.\n" + "No = Use existing files and install from them.", "Download again?", MessageBoxButtons.YesNo); doDownload = res == DialogResult.Yes; } if (doDownload) { // only delete after extraction; allows for resume if the fetch fails midway. if (Directory.Exists($"{settings.DownloadDir}\\{gameName}")) { Directory.Delete($"{settings.DownloadDir}\\{gameName}", true); } } } if (doDownload) { downloadDirectory = $"{settings.DownloadDir}\\{gameNameHash}"; _ = Logger.Log($"rclone copy \"Public:{SideloaderRCLONE.RcloneGamesFolder}/{gameName}\""); t1 = new Thread(() => { string rclonecommand = $"copy \":http:/{gameNameHash}/\" \"{downloadDirectory}\" {extraArgs} --progress --rc {bandwidthLimit}"; gameDownloadOutput = RCLONE.runRcloneCommand_PublicConfig(rclonecommand); }); Utilities.Metrics.CountDownload(packagename, versioncode); } else { t1 = new Thread(() => { gameDownloadOutput = new ProcessOutput("Download skipped."); }); } } else { _ = Directory.CreateDirectory(gameDirectory); downloadDirectory = $"{SideloaderRCLONE.RcloneGamesFolder}/{gameName}"; _ = Logger.Log($"rclone copy \"{currentRemote}:{downloadDirectory}\""); t1 = new Thread(() => { gameDownloadOutput = RCLONE.runRcloneCommand_DownloadConfig($"copy \"{currentRemote}:{downloadDirectory}\" \"{settings.DownloadDir}\\{gameName}\" {extraArgs} --progress --rc --retries 2 --low-level-retries 1 --check-first {bandwidthLimit}"); }); Utilities.Metrics.CountDownload(packagename, versioncode); } if (Directory.Exists(downloadDirectory)) { string[] partialFiles = Directory.GetFiles($"{downloadDirectory}", "*.partial"); foreach (string file in partialFiles) { File.Delete(file); _ = Logger.Log($"Deleted partial file: {file}"); } } t1.IsBackground = true; t1.Start(); changeTitle("Downloading game " + gameName); speedLabel.Text = "Starting download..."; // Track the highest valid progress to prevent brief progress bar flashes during multi-file transfers float highestValidPercent = 0; // Download while (t1.IsAlive) { try { HttpResponseMessage response = await client.PostAsync("http://127.0.0.1:5572/core/stats", null); string foo = await response.Content.ReadAsStringAsync(); dynamic results = JsonConvert.DeserializeObject(foo); if (results["transferring"] != null) { double totalSize = 0; double downloadedSize = 0; long fileCount = 0; long transfersComplete = 0; long totalChecks = 0; long globalEta = 0; float speed = 0; float downloadSpeed = 0; double estimatedFileCount = 0; totalSize = results["totalBytes"]; downloadedSize = results["bytes"]; fileCount = results["totalTransfers"]; totalChecks = results["totalChecks"]; transfersComplete = results["transfers"]; globalEta = results["eta"]; speed = results["speed"]; estimatedFileCount = Math.Ceiling(totalSize / 524288000); // maximum part size if (totalChecks > fileCount) { fileCount = totalChecks; } if (estimatedFileCount > fileCount) { fileCount = (long)estimatedFileCount; } downloadSpeed = speed / 1000000; totalSize /= 1000000; downloadedSize /= 1000000; progressBar.IsIndeterminate = false; float percent = 0; if (totalSize > 0) { percent = (float)(downloadedSize / totalSize * 100); } // Clamp to 0-99 while download is in progress to prevent brief 100% flashes percent = Math.Max(0, Math.Min(99, percent)); // Only allow progress to increase if (percent >= highestValidPercent) { highestValidPercent = percent; } else { // Progress went backwards? Keep showing the highest valid percent we've seen percent = highestValidPercent; } progressBar.Value = percent; TimeSpan time = TimeSpan.FromSeconds(globalEta); UpdateProgressStatus( "Downloading", (int)transfersComplete + 1, (int)fileCount, (int)Math.Round(percent), time, downloadSpeed); } } catch { } await Task.Delay(100); } if (removedownloading) { removedownloading = false; // Store game info before removing from queue string cancelledGame = gameName; string cancelledHash = gameNameHash; // Remove the cancelled item from queue if (gamesQueueList.Count > 0) { gamesQueueList.RemoveAt(0); } // Reset progress UI speedLabel.Text = String.Empty; progressBar.Value = 0; // Ask about keeping files changeTitle("Keep game files?"); try { DialogResult res = FlexibleMessageBox.Show( $"{cancelledGame} download was cancelled. Do you want to delete the partial files?\n\nClick NO to keep the files if you wish to resume your download later.", "Delete Temporary Files?", MessageBoxButtons.YesNo); if (res == DialogResult.Yes) { changeTitle("Deleting game files..."); if (UsingPublicConfig) { if (Directory.Exists($"{settings.DownloadDir}\\{cancelledHash}")) Directory.Delete($"{settings.DownloadDir}\\{cancelledHash}", true); if (Directory.Exists($"{settings.DownloadDir}\\{cancelledGame}")) Directory.Delete($"{settings.DownloadDir}\\{cancelledGame}", true); } else { if (Directory.Exists($"{settings.DownloadDir}\\{cancelledGame}")) Directory.Delete($"{settings.DownloadDir}\\{cancelledGame}", true); } } } catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}"); } changeTitle(""); continue; // Continue to next item in queue } { //Quota Errors bool isinstalltxt = false; string installTxtPath = null; bool quotaError = false; bool otherError = false; if (gameDownloadOutput.Error.Length > 0 && !isOffline) { string err = gameDownloadOutput.Error.ToLower(); err += gameDownloadOutput.Output.ToLower(); if ((err.Contains("quota") && err.Contains("exceeded")) || err.Contains("directory not found")) { quotaError = true; SwitchMirrors(); cleanupActiveDownloadStatus(); } else if (!gameDownloadOutput.Error.Contains("Serving remote control on http://127.0.0.1:5572/")) { otherError = true; //Remove current game cleanupActiveDownloadStatus(); _ = FlexibleMessageBox.Show(Program.form, $"Rclone error: {gameDownloadOutput.Error}"); output += new ProcessOutput("", "Download Failed"); } } if (UsingPublicConfig && otherError == false && gameDownloadOutput.Output != "Download skipped.") { // ETA tracking for extraction DateTime extractStart = DateTime.UtcNow; Thread extractionThread = new Thread(() => { Invoke(new Action(() => { speedLabel.Text = "Extracting..."; progressBar.IsIndeterminate = false; progressBar.Value = 0; progressBar.OperationType = "Extracting"; isInDownloadExtract = true; })); // Set up extraction callback Zip.ExtractionProgressCallback = (percent, eta) => { this.Invoke(() => { progressBar.Value = percent; UpdateProgressStatus("Extracting", percent: (int)Math.Round(percent), eta: eta); progressBar.StatusText = $"Extracting · {percent:0.0}%"; }); }; try { changeTitle("Extracting " + gameName); Zip.ExtractFile($"{settings.DownloadDir}\\{gameNameHash}\\{gameNameHash}.7z.001", $"{settings.DownloadDir}", PublicConfigFile.Password); changeTitle(""); } catch (ExtractionException ex) { Invoke(new Action(() => { cleanupActiveDownloadStatus(); })); otherError = true; this.Invoke(() => _ = FlexibleMessageBox.Show(Program.form, $"7zip error: {ex.Message}")); output += new ProcessOutput("", "Extract Failed"); } finally { // Clear callbacks Zip.ExtractionProgressCallback = null; Zip.ExtractionStatusCallback = null; } }) { IsBackground = true }; extractionThread.Start(); while (extractionThread.IsAlive) { await Task.Delay(100); } progressBar.StatusText = ""; // Clear status after extraction if (Directory.Exists($"{settings.DownloadDir}\\{gameNameHash}")) { Directory.Delete($"{settings.DownloadDir}\\{gameNameHash}", true); } } if (quotaError == false && otherError == false) { ADB.DeviceID = GetDeviceID(); quotaTries = 0; progressBar.Value = 0; progressBar.IsIndeterminate = false; changeTitle("Installing game APK " + gameName); if (File.Exists(Path.Combine(settings.DownloadDir, gameName, "install.txt"))) { isinstalltxt = true; installTxtPath = Path.Combine(settings.DownloadDir, gameName, "install.txt"); } else if (File.Exists(Path.Combine(settings.DownloadDir, gameName, "Install.txt"))) { isinstalltxt = true; installTxtPath = Path.Combine(settings.DownloadDir, gameName, "Install.txt"); } string[] files = Directory.GetFiles(settings.DownloadDir + "\\" + gameName); Debug.WriteLine("Game Folder is: " + settings.DownloadDir + "\\" + gameName); Debug.WriteLine("FILES IN GAME FOLDER: "); if (isinstalltxt) { // Only sideload if device is connected and sideloading not disabled if (!settings.NodeviceMode && !nodeviceonstart && DeviceConnected) { Thread installtxtThread = new Thread(() => { output += Sideloader.RunADBCommandsFromFile(installTxtPath); changeTitle(""); }); installtxtThread.Start(); while (installtxtThread.IsAlive) { await Task.Delay(100); } } else { output.Output = "Download complete (installation skipped).\n\nConnect a device or enable sideloading to install."; } } else { // Only sideload if device is connected and sideloading not disabled if (!settings.NodeviceMode && !nodeviceonstart && DeviceConnected) { // Find the APK file to install string apkFile = files.FirstOrDefault(file => Path.GetExtension(file) == ".apk"); if (apkFile != null) { CurrAPK = apkFile; CurrPCKG = packagename; System.Windows.Forms.Timer t = new System.Windows.Forms.Timer { Interval = 150000 // 150 seconds to fail }; t.Tick += new EventHandler(timer_Tick4); t.Start(); changeTitle($"Sideloading APK..."); progressBar.IsIndeterminate = false; progressBar.OperationType = "Installing"; progressBar.Value = 0; // Use async method with progress output += await ADB.SideloadWithProgressAsync( apkFile, (progress, eta) => this.Invoke(() => { if (progress == 0) { progressBar.IsIndeterminate = true; progressBar.OperationType = "Installing"; } else { progressBar.IsIndeterminate = false; progressBar.Value = progress; } UpdateProgressStatus("Installing", percent: (int)Math.Round(progress), eta: eta); progressBar.StatusText = $"Installing · {progress:0.0}%"; }), status => this.Invoke(() => { if (!string.IsNullOrEmpty(status)) { if (status.Contains("Completing Installation")) { // "Completing Installation..." speedLabel.Text = status; } progressBar.StatusText = status; } }), packagename, Sideloader.gameNameToSimpleName(gameName)); t.Stop(); progressBar.IsIndeterminate = false; progressBar.StatusText = ""; // Clear status after APK install Debug.WriteLine(wrDelimiter); if (Directory.Exists($"{settings.DownloadDir}\\{gameName}\\{packagename}")) { deleteOBB(packagename); changeTitle($"Copying {packagename} OBB to device..."); progressBar.Value = 0; progressBar.OperationType = "Copying OBB"; // Use async method with progress for OBB string currentObbStatusBase = string.Empty; // phase or filename output += await ADB.CopyOBBWithProgressAsync( $"{settings.DownloadDir}\\{gameName}\\{packagename}", (progress, eta) => this.Invoke(() => { progressBar.Value = progress; UpdateProgressStatus("Copying OBB", percent: (int)Math.Round(progress), eta: eta); if (!string.IsNullOrEmpty(currentObbStatusBase)) { progressBar.StatusText = $"{currentObbStatusBase} · {progress:0.0}%"; } else { progressBar.StatusText = $"{progress:0.0}%"; } }), status => this.Invoke(() => { currentObbStatusBase = status ?? string.Empty; }), Sideloader.gameNameToSimpleName(gameName)); progressBar.StatusText = ""; // Clear status after OBB copy changeTitle(""); if (!nodeviceonstart | DeviceConnected) { if (!output.Output.Contains("offline")) { try { obbsMismatch = await compareOBBSizes(packagename, gameName, output); } catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error comparing OBB sizes: {ex.Message}"); } } } } } } else { output.Output = "Download complete (installation skipped).\n\nConnect a device or enable sideloading to install."; } changeTitle($"Installation of {gameName} completed."); } // Only delete if setting enabled and device was connected (so we actually installed) if (settings.DeleteAllAfterInstall && !nodeviceonstart && DeviceConnected) { changeTitle("Deleting game files"); try { Directory.Delete(settings.DownloadDir + "\\" + gameName, true); } catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}"); } } // Remove current game cleanupActiveDownloadStatus(); } } } if (!obbsMismatch) { changeTitle("Refreshing games list, please wait...\n"); showAvailableSpace(); await RefreshGameListAsync(); // Only show output if there's content if (settings.EnableMessageBoxes && !string.IsNullOrWhiteSpace(output.Output + output.Error)) { ShowPrcOutput(output); } progressBar.IsIndeterminate = false; gamesAreDownloading = false; if (_queuePanel != null) _queuePanel.IsDownloading = false; isinstalling = false; changeTitle(""); } } } private void deleteOBB(string packagename) { changeTitle("Deleting old OBB Folder..."); Logger.Log("Attempting to delete old OBB Folder"); ADB.RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{packagename}\""); } private const string OBBFolderPath = "/sdcard/Android/obb/"; // Logic to compare OBB folders. private async Task compareOBBSizes(string packageName, string gameName, ProcessOutput output) { string localFolderPath = Path.Combine(settings.DownloadDir, gameName, packageName); if (!Directory.Exists(localFolderPath)) { return false; } try { changeTitle("Comparing OBBs..."); Logger.Log("Comparing OBBs"); DirectoryInfo localFolder = new DirectoryInfo(localFolderPath); long totalLocalFolderSize = localFolderSize(localFolder) / (1024 * 1024); string remoteFolderSizeResult = ADB.RunAdbCommandToString($"shell du -m \"{OBBFolderPath}{packageName}\"").Output; string cleanedRemoteFolderSize = cleanRemoteFolderSize(remoteFolderSizeResult); int localObbSize = (int)totalLocalFolderSize; int remoteObbSize = Convert.ToInt32(cleanedRemoteFolderSize); Logger.Log($"Total local folder size in bytes: {totalLocalFolderSize} Remote Size: {cleanedRemoteFolderSize}"); if (remoteObbSize < localObbSize) { return await handleObbSizeMismatchAsync(packageName, gameName, output); } return false; } catch (FormatException ex) { _ = FlexibleMessageBox.Show(Program.form, "The OBB Folder on the Quest seems to not exist or be empty\nPlease redownload the game or sideload the OBB manually.", "OBB Size Undetectable!", MessageBoxButtons.OK); Logger.Log($"Unable to compare OBBs with the exception: {ex.Message}", LogLevel.ERROR); FlexibleMessageBox.Show($"Error comparing OBB sizes: {ex.Message}"); return false; } catch (Exception ex) { Logger.Log($"Unexpected error occurred while comparing OBBs: {ex.Message}", LogLevel.ERROR); FlexibleMessageBox.Show(Program.form, $"Unexpected error comparing OBB sizes: {ex.Message}"); return false; } } private string cleanRemoteFolderSize(string rawSize) { string replaced = Regex.Replace(rawSize, "[^c]*$", ""); return Regex.Replace(replaced, "[^0-9]", ""); } // Logic to handle mismatches after comparison. private async Task handleObbSizeMismatchAsync(string packageName, string gameName, ProcessOutput output) { var dialogResult = MessageBox.Show(Program.form, "Warning! It seems like the OBB wasn't pushed correctly, this means that the game may not launch correctly.\n Do you want to retry the push?", "OBB Size Mismatch!", MessageBoxButtons.YesNo); if (dialogResult != DialogResult.Yes) { await refreshGamesListAsync(output); return true; } changeTitle("Retrying push"); string obbFolderPath = Path.Combine(settings.DownloadDir, gameName, packageName); if (!Directory.Exists(obbFolderPath)) { return false; } await Task.Run(() => { changeTitle($"Copying {packageName} OBB to device..."); output += ADB.RunAdbCommandToString($"push \"{obbFolderPath}\" \"{OBBFolderPath}\""); changeTitle(""); }); return await compareOBBSizes(packageName, gameName, output); } private async Task refreshGamesListAsync(ProcessOutput output) { changeTitle("Refreshing games list, please wait..."); showAvailableSpace(); listAppsBtn(); await RefreshGameListAsync(); if (settings.EnableMessageBoxes) { ShowPrcOutput(output); } progressBar.IsIndeterminate = false; gamesAreDownloading = false; if (_queuePanel != null) _queuePanel.IsDownloading = false; isinstalling = false; changeTitle(""); } static long localFolderSize(DirectoryInfo localFolder) { long totalLocalFolderSize = 0; // Get all files into the directory FileInfo[] allFiles = localFolder.GetFiles(); // Loop through every file and get size of it foreach (FileInfo file in allFiles) { totalLocalFolderSize += file.Length; } // Find all subdirectories DirectoryInfo[] subFolders = localFolder.GetDirectories(); // Loop through every subdirectory and get size of each foreach (DirectoryInfo dir in subFolders) { totalLocalFolderSize += localFolderSize(dir); } // Return the total size of folder return totalLocalFolderSize; } private void timer_Tick4(object sender, EventArgs e) { _ = new ProcessOutput("", ""); if (!timerticked) { timerticked = true; bool isinstalled = false; if (settings.InstalledApps.Contains(CurrPCKG)) { isinstalled = true; } if (isinstalled) { if (!settings.AutoReinstall) { DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "In place upgrade has failed." + "\n\nThis means the app must be uninstalled first before updating.\nRookie can attempt to " + "do this while retaining your savedata.\nWhile the vast majority of games can be backed up there " + "are some exceptions\n(we don't know which apps can't be backed up as there is no list online)\n\nDo you want " + "Rookie to uninstall and reinstall the app automatically?", "In place upgrade failed", MessageBoxButtons.OKCancel); if (dialogResult == DialogResult.Cancel) { return; } } changeTitle("Performing reinstall, please wait..."); _ = ADB.RunAdbCommandToString("kill-server"); _ = ADB.RunAdbCommandToString("devices"); _ = ADB.RunAdbCommandToString($"pull /sdcard/Android/data/{CurrPCKG} \"{Environment.CurrentDirectory}\""); _ = Sideloader.UninstallGame(CurrPCKG); changeTitle("Reinstalling game"); _ = ADB.RunAdbCommandToString($"install -g \"{CurrAPK}\""); _ = ADB.RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{CurrPCKG}\" /sdcard/Android/data/"); timerticked = false; if (Directory.Exists(Path.Combine(Environment.CurrentDirectory, CurrPCKG))) { Directory.Delete(Path.Combine(Environment.CurrentDirectory, CurrPCKG), true); } changeTitle(""); return; } else { DialogResult dialogResult2 = FlexibleMessageBox.Show(Program.form, "This installation is taking an unusual amount of time, you can keep waiting or abort the installation.\n" + "Would you like to cancel the installation?", "Cancel install?", MessageBoxButtons.YesNo); if (dialogResult2 == DialogResult.Yes) { changeTitle("Stopping installation..."); _ = ADB.RunAdbCommandToString("kill-server"); _ = ADB.RunAdbCommandToString("devices"); } else { timerticked = false; return; } } } } private async void Form1_FormClosing(object sender, FormClosingEventArgs e) { // Cleanup DNS helper (stops proxy) DnsHelper.Cleanup(); if (isinstalling) { DialogResult res1 = FlexibleMessageBox.Show(Program.form, "There are downloads and/or installations in progress,\nif you exit now you'll have to start the entire process over again.\nAre you sure you want to exit?", "Still downloading/installing.", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2); if (res1 != DialogResult.Yes) { e.Cancel = true; return; } else { RCLONE.killRclone(); } } else if (isuploading) { DialogResult res = FlexibleMessageBox.Show(Program.form, "There is an upload still in progress, if you exit now\nyou'll have to start the entire process over again.\nAre you sure you want to exit?", "Still uploading.", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2); if (res != DialogResult.Yes) { e.Cancel = true; return; } else { RCLONE.killRclone(); _ = ADB.RunAdbCommandToString("kill-server"); } } else { RCLONE.killRclone(); _ = ADB.RunAdbCommandToString("kill-server"); } } private async void ADBWirelessToggle_Click(object sender, EventArgs e) { // Check if wireless ADB is currently enabled by verifying actual connection bool isWirelessEnabled = false; if (File.Exists(storedIpPath) && !string.IsNullOrEmpty(settings.IPAddress)) { // Verify we're actually connected wirelessly by checking connected devices string devicesOutput = ADB.RunAdbCommandToString("devices").Output; string[] lines = devicesOutput.Split('\n'); foreach (string line in lines) { // Wireless devices show as IP:port format (e.g., "192.168.1.100:5555") if (line.Contains(":5555") && line.Contains("device")) { isWirelessEnabled = true; break; } } } // If enabled, offer to disable or switch device if (isWirelessEnabled) { string action = null; using (Form dialog = new Form()) { dialog.Text = "Wireless ADB Options"; dialog.Size = new Size(386, 130); dialog.StartPosition = FormStartPosition.CenterParent; dialog.FormBorderStyle = FormBorderStyle.FixedDialog; dialog.MaximizeBox = false; dialog.MinimizeBox = false; dialog.BackColor = Color.FromArgb(20, 24, 29); dialog.ForeColor = Color.White; var label = new Label { Text = "A device is currently connected via Wireless ADB.", ForeColor = Color.White, AutoSize = true, Location = new Point(15, 15) }; var btnSwitch = CreateStyledButton("Connect New Device", DialogResult.None, new Point(15, 45)); btnSwitch.Size = new Size(170, 32); btnSwitch.Click += (s, ev) => { action = "switch"; dialog.DialogResult = DialogResult.OK; }; var btnDisable = CreateStyledButton("Disable Wireless ADB", DialogResult.None, new Point(195, 45)); btnDisable.Size = new Size(160, 32); btnDisable.Click += (s, ev) => { action = "disable"; dialog.DialogResult = DialogResult.OK; }; dialog.Controls.AddRange(new Control[] { label, btnSwitch, btnDisable }); if (dialog.ShowDialog(this) != DialogResult.OK || action == null) { return; } } // Disable wireless ADB completely if (action == "disable") { ADB.wirelessadbON = false; changeTitle("Disabling wireless ADB..."); progressBar.IsIndeterminate = true; progressBar.OperationType = ""; await Task.Run(() => { ADB.RunAdbCommandToString("disconnect"); ADB.RunAdbCommandToString("kill-server"); ADB.RunAdbCommandToString("start-server"); }); settings.IPAddress = string.Empty; settings.Save(); if (File.Exists(storedIpPath)) { try { File.Delete(storedIpPath); } catch { } } progressBar.IsIndeterminate = false; _ = await CheckForDevice(); changeTitlebarToDevice(); changeTitle("Wireless ADB disabled.", true); UpdateWirelessADBButtonText(); UpdateStatusLabels(); return; } // User chose to switch device: disconnect current wireless connection changeTitle("Disconnecting current device..."); await Task.Run(() => ADB.RunAdbCommandToString("disconnect")); } // Connect: Show custom dialog with three options string connectionMethod = null; using (Form dialog = new Form()) { dialog.Text = "Wireless ADB Connection"; dialog.Size = new Size(456, 130); dialog.StartPosition = FormStartPosition.CenterParent; dialog.FormBorderStyle = FormBorderStyle.FixedDialog; dialog.MaximizeBox = false; dialog.MinimizeBox = false; dialog.BackColor = Color.FromArgb(20, 24, 29); dialog.ForeColor = Color.White; var label = new Label { Text = "How would you like to connect?", ForeColor = Color.White, AutoSize = true, Location = new Point(15, 15) }; var btnScan = CreateStyledButton("Automatic", DialogResult.None, new Point(15, 45)); btnScan.Size = new Size(130, 32); btnScan.Click += (s, ev) => { connectionMethod = "scan"; dialog.DialogResult = DialogResult.OK; }; var btnUSB = CreateStyledButton("Automatic (USB)", DialogResult.None, new Point(155, 45)); btnUSB.Size = new Size(130, 32); btnUSB.Click += (s, ev) => { connectionMethod = "usb"; dialog.DialogResult = DialogResult.OK; }; var btnManual = CreateStyledButton("Manual", DialogResult.None, new Point(295, 45)); btnManual.Size = new Size(130, 32); btnManual.Click += (s, ev) => { connectionMethod = "manual"; dialog.DialogResult = DialogResult.OK; }; dialog.Controls.AddRange(new Control[] { label, btnScan, btnUSB, btnManual }); if (dialog.ShowDialog(this) != DialogResult.OK || connectionMethod == null) { changeTitle(""); return; } } string ipAddress = null; if (connectionMethod == "scan") { ipAddress = await ShowNetworkScanDialogAsync(); } else if (connectionMethod == "manual") { ipAddress = ShowManualIPDialog(); } else if (connectionMethod == "usb") { // Setup via USB DialogResult usbResult = FlexibleMessageBox.Show( Program.form, "Please make sure your Quest is connected to your PC via USB, then click OK.\n" + "If you need more time, click Cancel and return when you're ready.", "Connect Your Quest", MessageBoxButtons.OKCancel); if (usbResult == DialogResult.Cancel) { changeTitle(""); return; } changeTitle("Setting up wireless ADB via USB..."); progressBar.IsIndeterminate = true; progressBar.OperationType = ""; // Check for device and enable TCP/IP mode await Task.Run(() => { _ = ADB.RunAdbCommandToString("devices"); _ = ADB.RunAdbCommandToString("tcpip 5555"); }); _ = FlexibleMessageBox.Show( Program.form, "Click OK to retrieve your Quest's local IP address.", "Get Local IP Address", MessageBoxButtons.OK); await Task.Delay(1000); // Get IP address changeTitle("Retrieving IP address..."); string input = await Task.Run(() => ADB.RunAdbCommandToString("shell ip route").Output); string[] strArrayOne = input.Split(' '); if (strArrayOne.Length > 8 && strArrayOne[0].Length > 7) { string IPaddr = strArrayOne[8]; string IPcmnd = "connect " + IPaddr + ":5555"; _ = FlexibleMessageBox.Show( Program.form, $"Your Quest's local IP address is: {IPaddr}\n\n" + $"Please disconnect your Quest and wait 2-3 seconds.\n" + $"Once that's done, click OK.", "Disconnect USB", MessageBoxButtons.OK); changeTitle("Connecting wirelessly..."); await Task.Delay(2000); // Attempt wireless connection await Task.Run(() => ADB.RunAdbCommandToString(IPcmnd)); await Task.Delay(2000); // Verify device is actually connected int deviceIndex = await CheckForDevice(); // success if (deviceIndex >= 0) { // Update UI with device info await Task.Run(() => { changeTitlebarToDevice(); showAvailableSpace(); }); settings.IPAddress = IPcmnd; settings.WirelessADB = true; settings.Save(); try { File.WriteAllText(storedIpPath, IPcmnd); } catch (Exception ex) { Logger.Log($"Unable to write to StoredIP.txt: {ex.Message}", LogLevel.ERROR); } ADB.wirelessadbON = true; // Configure WiFi wakeup settings await Task.Run(() => { _ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_available 1"); _ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_enabled 1"); }); progressBar.IsIndeterminate = false; changeTitle("Connected successfully!", true); UpdateWirelessADBButtonText(); UpdateStatusLabels(); } // failure else { progressBar.IsIndeterminate = false; changeTitle(""); _ = FlexibleMessageBox.Show(Program.form, "Device connection failed! Connect Quest via USB and try again."); } } else { progressBar.IsIndeterminate = false; changeTitle(""); _ = FlexibleMessageBox.Show(Program.form, "Device connection failed! Connect Quest via USB and try again."); } return; } if (string.IsNullOrEmpty(ipAddress)) { changeTitle(""); return; } // Connect to the device changeTitle($"Connecting to {ipAddress}..."); progressBar.IsIndeterminate = true; progressBar.OperationType = ""; string ipCommand = $"connect {ipAddress}:5555"; string connectResult = await Task.Run(() => ADB.RunAdbCommandToString(ipCommand).Output); progressBar.IsIndeterminate = false; if (connectResult.Contains("cannot resolve host") || connectResult.Contains("cannot connect to") || connectResult.Contains("failed") || connectResult.Contains("unable")) { changeTitle(""); _ = FlexibleMessageBox.Show( Program.form, $"Failed to connect to {ipAddress}\n\nPlease verify:\n" + "- The IP address is correct\n" + "- The device is on the same network\n" + "- Developer mode is enabled\n" + "- Wireless ADB with tcpip 5555 enabled (requires one-time setup*)\n\n" + "* Connect device via USB and run ADB command: tcpip 5555", "Connection Failed", MessageBoxButtons.OK); UpdateWirelessADBButtonText(); UpdateStatusLabels(); return; } // Success - save settings and configure device _ = await CheckForDevice(); changeTitlebarToDevice(); showAvailableSpace(); settings.IPAddress = ipCommand; settings.WirelessADB = true; settings.Save(); try { File.WriteAllText(storedIpPath, ipCommand); } catch (Exception ex) { Logger.Log($"Unable to write to StoredIP.txt: {ex.Message}", LogLevel.ERROR); } ADB.wirelessadbON = true; // Configure wake settings in background _ = Task.Run(() => { ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_available 1"); ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_enabled 1"); }); changeTitle("Connected successfully!", true); UpdateWirelessADBButtonText(); UpdateStatusLabels(); } private string ShowManualIPDialog() { // Get local subnet prefix to pre-fill string subnetPrefix = GetLocalIPv4(); using (Form dialog = new Form()) { dialog.Text = "Enter Quest IP Address"; dialog.Size = new Size(350, 150); dialog.StartPosition = FormStartPosition.CenterParent; dialog.FormBorderStyle = FormBorderStyle.FixedDialog; dialog.MaximizeBox = false; dialog.MinimizeBox = false; dialog.BackColor = Color.FromArgb(20, 24, 29); dialog.ForeColor = Color.White; var label = new Label { Text = "Enter your Quest's IP Address:", ForeColor = Color.White, AutoSize = true, Location = new Point(15, 15) }; var textBox = new TextBox { Location = new Point(15, 40), Size = new Size(300, 24), BackColor = Color.FromArgb(40, 44, 52), ForeColor = Color.White, BorderStyle = BorderStyle.FixedSingle, Text = subnetPrefix // Pre-fill with subnet prefix }; // Position cursor at end of pre-filled text textBox.SelectionStart = textBox.Text.Length; var okButton = CreateStyledButton("OK", DialogResult.OK, new Point(155, 75)); var cancelButton = CreateStyledButton("Cancel", DialogResult.Cancel, new Point(240, 75), false); dialog.Controls.AddRange(new Control[] { label, textBox, okButton, cancelButton }); dialog.AcceptButton = okButton; dialog.CancelButton = cancelButton; // Focus the textbox when dialog shows dialog.Shown += (s, e) => { textBox.Focus(); textBox.SelectionStart = textBox.Text.Length; }; if (dialog.ShowDialog(this) == DialogResult.OK) { return textBox.Text.Trim(); } } return null; } private string GetLocalIPv4() { try { foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) { if (ni.OperationalStatus != OperationalStatus.Up) continue; var ipProps = ni.GetIPProperties(); foreach (var ua in ipProps.UnicastAddresses) { if (ua.Address.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(ua.Address)) { string localIp = ua.Address.ToString(); string localPrefix = null; if (!string.IsNullOrEmpty(localIp)) { var o = localIp.Split('.'); if (o.Length == 4) { localPrefix = $"{o[0]}.{o[1]}.{o[2]}."; } } return localPrefix; } } } } catch (Exception ex) { Logger.Log($"Unable to get local IPv4: {ex.Message}", LogLevel.WARNING); } return null; } private async Task> ScanNetworkForAdbDevicesAsync() { var foundDevices = new List(); string localSubnet = GetLocalIPv4(); if (string.IsNullOrEmpty(localSubnet)) { Logger.Log("Could not determine local subnet for scanning", LogLevel.WARNING); return foundDevices; } changeTitle("Scanning network for ADB devices..."); progressBar.IsIndeterminate = true; progressBar.OperationType = ""; // Scan common IP range (1-254) on port 5555 var tasks = new List>(); for (int i = 1; i <= 254; i++) { string ip = $"{localSubnet}{i}"; tasks.Add(CheckAdbDeviceAsync(ip, 5555)); } var results = await Task.WhenAll(tasks); foundDevices.AddRange(results.Where(r => !string.IsNullOrEmpty(r))); progressBar.IsIndeterminate = false; changeTitle(""); return foundDevices; } private async Task CheckAdbDeviceAsync(string ip, int port) { try { using (var client = new TcpClient()) { // Timeout, 1000ms should be enough for local network var connectTask = client.ConnectAsync(ip, port); if (await Task.WhenAny(connectTask, Task.Delay(1000)) == connectTask) { if (client.Connected) { // Port is open, try ADB connect to verify it's actually an ADB device string result = ADB.RunAdbCommandToString($"connect {ip}:{port}").Output; if (result.Contains("connected") || result.Contains("already")) { // Disconnect immediately, we're just scanning ADB.RunAdbCommandToString($"disconnect {ip}:{port}"); return ip; } } } } } catch { // Ignore connection failures, device not present or not ADB } return null; } private async Task ShowNetworkScanDialogAsync() { var devices = await ScanNetworkForAdbDevicesAsync(); if (devices.Count == 0) { FlexibleMessageBox.Show(Program.form, "No ADB devices found on the network.\n\n" + "Please verify:\n" + "- The device is on the same network\n" + "- Developer mode is enabled\n" + "- Wireless ADB with tcpip 5555 enabled (requires one-time setup*)\n\n" + "* Connect device via USB and run ADB command: tcpip 5555", "No Devices Found"); return null; } if (devices.Count == 1) { return devices[0]; } // Multiple devices found - let user choose using (Form dialog = new Form()) { dialog.Text = "Select ADB Device"; dialog.Size = new Size(350, 150); dialog.StartPosition = FormStartPosition.CenterParent; dialog.FormBorderStyle = FormBorderStyle.FixedDialog; dialog.MaximizeBox = false; dialog.MinimizeBox = false; dialog.BackColor = Color.FromArgb(20, 24, 29); dialog.ForeColor = Color.White; var label = new Label { Text = $"Found {devices.Count} ADB devices:", ForeColor = Color.White, AutoSize = true, Location = new Point(15, 15) }; var comboBox = new ComboBox { Location = new Point(15, 40), Size = new Size(300, 24), DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Color.FromArgb(42, 45, 58), ForeColor = Color.White }; foreach (var device in devices) comboBox.Items.Add(device); comboBox.SelectedIndex = 0; var okButton = CreateStyledButton("Connect", DialogResult.OK, new Point(155, 75)); var cancelButton = CreateStyledButton("Cancel", DialogResult.Cancel, new Point(240, 75), false); dialog.Controls.AddRange(new Control[] { label, comboBox, okButton, cancelButton }); dialog.AcceptButton = okButton; dialog.CancelButton = cancelButton; if (dialog.ShowDialog(this) == DialogResult.OK) return comboBox.SelectedItem.ToString(); } return null; } private void UpdateWirelessADBButtonText() { bool isWirelessEnabled = File.Exists(storedIpPath) && !string.IsNullOrEmpty(settings.IPAddress); ADBWirelessToggle.Text = isWirelessEnabled ? "WIRELESS ADB" : "ENABLE WIRELESS ADB"; } private void devicesComboBox_SelectedIndexChanged(object sender, EventArgs e) { showAvailableSpace(); } private async void remotesList_SelectedIndexChanged(object sender, EventArgs e) { if (remotesList.SelectedItem != null) { string selectedRemote = remotesList.SelectedItem.ToString(); if (selectedRemote == "Public") { UsingPublicConfig = true; } else { UsingPublicConfig = false; remotesList.Invoke(() => { currentRemote = "VRP-mirror" + selectedRemote; }); } await refreshCurrentMirror("Refreshing App List..."); UpdateStatusLabels(); } } private void QuestOptionsButton_Click(object sender, EventArgs e) { QuestForm Form = new QuestForm(); Form.Show(Program.form); } private void listView1_ColumnClick(object sender, ColumnClickEventArgs e) { // Determine sort order if (e.Column == lvwColumnSorter.SortColumn) { lvwColumnSorter.Order = lvwColumnSorter.Order == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending; } else { lvwColumnSorter.SortColumn = e.Column; lvwColumnSorter.Order = (e.Column == 4 || e.Column == 5) ? SortOrder.Descending : (e.Column == 6) ? SortOrder.Ascending : SortOrder.Ascending; } // Update shared sort state _sharedSortField = ColumnIndexToSortField(e.Column); _sharedSortDirection = lvwColumnSorter.Order == SortOrder.Ascending ? SortDirection.Ascending : SortDirection.Descending; // Suspend drawing during sort gamesListView.BeginUpdate(); try { gamesListView.Sort(); } finally { gamesListView.EndUpdate(); } // Invalidate header to update sort indicators gamesListView.Invalidate(new Rectangle(0, 0, gamesListView.ClientSize.Width, gamesListView.Font.Height + 8)); } private void CheckEnter(object sender, System.Windows.Forms.KeyPressEventArgs e) { if (e.KeyChar == (char)Keys.Enter) { if (searchTextBox.Visible) { if (gamesListView.SelectedItems.Count > 0) { downloadInstallGameButton_Click(sender, e); } } searchTextBox.Visible = false; } if (e.KeyChar == (char)Keys.Escape) { searchTextBox.Visible = false; } } protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { if (keyData == (Keys.Control | Keys.F)) { // Show search box. searchTextBox.Clear(); searchTextBox.Visible = true; _ = searchTextBox.Focus(); } if (keyData == (Keys.Control | Keys.L)) { if (loaded) { StringBuilder copyGamesListView = new StringBuilder(); foreach (ListViewItem item in gamesListView.Items) { // Assuming the game name is in the first column (subitem index 0) copyGamesListView.Append(item.SubItems[0].Text).Append("\n"); } Clipboard.SetText(copyGamesListView.ToString()); _ = MessageBox.Show("Entire game list copied as a paragraph to clipboard!\nPress CTRL+V to paste it anywhere!"); } } if (keyData == (Keys.Alt | Keys.L)) { if (loaded) { StringBuilder copyGamesListView = new StringBuilder(); foreach (ListViewItem item in gamesListView.Items) { // Assuming the game name is in the first column (subitem index 0) copyGamesListView.Append(item.SubItems[0].Text).Append(", "); } // Remove the last ", " if there's any content in rookienamelist2 if (copyGamesListView.Length > 2) { copyGamesListView.Length -= 2; } Clipboard.SetText(copyGamesListView.ToString()); _ = MessageBox.Show("Entire game list copied as a paragraph to clipboard!\nPress CTRL+V to paste it anywhere!"); } } if (keyData == (Keys.Control | Keys.H)) { string HWID = SideloaderUtilities.UUID(); Clipboard.SetText(HWID); _ = FlexibleMessageBox.Show(Program.form, $"Your unique HWID is:\n\n{HWID}\n\nThis has been automatically copied to your clipboard. Press CTRL+V in a message to send it."); } if (keyData == (Keys.Control | Keys.R)) { btnRunAdbCmd_Click(this, EventArgs.Empty); } if (keyData == (Keys.Control | Keys.F4)) { try { // Relaunch the program using Sideloader Launcher _ = Process.Start(Application.StartupPath + "\\Sideloader Launcher.exe"); Process.GetCurrentProcess().Kill(); } catch { } } if (keyData == Keys.F3) { if (Application.OpenForms.OfType().Count() == 0) { QuestForm Form = new QuestForm(); Form.Show(Program.form); } } if (keyData == Keys.F4) { if (Application.OpenForms.OfType().Count() == 0) { SettingsForm Form = new SettingsForm(); Form.Show(Program.form); } } if (keyData == Keys.F5) { if (!DeviceConnected && Devices.Count == 0) { FlexibleMessageBox.Show(Program.form, "No device connected. Please connect your Quest and click 'RECONNECT DEVICE' first.", "Device Required", MessageBoxButtons.OK); return true; } changeTitle("Refreshing games list..."); listAppsBtn(); // Use RefreshGameListAsync to preserve filter state _ = RefreshGameListAsync(); } bool dialogIsUp = false; if (keyData == Keys.F1 && !dialogIsUp) { _ = FlexibleMessageBox.Show(Program.form, @"Keyboard Shortcuts F1 - Show shortcuts list F3 - Open Quest Settings F4 - Open Rookie Settings F5 - Refresh games list CTRL + R - Run custom ADB command CTRL + L - Copy all game names (one per line) ALT + L - Copy all game names (comma-separated in a single line) CTRL + P - Copy package name of selected game CTRL + F4 - Instantly relaunch Rookie Sideloader"); } if (keyData == (Keys.Control | Keys.P)) { DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Do you wish to copy Package Name of games selected from list to clipboard?", "Copy package to clipboard?", MessageBoxButtons.YesNo); if (dialogResult == DialogResult.Yes) { settings.PackageNameToCB = true; settings.Save(); } if (dialogResult == DialogResult.No) { settings.PackageNameToCB = false; settings.Save(); } } return base.ProcessCmdKey(ref msg, keyData); } private async void searchTextBox_TextChanged(object sender, EventArgs e) { _debounceTimer.Stop(); _debounceTimer.Start(); } private async Task RunSearch() { _debounceTimer.Stop(); // Cancel any ongoing searches _cts?.Cancel(); string searchTerm = searchTextBox.Text; // Ignore placeholder text if (searchTerm == "Search..." || string.IsNullOrWhiteSpace(searchTerm)) { RestoreFullList(); return; } _cts = new CancellationTokenSource(); var token = _cts.Token; try { // Perform search using index for faster lookups var matches = await Task.Run(() => { if (token.IsCancellationRequested) return new List(); var results = new HashSet(); // Avoid duplicates // Try exact match first using index if (_searchIndex != null && _searchIndex.TryGetValue(searchTerm, out var exactMatches)) { foreach (var match in exactMatches) { if (token.IsCancellationRequested) return new List(); results.Add(match); } } // Then do partial matches using index if (_searchIndex != null) { foreach (var kvp in _searchIndex) { if (token.IsCancellationRequested) return new List(); if (kvp.Key.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0) { foreach (var item in kvp.Value) { results.Add(item); } } } } else { // Fallback to linear search if index not built yet foreach (var item in _allItems) { if (token.IsCancellationRequested) return new List(); if (item.Text.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0 || (item.SubItems.Count > 1 && item.SubItems[1].Text.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0)) { results.Add(item); } } } return results.ToList(); }, token); // Check if cancelled before updating UI if (token.IsCancellationRequested) return; // Update UI on main thread gamesListView.BeginUpdate(); try { gamesListView.Items.Clear(); if (matches.Count > 0) { gamesListView.Items.AddRange(matches.ToArray()); } } finally { gamesListView.EndUpdate(); } // Refresh gallery view if active if (isGalleryView && gamesGalleryView.Visible) { _galleryDataSource = matches; PopulateGalleryView(); } } catch (OperationCanceledException) { // Search was cancelled - this is expected behavior } catch (Exception ex) { Logger.Log($"Error during search: {ex.Message}", LogLevel.ERROR); } } static string ExtractVideoId(string html) { // We want the first strict 11-char YouTube video ID after /watch?v= var m = Regex.Match(html, @"\/watch\?v=([A-Za-z0-9_\-]{11})"); return m.Success ? m.Groups[1].Value : string.Empty; } private async Task CreateEnvironment() { if (!settings.TrailersEnabled) return; // Fast path: already initialized if (webView21.CoreWebView2 != null) return; // Check if WebView2 runtime DLLs are present // (downloadFiles() should have already downloaded them, but check anyway) string runtimesPath = Path.Combine(Environment.CurrentDirectory, "runtimes"); string webView2LoaderArm64 = Path.Combine(runtimesPath, "win-arm64", "native", "WebView2Loader.dll"); string webView2LoaderX86 = Path.Combine(runtimesPath, "win-x86", "native", "WebView2Loader.dll"); string webView2LoaderX64 = Path.Combine(runtimesPath, "win-x64", "native", "WebView2Loader.dll"); bool runtimeExists = File.Exists(webView2LoaderX86) || File.Exists(webView2LoaderX64) || File.Exists(webView2LoaderArm64); if (!runtimeExists) { // Runtime wasn't downloaded during startup - disable trailers Logger.Log("WebView2 runtime not found, disabling trailer playback", LogLevel.WARNING); enviromentCreated = true; webView21.Hide(); return; } try { var appDataFolder = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "RSL"); var env = await CoreWebView2Environment.CreateAsync(userDataFolder: appDataFolder); await webView21.EnsureCoreWebView2Async(env); // Map local folder to a trusted origin (https://app.local) var webroot = Path.Combine(Environment.CurrentDirectory, "trailer"); Directory.CreateDirectory(webroot); webView21.CoreWebView2.SetVirtualHostNameToFolderMapping( "app.local", webroot, CoreWebView2HostResourceAccessKind.Allow); // Minimal settings required for the player page var s = webView21.CoreWebView2.Settings; s.IsScriptEnabled = true; // allow IFrame API s.IsWebMessageEnabled = true; // allow PostWebMessageAsString from host ApplyWebViewRoundedCorners(); } catch (Exception /* ex */) { enviromentCreated = true; webView21.Hide(); } } private void InitializeTrailerPlayer() { if (!settings.TrailersEnabled) return; if (_trailerPlayerInitialized) return; string webroot = Path.Combine(Environment.CurrentDirectory, "trailer"); Directory.CreateDirectory(webroot); string playerHtml = Path.Combine(webroot, "player.html"); // Lightweight HTML with YouTube IFrame API and WebView2 message bridge var html = @" Trailer Player
"; File.WriteAllText(playerHtml, html, Encoding.UTF8); _trailerPlayerInitialized = true; } // Ensure environment + initial navigation private async Task EnsureTrailerEnvironmentAsync() { if (!settings.TrailersEnabled) return; if (webView21.CoreWebView2 == null) { await CreateEnvironment(); } // Check again after CreateEnvironment - it may have failed if (webView21.CoreWebView2 == null) { Logger.Log("WebView2 CoreWebView2 is null after CreateEnvironment", LogLevel.WARNING); return; } InitializeTrailerPlayer(); if (!_trailerHtmlLoaded && webView21.CoreWebView2 != null) { webView21.CoreWebView2.NavigationCompleted += (s, e) => { _trailerHtmlLoaded = true; }; webView21.CoreWebView2.Navigate("https://app.local/player.html"); } } private async Task ShowVideoAsync(string videoId) { if (!settings.TrailersEnabled) return; if (string.IsNullOrEmpty(videoId)) return; try { await EnsureTrailerEnvironmentAsync(); // Check if WebView2 was successfully initialized if (webView21.CoreWebView2 == null) { return; } // If first load still in progress, small retry loop int tries = 0; while (!_trailerHtmlLoaded && tries < 50) { await Task.Delay(50); tries++; } // Double-check after waiting if (webView21.CoreWebView2 == null || !_trailerHtmlLoaded) { return; } // Post the raw ID; page builds final URL webView21.CoreWebView2.PostWebMessageAsString(videoId); HideVideoPlaceholder(); // Video is loading, hide placeholder } catch (Exception ex) { Logger.Log($"ShowVideoAsync error: {ex.Message}", LogLevel.WARNING); } } private async Task ResolveVideoIdAsync(string gameName) { if (!settings.TrailersEnabled) return string.Empty; if (string.IsNullOrWhiteSpace(gameName)) return string.Empty; if (_videoIdCache.TryGetValue(gameName, out var cached)) return cached; // Lightweight search try { string query = WebUtility.UrlEncode(gameName + " VR trailer"); string searchUrl = $"https://www.youtube.com/results?search_query={query}"; using (var http = new HttpClient()) { http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/119.0"); var html = await http.GetStringAsync(searchUrl); var vid = ExtractVideoId(html); if (!string.IsNullOrEmpty(vid)) { _videoIdCache[gameName] = vid; return vid; } } } catch { // swallow – return empty } return string.Empty; } public async void gamesListView_SelectedIndexChanged(object sender, EventArgs e) { // Hide the uninstall button initially if (_listViewUninstallButton != null) { _listViewUninstallButton.Visible = false; } if (gamesListView.SelectedItems.Count < 1) { selectedGameLabel.Text = ""; downloadInstallGameButton.Enabled = false; downloadInstallGameButton.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(59)))), ((int)(((byte)(67)))), ((int)(((byte)(82))))); return; } var selectedItem = gamesListView.SelectedItems[gamesListView.SelectedItems.Count - 1]; string CurrentPackageName = selectedItem.SubItems[SideloaderRCLONE.PackageNameIndex].Text; string CurrentReleaseName = selectedItem.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text; string CurrentGameName = selectedItem.SubItems[SideloaderRCLONE.GameNameIndex].Text; Console.WriteLine(CurrentGameName); downloadInstallGameButton.Enabled = true; downloadInstallGameButton.ForeColor = System.Drawing.Color.Black; // 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) { if (settings.PackageNameToCB) { Clipboard.SetText(CurrentPackageName); } keyheld = true; } string[] imageExtensions = { ".jpg", ".png" }; string ImagePath = String.Empty; foreach (string extension in imageExtensions) { string path = Path.Combine(SideloaderRCLONE.ThumbnailsFolder, $"{CurrentPackageName}{extension}"); if (File.Exists(path)) { ImagePath = path; break; } } // Dispose the old image first var oldImage = gamesPictureBox.BackgroundImage; gamesPictureBox.BackgroundImage = null; if (oldImage != null) { oldImage.Dispose(); } if (File.Exists(ImagePath)) { gamesPictureBox.BackgroundImage = Image.FromFile(ImagePath); } // If no image exists, BackgroundImage stays null and the Paint handler draws the placeholder gamesPictureBox.Invalidate(); // Force repaint to show placeholder // Fast trailer loading path if (settings.TrailersEnabled) { webView21.Enabled = true; webView21.Show(); try { var videoId = await ResolveVideoIdAsync(CurrentGameName); if (string.IsNullOrEmpty(videoId)) { changeTitle("No Trailer found"); ShowVideoPlaceholder(); } else { await ShowVideoAsync(videoId); } } catch (Exception ex) { Logger.Log("Error loading Trailer"); Logger.Log(ex.Message); ShowVideoPlaceholder(); } } else { ShowVideoPlaceholder(); } string NotePath = $"{SideloaderRCLONE.NotesFolder}\\{CurrentReleaseName}.txt"; if (!isGalleryView) { notesRichTextBox.Text = File.Exists(NotePath) ? File.ReadAllText(NotePath) : ""; UpdateNotesScrollBar(); } } private async void ListViewUninstallButton_Click(object sender, EventArgs e) { var item = _listViewUninstallButton.Tag as ListViewItem; if (item == null) return; _listViewUninstallButton.Visible = false; await UninstallGameAsync(item); } public void UpdateGamesButton_Click(object sender, EventArgs e) { if (!DeviceConnected && Devices.Count == 0) { FlexibleMessageBox.Show(Program.form, "No device connected. Please connect your Quest and click 'RECONNECT DEVICE' first.", "Device Required", MessageBoxButtons.OK); return; } changeTitle("Refreshing installed apps and checking for updates..."); listAppsBtn(); if (SideloaderRCLONE.games.Count < 1) { FlexibleMessageBox.Show(Program.form, "There are no games in rclone, please check your internet connection and verify the config is working properly."); return; } // Use RefreshGameListAsync to preserve filter state _ = RefreshGameListAsync(); } private void gamesListView_MouseDoubleClick(object sender, MouseEventArgs e) { if (gamesListView.SelectedItems.Count > 0) { downloadInstallGameButton_Click(sender, e); } } private void MountButton_Click(object sender, EventArgs e) { _ = ADB.RunAdbCommandToString("shell svc usb setFunctions mtp true"); } private void freeDisclaimer_Click(object sender, EventArgs e) { _ = Process.Start("https://github.com/VRPirates/rookie"); } private void searchTextBox_Enter(object sender, EventArgs e) { if (searchTextBox.Text == "Search...") { searchTextBox.Text = ""; } searchTextBox.Font = new Font("Segoe UI", 9F, FontStyle.Bold); searchTextBox.ForeColor = Color.FromArgb(((int)(((byte)(218)))), ((int)(((byte)(218)))), ((int)(((byte)(218))))); } private void searchTextBox_Leave(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(searchTextBox.Text)) { searchTextBox.Text = "Search..."; searchTextBox.Font = new Font("Segoe UI", 9F, FontStyle.Italic); } searchTextBox.ForeColor = Color.FromArgb(((int)(((byte)(180)))), ((int)(((byte)(180)))), ((int)(((byte)(180))))); _ = gamesListView.Focus(); } private void gamesListView_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == (char)Keys.Enter) { if (gamesListView.SelectedItems.Count > 0) { downloadInstallGameButton_Click(sender, e); } } } bool updateAvailableClicked = false; private void btnUpdateAvailable_Click(object sender, EventArgs e) { btnInstalled.Click -= btnInstalled_Click; btnUpdateAvailable.Click -= btnUpdateAvailable_Click; btnNewerThanList.Click -= btnNewerThanList_Click; if (upToDate_Clicked || NeedsDonation_Clicked) { upToDate_Clicked = false; NeedsDonation_Clicked = false; updateAvailableClicked = false; } if (!updateAvailableClicked) { updateAvailableClicked = true; FilterListByColor(ColorUpdateAvailable); // Update available color } else { updateAvailableClicked = false; RestoreFullList(); } // Update button visual states UpdateFilterButtonStates(); // Refresh gallery view if active if (isGalleryView) { PopulateGalleryView(); } btnInstalled.Click += btnInstalled_Click; btnUpdateAvailable.Click += btnUpdateAvailable_Click; btnNewerThanList.Click += btnNewerThanList_Click; } private async void pullAppToDesktopBtn_Click(object sender, EventArgs e) { string selectedApp = ShowInstalledAppSelector("Select an app to pull to desktop"); if (selectedApp == null) { return; } DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, $"Do you want to extract {selectedApp}'s APK and OBB to a folder on your desktop now?", "Extract app?", MessageBoxButtons.YesNo); if (dialogResult1 == DialogResult.No) { return; } if (!isworking) { isworking = true; progressBar.IsIndeterminate = true; progressBar.OperationType = "Loading"; string HWID = SideloaderUtilities.UUID(); string GameName = selectedApp; string packageName = Sideloader.gameNameToPackageName(GameName); string InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {packageName} | grep versionCode -F\"").Output; InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingBeforeFirst(InstalledVersionCode, "versionCode="); InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingAfterFirst(InstalledVersionCode, " "); ulong VersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode)); if (Directory.Exists($"{settings.MainDir}\\{packageName}")) { Directory.Delete($"{settings.MainDir}\\{packageName}", true); } ProcessOutput output = new ProcessOutput("", ""); changeTitle("Extracting APK...."); _ = Directory.CreateDirectory($"{settings.MainDir}\\{packageName}"); Thread t1 = new Thread(() => { output = Sideloader.getApk(GameName); }) { IsBackground = true }; t1.Start(); while (t1.IsAlive) { await Task.Delay(100); } changeTitle("Extracting OBB if it exists...."); Thread t2 = new Thread(() => { output += ADB.RunAdbCommandToString($"pull \"/sdcard/Android/obb/{packageName}\" \"{settings.MainDir}\\{packageName}\""); }) { IsBackground = true }; t2.Start(); while (t2.IsAlive) { await Task.Delay(100); } if (File.Exists($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip")) { File.Delete($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip"); } string path = $"{settings.MainDir}\\7z.exe"; string cmd = $"7z a -mx1 \"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip\" .\\{packageName}\\*"; changeTitle("Zipping extracted application..."); Thread t3 = new Thread(() => { _ = ADB.RunCommandToString(cmd, path); }) { IsBackground = true }; t3.Start(); while (t3.IsAlive) { await Task.Delay(100); } if (File.Exists($"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip")) { File.Delete($"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip"); } File.Copy($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip", $"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip"); File.Delete($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip"); Directory.Delete($"{settings.MainDir}\\{packageName}", true); isworking = false; changeTitle(""); progressBar.IsIndeterminate = false; _ = FlexibleMessageBox.Show(Program.form, $"{GameName} pulled to:\n\n{GameName} v{VersionInt} {packageName}.zip\n\nOn your desktop!"); } } bool upToDate_Clicked = false; private void btnInstalled_Click(object sender, EventArgs e) { btnInstalled.Click -= btnInstalled_Click; btnUpdateAvailable.Click -= btnUpdateAvailable_Click; btnNewerThanList.Click -= btnNewerThanList_Click; if (updateAvailableClicked || NeedsDonation_Clicked) { updateAvailableClicked = false; NeedsDonation_Clicked = false; upToDate_Clicked = false; } if (!upToDate_Clicked) { upToDate_Clicked = true; // Filter to show installed, update available and newer than list entries FilterListByColors(new[] { ColorInstalled, ColorUpdateAvailable, ColorDonateGame }); } else { upToDate_Clicked = false; RestoreFullList(); } // Update button visual states UpdateFilterButtonStates(); // Refresh gallery view if active if (isGalleryView) { PopulateGalleryView(); } btnInstalled.Click += btnInstalled_Click; btnUpdateAvailable.Click += btnUpdateAvailable_Click; btnNewerThanList.Click += btnNewerThanList_Click; } bool NeedsDonation_Clicked = false; private void btnNewerThanList_Click(object sender, EventArgs e) { btnInstalled.Click -= btnInstalled_Click; btnUpdateAvailable.Click -= btnUpdateAvailable_Click; btnNewerThanList.Click -= btnNewerThanList_Click; if (updateAvailableClicked || upToDate_Clicked) { updateAvailableClicked = false; upToDate_Clicked = false; NeedsDonation_Clicked = false; } if (!NeedsDonation_Clicked) { NeedsDonation_Clicked = true; FilterListByColor(ColorDonateGame); // Needs donation color } else { NeedsDonation_Clicked = false; RestoreFullList(); } // Update button visual states UpdateFilterButtonStates(); // Refresh gallery view if active if (isGalleryView) { PopulateGalleryView(); } btnInstalled.Click += btnInstalled_Click; btnUpdateAvailable.Click += btnUpdateAvailable_Click; btnNewerThanList.Click += btnNewerThanList_Click; } private void FilterListByColors(Color[] targetColors) { changeTitle("Filtering Game List..."); if (_allItems == null || _allItems.Count == 0) { changeTitle("No games to filter"); return; } var targetArgbs = new HashSet(targetColors.Select(c => c.ToArgb())); var filteredItems = _allItems .Where(item => targetArgbs.Contains(item.ForeColor.ToArgb())) .ToList(); gamesListView.BeginUpdate(); gamesListView.Items.Clear(); gamesListView.Items.AddRange(filteredItems.ToArray()); gamesListView.EndUpdate(); // Refresh gallery view if active - set data source before calling PopulateGalleryView if (isGalleryView) { _galleryDataSource = filteredItems; PopulateGalleryView(); } changeTitle(""); } private void FilterListByColor(Color targetColor) { changeTitle("Filtering Game List..."); if (_allItems == null || _allItems.Count == 0) { changeTitle("No games to filter"); return; } var filteredItems = _allItems .Where(item => item.ForeColor.ToArgb() == targetColor.ToArgb()) .ToList(); gamesListView.BeginUpdate(); gamesListView.Items.Clear(); gamesListView.Items.AddRange(filteredItems.ToArray()); gamesListView.EndUpdate(); // Refresh gallery view if active - set data source before calling PopulateGalleryView if (isGalleryView) { _galleryDataSource = filteredItems; PopulateGalleryView(); } changeTitle(""); } private void RestoreFullList() { if (_allItems == null || _allItems.Count == 0) { changeTitle("No games to restore"); return; } gamesListView.BeginUpdate(); gamesListView.Items.Clear(); gamesListView.Items.AddRange(_allItems.ToArray()); gamesListView.EndUpdate(); // Refresh gallery view if active if (isGalleryView && gamesGalleryView.Visible) { _galleryDataSource = _allItems; PopulateGalleryView(); } changeTitle(""); } public static void OpenDirectory(string directoryPath) { if (Directory.Exists(directoryPath)) { ProcessStartInfo p = new ProcessStartInfo { Arguments = directoryPath, FileName = "explorer.exe" }; Process.Start(p); } } private void searchTextBox_Click(object sender, EventArgs e) { searchTextBox.Clear(); _ = searchTextBox.Focus(); } private void btnRunAdbCmd_Click(object sender, EventArgs e) { using (var adbForm = new AdbCommandForm()) { if (adbForm.ShowDialog(this) == DialogResult.OK) { string command = adbForm.Command; if (!string.IsNullOrWhiteSpace(command)) { string sentCommand = command.Replace("adb", "").Trim(); changeTitle($"Running ADB command: ADB {sentCommand}"); string output = ADB.RunAdbCommandToString(command).Output; if (adbForm.ToggleUpdatesClicked) { bool isNowDisabled = output.Contains("disabled") || command.Contains("disable"); string status = isNowDisabled ? "disabled" : "enabled"; _ = JR.Utils.GUI.Forms.FlexibleMessageBox.Show(this, $"OS Updates have been {status}.\n\nOutput:\n{output}"); } else { _ = JR.Utils.GUI.Forms.FlexibleMessageBox.Show(this, $"Ran ADB command: ADB {sentCommand}\r\nOutput:\r\n{output}"); } changeTitle(""); } } } } private void btnOpenDownloads_Click(object sender, EventArgs e) { string pathToOpen = settings.CustomDownloadDir ? $"{settings.DownloadDir}" : $"{Environment.CurrentDirectory}"; OpenDirectory(pathToOpen); } private void btnNoDevice_Click(object sender, EventArgs e) { bool currentStatus = settings.NodeviceMode || false; if (currentStatus) { settings.NodeviceMode = false; btnNoDevice.Text = "DISABLE SIDELOADING"; UpdateStatusLabels(); // No Device Mode is currently On. Toggle it Off (enable sideloading) // Ask user about delete after install preference DialogResult deleteResult = FlexibleMessageBox.Show(Program.form, "Delete game files after install?", "Sideloading enabled", MessageBoxButtons.YesNo); settings.DeleteAllAfterInstall = (deleteResult == DialogResult.Yes); } else { settings.NodeviceMode = true; settings.DeleteAllAfterInstall = false; btnNoDevice.Text = "ENABLE SIDELOADING"; UpdateStatusLabels(); } settings.Save(); } private ListViewItem _rightClickedItem; private void gamesListView_MouseClick(object sender, MouseEventArgs e) { if (e.Button != MouseButtons.Right) return; _rightClickedItem = gamesListView.GetItemAt(e.X, e.Y); if (_rightClickedItem == null) return; gamesListView.SelectedItems.Clear(); _rightClickedItem.Selected = true; UpdateFavoriteMenuItemText(); favoriteGame.Show(gamesListView, e.Location); } private void favoriteButton_Click(object sender, EventArgs e) { if (_rightClickedItem == null) return; string packageName = _rightClickedItem.SubItems[1].Text; if (settings.FavoritedGames.Contains(packageName)) settings.RemoveFavoriteGame(packageName); else settings.AddFavoriteGame(packageName); UpdateFavoriteMenuItemText(); } private void UpdateFavoriteMenuItemText() { if (_rightClickedItem == null) return; string packageName = _rightClickedItem.SubItems[1].Text; favoriteButton.Text = settings.FavoritedGames.Contains(packageName) ? "Remove from Favorites" : "★ Add to Favorites"; } private void favoriteSwitcher_Click(object sender, EventArgs e) { // Guard: ensure _allItems is populated if (_allItems == null || _allItems.Count == 0) { Logger.Log("favoriteSwitcher_Click: _allItems is null or empty"); return; } bool showFavoritesOnly = favoriteSwitcher.Text == "FAVORITES"; if (showFavoritesOnly) { favoriteSwitcher.Text = "ALL"; 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); } } else { favoriteSwitcher.Text = "FAVORITES"; gamesListView.BeginUpdate(); gamesListView.Items.Clear(); gamesListView.Items.AddRange(_allItems.ToArray()); gamesListView.EndUpdate(); _galleryDataSource = _allItems; if (isGalleryView && _fastGallery != null) { _fastGallery.RefreshFavoritesCache(); _fastGallery.UpdateItems(_allItems); } } // Clear other filter states updateAvailableClicked = false; upToDate_Clicked = false; NeedsDonation_Clicked = false; UpdateFilterButtonStates(); } public async void UpdateQuestInfoPanel() { // Check if device is actually connected by checking Devices list bool hasDevice = Devices != null && Devices.Count > 0 && !Devices.Contains("unauthorized"); bool bShowStatus = true; if ((!settings.NodeviceMode && hasDevice) || DeviceConnected) { try { ADB.DeviceID = GetDeviceID(); // Get device model string deviceModel = ADB.RunAdbCommandToString("shell getprop ro.product.model").Output.Trim(); if (string.IsNullOrEmpty(deviceModel)) { deviceModel = "No Device Found"; SetQuestStorageProgress(0); bShowStatus = false; } string firmware = ADB.RunAdbCommandToString("shell getprop ro.build.branch").Output.Trim(); // releases-oculus-14.0-v78 if (string.IsNullOrEmpty(firmware)) { firmware = string.Empty; } else { firmware = Utilities.StringUtilities.RemoveEverythingBeforeFirst(firmware, "-v"); firmware = Utilities.StringUtilities.KeepOnlyNumbers(firmware); } // Get storage info string storageOutput = ADB.RunAdbCommandToString("shell df /sdcard").Output; string[] lines = storageOutput.Split('\n'); long totalSpace = 0; long usedSpace = 0; long freeSpace = 0; if (lines.Length > 1) { string[] parts = lines[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 4) { if (long.TryParse(parts[1], out totalSpace) && long.TryParse(parts[2], out usedSpace) && long.TryParse(parts[3], out freeSpace)) { totalSpace = totalSpace / 1024; // Convert to MB usedSpace = usedSpace / 1024; freeSpace = freeSpace / 1024; // Format free space display if (freeSpace > 1024) { freeSpaceText = $"{(freeSpace / 1024.0):F2} GB AVAILABLE"; } else { freeSpaceText = $"{freeSpace} MB AVAILABLE"; } freeSpaceTextDetailed = $"{(usedSpace / 1024.0):F0} GB OF {(totalSpace / 1024.0):F0} GB USED"; } } } // Calculate storage percentage used - clamped to 1%..100% int storagePercentUsed = Math.Min(100, Math.Max(1, (100 - (totalSpace > 0 ? (int)((usedSpace * 100) / totalSpace) : 0)))); // Update UI on main thread questInfoPanel.Invoke(() => { string qinfo = deviceModel; if (!string.IsNullOrEmpty(firmware)) { qinfo = $"{qinfo} (v{firmware})"; } questInfoLabel.Text = qinfo; diskLabel.Text = freeSpaceText; SetQuestStorageProgress(storagePercentUsed); }); } catch (Exception ex) { Logger.Log($"Unable to update quest info panel: {ex.Message}", LogLevel.ERROR); questInfoPanel.Invoke(() => { questInfoLabel.Text = "No Device Found"; SetQuestStorageProgress(0); bShowStatus = false; }); } } else { questInfoPanel.Invoke(() => { questInfoLabel.Text = "No Device Found"; SetQuestStorageProgress(0); bShowStatus = false; }); } // Toggle visibility atomically on UI thread, based on device status questInfoPanel.Invoke(() => { questStorageProgressBar.Visible = true; batteryLevImg.Visible = bShowStatus; batteryLabel.Visible = bShowStatus; questInfoLabel.Visible = true; diskLabel.Visible = bShowStatus; }); } private void QuestInfoHoverEnter(object sender, EventArgs e) { // Only react when device info is shown if (!questStorageProgressBar.Visible) return; diskLabel.Text = freeSpaceTextDetailed; } // Restore the original baseline text ("XX GB FREE") private void QuestInfoHoverLeave(object sender, EventArgs e) { // Only react when device info is shown if (!questStorageProgressBar.Visible) return; // Ignore leave fired when moving between child controls inside the container var panel = questInfoPanel; var mouse = panel.PointToClient(MousePosition); if (panel.ClientRectangle.Contains(mouse)) return; diskLabel.Text = freeSpaceText; } private void questStorageProgressBar_Paint(object sender, PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.Clear(questStorageProgressBar.BackColor); int w = questStorageProgressBar.ClientSize.Width; int h = questStorageProgressBar.ClientSize.Height; if (w <= 0 || h <= 0) return; // Rounded rectangle parameters (outer + fill share the same geometry). int radius = 10; Color bgColor = Color.FromArgb(28, 32, 38); Color borderColor = Color.FromArgb(60, 65, 75); // Modern fill gradient (adjust as desired) Color progressStart = Color.FromArgb(43, 160, 140); Color progressEnd = Color.FromArgb(30, 110, 95); // Build the rounded outer path (used for background and clipping). var outer = new RoundedRectangleF(w, h, radius); // Paint background using (var bgBrush = new SolidBrush(bgColor)) using (var borderPen = new Pen(borderColor, 1f)) { g.FillPath(bgBrush, outer.Path); g.DrawPath(borderPen, outer.Path); } // Progress fraction and width float p = Math.Max(0f, Math.Min(1f, _questStorageProgress / 100f)); int progressWidth = Math.Max(0, (int)(w * p)); if (progressWidth <= 0) return; // Near-full rounding behavior: // As progress approaches 100%, progressively include the outer right-rounded corners. // Threshold start at 97%. At 97% -> straight cut; at 100% -> fully rounded outer corners. float t = 0f; if (p > 0.97f) { t = Math.Min(1f, (p - 0.97f) / 0.03f); // 0..1 over last 3% } // Build a clipping region for the fill: intersection of outer rounded rect with // the left rectangular portion [0..progressWidth] // plus a progressive right-cap region that extends into the rounded corners // with width up to 2*radius, scaled by t. Region fillClip = new Region(new Rectangle(0, 0, progressWidth, h)); if (t > 0f) { int capWidth = (int)(t * (2 * radius)); if (capWidth > 0) { // This rectangle sits inside the area of the right rounded corners, // so union-ing it with the rectangular clip allows the fill to // progressively "wrap" into the curvature. var rightCapRect = new Rectangle(w - (2 * radius), 0, capWidth, h); fillClip.Union(rightCapRect); } } Region prevClip = g.Clip; try { // Final fill region = outer rounded path ∩ fillClip using (var outerRegion = new Region(outer.Path)) { outerRegion.Intersect(fillClip); g.SetClip(outerRegion, CombineMode.Replace); using (var progressBrush = new LinearGradientBrush( new Rectangle(0, 0, Math.Max(1, progressWidth), h), progressStart, progressEnd, LinearGradientMode.Horizontal)) { // Fill the outer path; clipping ensures the fill grows left to right, // stays fully flush to the outer geometry, and never exceeds it. g.FillPath(progressBrush, outer.Path); } } } finally { // Restore clip and re-stroke border to keep outline crisp g.Clip = prevClip; using (var borderPen = new Pen(borderColor, 1f)) { g.DrawPath(borderPen, outer.Path); } } } private void SetQuestStorageProgress(int percentage) { _questStorageProgress = Math.Max(0, Math.Min(100, percentage)); if (questStorageProgressBar.InvokeRequired) { questStorageProgressBar.Invoke(new Action(() => questStorageProgressBar.Invalidate())); } else { questStorageProgressBar.Invalidate(); } } private void btnViewToggle_Click(object sender, EventArgs e) { // Capture currently selected item before switching views string selectedReleaseName = null; if (gamesListView.SelectedItems.Count > 0) { var selectedItem = gamesListView.SelectedItems[0]; if (selectedItem.SubItems.Count > 1) { selectedReleaseName = selectedItem.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text; } } // Capture current sort state before switching if (isGalleryView && _fastGallery != null) { // Save gallery sort state _sharedSortField = _fastGallery.CurrentSortField; _sharedSortDirection = _fastGallery.CurrentSortDirection; // Flip popularity direction when going from gallery to list due to flipped underlying logic // Gallery: Descending = Most Popular first // List: Ascending = Most Popular first if (_sharedSortField == SortField.Popularity) { _sharedSortDirection = _sharedSortDirection == SortDirection.Ascending ? SortDirection.Descending : SortDirection.Ascending; } } else if (!isGalleryView && _listViewRenderer != null && lvwColumnSorter != null) { // Save list view sort state _sharedSortField = ColumnIndexToSortField(lvwColumnSorter.SortColumn); _sharedSortDirection = lvwColumnSorter.Order == SortOrder.Ascending ? SortDirection.Ascending : SortDirection.Descending; // Flip popularity direction when going from list to gallery due to flipped underlying logic // List: Ascending = Most Popular first // Gallery: Descending = Most Popular first if (_sharedSortField == SortField.Popularity) { _sharedSortDirection = _sharedSortDirection == SortDirection.Ascending ? SortDirection.Descending : SortDirection.Ascending; } } isGalleryView = !isGalleryView; // Save user preference settings.UseGalleryView = isGalleryView; settings.Save(); if (isGalleryView) { btnViewToggle.Text = "LIST"; gamesListView.Visible = false; gamesGalleryView.Visible = true; // Only populate if data is available, otherwise it will be populated when initListView completes if (_allItems != null && _allItems.Count > 0) { PopulateGalleryView(); // Scroll to the previously selected item in gallery view if (!string.IsNullOrEmpty(selectedReleaseName) && _fastGallery != null) { _fastGallery.ScrollToPackage(selectedReleaseName); } } } else { btnViewToggle.Text = "GALLERY"; gamesGalleryView.Visible = false; gamesListView.Visible = true; CleanupGalleryView(); // Apply shared sort state to list view ApplySortToListView(); } } private SortField ColumnIndexToSortField(int columnIndex) { switch (columnIndex) { case 0: return SortField.Name; case 4: return SortField.LastUpdated; case 5: return SortField.Size; case 6: return SortField.Popularity; default: return SortField.Name; } } private int SortFieldToColumnIndex(SortField field) { switch (field) { case SortField.Name: return 0; case SortField.LastUpdated: return 4; case SortField.Size: return 5; case SortField.Popularity: return 6; default: return 0; } } private void ApplySortToListView() { if (_listViewRenderer == null || lvwColumnSorter == null) return; int columnIndex = SortFieldToColumnIndex(_sharedSortField); SortOrder order = _sharedSortDirection == SortDirection.Ascending ? SortOrder.Ascending : SortOrder.Descending; _listViewRenderer.ApplySort(columnIndex, order); } private void PopulateGalleryView() { // If _galleryDataSource was already set (by search or filter), use it // Otherwise, determine what to display based on current state if (_galleryDataSource == null) { if (updateAvailableClicked || upToDate_Clicked || NeedsDonation_Clicked) { _galleryDataSource = gamesListView.Items.Cast().ToList(); } else { _galleryDataSource = _allItems ?? gamesListView.Items.Cast().ToList(); } } if (_galleryDataSource == null) { _galleryDataSource = new List(); } // If gallery already exists, just update the data source if (_fastGallery != null && !_fastGallery.IsDisposed) { _fastGallery.UpdateItems(_galleryDataSource); return; } // First time creation CleanupGalleryView(); int targetWidth = gamesGalleryView.ClientSize.Width > 0 ? gamesGalleryView.ClientSize.Width : gamesGalleryView.Width; int targetHeight = gamesGalleryView.ClientSize.Height > 0 ? gamesGalleryView.ClientSize.Height : gamesGalleryView.Height; if (targetHeight <= 0) targetHeight = 350; if (targetWidth <= 0) targetWidth = 1145; gamesGalleryView.AutoScroll = false; gamesGalleryView.Padding = Padding.Empty; gamesGalleryView.Controls.Clear(); _fastGallery = new FastGalleryPanel(_galleryDataSource, TILE_WIDTH, TILE_HEIGHT, TILE_SPACING, targetWidth, targetHeight); _fastGallery.TileClicked += FastGallery_TileClicked; _fastGallery.TileDoubleClicked += FastGallery_TileDoubleClicked; _fastGallery.TileDeleteClicked += FastGallery_TileDeleteClicked; _fastGallery.SortChanged += FastGallery_SortChanged; // Apply current shared sort state to gallery _fastGallery.SetSortState(_sharedSortField, _sharedSortDirection); gamesGalleryView.Controls.Add(_fastGallery); _fastGallery.Anchor = AnchorStyles.None; gamesGalleryView.Resize += GamesGalleryView_Resize; } private async void FastGallery_TileDeleteClicked(object sender, int index) { if (index < 0 || _fastGallery == null) return; var item = _fastGallery.GetItemAtIndex(index); if (item == null) return; await UninstallGameAsync(item); } private void FastGallery_SortChanged(object sender, SortField field) { // Update shared state from gallery if (_fastGallery != null) { _sharedSortField = _fastGallery.CurrentSortField; _sharedSortDirection = _fastGallery.CurrentSortDirection; } } private void GamesGalleryView_Resize(object sender, EventArgs e) { if (_fastGallery != null && !_fastGallery.IsDisposed) { _fastGallery.Size = gamesGalleryView.ClientSize; } } private void CleanupGalleryView() { gamesGalleryView.Resize -= GamesGalleryView_Resize; if (_fastGallery != null) { _fastGallery.Dispose(); _fastGallery = null; } } private void FastGallery_TileClicked(object sender, int itemIndex) { if (itemIndex < 0 || _fastGallery == null) return; // Get the actual item from the gallery's current (sorted) list var item = _fastGallery.GetItemAtIndex(itemIndex); if (item == null || item.SubItems.Count <= 2) return; string releaseName = item.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text; string gameName = item.SubItems[SideloaderRCLONE.GameNameIndex].Text; // Clear all selections first - must deselect each item individually // because SelectedItems.Clear() doesn't work reliably when ListView is hidden foreach (ListViewItem listItem in gamesListView.Items) { listItem.Selected = false; } // Find and select the matching item in gamesListView using release name foreach (ListViewItem listItem in gamesListView.Items) { if (listItem.SubItems.Count > 1 && listItem.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text.Equals(releaseName, StringComparison.OrdinalIgnoreCase)) { listItem.Selected = true; listItem.EnsureVisible(); break; } } // Load release notes string notePath = Path.Combine(SideloaderRCLONE.NotesFolder, $"{releaseName}.txt"); notesRichTextBox.Text = File.Exists(notePath) ? File.ReadAllText(notePath) : ""; UpdateNotesScrollBar(); } private void FastGallery_TileDoubleClicked(object sender, int itemIndex) { if (itemIndex < 0 || _fastGallery == null) return; // Get the actual item from the gallery's current (sorted) list var item = _fastGallery.GetItemAtIndex(itemIndex); if (item == null || item.SubItems.Count <= 2) return; // Use release name to match the correct entry string releaseName = item.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text; // Clear all selections first - must deselect each item individually // because SelectedItems.Clear() doesn't work reliably when ListView is hidden foreach (ListViewItem listItem in gamesListView.Items) { listItem.Selected = false; } // Find and select the matching item in gamesListView by release name foreach (ListViewItem listItem in gamesListView.Items) { if (listItem.SubItems.Count > 1 && listItem.SubItems[SideloaderRCLONE.ReleaseNameIndex].Text.Equals(releaseName, StringComparison.OrdinalIgnoreCase)) { listItem.Selected = true; downloadInstallGameButton_Click(downloadInstallGameButton, EventArgs.Empty); break; } } } private void ListViewUninstallButton_Paint(object sender, PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(gamesListView.BackColor); int w = _listViewUninstallButton.Width; int h = _listViewUninstallButton.Height; var btnRect = new Rectangle(0, 0, w, h); // Colors matching GalleryPanel's uninstall button Color bgColor = _listViewUninstallButtonHovered ? Color.FromArgb(255, 220, 70, 70) // DeleteButtonHoverBg : Color.FromArgb(255, 180, 50, 50); // DeleteButtonBg // Draw rectangle background using (var bgBrush = new SolidBrush(bgColor)) { g.FillRectangle(bgBrush, btnRect); } // Draw trash icon int iconPadding = 3; int iconX = iconPadding; int iconY = iconPadding - 1; int iconSize = w - iconPadding * 2; using (var pen = new Pen(Color.White, 1.5f)) { // Trash can body int bodyTop = iconY + 4; int bodyBottom = iconY + iconSize; int bodyLeft = iconX + 2; int bodyRight = iconX + iconSize - 2; // Draw body outline (trapezoid-ish shape) g.DrawLine(pen, bodyLeft, bodyTop, bodyLeft + 1, bodyBottom); g.DrawLine(pen, bodyLeft + 1, bodyBottom, bodyRight - 1, bodyBottom); g.DrawLine(pen, bodyRight - 1, bodyBottom, bodyRight, bodyTop); // Draw lid g.DrawLine(pen, iconX, bodyTop, iconX + iconSize, bodyTop); // Draw handle on lid int handleLeft = iconX + iconSize / 2 - 3; int handleRight = iconX + iconSize / 2 + 3; int handleTop = iconY + 1; g.DrawLine(pen, handleLeft, bodyTop, handleLeft, handleTop); g.DrawLine(pen, handleLeft, handleTop, handleRight, handleTop); g.DrawLine(pen, handleRight, handleTop, handleRight, bodyTop); // Draw vertical lines inside trash int lineY1 = bodyTop + 3; int lineY2 = bodyBottom - 3; g.DrawLine(pen, iconX + iconSize / 2, lineY1, iconX + iconSize / 2, lineY2); if (iconSize > 10) { g.DrawLine(pen, iconX + iconSize / 2 - 4, lineY1, iconX + iconSize / 2 - 4, lineY2); g.DrawLine(pen, iconX + iconSize / 2 + 4, lineY1, iconX + iconSize / 2 + 4, lineY2); } } } private void gamesPictureBox_Paint(object sender, PaintEventArgs e) { // Only draw placeholder if no image is loaded if (gamesPictureBox.BackgroundImage != null && gamesPictureBox.BackgroundImage.Width > 1 && gamesPictureBox.BackgroundImage.Height > 1) { return; } var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var thumbRect = new Rectangle(0, 0, gamesPictureBox.Width, gamesPictureBox.Height); // Draw placeholder background using (var brush = new SolidBrush(Color.FromArgb(35, 35, 40))) { g.FillRectangle(brush, thumbRect); } // When disclaimer is gone if (freeDisclaimer.Enabled == false) { // Draw emoji placeholder using (var textBrush = new SolidBrush(Color.FromArgb(70, 70, 80))) { var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; g.DrawString("🎮", new Font("Segoe UI Emoji", 32f), textBrush, thumbRect, sf); } } } private void webViewPlaceholderPanel_Paint(object sender, PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.PixelOffsetMode = PixelOffsetMode.HighQuality; int radius = 8; var rect = new Rectangle(0, 0, webViewPlaceholderPanel.Width - 1, webViewPlaceholderPanel.Height - 1); Color panelColor = Color.FromArgb(24, 26, 30); Color cornerBgColor = Color.FromArgb(32, 35, 45); // Clear with corner background color first g.Clear(cornerBgColor); using (var path = CreateRoundedRectPath(rect, radius)) { // Draw rounded background using (var brush = new SolidBrush(panelColor)) { g.FillPath(brush, path); } } // Apply rounded region to clip the panel using (var regionPath = CreateRoundedRectPath(new Rectangle(0, 0, webViewPlaceholderPanel.Width, webViewPlaceholderPanel.Height), radius)) { webViewPlaceholderPanel.Region = new Region(regionPath); } // Draw emoji placeholder using (var textBrush = new SolidBrush(Color.FromArgb(60, 65, 70))) { var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; g.DrawString("🎮", new Font("Segoe UI Emoji", 32f), textBrush, rect, sf); } } public void ShowVideoPlaceholder() { webViewPlaceholderPanel.Visible = true; webViewPlaceholderPanel.BringToFront(); } public void HideVideoPlaceholder() { webViewPlaceholderPanel.Visible = false; } private void SubscribeToHoverEvents(Control parent) { parent.MouseEnter += QuestInfoHoverEnter; parent.MouseLeave += QuestInfoHoverLeave; foreach (Control child in parent.Controls) { SubscribeToHoverEvents(child); } } private string ShowInstalledAppSelector(string promptText = "Select an installed app...") { // Refresh the list of installed apps listAppsBtn(); if (m_combo.Items.Count == 0) { FlexibleMessageBox.Show(Program.form, "No installed apps found on the device."); return null; } // Create a dialog to show the combo selection using (Form dialog = new Form()) { dialog.Text = promptText; dialog.Size = new Size(450, 150); dialog.StartPosition = FormStartPosition.CenterParent; dialog.FormBorderStyle = FormBorderStyle.FixedDialog; dialog.MaximizeBox = false; dialog.MinimizeBox = false; dialog.BackColor = Color.FromArgb(20, 24, 29); dialog.ForeColor = Color.White; var label = new Label { Text = promptText, ForeColor = Color.White, AutoSize = true, Location = new Point(15, 15) }; var comboBox = new ComboBox { Location = new Point(15, 40), Size = new Size(400, 24), DropDownStyle = ComboBoxStyle.DropDown, BackColor = Color.FromArgb(42, 45, 58), ForeColor = Color.White, FlatStyle = FlatStyle.Standard }; // Copy items from m_combo foreach (var item in m_combo.Items) { comboBox.Items.Add(item); } var okButton = CreateStyledButton("OK", DialogResult.OK, new Point(255, 75)); var cancelButton = CreateStyledButton("Cancel", DialogResult.Cancel, new Point(340, 75), false); dialog.Controls.AddRange(new Control[] { label, comboBox, okButton, cancelButton }); dialog.AcceptButton = okButton; dialog.CancelButton = cancelButton; if (dialog.ShowDialog(this) == DialogResult.OK && comboBox.SelectedIndex != -1) { return comboBox.SelectedItem.ToString(); } } return null; } private string ShowDeviceSelector(string promptText = "Select a device") { // Refresh the list of devices first string output = ADB.RunAdbCommandToString("devices").Output; string[] lines = output.Split('\n'); var deviceList = new List(); for (int i = 1; i < lines.Length; i++) { string line = lines[i].Trim(); if (line.Length > 0 && !string.IsNullOrWhiteSpace(line)) { string deviceId = line.Split('\t')[0]; if (!string.IsNullOrEmpty(deviceId)) { deviceList.Add(deviceId); } } } if (deviceList.Count == 0) { FlexibleMessageBox.Show(Program.form, "No devices found. Please connect a device and try again."); return null; } // If only one device, return it directly if (deviceList.Count == 1) { // Update internal combo for compatibility devicesComboBox.Items.Clear(); devicesComboBox.Items.Add(deviceList[0]); devicesComboBox.SelectedIndex = 0; FlexibleMessageBox.Show(this, $"Selected device: {deviceList[0]}\n\nNo other devices detected"); return deviceList[0]; } // Create a dialog to show the device selection using (Form dialog = new Form()) { dialog.Text = promptText; dialog.Size = new Size(400, 150); dialog.StartPosition = FormStartPosition.CenterParent; dialog.FormBorderStyle = FormBorderStyle.FixedDialog; dialog.MaximizeBox = false; dialog.MinimizeBox = false; dialog.BackColor = Color.FromArgb(20, 24, 29); dialog.ForeColor = Color.White; var label = new Label { Text = promptText, ForeColor = Color.White, AutoSize = true, Location = new Point(15, 15) }; var comboBox = new ComboBox { Location = new Point(15, 40), Size = new Size(350, 24), DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Color.FromArgb(42, 45, 58), ForeColor = Color.White, FlatStyle = FlatStyle.Standard }; // Add devices to combo foreach (var device in deviceList) { comboBox.Items.Add(device); } if (comboBox.Items.Count > 0) { comboBox.SelectedIndex = 0; } var okButton = CreateStyledButton("OK", DialogResult.OK, new Point(205, 75)); var cancelButton = CreateStyledButton("Cancel", DialogResult.Cancel, new Point(290, 75), false); dialog.Controls.AddRange(new Control[] { label, comboBox, okButton, cancelButton }); dialog.AcceptButton = okButton; dialog.CancelButton = cancelButton; if (dialog.ShowDialog(this) == DialogResult.OK && comboBox.SelectedIndex != -1) { string selectedDevice = comboBox.SelectedItem.ToString(); // Update internal combo for compatibility devicesComboBox.Items.Clear(); foreach (var device in deviceList) { devicesComboBox.Items.Add(device); } devicesComboBox.SelectedItem = selectedDevice; return selectedDevice; } } return null; } private void selectDeviceButton_Click(object sender, EventArgs e) { string selectedDevice = ShowDeviceSelector("Select a device"); if (selectedDevice != null) { ADB.DeviceID = selectedDevice; changeTitlebarToDevice(); showAvailableSpace(); changeTitle($"Selected device: {selectedDevice}", true); } } private void selectMirrorButton_Click(object sender, EventArgs e) { string selectedMirror = ShowMirrorSelector("Select a mirror"); if (selectedMirror != null) { // Find and select the mirror in the hidden remotesList for (int i = 0; i < remotesList.Items.Count; i++) { if (remotesList.Items[i].ToString() == selectedMirror) { remotesList.SelectedIndex = i; break; } } } } private string ShowMirrorSelector(string promptText = "Select a mirror") { if (remotesList.Items.Count == 0) { FlexibleMessageBox.Show(this, "No mirrors available."); return null; } // If only one mirror, just inform the user if (remotesList.Items.Count == 1) { string onlyMirror = remotesList.Items[0].ToString(); FlexibleMessageBox.Show(this, $"Currently using mirror: {onlyMirror}\n\nNo other mirrors available"); return onlyMirror; } using (Form dialog = new Form()) { dialog.Text = promptText; dialog.Size = new Size(350, 150); dialog.StartPosition = FormStartPosition.CenterParent; dialog.FormBorderStyle = FormBorderStyle.FixedDialog; dialog.MaximizeBox = false; dialog.MinimizeBox = false; dialog.BackColor = Color.FromArgb(20, 24, 29); dialog.ForeColor = Color.White; var label = new Label { Text = $"{promptText} (Current: {remotesList.SelectedItem ?? "None"})", ForeColor = Color.White, AutoSize = true, Location = new Point(15, 15) }; var comboBox = new ComboBox { Location = new Point(15, 40), Size = new Size(300, 24), DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Color.FromArgb(42, 45, 58), ForeColor = Color.White, FlatStyle = FlatStyle.Standard }; // Add mirrors to combo foreach (var item in remotesList.Items) { comboBox.Items.Add(item.ToString()); } // Select current mirror if (remotesList.SelectedIndex >= 0) { comboBox.SelectedIndex = remotesList.SelectedIndex; } else if (comboBox.Items.Count > 0) { comboBox.SelectedIndex = 0; } var okButton = CreateStyledButton("OK", DialogResult.OK, new Point(155, 75)); var cancelButton = CreateStyledButton("Cancel", DialogResult.Cancel, new Point(240, 75), false); dialog.Controls.AddRange(new Control[] { label, comboBox, okButton, cancelButton }); dialog.AcceptButton = okButton; dialog.CancelButton = cancelButton; if (dialog.ShowDialog(this) == DialogResult.OK && comboBox.SelectedIndex != -1) { return comboBox.SelectedItem.ToString(); } } return null; } private Button CreateStyledButton(string text, DialogResult dialogResult, Point location, bool isPrimary = true) { var button = new Button { Text = text, DialogResult = dialogResult, Location = location, Size = new Size(75, 28), FlatStyle = FlatStyle.Flat, BackColor = Color.FromArgb(42, 45, 58), ForeColor = Color.White, Font = new Font("Segoe UI", 9F), Cursor = Cursors.Hand }; button.FlatAppearance.BorderSize = 0; // Track hover state bool isHovered = false; button.MouseEnter += (s, e) => { isHovered = true; button.Invalidate(); }; button.MouseLeave += (s, e) => { isHovered = false; button.Invalidate(); }; button.Paint += (s, e) => { var btn = s as Button; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.PixelOffsetMode = PixelOffsetMode.HighQuality; int radius = 4; Rectangle drawRect = new Rectangle(1, 1, btn.Width - 2, btn.Height - 2); // Clear with parent background using (SolidBrush clearBrush = new SolidBrush(btn.Parent?.BackColor ?? Color.FromArgb(20, 24, 29))) { g.FillRectangle(clearBrush, 0, 0, btn.Width, btn.Height); } using (GraphicsPath path = CreateRoundedRectPath(drawRect, radius)) { // Hover: accent color, Normal: dark button color Color bgColor = isHovered ? Color.FromArgb(93, 203, 173) : btn.BackColor; Color textColor = isHovered ? Color.FromArgb(20, 20, 20) : btn.ForeColor; using (SolidBrush brush = new SolidBrush(bgColor)) { g.FillPath(brush, path); } // Subtle border on normal state if (!isHovered) { using (Pen borderPen = new Pen(Color.FromArgb(70, 75, 90), 1)) { g.DrawPath(borderPen, path); } } TextRenderer.DrawText(g, btn.Text, btn.Font, new Rectangle(0, 0, btn.Width, btn.Height), textColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); } // Set rounded region using (GraphicsPath regionPath = CreateRoundedRectPath(new Rectangle(0, 0, btn.Width, btn.Height), radius)) { btn.Region = new Region(regionPath); } }; return button; } private GraphicsPath CreateRoundedRectPath(Rectangle rect, int radius) { GraphicsPath path = new GraphicsPath(); if (radius <= 0) { path.AddRectangle(rect); return path; } int diameter = radius * 2; diameter = Math.Min(diameter, Math.Min(rect.Width, rect.Height)); radius = diameter / 2; Rectangle arcRect = new Rectangle(rect.Location, new Size(diameter, diameter)); path.AddArc(arcRect, 180, 90); arcRect.X = rect.Right - diameter; path.AddArc(arcRect, 270, 90); arcRect.Y = rect.Bottom - diameter; path.AddArc(arcRect, 0, 90); arcRect.X = rect.Left; path.AddArc(arcRect, 90, 90); path.CloseFigure(); return path; } private void UpdateFilterButtonStates() { Color inactiveStroke = Color.FromArgb(74, 74, 74); Color activeBg = Color.FromArgb(40, 45, 55); Color inactiveBg = Color.FromArgb(32, 35, 45); // btnInstalled state if (upToDate_Clicked) { btnInstalled.StrokeColor = ColorInstalled; btnInstalled.Inactive1 = activeBg; btnInstalled.Inactive2 = activeBg; } else { btnInstalled.StrokeColor = inactiveStroke; btnInstalled.Inactive1 = inactiveBg; btnInstalled.Inactive2 = inactiveBg; } // btnUpdateAvailable state if (updateAvailableClicked) { btnUpdateAvailable.StrokeColor = ColorUpdateAvailable; btnUpdateAvailable.Inactive1 = activeBg; btnUpdateAvailable.Inactive2 = activeBg; } else { btnUpdateAvailable.StrokeColor = inactiveStroke; btnUpdateAvailable.Inactive1 = inactiveBg; btnUpdateAvailable.Inactive2 = inactiveBg; } // btnNewerThanList state if (NeedsDonation_Clicked) { btnNewerThanList.StrokeColor = ColorDonateGame; btnNewerThanList.Inactive1 = activeBg; btnNewerThanList.Inactive2 = activeBg; } else { btnNewerThanList.StrokeColor = inactiveStroke; btnNewerThanList.Inactive1 = inactiveBg; btnNewerThanList.Inactive2 = inactiveBg; } // Force repaint btnInstalled.Invalidate(); btnUpdateAvailable.Invalidate(); btnNewerThanList.Invalidate(); } private void UnfocusSearchTextBox(object sender, EventArgs e) { // Only unfocus if the search text box currently has focus if (searchTextBox.Focused) { // Move focus to the appropriate view if (isGalleryView && gamesGalleryView.Visible) { gamesGalleryView.Focus(); } else { gamesListView.Focus(); } } } private void UpdateNotesScrollBar() { // Check if content height exceeds visible height int contentHeight = notesRichTextBox.GetPositionFromCharIndex(notesRichTextBox.TextLength).Y + notesRichTextBox.Font.Height; if (contentHeight > notesRichTextBox.ClientSize.Height) { notesRichTextBox.ScrollBars = RichTextBoxScrollBars.Vertical; } else { notesRichTextBox.ScrollBars = RichTextBoxScrollBars.None; } } public class CenteredMenuRenderer : ToolStripProfessionalRenderer { protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) { var rect = new Rectangle(Point.Empty, e.Item.Size); Color bgColor = e.Item.Selected ? Color.FromArgb(55, 58, 65) : Color.FromArgb(40, 42, 48); using (var brush = new SolidBrush(bgColor)) e.Graphics.FillRectangle(brush, rect); } protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) { // Use the full item bounds for centered text var textRect = new Rectangle(0, 0, e.Item.Width, e.Item.Height); TextRenderer.DrawText(e.Graphics, e.Text, e.TextFont, textRect, e.TextColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); } protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) { using (var brush = new SolidBrush(Color.FromArgb(40, 42, 48))) e.Graphics.FillRectangle(brush, e.AffectedBounds); } protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) { using (var pen = new Pen(Color.FromArgb(60, 63, 70))) e.Graphics.DrawRectangle(pen, 0, 0, e.AffectedBounds.Width - 1, e.AffectedBounds.Height - 1); } } public void SetTrailerVisibility(bool visible) { webView21.Enabled = visible; webView21.Visible = visible; if (!visible) ShowVideoPlaceholder(); } private void InitializeModernPanels() { Color panelColor = Color.FromArgb(24, 26, 30); // Initialize modern queue panel _queuePanel = new ModernQueuePanel { Size = new Size(250, 150), // Placeholder; resized by LayoutBottomPanels BackColor = panelColor }; _queuePanel.ItemRemoved += QueuePanel_ItemRemoved; _queuePanel.ItemReordered += QueuePanel_ItemReordered; // Sync with binding list gamesQueueList.ListChanged += (s, e) => SyncQueuePanel(); // Notes panel notesPanel = CreateRoundedPanel(notesRichTextBox, panelColor, 8, true); // Queue panel queuePanel = CreateQueuePanel(panelColor, 8); gamesQueueLabel.BringToFront(); lblNotes.BringToFront(); // Trigger initial layout LayoutBottomPanels(); } private Panel CreateQueuePanel(Color panelColor, int radius) { var panel = new Panel { Location = gamesQueListBox.Location, Size = new Size( gamesQueListBox.Width + ChildHorizontalPadding + ChildRightMargin, gamesQueListBox.Height + ReservedLabelHeight ), BackColor = Color.Transparent, Padding = new Padding(ChildHorizontalPadding, ChildTopMargin, ChildRightMargin, ChildTopMargin) }; // Double buffering typeof(Panel).InvokeMember("DoubleBuffered", System.Reflection.BindingFlags.SetProperty | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, null, panel, new object[] { true }); panel.Paint += (s, e) => { var p = (Panel)s; e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; var rect = new Rectangle(0, 0, p.Width - 1, p.Height - 1); using (var path = CreateRoundedRectPath(rect, radius)) using (var brush = new SolidBrush(panelColor)) { e.Graphics.FillPath(brush, path); } using (var regionPath = CreateRoundedRectPath(new Rectangle(0, 0, p.Width, p.Height), radius)) { p.Region = new Region(regionPath); } }; var parent = gamesQueListBox.Parent; parent.Controls.Remove(gamesQueListBox); gamesQueListBox.Dispose(); _queuePanel.Location = new Point(ChildHorizontalPadding, ChildTopMargin); _queuePanel.Anchor = AnchorStyles.None; panel.Controls.Add(_queuePanel); parent.Controls.Add(panel); panel.BringToFront(); return panel; } private void SyncQueuePanel() { if (_queuePanel == null) return; _queuePanel.SetItems(gamesQueueList); _queuePanel.IsDownloading = gamesAreDownloading && gamesQueueList.Count > 0; } private void QueuePanel_ItemRemoved(object sender, int index) { if (index == 0 && gamesQueueList.Count >= 1) { removedownloading = true; RCLONE.killRclone(); } else if (index > 0 && index < gamesQueueList.Count) { gamesQueueList.RemoveAt(index); } } private void QueuePanel_ItemReordered(object sender, ReorderEventArgs e) { if (e.FromIndex <= 0 || e.FromIndex >= gamesQueueList.Count) return; var item = gamesQueueList[e.FromIndex]; gamesQueueList.RemoveAt(e.FromIndex); int insertAt = Math.Max(1, Math.Min(e.ToIndex, gamesQueueList.Count)); gamesQueueList.Insert(insertAt, item); } private void notesRichTextBox_LinkClicked(object sender, LinkClickedEventArgs e) { try { Process.Start(e.LinkText); } catch (Exception ex) { Logger.Log($"Failed to open link: {ex.Message}", LogLevel.WARNING); } } private void ApplyWebViewRoundedCorners() { if (webView21 == null) return; int radius = 8; using (var path = CreateRoundedRectPath(new Rectangle(0, 0, webView21.Width, webView21.Height), radius)) { webView21.Region = new Region(path); } // Re-apply on resize webView21.SizeChanged -= WebView21_SizeChanged; webView21.SizeChanged += WebView21_SizeChanged; } private void WebView21_SizeChanged(object sender, EventArgs e) { if (webView21 == null || webView21.Width <= 0 || webView21.Height <= 0) return; int radius = 8; using (var path = CreateRoundedRectPath(new Rectangle(0, 0, webView21.Width, webView21.Height), radius)) { webView21.Region = new Region(path); } } private void MainForm_Resize(object sender, EventArgs e) { LayoutBottomPanels(); } private void LayoutChildInPanel(Panel panel, Control child, bool isNotesPanel) { int leftMargin = isNotesPanel ? NotesLeftMargin : ChildHorizontalPadding; child.Location = new Point(leftMargin, ChildTopMargin); // Width: panel width minus left + right margins int widthReduction = leftMargin + ChildRightMargin; int childWidth = Math.Max(0, panel.Width - widthReduction); // Height: panel height minus vertical padding and reserved label area int childHeight = Math.Max(0, panel.Height - (ChildHorizontalPadding * 2) - ReservedLabelHeight); child.Size = new Size(childWidth, childHeight); } private Panel CreateRoundedPanel(Control childControl, Color panelColor, int radius, bool isNotesPanel) { // Create wrapper panel var panel = new Panel { Location = childControl.Location, Size = new Size( childControl.Width + ChildHorizontalPadding + ChildRightMargin, childControl.Height + ReservedLabelHeight ), Anchor = AnchorStyles.None, BackColor = Color.Transparent, Padding = new Padding(ChildHorizontalPadding, ChildTopMargin, ChildRightMargin, ChildTopMargin) }; // Enable double buffering typeof(Panel).InvokeMember( "DoubleBuffered", System.Reflection.BindingFlags.SetProperty | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, null, panel, new object[] { true }); // Add paint handler for rounded corners panel.Paint += (sender, e) => { var p = (Panel)sender; e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; var rect = new Rectangle(0, 0, p.Width - 1, p.Height - 1); using (var path = CreateRoundedRectPath(rect, radius)) using (var brush = new SolidBrush(panelColor)) { e.Graphics.FillPath(brush, path); } // Apply rounded region using (var regionPath = CreateRoundedRectPath( new Rectangle(0, 0, p.Width, p.Height), radius)) { p.Region = new Region(regionPath); } }; // Move child control into panel var parent = childControl.Parent; parent.Controls.Add(panel); parent.Controls.Remove(childControl); // Layout child inside panel using shared helper childControl.Anchor = AnchorStyles.None; childControl.BackColor = panelColor; LayoutChildInPanel(panel, childControl, isNotesPanel); panel.Controls.Add(childControl); panel.BringToFront(); return panel; } private void LayoutBottomPanels() { // Skip if panels aren't initialized yet if (notesPanel == null || queuePanel == null) return; if (notesRichTextBox == null) return; // Panels start after webView21 (webView21 ends at 259 + 384 = 643, add spacing) int panelsStartX = 654; int availableWidth = this.ClientSize.Width - panelsStartX - RightMargin; // Queue panel gets fixed width, notes panel fills remaining space int desiredQueueWidth = 290; int queueWidth = Math.Max(200, Math.Min(desiredQueueWidth, availableWidth / 2)); int notesWidth = availableWidth - queueWidth - PanelSpacing; notesWidth = Math.Max(200, notesWidth); int panelY = this.ClientSize.Height - BottomPanelHeight - BottomMargin; // Queue panel queuePanel.Location = new Point(panelsStartX, panelY); queuePanel.Size = new Size(queueWidth, BottomPanelHeight); // Layout queue panel child (_queuePanel) if (_queuePanel != null) { _queuePanel.Location = new Point(ChildHorizontalPadding, ChildTopMargin); _queuePanel.Size = new Size( queuePanel.Width - ChildHorizontalPadding - ChildRightMargin, queuePanel.Height - ChildTopMargin * 2 - ReservedLabelHeight ); } // Notes panel int notesX = panelsStartX + queueWidth + PanelSpacing; notesPanel.Location = new Point(notesX, panelY); notesPanel.Size = new Size(this.ClientSize.Width - notesX - RightMargin, BottomPanelHeight); // Layout notes child LayoutChildInPanel(notesPanel, notesRichTextBox, isNotesPanel: true); // Position labels at bottom of their panels gamesQueueLabel.Location = new Point( queuePanel.Location.X + ChildHorizontalPadding + 3, queuePanel.Location.Y + queuePanel.Height - (LabelHeight + LabelBottomOffset) ); lblNotes.Location = new Point( notesPanel.Location.X + ChildHorizontalPadding, notesPanel.Location.Y + notesPanel.Height - (LabelHeight + LabelBottomOffset) ); // Ensure labels are visible gamesQueueLabel.BringToFront(); lblNotes.BringToFront(); // Force repaint of panels to update rounded corners queuePanel.Invalidate(); notesPanel.Invalidate(); } private async Task UninstallGameAsync(ListViewItem item) { string packageName = item.SubItems.Count > 2 ? item.SubItems[2].Text : ""; string gameName = item.Text; if (string.IsNullOrEmpty(packageName)) return; // Confirm uninstall DialogResult dialogresult = FlexibleMessageBox.Show( $"Are you sure you want to uninstall {gameName}?", "Proceed with uninstall?", MessageBoxButtons.YesNo); if (dialogresult == DialogResult.No) return; // Ask about backup if (!settings.CustomBackupDir) { backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups"); } else { backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups"); } DialogResult dialogresult2 = FlexibleMessageBox.Show( $"Do you want to attempt to automatically backup any saves to {backupFolder}\\{DateTime.Today.ToString("yyyy.MM.dd")}\\", "Attempt Game Backup?", MessageBoxButtons.YesNo); if (dialogresult2 == DialogResult.Yes) { Sideloader.BackupGame(packageName); } // Perform uninstall ProcessOutput output = new ProcessOutput("", ""); progressBar.IsIndeterminate = true; progressBar.OperationType = ""; await Task.Run(() => { output += Sideloader.UninstallGame(packageName); }); ShowPrcOutput(output); showAvailableSpace(); progressBar.IsIndeterminate = false; // Remove from combo box for (int i = 0; i < m_combo.Items.Count; i++) { string comboItem = m_combo.Items[i].ToString(); if (comboItem.Equals(gameName, StringComparison.OrdinalIgnoreCase) || comboItem.Equals(packageName, StringComparison.OrdinalIgnoreCase)) { m_combo.Items.RemoveAt(i); break; } } await RefreshGameListAsync(); } private async Task RefreshGameListAsync() { // Save current filter state before refreshing bool wasUpdateAvailableClicked = updateAvailableClicked; bool wasUpToDateClicked = upToDate_Clicked; bool wasNeedsDonationClicked = NeedsDonation_Clicked; bool wasFavoritesView = favoriteSwitcher.Text == "ALL"; // Save the currently selected package name to restore after refresh string selectedPackageName = null; if (gamesListView.SelectedItems.Count > 0) { var selectedItem = gamesListView.SelectedItems[0]; if (selectedItem.SubItems.Count > 2) { selectedPackageName = selectedItem.SubItems[2].Text; } } else if (isGalleryView && _fastGallery != null) { var galleryItem = _fastGallery.GetItemAtIndex(_fastGallery._selectedIndex); if (galleryItem != null && galleryItem.SubItems.Count > 2) { selectedPackageName = galleryItem.SubItems[2].Text; } } // Temporarily clear filter states updateAvailableClicked = false; upToDate_Clicked = false; NeedsDonation_Clicked = false; // Refresh the list to update installed status _allItemsInitialized = false; _galleryDataSource = null; listAppsBtn(); bool wasGalleryView = isGalleryView; isGalleryView = false; initListView(false); // Wait for initListView to finish rebuilding _allItems while (!_allItemsInitialized || !loaded) { await Task.Delay(50); } isGalleryView = wasGalleryView; // Reapply the active filter if (wasFavoritesView) { // Reapply favorites filter favoriteSwitcher.Text = "FAVORITES"; // Reset text first favoriteSwitcher_Click(favoriteSwitcher, EventArgs.Empty); // This will toggle to show favorites and set text to "ALL" } if (wasUpToDateClicked) { upToDate_Clicked = true; FilterListByColors(new[] { ColorInstalled, ColorUpdateAvailable, ColorDonateGame }); } else if (wasUpdateAvailableClicked) { updateAvailableClicked = true; FilterListByColor(ColorUpdateAvailable); } else if (wasNeedsDonationClicked) { NeedsDonation_Clicked = true; FilterListByColor(ColorDonateGame); } else if (isGalleryView) { gamesListView.Visible = false; gamesGalleryView.Visible = true; PopulateGalleryView(); } // Restore selection and scroll to the previously selected item if (!string.IsNullOrEmpty(selectedPackageName)) { RestoreSelectionByPackageName(selectedPackageName); } } private void RestoreSelectionByPackageName(string packageName) { if (string.IsNullOrEmpty(packageName)) return; // Restore in ListView foreach (ListViewItem item in gamesListView.Items) { if (item.SubItems.Count > 2 && item.SubItems[2].Text.Equals(packageName, StringComparison.OrdinalIgnoreCase)) { item.Selected = true; item.Focused = true; item.EnsureVisible(); break; } } // Restore in Gallery view if (isGalleryView && _fastGallery != null) { _fastGallery.ScrollToPackage(packageName); } } private async void timer_DeviceCheck(object sender, EventArgs e) { // Skip if a device is connected, we're in the middle of loading or other operations if (DeviceConnected || isLoading || isinstalling || isuploading) return; // Run a quick device check in background try { string output = await Task.Run(() => ADB.RunAdbCommandToString("devices", suppressLogging: true).Output); string[] lines = output.Split('\n'); bool hasDeviceNow = false; for (int i = 1; i < lines.Length; i++) { string line = lines[i].Trim(); if (line.Length > 0 && !string.IsNullOrWhiteSpace(line) && !line.Contains("unauthorized")) { hasDeviceNow = true; break; } } // Device state changed if (hasDeviceNow) { // Device connected - do a full refresh await CheckForDevice(); changeTitlebarToDevice(); showAvailableSpace(); listAppsBtn(); // Use RefreshGameListAsync to preserve filter state await RefreshGameListAsync(); UpdateStatusLabels(); } } catch { // Silently catch errors } } private void UpdateStatusLabels() { // Device ID if (DeviceConnected && Devices.Count > 0 && !string.IsNullOrEmpty(Devices[0])) { string deviceId = Devices[0].Replace("device", "").Trim(); // Truncate if too long if (deviceId.Length > 20) deviceId = deviceId.Substring(0, 17) + "..."; deviceIdLabel.Text = $"Device: {deviceId}"; } else { deviceIdLabel.Text = "Device: Not connected"; } // Active Mirror string mirrorName = "None"; if (UsingPublicConfig) { mirrorName = "Public"; } else if (remotesList.SelectedItem != null) { mirrorName = remotesList.SelectedItem.ToString(); } activeMirrorLabel.Text = $"Mirror: {mirrorName}"; UpdateSideloadingUI(); } public void UpdateSideloadingUI() { // Update the sideload button text if (settings.NodeviceMode) { btnNoDevice.Text = "ENABLE SIDELOADING"; } else { btnNoDevice.Text = "DISABLE SIDELOADING"; } // Sideloading Status if (settings.NodeviceMode) { sideloadingStatusLabel.Text = "Sideloading: Disabled"; sideloadingStatusLabel.ForeColor = Color.FromArgb(255, 100, 100); // Red-ish for disabled } else { sideloadingStatusLabel.Text = "Sideloading: Enabled"; sideloadingStatusLabel.ForeColor = Color.FromArgb(93, 203, 173); // Accent green for enabled } } public void UpdateProgressStatus(string operation, int current = 0, int total = 0, int percent = 0, TimeSpan? eta = null, double? speedMBps = null) { if (this.InvokeRequired) { this.Invoke(() => UpdateProgressStatus(operation, current, total, percent, eta, speedMBps)); return; } // Sync the progress bar's operation type to the current operation progressBar.OperationType = operation; var sb = new StringBuilder(); // Operation name sb.Append(operation); // Percentage if (percent > 0) { sb.Append($" ({percent}%)"); } // Speed if (speedMBps.HasValue && speedMBps.Value > 0) { sb.Append($" @ {speedMBps.Value:F1} MB/s"); } // File count if applicable if (total > 1) { sb.Append($" ({current}/{total})"); } // ETA if (eta.HasValue && eta.Value.TotalSeconds > 0) { sb.Append($" • ETA: {eta.Value:hh\\:mm\\:ss}"); } speedLabel.Text = sb.ToString(); } public void ClearProgressStatus() { if (this.InvokeRequired) { this.Invoke(ClearProgressStatus); return; } speedLabel.Text = ""; } } public static class ControlExtensions { public static void Invoke(this Control control, Action action) { if (control.InvokeRequired) { _ = control.Invoke(new MethodInvoker(action), null); } else { action.Invoke(); } } public static void SetStyle(this Control control, ControlStyles styles, bool value) { typeof(Control).GetMethod("SetStyle", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) ?.Invoke(control, new object[] { styles, value }); } } }