Files
rookie/MainForm.cs
jp64k 3e4787d1a2 Reworked DonorsListView UI, NewApps UI and ADB command UI
Introduced a cleaner AdbCommandForm for running custom ADB commands and toggling OS updates. The DonorsListView and MainForm UIs are modernized with rounded, shadowed windows, updated color schemes, and improved button and label styling. Removed legacy data bindings and redundant controls.
2025-12-07 23:30:16 +01:00

6752 lines
270 KiB
C#
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Configuration;
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<UploadGame> gamesToUpload = new List<UploadGame>();
public static string currremotesimple = String.Empty;
#else
public bool keyheld;
public static string CurrAPK;
public static string CurrPCKG;
private readonly List<UploadGame> gamesToUpload = new List<UploadGame>();
public static bool debugMode = false;
public bool DeviceConnected = false;
public static string currremotesimple = "";
#endif
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<ListViewItem> _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<string, string> _videoIdCache = new Dictionary<string, string>(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<ListViewItem> _allItems;
private Dictionary<string, List<ListViewItem>> _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();
// 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();
gamesQueListBox.DataSource = gamesQueueList;
SetCurrentLogPath();
StartTimers();
lvwColumnSorter = new ListViewColumnSorter();
gamesListView.ListViewItemSorter = lvwColumnSorter;
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 // 30ms
};
t2.Tick += new EventHandler(timer_Tick2);
t2.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<PublicConfig>(configFileData);
if (config != null && !string.IsNullOrWhiteSpace(config.BaseUri) && !string.IsNullOrWhiteSpace(config.Password))
{
PublicConfigFile = config;
hasPublicConfig = true;
}
}
}
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;
etaLabel.Text = String.Empty;
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"));
}
}
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.Style = ProgressBarStyle.Marquee;
// 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<Task>();
if (deviceConnectionTask != null) tasksToWait.Add(deviceConnectionTask);
if (metadataTask != null) tasksToWait.Add(metadataTask);
if (tasksToWait.Count > 0)
{
await Task.WhenAll(tasksToWait);
}
progressBar.Style = ProgressBarStyle.Marquee;
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();
}
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; });
if (!reset)
{
return;
}
await Task.Delay(TimeSpan.FromSeconds(5));
// Reset to base title without any status message
this.Invoke(() => { Text = "Rookie Sideloader " + Updater.LocalVersion; });
}
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<string> Devices = new List<string>();
public async Task<int> CheckForDevice()
{
Devices.Clear();
string output = string.Empty;
string error = string.Empty;
string battery = string.Empty;
ADB.DeviceID = GetDeviceID();
Thread t1 = new Thread(() =>
{
output = ADB.RunAdbCommandToString("devices").Output;
});
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
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)
{
Devices.Add(currLine.Split(' ')[0]);
_ = devicesComboBox.Items.Add(currLine.Split(' ')[0]);
_ = Logger.Log(currLine.Split(' ')[0] + "\n", LogLevel.INFO, false);
}
Debug.WriteLine(currLine);
i++;
}
if (devicesComboBox.Items.Count > 0)
{
devicesComboBox.SelectedIndex = 0;
}
battery = ADB.RunAdbCommandToString("shell dumpsys battery").Output;
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))
{
progressBar.Style = ProgressBarStyle.Marquee;
string path = dialog.FileName;
changeTitle($"Copying {path} OBB to device...");
Thread t1 = new Thread(() =>
{
output += output += ADB.CopyOBB(path);
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
Program.form.changeTitle("Done.");
showAvailableSpace();
ShowPrcOutput(output);
Program.form.changeTitle("");
}
}
public void changeTitlebarToDevice()
{
if (Devices.Contains("unauthorized"))
{
DeviceConnected = false;
this.Invoke(() =>
{
Text = "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
{
return;
}
});
}
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 = "Device Connected with ID | " + Devices[0].Replace("device", String.Empty); });
DeviceConnected = true;
}
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;
}
}
});
}
}
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<string> displayNames = new List<string>();
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.Style = ProgressBarStyle.Marquee;
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.Style = ProgressBarStyle.Continuous;
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.Style = ProgressBarStyle.Marquee;
Thread t1 = new Thread(() =>
{
output += Sideloader.UninstallGame(packagename);
});
t1.Start();
t1.IsBackground = true;
while (t1.IsAlive)
{
await Task.Delay(100);
}
ShowPrcOutput(output);
showAvailableSpace();
progressBar.Style = ProgressBarStyle.Continuous;
}
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);
}
Program.form.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.Style = ProgressBarStyle.Marquee;
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");
Program.form.changeTitle($"Copying {data} to device...");
Thread t2 = new Thread(() =>
{
output += ADB.CopyOBB(data);
})
{
IsBackground = true
};
t2.Start();
while (t2.IsAlive)
{
await Task.Delay(100);
}
Program.form.changeTitle("");
settings.CurrPckg = dir;
settings.Save();
}
Program.form.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();
Program.form.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}");
Program.form.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");
Program.form.changeTitle($"Copying {folder} to device...");
Thread t2 = new Thread(() =>
{
output += ADB.CopyOBB(folder);
})
{
IsBackground = true
};
t2.Start();
while (t2.IsAlive)
{
await Task.Delay(100);
}
Program.form.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}");
Program.form.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}");
Program.form.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.Style = ProgressBarStyle.Continuous;
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<string> newGamesList = new List<string>();
private List<string> newGamesToUploadList = new List<string>();
private readonly List<UpdateGameData> gamesToAskForUpdate = new List<UpdateGameData>();
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<string>(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");
loaded = true;
return;
}
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<string[]>(jsonContent);
if (localBlacklist != null && localBlacklist.Length > 0)
{
var combined = new List<string>(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<ListViewItem>(expectedGameCount);
var rookieList = new List<string>(expectedGameCount);
var installedGamesSet = new HashSet<string>(packageList, StringComparer.OrdinalIgnoreCase);
var blacklistSet = new HashSet<string>(blacklist, StringComparer.OrdinalIgnoreCase);
var whitelistSet = new HashSet<string>(whitelist, StringComparer.OrdinalIgnoreCase);
newGamesToUploadList = whitelistSet.Intersect(installedGamesSet, StringComparer.OrdinalIgnoreCase).ToList();
if (SideloaderRCLONE.games.Count > 5)
{
progressBar.Style = ProgressBarStyle.Marquee;
// Use full dumpsys to get all version codes at once
Dictionary<string, ulong> installedVersions = new Dictionary<string, ulong>(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<string, ulong>(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();
// 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);
if (installedVersions.TryGetValue(packagename, out ulong installedVersionInt))
{
item.ForeColor = ColorInstalled;
try
{
cloudMaxVersionByPackage.TryGetValue(packagename, out ulong cloudVersionInt);
if (installedVersionInt == cloudVersionInt)
{
upToDateCount++;
}
else if (installedVersionInt < cloudVersionInt)
{
item.ForeColor = ColorUpdateAvailable;
updateAvailableCount++;
}
else if (installedVersionInt > cloudVersionInt)
{
newerThanListCount++;
bool dontget = blacklistSet.Contains(packagename);
if (!dontget)
{
item.ForeColor = ColorDonateGame;
if (!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 (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<string>(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.Style = ProgressBarStyle.Continuous;
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<ListViewItem>().ToList();
_searchIndex = new Dictionary<string, List<ListViewItem>>(_allItems.Count * 2, StringComparer.OrdinalIgnoreCase);
foreach (var item in _allItems)
{
string gameNameKey = item.Text;
if (!_searchIndex.TryGetValue(gameNameKey, out var list))
{
list = new List<ListViewItem>(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<ListViewItem>(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<string> newGamesList, List<string> blacklistItems)
{
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
{
changeTitle("Unrecognized App found. Downloading APK to take a closer look. (This may take a minute)");
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)
{
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.Style = ProgressBarStyle.Marquee;
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.Style = ProgressBarStyle.Continuous;
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}
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 connected devices, installed apps and update list...";
changeTitle(titleMessage);
if (isLoading) { return; }
isLoading = true;
progressBar.Style = ProgressBarStyle.Marquee;
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.Style = ProgressBarStyle.Marquee;
Thread t1 = new Thread(() =>
{
if (!UsingPublicConfig)
{
SideloaderRCLONE.initGames(currentRemote);
}
listAppsBtn();
})
{
IsBackground = false
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
initListView(false);
isLoading = false;
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 readonly BindingList<string> gamesQueueList = new BindingList<string>();
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 (quotaTries > remotesList.Items.Count)
{
ShowError_QuotaExceeded();
if (System.Windows.Forms.Application.MessageLoop)
{
// Process.GetCurrentProcess().Kill();
isOffline = true;
success = false;
return;
}
}
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;
}
return success;
}
private static void ShowError_QuotaExceeded()
{
string errorMessage =
$@"Unable to connect to Remote Server. Rookie is unable to connect to our Servers.
First time launching Rookie? Please relaunch and try again.
Please 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");
}
public async void cleanupActiveDownloadStatus()
{
speedLabel.Text = String.Empty;
etaLabel.Text = String.Empty;
progressBar.Value = 0;
gamesQueueList.RemoveAt(0);
}
public void SetProgress(int progress)
{
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.Style = ProgressBarStyle.Marquee;
if (gamesListView.SelectedItems.Count == 0)
{
progressBar.Style = ProgressBarStyle.Continuous;
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.Style = ProgressBarStyle.Continuous;
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;
//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} exists in destination directory.\r\nWould you like to overwrite it?",
"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...";
etaLabel.Text = "Please wait...";
//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();
//Debug.WriteLine("RESP CONTENT " + foo);
dynamic results = JsonConvert.DeserializeObject<dynamic>(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;
// Logger.Log("Files: " + transfersComplete.ToString() + "/" + fileCount.ToString() + " (" + Convert.ToInt32((downloadedSize / totalSize) * 100).ToString() + "% Complete)");
// Logger.Log("Downloaded: " + downloadedSize.ToString() + " of " + totalSize.ToString());
progressBar.Style = ProgressBarStyle.Continuous;
progressBar.Value = Convert.ToInt32((downloadedSize / totalSize) * 100);
TimeSpan time = TimeSpan.FromSeconds(globalEta);
etaLabel.Text = etaLabel.Text = "ETA: " + time.ToString(@"hh\:mm\:ss") + " left";
speedLabel.Text = "DLS: " + transfersComplete.ToString() + "/" + fileCount.ToString() + " files - " + string.Format("{0:0.00}", downloadSpeed) + " MB/s";
}
}
catch
{
}
await Task.Delay(100);
}
if (removedownloading)
{
changeTitle("Keep game files?");
try
{
cleanupActiveDownloadStatus();
DialogResult res = FlexibleMessageBox.Show(
$"{gameName} exists in destination directory, do you want to delete it?\n\nClick NO to keep the files if you wish to resume your download later.",
"Delete Temporary Files?", MessageBoxButtons.YesNo);
if (res == DialogResult.Yes)
{
changeTitle("Deleting game files");
if (UsingPublicConfig)
{
if (Directory.Exists($"{settings.DownloadDir}\\{gameNameHash}"))
{
Directory.Delete($"{settings.DownloadDir}\\{gameNameHash}", true);
}
if (Directory.Exists($"{settings.DownloadDir}\\{gameName}"))
{
Directory.Delete($"{settings.DownloadDir}\\{gameName}", true);
}
}
else
{
Directory.Delete(settings.DownloadDir + "\\" + gameName, true);
}
}
}
catch (Exception ex)
{
_ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}");
}
changeTitle("");
break;
}
{
//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.")
{
Thread extractionThread = new Thread(() =>
{
Invoke(new Action(() =>
{
speedLabel.Text = "Extracting..."; etaLabel.Text = "Please wait...";
progressBar.Style = ProgressBarStyle.Continuous;
progressBar.Value = 0;
isInDownloadExtract = true;
}));
try
{
changeTitle("Extracting " + gameName);
Zip.ExtractFile($"{settings.DownloadDir}\\{gameNameHash}\\{gameNameHash}.7z.001", $"{settings.DownloadDir}", PublicConfigFile.Password);
Program.form.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");
}
})
{
IsBackground = true
};
extractionThread.Start();
while (extractionThread.IsAlive)
{
await Task.Delay(100);
}
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.Style = ProgressBarStyle.Continuous;
changeTitle("Installing game APK " + gameName);
etaLabel.Text = "ETA: Wait for install...";
speedLabel.Text = "DLS: Finished";
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)
{
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 = "\n--- SIDELOADING DISABLED ---\nAll tasks finished.";
}
}
else
{
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();
Thread apkThread = new Thread(() =>
{
Program.form.changeTitle($"Sideloading APK...");
etaLabel.Text = "Sideloading APK...";
output += ADB.Sideload(apkFile, packagename);
})
{
IsBackground = true
};
apkThread.Start();
while (apkThread.IsAlive)
{
await Task.Delay(100);
}
t.Stop();
Debug.WriteLine(wrDelimiter);
if (Directory.Exists($"{settings.DownloadDir}\\{gameName}\\{packagename}"))
{
deleteOBB(packagename);
Thread obbThread = new Thread(() =>
{
changeTitle($"Copying {packagename} OBB to device...");
ADB.RunAdbCommandToString($"shell mkdir \"/sdcard/Android/obb/{packagename}\"");
output += ADB.RunAdbCommandToString($"push \"{settings.DownloadDir}\\{gameName}\\{packagename}\" \"/sdcard/Android/obb\"");
Program.form.changeTitle("");
})
{
IsBackground = true
};
obbThread.Start();
while (obbThread.IsAlive)
{
await Task.Delay(100);
}
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 = "\n--- SIDELOADING DISABLED ---\nAll tasks finished.\n";
}
changeTitle($"Installation of {gameName} completed.");
}
if (settings.DeleteAllAfterInstall)
{
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 (removedownloading)
{
removedownloading = false;
gamesAreDownloading = false;
isinstalling = false;
return;
}
if (!obbsMismatch)
{
changeTitle("Refreshing games list, please wait...\n");
showAvailableSpace();
listAppsBtn();
// Reset the initialized flag so initListView rebuilds _allItems with fresh install status
_allItemsInitialized = false;
_galleryDataSource = null;
if (!updateAvailableClicked && !upToDate_Clicked && !NeedsDonation_Clicked && !settings.NodeviceMode && !gamesQueueList.Any())
{
initListView(false);
}
if (settings.EnableMessageBoxes)
{
ShowPrcOutput(output);
}
progressBar.Style = ProgressBarStyle.Continuous;
etaLabel.Text = "ETA: Finished Queue";
speedLabel.Text = "DLS: Finished Queue";
gamesAreDownloading = 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<bool> 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<bool> 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}\"");
Program.form.changeTitle("");
});
return await compareOBBSizes(packageName, gameName, output);
}
private async Task refreshGamesListAsync(ProcessOutput output)
{
changeTitle("Refreshing games list, please wait...");
showAvailableSpace();
listAppsBtn();
if (!updateAvailableClicked && !upToDate_Clicked && !NeedsDonation_Clicked && !settings.NodeviceMode && !gamesQueueList.Any())
{
initListView(false);
}
if (settings.EnableMessageBoxes)
{
ShowPrcOutput(output);
}
progressBar.Style = ProgressBarStyle.Continuous;
etaLabel.Text = "ETA: Finished Queue";
speedLabel.Text = "DLS: Finished Queue";
gamesAreDownloading = 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)
{
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
bool isWirelessEnabled = File.Exists(storedIpPath) && !string.IsNullOrEmpty(settings.IPAddress);
if (isWirelessEnabled)
{
// Disable wireless ADB
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Are you sure you want to disable wireless ADB and remove your saved IP address?", "Disable Wireless ADB?", MessageBoxButtons.YesNo);
if (dialogResult == DialogResult.No)
{
return;
}
ADB.wirelessadbON = false;
_ = FlexibleMessageBox.Show(Program.form, "Make sure your device is not connected to USB and press OK.");
_ = ADB.RunAdbCommandToString("devices");
_ = ADB.RunAdbCommandToString("shell USB");
await Task.Delay(2000);
_ = ADB.RunAdbCommandToString("disconnect");
await Task.Delay(2000);
_ = ADB.RunAdbCommandToString("kill-server");
await Task.Delay(2000);
_ = ADB.RunAdbCommandToString("start-server");
settings.IPAddress = String.Empty;
settings.Save();
_ = Program.form.GetDeviceID();
Program.form.changeTitlebarToDevice();
_ = FlexibleMessageBox.Show(Program.form, "Wireless ADB disabled. Relaunch Rookie to switch back to USB.");
if (File.Exists(storedIpPath))
{
File.Delete(storedIpPath);
}
}
else
{
// Enable wireless ADB
DialogResult res = FlexibleMessageBox.Show(Program.form, "Do you want Rookie to find the IP automatically or enter it manually?\nYes = Automatic\nNo = Manual", "Automatic/Manual", MessageBoxButtons.YesNo);
bool manual = res == DialogResult.No;
if (manual)
{
// Show a simple input dialog for IP address
string ipAddress = ShowManualIPDialog();
if (!string.IsNullOrEmpty(ipAddress))
{
string IPcmnd = "connect " + ipAddress + ":5555";
await Task.Delay(1000);
string errorChecker = ADB.RunAdbCommandToString(IPcmnd).Output;
if (errorChecker.Contains("cannot resolve host") || errorChecker.Contains("cannot connect to"))
{
changeTitle("");
_ = FlexibleMessageBox.Show(Program.form, "Manual ADB over WiFi Connection failed\nExiting...", "Manual IP Connection Failed!", MessageBoxButtons.OK);
}
else
{
_ = await CheckForDevice();
showAvailableSpace();
settings.IPAddress = IPcmnd;
settings.Save();
try { File.WriteAllText(storedIpPath, IPcmnd); }
catch (Exception ex) { Logger.Log($"Unable to write to StoredIP.txt due to {ex.Message}", LogLevel.ERROR); }
ADB.wirelessadbON = true;
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_available 1");
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_enabled 1");
changeTitlebarToDevice();
}
}
Program.form.changeTitle("", true);
}
else
{
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Make sure your Quest is plugged in VIA USB then press OK.", "Connect Quest now.", MessageBoxButtons.OKCancel);
if (dialogResult == DialogResult.Cancel)
{
return;
}
_ = ADB.RunAdbCommandToString("devices");
_ = ADB.RunAdbCommandToString("tcpip 5555");
_ = FlexibleMessageBox.Show(Program.form, "Press OK to get your Quest's local IP address.", "Obtain local IP address", MessageBoxButtons.OKCancel);
await Task.Delay(1000);
string input = ADB.RunAdbCommandToString("shell ip route").Output;
settings.WirelessADB = true;
settings.Save();
string[] strArrayOne = input.Split(' ');
if (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\nPlease disconnect your Quest then wait 2 seconds.\nOnce it is disconnected hit OK", "", MessageBoxButtons.OK);
await Task.Delay(2000);
_ = ADB.RunAdbCommandToString(IPcmnd);
_ = await Program.form.CheckForDevice();
Program.form.changeTitlebarToDevice();
Program.form.showAvailableSpace();
settings.IPAddress = IPcmnd;
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;
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_available 1");
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_enabled 1");
}
else
{
_ = FlexibleMessageBox.Show(Program.form, "No device connected! Connect Quest via USB and try again.");
}
}
}
// Update button text to reflect new state
UpdateWirelessADBButtonText();
}
private string ShowManualIPDialog()
{
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
};
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;
if (dialog.ShowDialog(this) == DialogResult.OK)
{
return textBox.Text.Trim();
}
}
return null;
}
private void UpdateWirelessADBButtonText()
{
bool isWirelessEnabled = File.Exists(storedIpPath) && !string.IsNullOrEmpty(settings.IPAddress);
ADBWirelessToggle.Text = isWirelessEnabled ? "DISABLE WIRELESS ADB" : "ENABLE WIRELESS ADB";
}
private void gamesQueListBox_MouseClick(object sender, MouseEventArgs e)
{
if (gamesQueListBox.SelectedIndex == 0 && gamesQueueList.Count == 1)
{
removedownloading = true;
RCLONE.killRclone();
}
if (gamesQueListBox.SelectedIndex != -1 && gamesQueListBox.SelectedIndex != 0)
{
_ = gamesQueueList.Remove(gamesQueListBox.SelectedItem.ToString());
}
}
private void devicesComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
showAvailableSpace();
}
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...");
}
}
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 if clicked column is already the column that is being sorted.
if (e.Column == lvwColumnSorter.SortColumn)
{
// Reverse the current sort direction for this column.
lvwColumnSorter.Order = lvwColumnSorter.Order == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending;
}
else
{
lvwColumnSorter.SortColumn = e.Column;
lvwColumnSorter.Order = e.Column == 4 ? SortOrder.Descending : SortOrder.Ascending;
}
// Perform the sort with these new sort options.
gamesListView.Sort();
}
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<QuestForm>().Count() == 0)
{
QuestForm Form = new QuestForm();
Form.Show(Program.form);
}
}
if (keyData == Keys.F4)
{
if (Application.OpenForms.OfType<SettingsForm>().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();
initListView(false);
}
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<ListViewItem>();
var results = new HashSet<ListViewItem>(); // 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<ListViewItem>();
results.Add(match);
}
}
// Then do partial matches using index
if (_searchIndex != null)
{
foreach (var kvp in _searchIndex)
{
if (token.IsCancellationRequested) return new List<ListViewItem>();
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<ListViewItem>();
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
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)
{
try
{
changeTitle("Downloading Runtime...");
string archivePath = Path.Combine(Environment.CurrentDirectory, "runtimes.7z");
using (var client = new WebClient())
{
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
await Task.Run(() => client.DownloadFile("https://vrpirates.wiki/downloads/runtimes.7z", archivePath));
}
changeTitle("Extracting Runtime...");
await Task.Run(() => Utilities.Zip.ExtractFile(archivePath, Environment.CurrentDirectory));
File.Delete(archivePath);
}
catch (Exception ex)
{
Logger.Log($"Failed to download WebView2 runtime: {ex.Message}", LogLevel.ERROR);
_ = FlexibleMessageBox.Show(Program.form,
$"Unable to download WebView2 runtime: {ex.Message}\n\nTrailer playback will be disabled.",
"WebView2 Download Failed");
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, "webroot");
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 */)
{
/*
Logger.Log($"Failed to initialize WebView2: {ex.Message}", LogLevel.ERROR);
_ = FlexibleMessageBox.Show(Program.form,
$"WebView2 Runtime is not installed on this system.\n\n" +
"Please download from: https://go.microsoft.com/fwlink/p/?LinkId=2124703\n\n" +
"Trailer playback will be disabled.",
"WebView2 Runtime Required");
*/
enviromentCreated = true;
webView21.Hide();
}
}
private void InitializeTrailerPlayer()
{
if (!settings.TrailersEnabled) return;
if (_trailerPlayerInitialized) return;
string webroot = Path.Combine(Environment.CurrentDirectory, "webroot");
Directory.CreateDirectory(webroot);
string playerHtml = Path.Combine(webroot, "player.html");
// Lightweight HTML with YouTube IFrame API and WebView2 message bridge
var html = @"<!doctype html>
<html>
<head>
<meta charset=""utf-8"">
<meta name=""viewport"" content=""width=device-width,initial-scale=1""/>
<title>Trailer Player</title>
<style>
html,body { margin:0; background:#181A1E; height:100%; overflow:hidden; }
#player { width:100vw; height:100vh; }
</style>
<script src=""https://www.youtube.com/iframe_api""></script>
<script>
let player;
let pendingId = null;
// Youtube trailer player
function onYouTubeIframeAPIReady() {
// Initialize without video
player = new YT.Player('player', {
playerVars: {
autoplay: 0, // prevent autoplay at initialization
mute: 1, // keep muted so subsequent loads can play instantly if desired
playsinline: 1,
rel: 0,
modestbranding: 1
},
events: {
'onReady': () => {
// Do nothing by default. Only play after we receive a video id message.
if (pendingId) {
// If we received a message before ready, load now.
player.loadVideoById(pendingId);
pendingId = null;
}
}
}
});
}
// WebView2 message hook: app posts the 11-char video id.
(function(){
if (window.chrome && window.chrome.webview) {
window.chrome.webview.addEventListener('message', e => {
const id = (e && e.data) ? String(e.data).trim() : '';
if (!/^[A-Za-z0-9_\-]{11}$/.test(id)) return;
if (player && player.loadVideoById) {
player.loadVideoById(id);
} else {
pendingId = id;
}
});
}
})();
</script>
</head>
<body>
<div id=""player""></div>
</body>
</html>";
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<string> 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;
string packageName = item.SubItems.Count > 2 ? item.SubItems[2].Text : "";
string gameName = item.Text;
if (string.IsNullOrEmpty(packageName))
return;
// Hide the button immediately
_listViewUninstallButton.Visible = false;
// 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.Style = ProgressBarStyle.Marquee;
Thread t1 = new Thread(() =>
{
output += Sideloader.UninstallGame(packageName);
});
t1.Start();
t1.IsBackground = true;
while (t1.IsAlive)
{
await Task.Delay(100);
}
ShowPrcOutput(output);
showAvailableSpace();
progressBar.Style = ProgressBarStyle.Continuous;
// Remove from combo box if present
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;
}
}
// Refresh the list to update installed status
_allItemsInitialized = false;
_galleryDataSource = null;
listAppsBtn();
initListView(false);
}
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();
initListView(false);
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;
}
}
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 void gamesQueListBox_MouseDown(object sender, MouseEventArgs e)
{
if (gamesQueListBox.SelectedItem == null)
{
return;
}
_ = gamesQueListBox.DoDragDrop(gamesQueListBox.SelectedItem, DragDropEffects.Move);
}
private void gamesQueListBox_DragOver(object sender, DragEventArgs e)
{
e.Effect = DragDropEffects.Move;
}
private async void pullAppToDesktopBtn_Click(object sender, EventArgs e)
{
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.Style = ProgressBarStyle.Marquee;
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}\\*";
Program.form.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;
Program.form.changeTitle("");
progressBar.Style = ProgressBarStyle.Continuous;
_ = 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<int>(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)
{
// No Device Mode is currently On. Toggle it Off
settings.NodeviceMode = false;
btnNoDevice.Text = "DISABLE SIDELOADING";
changeTitle($"Sideloading ENABLED");
}
else
{
settings.NodeviceMode = true;
settings.DeleteAllAfterInstall = false;
btnNoDevice.Text = "ENABLE SIDELOADING";
changeTitle($"Sideloading DISABLED. Games will only download");
}
settings.Save();
}
private ListViewItem _rightClickedItem;
private void gamesListView_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
_rightClickedItem = gamesListView.GetItemAt(e.X, e.Y);
gamesListView.SelectedItems.Clear();
if (_rightClickedItem != null)
{
_rightClickedItem.Selected = true;
}
// Get the name of the release of the right-clicked item
string packageName = _rightClickedItem.SubItems[1].Text;
// Check if the game is favorited and update the menu item text accordingly
ToolStripMenuItem favoriteMenuItem = favoriteGame.Items[0] as ToolStripMenuItem;
if (SettingsManager.Instance.FavoritedGames.Contains(packageName))
{
favoriteButton.Text = "Remove from Favorites"; // If it's already favorited, show "Unfavorite"
}
else
{
favoriteButton.Text = "★ Add to Favorites"; // If it's not favorited, show "Favorite"
}
// Show the context menu at the mouse position
favoriteGame.Show(gamesListView, e.Location);
}
}
private void favoriteButton_Click(object sender, EventArgs e)
{
if (_rightClickedItem != null)
{
string packageName = _rightClickedItem.SubItems[1].Text;
// Check the menu item's text to decide whether to add or remove the game from favorites
if ((sender as ToolStripMenuItem).Text == "★ Add to Favorites")
{
// Add to favorites
settings.AddFavoriteGame(packageName);
Console.WriteLine($"{packageName} has been added to favorites.");
}
else if ((sender as ToolStripMenuItem).Text == "Remove from Favorites")
{
// Remove from favorites
settings.RemoveFavoriteGame(packageName);
Console.WriteLine($"{packageName} has been removed from favorites.");
}
// After adding/removing, update the context menu text
ToolStripMenuItem favoriteMenuItem = sender as ToolStripMenuItem;
if (settings.FavoritedGames.Contains(packageName))
{
favoriteMenuItem.Text = "Remove from Favorites";
}
else
{
favoriteMenuItem.Text = "★ 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<string>(settings.FavoritedGames, StringComparer.OrdinalIgnoreCase);
// DEBUG: Check if favorites exist
if (favSet.Count == 0)
{
changeTitle("No favorites saved yet!");
return;
}
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);
}
}
}
public async void UpdateQuestInfoPanel()
{
// Check if device is actually connected by checking Devices list
bool hasDevice = Devices != null && Devices.Count > 0 && !Devices.Contains("unauthorized");
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 = "Device";
}
// 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(() =>
{
questInfoLabel.Text = deviceModel;
diskLabel.Text = freeSpaceText;
// Update custom progress bar
SetQuestStorageProgress(storagePercentUsed);
});
}
catch (Exception ex)
{
Logger.Log($"Unable to update quest info panel: {ex.Message}", LogLevel.ERROR);
questInfoPanel.Invoke(() =>
{
questInfoLabel.Text = "Device Info Unavailable";
SetQuestStorageProgress(0);
});
}
}
else
{
questInfoPanel.Invoke(() =>
{
questInfoLabel.Text = "No Device Connected";
SetQuestStorageProgress(0);
});
}
// Determine visibility based on device existence
bool showQuestInfo = ((!settings.NodeviceMode && hasDevice) || DeviceConnected);
// Toggle visibility atomically on UI thread
questInfoPanel.Invoke(() =>
{
questStorageProgressBar.Visible = showQuestInfo;
batteryLevImg.Visible = showQuestInfo;
batteryLabel.Visible = showQuestInfo;
questInfoLabel.Visible = showQuestInfo;
diskLabel.Visible = showQuestInfo;
});
}
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)
{
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();
}
}
else
{
btnViewToggle.Text = "GALLERY";
gamesGalleryView.Visible = false;
gamesListView.Visible = true;
CleanupGalleryView();
}
}
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<ListViewItem>().ToList();
}
else
{
_galleryDataSource = _allItems ?? gamesListView.Items.Cast<ListViewItem>().ToList();
}
}
if (_galleryDataSource == null)
{
_galleryDataSource = new List<ListViewItem>();
}
// 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;
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;
// Get the actual item from the gallery's current (sorted) list
var item = _fastGallery.GetItemAtIndex(index);
if (item == null) return;
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.Style = ProgressBarStyle.Marquee;
Thread t1 = new Thread(() =>
{
output += Sideloader.UninstallGame(packageName);
});
t1.Start();
t1.IsBackground = true;
while (t1.IsAlive)
{
await Task.Delay(100);
}
ShowPrcOutput(output);
showAvailableSpace();
progressBar.Style = ProgressBarStyle.Continuous;
// Remove from combo box if present
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;
}
}
// Refresh the list to update installed status
_allItemsInitialized = false;
_galleryDataSource = null;
listAppsBtn();
initListView(false);
}
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 packageName = item.SubItems[SideloaderRCLONE.PackageNameIndex].Text;
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
foreach (ListViewItem listItem in gamesListView.Items)
{
if (listItem.SubItems.Count > 2 && listItem.SubItems[2].Text == packageName)
{
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;
string packageName = item.SubItems[2].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, then trigger download
foreach (ListViewItem listItem in gamesListView.Items)
{
if (listItem.SubItems.Count > 2 && listItem.SubItems[2].Text == packageName)
{
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.Flat
};
// 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<string>();
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;
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.Flat
};
// 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 '{onlyMirror}' mirror. No 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.Flat
};
// 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);
// Create rounded panel for notesRichTextBox
notesPanel = CreateRoundedPanel(notesRichTextBox, panelColor, 8, true);
// Create rounded panel for gamesQueListBox
queuePanel = CreateRoundedPanel(gamesQueListBox, panelColor, 8, false);
// Bring labels to front so they appear above the panels
gamesQueueLabel.BringToFront();
lblNotes.BringToFront();
}
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 gamesQueListBox_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0) return;
// Determine colors based on selection state
Color backColor = (e.State & DrawItemState.Selected) == DrawItemState.Selected
? Color.FromArgb(93, 203, 173) // Accent color for selected
: gamesQueListBox.BackColor;
Color foreColor = (e.State & DrawItemState.Selected) == DrawItemState.Selected
? Color.FromArgb(20, 20, 20) // Dark text on accent
: gamesQueListBox.ForeColor;
Font font = (e.State & DrawItemState.Selected) == DrawItemState.Selected
? new Font("Microsoft Sans Serif", 10F, FontStyle.Bold)
: new Font("Microsoft Sans Serif", 10F, FontStyle.Regular);
// Clear the item background first
using (SolidBrush clearBrush = new SolidBrush(gamesQueListBox.BackColor))
{
e.Graphics.FillRectangle(clearBrush, e.Bounds);
}
// Draw rounded background
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
int radius = 1;
int margin = 4;
Rectangle roundedRect = new Rectangle(
e.Bounds.X,
e.Bounds.Y,
e.Bounds.Width - (margin * 2),
e.Bounds.Height
);
using (GraphicsPath path = CreateRoundedRectPath(roundedRect, radius))
using (SolidBrush backgroundBrush = new SolidBrush(backColor))
{
e.Graphics.FillPath(backgroundBrush, path);
}
// Draw text with padding
string text = gamesQueListBox.Items[e.Index].ToString();
Rectangle textRect = new Rectangle(
roundedRect.X + 4,
roundedRect.Y,
roundedRect.Width - 8,
roundedRect.Height
);
using (SolidBrush textBrush = new SolidBrush(foreColor))
{
var sf = new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter,
FormatFlags = StringFormatFlags.NoWrap
};
e.Graphics.DrawString(text, font, textBrush, textRect, sf);
}
}
private void ApplyWebViewRoundedCorners()
{
if (webView21 == null) return;
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 (gamesQueListBox == null || 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);
// Notes panel
int notesX = panelsStartX + queueWidth + PanelSpacing;
notesPanel.Location = new Point(notesX, panelY);
notesPanel.Size = new Size(this.ClientSize.Width - notesX - RightMargin, BottomPanelHeight);
// Layout children using the same helper as CreateRoundedPanel
LayoutChildInPanel(queuePanel, gamesQueListBox, isNotesPanel: false);
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();
}
}
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();
}
}
}
}