- Removed separate DLS+ETA labels, unified into a clearer single label - Repositioned and resized that label slightly to avoid top of label getting cut off - Added guards to prevent brief progress bar flashes during multi-file downloads - Added ETA for file extraction, APK installation, and OBB copy operations by tracking elapsed time and calculating a smoothed ETA based on the rate of progress
7267 lines
288 KiB
C#
Executable File
7267 lines
288 KiB
C#
Executable File
using AndroidSideloader.Models;
|
||
using AndroidSideloader.Utilities;
|
||
using JR.Utils.GUI.Forms;
|
||
using Microsoft.Web.WebView2.Core;
|
||
using Newtonsoft.Json;
|
||
using SergeUtils;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.ComponentModel;
|
||
using System.Configuration;
|
||
using System.Diagnostics;
|
||
using System.Drawing;
|
||
using System.Drawing.Drawing2D;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net;
|
||
using System.Net.Http;
|
||
using System.Net.NetworkInformation;
|
||
using System.Net.Sockets;
|
||
using System.Runtime.InteropServices;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using System.Windows.Forms;
|
||
|
||
namespace AndroidSideloader
|
||
{
|
||
public partial class MainForm : Form
|
||
{
|
||
private readonly ListViewColumnSorter lvwColumnSorter;
|
||
private static readonly SettingsManager settings = SettingsManager.Instance;
|
||
#if DEBUG
|
||
public static bool debugMode = true;
|
||
public bool DeviceConnected;
|
||
public bool keyheld;
|
||
public bool keyheld2;
|
||
public static string CurrAPK;
|
||
public static string CurrPCKG;
|
||
List<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();
|
||
|
||
// Use same icon as the executable
|
||
this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
|
||
|
||
// Load user's preferred view from settings
|
||
isGalleryView = settings.UseGalleryView;
|
||
|
||
// Always start with ListView visible so selections work properly
|
||
// We'll switch to gallery view after initListView completes if needed
|
||
gamesListView.Visible = true;
|
||
gamesGalleryView.Visible = false;
|
||
btnViewToggle.Text = isGalleryView ? "LIST" : "GALLERY";
|
||
|
||
favoriteGame.Renderer = new CenteredMenuRenderer();
|
||
|
||
// Set initial wireless ADB button text based on current state
|
||
UpdateWirelessADBButtonText();
|
||
|
||
_debounceTimer = new System.Windows.Forms.Timer { Interval = 100, Enabled = false };
|
||
_debounceTimer.Tick += async (sender, e) => await RunSearch();
|
||
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 // 300ms
|
||
};
|
||
t2.Tick += new EventHandler(timer_Tick2);
|
||
t2.Start();
|
||
|
||
// Device connection check timer, runs every second
|
||
System.Windows.Forms.Timer deviceCheckTimer = new System.Windows.Forms.Timer
|
||
{
|
||
Interval = 1000 // 1 second
|
||
};
|
||
deviceCheckTimer.Tick += new EventHandler(timer_DeviceCheck);
|
||
deviceCheckTimer.Start();
|
||
}
|
||
|
||
private async Task GetPublicConfigAsync()
|
||
{
|
||
await Task.Run(() => GetDependencies.updatePublicConfig());
|
||
|
||
try
|
||
{
|
||
string configFilePath = Path.Combine(Environment.CurrentDirectory, "vrp-public.json");
|
||
if (File.Exists(configFilePath))
|
||
{
|
||
string configFileData = File.ReadAllText(configFilePath);
|
||
PublicConfig config = JsonConvert.DeserializeObject<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;
|
||
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.IsIndeterminate = true;
|
||
progressBar.OperationType = "Loading";
|
||
|
||
// Update check
|
||
if (!debugMode && settings.CheckForUpdates && !isOffline)
|
||
{
|
||
Updater.AppName = "AndroidSideloader";
|
||
Updater.Repository = "VRPirates/rookie";
|
||
await Updater.Update();
|
||
}
|
||
|
||
if (!isOffline)
|
||
{
|
||
changeTitle("Getting Upload Config...");
|
||
await Task.Run(() => SideloaderRCLONE.updateUploadConfig());
|
||
|
||
_ = Logger.Log("Initializing Servers");
|
||
changeTitle("Initializing Servers...");
|
||
|
||
await initMirrors();
|
||
|
||
if (!UsingPublicConfig)
|
||
{
|
||
changeTitle("Grabbing the Games List...");
|
||
await Task.Run(() => SideloaderRCLONE.initGames(currentRemote));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
changeTitle("Offline mode enabled, no Rclone");
|
||
}
|
||
|
||
// Device connection and Metadata can run simultaneously
|
||
Task metadataTask = null;
|
||
Task deviceConnectionTask = null;
|
||
|
||
// Start device connection task
|
||
deviceConnectionTask = Task.Run(() =>
|
||
{
|
||
changeTitle("Connecting to device...");
|
||
if (!string.IsNullOrEmpty(settings.IPAddress))
|
||
{
|
||
string path = Path.Combine(Environment.CurrentDirectory, "platform-tools", "adb.exe");
|
||
ProcessOutput wakeywakey = ADB.RunCommandToString($"\"{path}\" shell input keyevent KEYCODE_WAKEUP", path);
|
||
if (wakeywakey.Output.Contains("more than one"))
|
||
{
|
||
settings.Wired = true;
|
||
settings.Save();
|
||
}
|
||
else if (wakeywakey.Output.Contains("found"))
|
||
{
|
||
settings.Wired = false;
|
||
settings.Save();
|
||
}
|
||
}
|
||
|
||
if (File.Exists(storedIpPath) && !settings.Wired)
|
||
{
|
||
string IPcmndfromtxt = File.ReadAllText(storedIpPath);
|
||
settings.IPAddress = IPcmndfromtxt;
|
||
settings.Save();
|
||
ProcessOutput IPoutput = ADB.RunAdbCommandToString(IPcmndfromtxt);
|
||
if (IPoutput.Output.Contains("attempt failed") || IPoutput.Output.Contains("refused"))
|
||
{
|
||
this.Invoke(() =>
|
||
{
|
||
_ = FlexibleMessageBox.Show(Program.form,
|
||
"Attempt to connect to saved IP has failed. This is usually due to rebooting the device or not having a STATIC IP set in your router.\nYou must enable Wireless ADB again!");
|
||
});
|
||
settings.IPAddress = "";
|
||
settings.Save();
|
||
try { File.Delete(storedIpPath); }
|
||
catch (Exception ex) { Logger.Log($"Unable to delete StoredIP.txt due to {ex.Message}", LogLevel.ERROR); }
|
||
}
|
||
else
|
||
{
|
||
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_available 1");
|
||
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_enabled 1");
|
||
}
|
||
}
|
||
else if (!File.Exists(storedIpPath))
|
||
{
|
||
settings.IPAddress = "";
|
||
settings.Save();
|
||
}
|
||
});
|
||
|
||
// Start metadata task in parallel
|
||
if (UsingPublicConfig)
|
||
{
|
||
metadataTask = Task.Run(() =>
|
||
{
|
||
changeTitle("Updating Metadata...");
|
||
SideloaderRCLONE.UpdateMetadataFromPublic();
|
||
|
||
changeTitle("Processing Metadata...");
|
||
SideloaderRCLONE.ProcessMetadataFromPublic();
|
||
});
|
||
}
|
||
else if (!isOffline)
|
||
{
|
||
metadataTask = Task.Run(() =>
|
||
{
|
||
changeTitle("Updating Game Notes...");
|
||
SideloaderRCLONE.UpdateGameNotes(currentRemote);
|
||
|
||
changeTitle("Updating Game Thumbnails...");
|
||
SideloaderRCLONE.UpdateGamePhotos(currentRemote);
|
||
|
||
SideloaderRCLONE.UpdateNouns(currentRemote);
|
||
|
||
if (!Directory.Exists(SideloaderRCLONE.ThumbnailsFolder) ||
|
||
!Directory.Exists(SideloaderRCLONE.NotesFolder))
|
||
{
|
||
this.Invoke(() =>
|
||
{
|
||
_ = FlexibleMessageBox.Show(Program.form,
|
||
"It seems you are missing the thumbnails and/or notes database, the first start of the sideloader takes a bit more time, so dont worry if it looks stuck!");
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Wait for both tasks to complete
|
||
var tasksToWait = new List<Task>();
|
||
if (deviceConnectionTask != null) tasksToWait.Add(deviceConnectionTask);
|
||
if (metadataTask != null) tasksToWait.Add(metadataTask);
|
||
|
||
if (tasksToWait.Count > 0)
|
||
{
|
||
await Task.WhenAll(tasksToWait);
|
||
}
|
||
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "Loading";
|
||
changeTitle("Populating Game List...");
|
||
|
||
_ = await CheckForDevice();
|
||
if (ADB.DeviceID.Length < 5)
|
||
{
|
||
nodeviceonstart = true;
|
||
}
|
||
|
||
// Parallel execution
|
||
await Task.WhenAll(
|
||
Task.Run(() => listAppsBtn())
|
||
);
|
||
|
||
isLoading = false;
|
||
|
||
// Initialize list view
|
||
initListView(false);
|
||
|
||
// Cleanup in background
|
||
_ = Task.Run(() =>
|
||
{
|
||
string[] files = Directory.GetFiles(Environment.CurrentDirectory);
|
||
foreach (string file in files)
|
||
{
|
||
string fileName = Path.GetFileName(file);
|
||
if (!fileName.Contains(settings.CurrentLogName) &&
|
||
!fileName.Contains(settings.CurrentCrashName) &&
|
||
!fileName.Contains("debuglog") &&
|
||
fileName.EndsWith(".txt"))
|
||
{
|
||
try { System.IO.File.Delete(file); } catch { }
|
||
}
|
||
}
|
||
});
|
||
|
||
searchTextBox.Enabled = true;
|
||
|
||
if (isOffline)
|
||
{
|
||
remotesList.Size = System.Drawing.Size.Empty;
|
||
_ = Logger.Log($"Using Offline Mode");
|
||
}
|
||
|
||
changeTitlebarToDevice();
|
||
UpdateStatusLabels();
|
||
}
|
||
|
||
private void timer_Tick(object sender, EventArgs e)
|
||
{
|
||
_ = ADB.RunAdbCommandToString("shell input keyevent KEYCODE_WAKEUP");
|
||
}
|
||
|
||
private void timer_Tick2(object sender, EventArgs e)
|
||
{
|
||
keyheld = false;
|
||
}
|
||
|
||
public async void changeTitle(string txt, bool reset = false)
|
||
{
|
||
try
|
||
{
|
||
string titleSuffix = string.IsNullOrWhiteSpace(txt) ? "" : " | " + txt;
|
||
this.Invoke(() => {
|
||
Text = "Rookie Sideloader " + Updater.LocalVersion + titleSuffix;
|
||
rookieStatusLabel.Text = txt;
|
||
});
|
||
|
||
if (!reset)
|
||
{
|
||
return;
|
||
}
|
||
|
||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||
// Reset to base title without any status message
|
||
this.Invoke(() => {
|
||
Text = "Rookie Sideloader " + Updater.LocalVersion;
|
||
rookieStatusLabel.Text = "";
|
||
});
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
|
||
private async void startsideloadbutton_Click(object sender, EventArgs e)
|
||
{
|
||
ProcessOutput output = new ProcessOutput("", "");
|
||
string path = string.Empty;
|
||
using (OpenFileDialog openFileDialog = new OpenFileDialog())
|
||
{
|
||
openFileDialog.Filter = "Android apps (*.apk)|*.apk";
|
||
openFileDialog.FilterIndex = 2;
|
||
openFileDialog.RestoreDirectory = true;
|
||
|
||
if (openFileDialog.ShowDialog() == DialogResult.OK)
|
||
{
|
||
path = openFileDialog.FileName;
|
||
}
|
||
else
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
ADB.DeviceID = GetDeviceID();
|
||
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
output += ADB.Sideload(path);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
showAvailableSpace();
|
||
|
||
ShowPrcOutput(output);
|
||
}
|
||
|
||
public void ShowPrcOutput(ProcessOutput prcout)
|
||
{
|
||
string message = $"{prcout.Output}";
|
||
if (prcout.Error.Length != 0)
|
||
{
|
||
message += $"\nError: {prcout.Error}";
|
||
}
|
||
_ = FlexibleMessageBox.Show(Program.form, message);
|
||
}
|
||
|
||
public List<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))
|
||
{
|
||
string path = dialog.FileName;
|
||
string folderName = Path.GetFileName(path);
|
||
|
||
changeTitle($"Copying {folderName} OBB to device...");
|
||
progressBar.IsIndeterminate = false;
|
||
progressBar.Value = 0;
|
||
progressBar.OperationType = "Copying OBB";
|
||
|
||
output = await ADB.CopyOBBWithProgressAsync(
|
||
path,
|
||
(progress, eta) => this.Invoke(() => {
|
||
progressBar.Value = progress;
|
||
string etaStr = eta.HasValue && eta.Value.TotalSeconds > 0
|
||
? $" · ETA: {eta.Value:mm\\:ss}"
|
||
: "";
|
||
speedLabel.Text = $"Progress: {progress}%{etaStr}";
|
||
}),
|
||
status => this.Invoke(() => {
|
||
progressBar.StatusText = status;
|
||
}),
|
||
folderName);
|
||
|
||
progressBar.Value = 100;
|
||
progressBar.StatusText = "";
|
||
changeTitle("Done.");
|
||
showAvailableSpace();
|
||
|
||
ShowPrcOutput(output);
|
||
changeTitle("");
|
||
speedLabel.Text = "";
|
||
}
|
||
}
|
||
|
||
public void changeTitlebarToDevice()
|
||
{
|
||
if (Devices.Contains("unauthorized"))
|
||
{
|
||
DeviceConnected = false;
|
||
this.Invoke(() =>
|
||
{
|
||
Text = "Rookie Sideloader " + Updater.LocalVersion + " | Device Not Authorized";
|
||
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Please check inside your headset for ADB DEBUGGING prompt/notification, check the box \"Always allow from this computer.\" and hit OK.", "Not Authorized", MessageBoxButtons.RetryCancel);
|
||
if (dialogResult == DialogResult.Retry)
|
||
{
|
||
devicesbutton.PerformClick();
|
||
}
|
||
});
|
||
}
|
||
else if (Devices.Count > 0 && Devices[0].Length > 1) // Check if Devices list is not empty and the first device has a valid length
|
||
{
|
||
this.Invoke(() => { Text = "Rookie Sideloader " + Updater.LocalVersion + " | Device Connected: " + Devices[0].Replace("device", String.Empty).Trim(); });
|
||
DeviceConnected = true;
|
||
nodeviceonstart = false; // Device connected, clear the flag
|
||
}
|
||
else
|
||
{
|
||
this.Invoke(() =>
|
||
{
|
||
DeviceConnected = false;
|
||
Text = "No Device Connected";
|
||
if (!settings.NodeviceMode)
|
||
{
|
||
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "No device found. Please ensure the following:\n\n - Developer mode is enabled\n - ADB drivers are installed\n - ADB connection is enabled on your device (this can reset)\n - Your device is plugged in\n\nThen press \"Retry\"", "No device found.", MessageBoxButtons.RetryCancel);
|
||
if (dialogResult == DialogResult.Retry)
|
||
{
|
||
devicesbutton.PerformClick();
|
||
}
|
||
else
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
nodeviceonstart = true;
|
||
Text = "Rookie Sideloader " + Updater.LocalVersion + " | No Device (Download-Only Mode)";
|
||
});
|
||
}
|
||
|
||
UpdateQuestInfoPanel();
|
||
UpdateStatusLabels();
|
||
}
|
||
|
||
public async void showAvailableSpace()
|
||
{
|
||
string AvailableSpace = string.Empty;
|
||
if (!settings.NodeviceMode || DeviceConnected)
|
||
{
|
||
try
|
||
{
|
||
ADB.DeviceID = GetDeviceID();
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
AvailableSpace = ADB.GetAvailableSpace();
|
||
});
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
diskLabel.Invoke(() => { diskLabel.Text = AvailableSpace; });
|
||
|
||
UpdateQuestInfoPanel();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_ = Logger.Log($"Unable to get available space with the exception: {ex}", LogLevel.ERROR);
|
||
}
|
||
}
|
||
}
|
||
|
||
public string GetDeviceID()
|
||
{
|
||
string deviceId = string.Empty;
|
||
int index = -1;
|
||
int itemCount = 0;
|
||
|
||
devicesComboBox.Invoke(() =>
|
||
{
|
||
index = devicesComboBox.SelectedIndex;
|
||
itemCount = devicesComboBox.Items.Count;
|
||
});
|
||
|
||
if (index != -1)
|
||
{
|
||
devicesComboBox.Invoke(() => { deviceId = devicesComboBox.SelectedItem.ToString(); });
|
||
}
|
||
else if (itemCount > 1)
|
||
{
|
||
// Multiple devices but none selected - prompt user
|
||
deviceId = ShowDeviceSelector("Multiple devices detected - Select a device");
|
||
}
|
||
else if (itemCount == 1)
|
||
{
|
||
// Only one device, select it automatically
|
||
devicesComboBox.Invoke(() =>
|
||
{
|
||
devicesComboBox.SelectedIndex = 0;
|
||
deviceId = devicesComboBox.SelectedItem.ToString();
|
||
});
|
||
}
|
||
|
||
return deviceId ?? string.Empty;
|
||
}
|
||
|
||
public static string taa = String.Empty;
|
||
|
||
private async void backupadbbutton_Click(object sender, EventArgs e)
|
||
{
|
||
string selectedApp = ShowInstalledAppSelector("Select an app to backup with ADB");
|
||
if (selectedApp == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!settings.CustomBackupDir)
|
||
{
|
||
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||
}
|
||
else
|
||
{
|
||
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||
}
|
||
if (!Directory.Exists(backupFolder))
|
||
{
|
||
_ = Directory.CreateDirectory(backupFolder);
|
||
}
|
||
string output = String.Empty;
|
||
|
||
string date_str = "ab." + DateTime.Today.ToString("yyyy.MM.dd");
|
||
string CurrBackups = Path.Combine(backupFolder, date_str);
|
||
Program.form.Invoke(new Action(() =>
|
||
{
|
||
FlexibleMessageBox.Show(Program.form, $"Backing up Game Data to {backupFolder}\\{date_str}");
|
||
}));
|
||
_ = Directory.CreateDirectory(CurrBackups);
|
||
|
||
string GameName = selectedApp;
|
||
string packageName = Sideloader.gameNameToPackageName(GameName);
|
||
string InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {packageName} | grep versionCode -F\"").Output;
|
||
|
||
changeTitle("Running ADB Backup...");
|
||
_ = FlexibleMessageBox.Show(Program.form, "Click OK on this Message...\r\nThen on your Quest, Unlock your device and confirm the backup operation by clicking on 'Back Up My Data'");
|
||
output = ADB.RunAdbCommandToString($"adb backup -f \"{CurrBackups}\\{packageName}.ab\" {packageName}").Output;
|
||
|
||
changeTitle("");
|
||
}
|
||
|
||
private async void backupbutton_Click(object sender, EventArgs e)
|
||
{
|
||
if (!settings.CustomBackupDir)
|
||
{
|
||
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||
}
|
||
else
|
||
{
|
||
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||
}
|
||
if (!Directory.Exists(backupFolder))
|
||
{
|
||
_ = Directory.CreateDirectory(backupFolder);
|
||
}
|
||
DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, $"Do you want to backup to {backupFolder}?", "Backup?", MessageBoxButtons.YesNo);
|
||
if (dialogResult1 == DialogResult.No)
|
||
{
|
||
return;
|
||
}
|
||
ProcessOutput output = new ProcessOutput(String.Empty, String.Empty);
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
string date_str = DateTime.Today.ToString("yyyy.MM.dd");
|
||
string CurrBackups = Path.Combine(backupFolder, date_str);
|
||
Program.form.Invoke(new Action(() =>
|
||
{
|
||
FlexibleMessageBox.Show(Program.form, $"This may take up to a minute. Backing up gamesaves to {backupFolder}\\{date_str} (year.month.date)");
|
||
}));
|
||
_ = Directory.CreateDirectory(CurrBackups);
|
||
output = ADB.RunAdbCommandToString($"pull \"/sdcard/Android/data\" \"{CurrBackups}\"");
|
||
changeTitle("Backing up Game Data in SD/Android/data...");
|
||
try
|
||
{
|
||
Directory.Move(ADB.adbFolderPath + "\\data", CurrBackups + "\\data");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_ = Logger.Log($"Exception on backup: {ex}", LogLevel.ERROR);
|
||
}
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
changeTitle("Backing up Game Data in SD/Android/data...");
|
||
}
|
||
ShowPrcOutput(output);
|
||
changeTitle("");
|
||
}
|
||
|
||
private async void restorebutton_Click(object sender, EventArgs e)
|
||
{
|
||
ProcessOutput output = new ProcessOutput("", "");
|
||
string output_abRestore = string.Empty;
|
||
|
||
if (!settings.CustomBackupDir)
|
||
{
|
||
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||
}
|
||
else
|
||
{
|
||
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||
}
|
||
|
||
|
||
FileDialog fileDialog = new OpenFileDialog();
|
||
fileDialog.Title = "Select a .ab Backup file or press Cancel to select a Folder";
|
||
fileDialog.CheckFileExists = true;
|
||
fileDialog.CheckPathExists = true;
|
||
fileDialog.ValidateNames = false;
|
||
fileDialog.InitialDirectory = backupFolder;
|
||
fileDialog.Filter = "Android Backup Files (*.ab)|*.ab|All Files (*.*)|*.*";
|
||
|
||
FolderBrowserDialog folderDialog = new FolderBrowserDialog();
|
||
folderDialog.Description = "Select Game Backup folder";
|
||
folderDialog.SelectedPath = backupFolder;
|
||
folderDialog.ShowNewFolderButton = false; // To prevent creating new folders
|
||
|
||
DialogResult fileDialogResult = fileDialog.ShowDialog();
|
||
DialogResult folderDialogResult = DialogResult.Cancel;
|
||
|
||
if (fileDialogResult == DialogResult.OK)
|
||
{
|
||
string selectedPath = fileDialog.FileName;
|
||
Logger.Log("Selected .ab file: " + selectedPath);
|
||
|
||
_ = FlexibleMessageBox.Show(Program.form, "Click OK on this Message...\r\nThen on your Quest, Unlock your device and confirm the backup operation by clicking on 'Restore My Data'\r\nRookie will remain frozen until the process is completed.");
|
||
output_abRestore = ADB.RunAdbCommandToString($"adb restore \"{selectedPath}\"").Output;
|
||
}
|
||
if (fileDialogResult != DialogResult.OK)
|
||
{
|
||
folderDialogResult = folderDialog.ShowDialog();
|
||
}
|
||
|
||
if (folderDialogResult == DialogResult.OK)
|
||
{
|
||
string selectedFolder = folderDialog.SelectedPath;
|
||
Logger.Log("Selected folder: " + selectedFolder);
|
||
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
if (selectedFolder.Contains("data"))
|
||
{
|
||
output += ADB.RunAdbCommandToString($"push \"{selectedFolder}\" /sdcard/Android/");
|
||
}
|
||
else
|
||
{
|
||
output += ADB.RunAdbCommandToString($"push \"{selectedFolder}\" /sdcard/Android/data/");
|
||
}
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
}
|
||
|
||
if (folderDialogResult == DialogResult.OK)
|
||
{
|
||
ShowPrcOutput(output);
|
||
}
|
||
else if (fileDialogResult == DialogResult.OK)
|
||
{
|
||
_ = FlexibleMessageBox.Show(Program.form, $"{output_abRestore}");
|
||
}
|
||
}
|
||
|
||
private string listApps()
|
||
{
|
||
ADB.DeviceID = GetDeviceID();
|
||
return ADB.RunAdbCommandToString("shell pm list packages -3").Output;
|
||
}
|
||
|
||
public void listAppsBtn()
|
||
{
|
||
m_combo.Invoke(() => { m_combo.Items.Clear(); });
|
||
|
||
string[] packages = listApps()
|
||
.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||
.Select(p => p.StartsWith("package:") ? p.Substring(8).Trim() : p.Trim())
|
||
.ToArray();
|
||
|
||
// Save the full string for settings
|
||
settings.InstalledApps = string.Join("\n", packages);
|
||
settings.Save();
|
||
|
||
List<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.IsIndeterminate = true;
|
||
progressBar.OperationType = "Loading";
|
||
string HWID = SideloaderUtilities.UUID();
|
||
string GameName = selectedApp;
|
||
string packageName = Sideloader.gameNameToPackageName(GameName);
|
||
string InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {packageName} | grep versionCode -F\"").Output;
|
||
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingBeforeFirst(InstalledVersionCode, "versionCode=");
|
||
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingAfterFirst(InstalledVersionCode, " ");
|
||
ulong VersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode));
|
||
|
||
string gameName = $"{GameName} v{VersionInt} {packageName} {HWID.Substring(0, 1)} {deviceCodeName}";
|
||
string gameZipName = $"{gameName}.zip";
|
||
|
||
// Delete both zip & txt if the files exist, most likely due to a failed upload.
|
||
if (File.Exists($"{settings.MainDir}\\{gameZipName}"))
|
||
{
|
||
File.Delete($"{settings.MainDir}\\{gameZipName}");
|
||
}
|
||
|
||
if (File.Exists($"{settings.MainDir}\\{gameName}.txt"))
|
||
{
|
||
File.Delete($"{settings.MainDir}\\{gameName}.txt");
|
||
}
|
||
|
||
ProcessOutput output = new ProcessOutput("", "");
|
||
changeTitle("Extracting APK....");
|
||
|
||
_ = Directory.CreateDirectory($"{settings.MainDir}\\{packageName}");
|
||
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
output = Sideloader.getApk(GameName);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
changeTitle("Extracting OBB if it exists....");
|
||
Thread t2 = new Thread(() =>
|
||
{
|
||
output += ADB.RunAdbCommandToString($"pull \"/sdcard/Android/obb/{packageName}\" \"{settings.MainDir}\\{packageName}\"");
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t2.Start();
|
||
|
||
while (t2.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
File.WriteAllText($"{settings.MainDir}\\{packageName}\\HWID.txt", HWID);
|
||
File.WriteAllText($"{settings.MainDir}\\{packageName}\\uploadMethod.txt", "manual");
|
||
changeTitle("Zipping extracted application...");
|
||
string cmd = $"7z a -mx1 \"{gameZipName}\" .\\{packageName}\\*";
|
||
string path = $"{settings.MainDir}\\7z.exe";
|
||
progressBar.IsIndeterminate = false;
|
||
Thread t4 = new Thread(() =>
|
||
{
|
||
_ = ADB.RunCommandToString(cmd, path);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t4.Start();
|
||
while (t4.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
changeTitle("Uploading to server, you can continue to use Rookie while it uploads.");
|
||
ULLabel.Visible = true;
|
||
isworking = false;
|
||
isuploading = true;
|
||
Thread t3 = new Thread(() =>
|
||
{
|
||
string currentlyUploading = GameName;
|
||
changeTitle("Uploading to server, you can continue to use Rookie while it uploads.");
|
||
|
||
// Get size of pending zip upload and write to text file
|
||
long zipSize = new FileInfo($"{settings.MainDir}\\{gameZipName}").Length;
|
||
File.WriteAllText($"{settings.MainDir}\\{gameName}.txt", zipSize.ToString());
|
||
// Upload size file.
|
||
_ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameName}.txt\" RSL-gameuploads:");
|
||
// Upload zip.
|
||
_ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameZipName}\" RSL-gameuploads:");
|
||
|
||
// Delete files once uploaded.
|
||
File.Delete($"{settings.MainDir}\\{gameName}.txt");
|
||
File.Delete($"{settings.MainDir}\\{gameZipName}");
|
||
|
||
this.Invoke(() => FlexibleMessageBox.Show(Program.form, $"Upload of {currentlyUploading} is complete! Thank you for your contribution!"));
|
||
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t3.Start();
|
||
isuploading = true;
|
||
|
||
while (t3.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
changeTitle("");
|
||
isuploading = false;
|
||
ULLabel.Visible = false;
|
||
}
|
||
else
|
||
{
|
||
_ = MessageBox.Show("You must wait until each app is finished uploading to start another.");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
FlexibleMessageBox.Show(Program.form, $"You are attempting to upload from an unknown device, please connect a Meta Quest device to upload", "Unknown Device", MessageBoxButtons.OK);
|
||
}
|
||
}
|
||
|
||
private async void uninstallAppButton_Click(object sender, EventArgs e)
|
||
{
|
||
string selectedApp = ShowInstalledAppSelector("Select an app to uninstall");
|
||
if (selectedApp == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!settings.CustomBackupDir)
|
||
{
|
||
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||
}
|
||
else
|
||
{
|
||
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||
}
|
||
|
||
string packagename;
|
||
string GameName = selectedApp;
|
||
DialogResult dialogresult = FlexibleMessageBox.Show($"Are you sure you want to uninstall {GameName}?", "Proceed with uninstall?", MessageBoxButtons.YesNo);
|
||
if (dialogresult == DialogResult.No)
|
||
{
|
||
return;
|
||
}
|
||
DialogResult dialogresult2 = FlexibleMessageBox.Show($"Do you want to attempt to automatically backup any saves to {backupFolder}\\{DateTime.Today.ToString("yyyy.MM.dd")}\\", "Attempt Game Backup?", MessageBoxButtons.YesNo);
|
||
packagename = !GameName.Contains(".") ? Sideloader.gameNameToPackageName(GameName) : GameName;
|
||
if (dialogresult2 == DialogResult.Yes)
|
||
{
|
||
Sideloader.BackupGame(packagename);
|
||
}
|
||
ProcessOutput output = new ProcessOutput("", "");
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "Loading";
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
output += Sideloader.UninstallGame(packagename);
|
||
});
|
||
t1.Start();
|
||
t1.IsBackground = true;
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
ShowPrcOutput(output);
|
||
showAvailableSpace();
|
||
progressBar.IsIndeterminate = false;
|
||
}
|
||
|
||
private async void copyBulkObbButton_Click(object sender, EventArgs e)
|
||
{
|
||
FolderSelectDialog dialog = new FolderSelectDialog
|
||
{
|
||
Title = "Select your folder with OBBs"
|
||
};
|
||
if (dialog.Show(Handle))
|
||
{
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
Sideloader.RecursiveOutput = new ProcessOutput(String.Empty, String.Empty);
|
||
Sideloader.RecursiveCopyOBB(dialog.FileName);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
|
||
showAvailableSpace();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
ShowPrcOutput(Sideloader.RecursiveOutput);
|
||
}
|
||
}
|
||
|
||
private async void Form1_DragDrop(object sender, DragEventArgs e)
|
||
{
|
||
if (nodeviceonstart && !updatesNotified)
|
||
{
|
||
_ = await CheckForDevice();
|
||
changeTitlebarToDevice();
|
||
showAvailableSpace();
|
||
changeTitle("Device detected... refreshing update list.");
|
||
listAppsBtn();
|
||
initListView(false);
|
||
}
|
||
|
||
changeTitle($"Processing dropped file. If Rookie freezes, please wait. Do not close Rookie!");
|
||
|
||
DragDropLbl.Visible = false;
|
||
ProcessOutput output = new ProcessOutput(String.Empty, String.Empty);
|
||
ADB.DeviceID = GetDeviceID();
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "Loading";
|
||
CurrPCKG = String.Empty;
|
||
string[] datas = (string[])e.Data.GetData(DataFormats.FileDrop);
|
||
foreach (string data in datas)
|
||
{
|
||
string directory = Path.GetDirectoryName(data);
|
||
//if is directory
|
||
string dir = Path.GetDirectoryName(data);
|
||
string path = $"{dir}\\Install.txt";
|
||
if (Directory.Exists(data))
|
||
{
|
||
string installFilePath = Directory.GetFiles(data)
|
||
.FirstOrDefault(f => string.Equals(Path.GetFileName(f), "install.txt", StringComparison.OrdinalIgnoreCase));
|
||
|
||
if (installFilePath != null)
|
||
{
|
||
// Run commands from install.txt
|
||
output += Sideloader.RunADBCommandsFromFile(installFilePath);
|
||
continue; // Skip further processing if install.txt is found
|
||
}
|
||
|
||
if (!data.Contains("+") && !data.Contains("_") && data.Contains("."))
|
||
{
|
||
_ = Logger.Log($"Copying {data} to device");
|
||
changeTitle($"Copying {data} to device...");
|
||
|
||
Thread t2 = new Thread(() =>
|
||
|
||
{
|
||
output += ADB.CopyOBB(data);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t2.Start();
|
||
|
||
while (t2.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
changeTitle("");
|
||
settings.CurrPckg = dir;
|
||
settings.Save();
|
||
}
|
||
|
||
changeTitle("");
|
||
string extension = Path.GetExtension(data);
|
||
string[] files = Directory.GetFiles(data);
|
||
|
||
foreach (string file2 in files)
|
||
{
|
||
if (File.Exists(file2))
|
||
{
|
||
if (file2.EndsWith(".apk"))
|
||
{
|
||
string pathname = Path.GetDirectoryName(file2);
|
||
string filename = file2.Replace($"{pathname}\\", String.Empty);
|
||
|
||
string cmd = $"\"{aaptPath}\" dump badging \"{file2}\" | findstr -i \"package: name\"";
|
||
|
||
_ = Logger.Log($"Running adb command-{cmd}");
|
||
string cmdout = ADB.RunCommandToString(cmd, file2).Output;
|
||
cmdout = Utilities.StringUtilities.RemoveEverythingBeforeFirst(cmdout, "=");
|
||
cmdout = Utilities.StringUtilities.RemoveEverythingAfterFirst(cmdout, " ");
|
||
cmdout = cmdout.Replace("'", String.Empty);
|
||
cmdout = cmdout.Replace("=", String.Empty);
|
||
CurrPCKG = cmdout;
|
||
CurrAPK = file2;
|
||
System.Windows.Forms.Timer t3 = new System.Windows.Forms.Timer
|
||
{
|
||
Interval = 150000 // 150 seconds to fail
|
||
};
|
||
t3.Tick += timer_Tick4;
|
||
t3.Start();
|
||
changeTitle($"Sideloading APK ({filename})");
|
||
|
||
Thread t2 = new Thread(() =>
|
||
{
|
||
output += ADB.Sideload(file2);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t2.Start();
|
||
while (t2.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
t3.Stop();
|
||
if (Directory.Exists($"{pathname}\\{cmdout}"))
|
||
{
|
||
_ = Logger.Log($"Copying OBB folder to device- {cmdout}");
|
||
changeTitle($"Copying OBB folder to device...");
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
if (!string.IsNullOrEmpty(cmdout))
|
||
{
|
||
_ = ADB.RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{cmdout}\" && mkdir \"/sdcard/Android/obb/{cmdout}\"");
|
||
}
|
||
_ = ADB.RunAdbCommandToString($"push \"{pathname}\\{cmdout}\" /sdcard/Android/obb/");
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (file2.EndsWith(".zip") && settings.BMBFChecked)
|
||
{
|
||
string datazip = file2;
|
||
string zippath = Path.GetDirectoryName(data);
|
||
datazip = datazip.Replace(zippath, "");
|
||
datazip = Utilities.StringUtilities.RemoveEverythingAfterFirst(datazip, ".");
|
||
datazip = datazip.Replace(".", "");
|
||
string command2 = $"\"{settings.MainDir}\\7z.exe\" e \"{file2}\" -o\"{zippath}\\{datazip}\\\"";
|
||
|
||
Thread t2 = new Thread(() =>
|
||
|
||
{
|
||
_ = ADB.RunCommandToString(command2, file2);
|
||
output += ADB.RunAdbCommandToString($"push \"{zippath}\\{datazip}\" \"/sdcard/ModData/com.beatgames.beatsaber/Mods/SongLoader/CustomLevels/\"");
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t2.Start();
|
||
|
||
while (t2.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
Directory.Delete($"{zippath}\\{datazip}", true);
|
||
}
|
||
}
|
||
}
|
||
string[] folders = Directory.GetDirectories(data);
|
||
foreach (string folder in folders)
|
||
{
|
||
_ = Logger.Log($"Copying {folder} to device");
|
||
changeTitle($"Copying {folder} to device...");
|
||
|
||
Thread t2 = new Thread(() =>
|
||
|
||
{
|
||
output += ADB.CopyOBB(folder);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t2.Start();
|
||
|
||
while (t2.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
changeTitle("");
|
||
settings.CurrPckg = dir;
|
||
settings.Save();
|
||
}
|
||
}
|
||
//if it's a file
|
||
else if (File.Exists(data))
|
||
{
|
||
|
||
string extension = Path.GetExtension(data);
|
||
if (extension == ".apk")
|
||
{
|
||
if (File.Exists($"{dir}\\Install.txt"))
|
||
{
|
||
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Special instructions have been found with this file, would you like to run them automatically?", "Special Instructions found!", MessageBoxButtons.OKCancel);
|
||
if (dialogResult == DialogResult.Cancel)
|
||
{
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
_ = Logger.Log($"Sideloading custom install.txt");
|
||
changeTitle("Sideloading custom install.txt automatically.");
|
||
|
||
Thread t1 = new Thread(() =>
|
||
|
||
{
|
||
output += Sideloader.RunADBCommandsFromFile(path);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
changeTitle("");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
string pathname = Path.GetDirectoryName(data);
|
||
string dataname = data.Replace($"{pathname}\\", "");
|
||
string cmd = $"\"{aaptPath}\" dump badging \"{data}\" | findstr -i \"package: name\"";
|
||
_ = Logger.Log($"Running adb command-{cmd}");
|
||
string cmdout = ADB.RunCommandToString(cmd, data).Output;
|
||
cmdout = Utilities.StringUtilities.RemoveEverythingBeforeFirst(cmdout, "=");
|
||
cmdout = Utilities.StringUtilities.RemoveEverythingAfterFirst(cmdout, " ");
|
||
cmdout = cmdout.Replace("'", "");
|
||
cmdout = cmdout.Replace("=", "");
|
||
CurrPCKG = cmdout;
|
||
CurrAPK = data;
|
||
System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer
|
||
{
|
||
Interval = 150000 // 150 seconds to fail
|
||
};
|
||
timer.Tick += timer_Tick4;
|
||
timer.Start();
|
||
|
||
changeTitle($"Installing {dataname}...");
|
||
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
output += ADB.Sideload(data);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
timer.Stop();
|
||
|
||
if (Directory.Exists($"{pathname}\\{cmdout}"))
|
||
{
|
||
_ = Logger.Log($"Copying OBB folder to device- {cmdout}");
|
||
changeTitle($"Copying OBB folder to device...");
|
||
Thread t2 = new Thread(() =>
|
||
{
|
||
if (!string.IsNullOrEmpty(cmdout))
|
||
{
|
||
_ = ADB.RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{cmdout}\" && mkdir \"/sdcard/Android/obb/{cmdout}\"");
|
||
}
|
||
_ = ADB.RunAdbCommandToString($"push \"{pathname}\\{cmdout}\" /sdcard/Android/obb/");
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t2.Start();
|
||
while (t2.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
changeTitle("");
|
||
}
|
||
}
|
||
}
|
||
//If obb is dragged and dropped alone onto Rookie, Rookie will recreate its obb folder automatically with this code.
|
||
else if (extension == ".obb")
|
||
{
|
||
string filename = Path.GetFileName(data);
|
||
string foldername = filename.Substring(filename.IndexOf('.') + 1);
|
||
foldername = foldername.Substring(foldername.IndexOf('.') + 1);
|
||
foldername = foldername.Replace(".obb", "");
|
||
foldername = Path.Combine(Environment.CurrentDirectory, foldername);
|
||
_ = Directory.CreateDirectory(foldername);
|
||
File.Copy(data, Path.Combine(foldername, filename));
|
||
path = foldername;
|
||
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
output += ADB.CopyOBB(path);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
_ = Logger.Log($"Copying OBB folder to device- {path}");
|
||
changeTitle($"Copying OBB folder to device ({filename})");
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
Directory.Delete(foldername, true);
|
||
changeTitle("");
|
||
}
|
||
// BMBF Zip extraction then push to BMBF song folder on Quest.
|
||
else if (extension == ".zip" && settings.BMBFChecked)
|
||
{
|
||
string datazip = data;
|
||
string zippath = Path.GetDirectoryName(data);
|
||
datazip = datazip.Replace(zippath, "");
|
||
datazip = Utilities.StringUtilities.RemoveEverythingAfterFirst(datazip, ".");
|
||
datazip = datazip.Replace(".", "");
|
||
|
||
string command = $"\"{settings.MainDir}\\7z.exe\" e \"{data}\" -o\"{zippath}\\{datazip}\\\"";
|
||
|
||
Thread t1 = new Thread(() =>
|
||
|
||
{
|
||
_ = ADB.RunCommandToString(command, data);
|
||
output += ADB.RunAdbCommandToString($"push \"{zippath}\\{datazip}\" \"/sdcard/ModData/com.beatgames.beatsaber/Mods/SongLoader/CustomLevels/\"");
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
Directory.Delete($"{zippath}\\{datazip}", true);
|
||
}
|
||
else if (extension == ".txt")
|
||
{
|
||
changeTitle("Sideloading custom install.txt automatically.");
|
||
|
||
Thread t1 = new Thread(() =>
|
||
|
||
{
|
||
output += Sideloader.RunADBCommandsFromFile(path);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
changeTitle("");
|
||
}
|
||
}
|
||
}
|
||
|
||
progressBar.IsIndeterminate = false;
|
||
|
||
showAvailableSpace();
|
||
|
||
DragDropLbl.Visible = false;
|
||
|
||
ShowPrcOutput(output);
|
||
listAppsBtn();
|
||
}
|
||
|
||
private void Form1_DragEnter(object sender, DragEventArgs e)
|
||
{
|
||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||
{
|
||
e.Effect = DragDropEffects.Copy;
|
||
}
|
||
|
||
DragDropLbl.Visible = true;
|
||
DragDropLbl.Text = "Drag APK or OBB";
|
||
changeTitle(DragDropLbl.Text);
|
||
}
|
||
|
||
private void Form1_DragLeave(object sender, EventArgs e)
|
||
{
|
||
DragDropLbl.Visible = false;
|
||
DragDropLbl.Text = String.Empty;
|
||
|
||
changeTitle("");
|
||
}
|
||
|
||
private List<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, continuing in download-only mode");
|
||
nodeviceonstart = true;
|
||
}
|
||
|
||
string[] blacklist = new string[] { };
|
||
string[] whitelist = new string[] { };
|
||
|
||
// Load blacklists/whitelists concurrently
|
||
await Task.WhenAll(
|
||
Task.Run(() =>
|
||
{
|
||
if (File.Exists($"{settings.MainDir}\\nouns\\blacklist.txt"))
|
||
{
|
||
blacklist = File.ReadAllLines($"{settings.MainDir}\\nouns\\blacklist.txt");
|
||
}
|
||
|
||
string localBlacklistPath = Path.Combine(settings.MainDir, "blacklist.json");
|
||
if (File.Exists(localBlacklistPath))
|
||
{
|
||
try
|
||
{
|
||
string jsonContent = File.ReadAllText(localBlacklistPath);
|
||
string[] localBlacklist = JsonConvert.DeserializeObject<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.IsIndeterminate = true;
|
||
progressBar.OperationType = "";
|
||
|
||
// 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.IsIndeterminate = false;
|
||
|
||
if (either && !updatesNotified && !noAppCheck)
|
||
{
|
||
changeTitle("");
|
||
DonorsListViewForm donorForm = new DonorsListViewForm();
|
||
_ = donorForm.ShowDialog(this);
|
||
_ = Focus();
|
||
}
|
||
|
||
// Update UI with computed list
|
||
this.Invoke(() =>
|
||
{
|
||
changeTitle("Populating update list...\n\n");
|
||
int installedTotal = upToDateCount + updateAvailableCount;
|
||
btnInstalled.Text = $"{installedTotal} INSTALLED";
|
||
btnInstalled.ForeColor = ColorInstalled;
|
||
if (updateAvailableCount != 1) btnUpdateAvailable.Text = $"{updateAvailableCount} UPDATES AVAILABLE";
|
||
else btnUpdateAvailable.Text = $"{updateAvailableCount} UPDATE AVAILABLE";
|
||
btnUpdateAvailable.ForeColor = ColorUpdateAvailable;
|
||
btnNewerThanList.Text = $"{newerThanListCount} NEWER THAN LIST";
|
||
btnNewerThanList.ForeColor = ColorDonateGame;
|
||
|
||
ListViewItem[] arr = GameList.ToArray();
|
||
gamesListView.BeginUpdate();
|
||
gamesListView.Items.Clear();
|
||
gamesListView.Items.AddRange(arr);
|
||
gamesListView.EndUpdate();
|
||
});
|
||
|
||
Logger.Log($"UI updated in {sw.ElapsedMilliseconds}ms");
|
||
sw.Restart();
|
||
|
||
changeTitle("");
|
||
|
||
if (!_allItemsInitialized)
|
||
{
|
||
_allItems = gamesListView.Items.Cast<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)
|
||
{
|
||
await Task.Run(() =>
|
||
{
|
||
foreach (UpdateGameData gameData in gamesToAskForUpdate)
|
||
{
|
||
if (!updatesNotified && !settings.SubmittedUpdates.Contains(gameData.Packagename))
|
||
{
|
||
either = true;
|
||
updates = true;
|
||
donorApps += gameData.GameName + ";" + gameData.Packagename + ";" + gameData.InstalledVersionInt + ";" + "Update" + "\n";
|
||
}
|
||
}
|
||
|
||
string baseApkPath = Path.Combine(Environment.CurrentDirectory, "platform-tools", "base.apk");
|
||
if (blacklistItems.Count > 100 && !noAppCheck)
|
||
{
|
||
foreach (string newGamesToUpload in newGamesList)
|
||
{
|
||
try
|
||
{
|
||
bool onapplist = false;
|
||
string NewApp = settings.NonAppPackages + "\n" + settings.AppPackages;
|
||
if (NewApp.Contains(newGamesToUpload))
|
||
{
|
||
onapplist = true;
|
||
Logger.Log($"App '{newGamesToUpload}' found in app list.", LogLevel.INFO);
|
||
}
|
||
|
||
string RlsName = Sideloader.PackageNametoGameName(newGamesToUpload);
|
||
Logger.Log($"Release name obtained: {RlsName}", LogLevel.INFO);
|
||
|
||
if (!updatesNotified && !onapplist && newint < 6)
|
||
{
|
||
changeTitle("Unrecognized App found. Downloading APK to take a closer look. (This may take a minute)");
|
||
|
||
either = true;
|
||
newapps = true;
|
||
Logger.Log($"New app detected: {newGamesToUpload}, starting APK extraction and AAPT process.", LogLevel.INFO);
|
||
|
||
string apppath = ADB.RunAdbCommandToString($"shell pm path {newGamesToUpload}").Output;
|
||
Logger.Log($"ADB command 'pm path' executed. Path: {apppath}", LogLevel.INFO);
|
||
apppath = Utilities.StringUtilities.RemoveEverythingBeforeFirst(apppath, "/");
|
||
apppath = Utilities.StringUtilities.RemoveEverythingAfterFirst(apppath, "\r\n");
|
||
|
||
if (File.Exists(baseApkPath))
|
||
{
|
||
File.Delete(baseApkPath);
|
||
Logger.Log("Old base.apk file deleted.", LogLevel.INFO);
|
||
}
|
||
|
||
Logger.Log($"Pulling APK from path: {apppath}", LogLevel.INFO);
|
||
_ = ADB.RunAdbCommandToString($"pull \"{apppath}\"");
|
||
|
||
string cmd = $"\"{aaptPath}\" dump badging \"{baseApkPath}\" | findstr -i \"application-label\"";
|
||
Logger.Log($"Running AAPT command: {cmd}", LogLevel.INFO);
|
||
string ReleaseName = ADB.RunCommandToString(cmd, aaptPath).Output;
|
||
Logger.Log($"AAPT command output: {ReleaseName}", LogLevel.INFO);
|
||
|
||
ReleaseName = Utilities.StringUtilities.RemoveEverythingBeforeFirst(ReleaseName, "'");
|
||
ReleaseName = Utilities.StringUtilities.RemoveEverythingAfterFirst(ReleaseName, "\r\n");
|
||
ReleaseName = ReleaseName.Replace("'", "");
|
||
File.Delete(baseApkPath);
|
||
Logger.Log("Base.apk deleted after extracting release name.", LogLevel.INFO);
|
||
|
||
if (ReleaseName.Contains("Microsoft Windows"))
|
||
{
|
||
ReleaseName = RlsName;
|
||
Logger.Log("Release name fallback to RlsName due to Microsoft Windows detection.", LogLevel.INFO);
|
||
}
|
||
|
||
Logger.Log($"Final Release Name: {ReleaseName}", LogLevel.INFO);
|
||
|
||
string GameName = Sideloader.gameNameToSimpleName(RlsName);
|
||
Logger.Log($"Fetching version code for app: {newGamesToUpload}", LogLevel.INFO);
|
||
|
||
string InstalledVersionCode;
|
||
InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {newGamesToUpload} | grep versionCode -F\"").Output;
|
||
Logger.Log($"Version code command output: {InstalledVersionCode}", LogLevel.INFO);
|
||
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingBeforeFirst(InstalledVersionCode, "versionCode=");
|
||
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingAfterFirst(InstalledVersionCode, " ");
|
||
ulong installedVersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode));
|
||
Logger.Log($"Parsed installed version code: {installedVersionInt}", LogLevel.INFO);
|
||
|
||
donorApps += ReleaseName + ";" + newGamesToUpload + ";" + installedVersionInt + ";" + "New App" + "\n";
|
||
Logger.Log($"Donor app info updated: {ReleaseName}; {newGamesToUpload}; {installedVersionInt}", LogLevel.INFO);
|
||
newint++;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Log($"Exception occured in ProcessNewApps: {ex.Message}", LogLevel.ERROR);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
private static readonly HttpClient HttpClient = new HttpClient();
|
||
public static async void doUpload()
|
||
{
|
||
Program.form.changeTitle("Uploading to server, you can continue to use Rookie while it uploads.");
|
||
Program.form.ULLabel.Visible = true;
|
||
isworking = true;
|
||
string deviceCodeName = ADB.RunAdbCommandToString("shell getprop ro.product.device").Output.ToLower().Trim();
|
||
string codeNamesLink = "https://raw.githubusercontent.com/VRPirates/rookie/master/codenames";
|
||
bool codenameExists = false;
|
||
try
|
||
{
|
||
codenameExists = HttpClient.GetStringAsync(codeNamesLink).Result.Contains(deviceCodeName);
|
||
}
|
||
catch
|
||
{
|
||
_ = Logger.Log("Unable to download Codenames file.");
|
||
FlexibleMessageBox.Show(Program.form, $"Error downloading Codenames File from Github", "Verification Error", MessageBoxButtons.OK);
|
||
}
|
||
|
||
if (codenameExists)
|
||
{
|
||
foreach (UploadGame game in Program.form.gamesToUpload)
|
||
{
|
||
|
||
Thread t3 = new Thread(() =>
|
||
{
|
||
string packagename = game.Pckgcommand;
|
||
string gameName = $"{game.Uploadgamename} v{game.Uploadversion} {game.Pckgcommand} {SideloaderUtilities.UUID().Substring(0, 1)} {deviceCodeName}";
|
||
string gameZipName = $"{gameName}.zip";
|
||
|
||
// Delete both zip & txt if the files exist, most likely due to a failed upload.
|
||
if (File.Exists($"{settings.MainDir}\\{gameZipName}"))
|
||
{
|
||
File.Delete($"{settings.MainDir}\\{gameZipName}");
|
||
}
|
||
|
||
if (File.Exists($"{settings.MainDir}\\{gameName}.txt"))
|
||
{
|
||
File.Delete($"{settings.MainDir}\\{gameName}.txt");
|
||
}
|
||
|
||
string path = $"{settings.MainDir}\\7z.exe";
|
||
string cmd = $"7z a -mx1 \"{settings.MainDir}\\{gameZipName}\" .\\{game.Pckgcommand}\\*";
|
||
Program.form.changeTitle("Zipping extracted application...");
|
||
_ = ADB.RunCommandToString(cmd, path);
|
||
if (Directory.Exists($"{settings.MainDir}\\{game.Pckgcommand}"))
|
||
{
|
||
Directory.Delete($"{settings.MainDir}\\{game.Pckgcommand}", true);
|
||
}
|
||
Program.form.changeTitle("Uploading to server, you can continue to use Rookie while it uploads.");
|
||
|
||
// Get size of pending zip upload and write to text file
|
||
long zipSize = new FileInfo($"{settings.MainDir}\\{gameZipName}").Length;
|
||
File.WriteAllText($"{settings.MainDir}\\{gameName}.txt", zipSize.ToString());
|
||
// Upload size file.
|
||
_ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameName}.txt\" RSL-gameuploads:");
|
||
// Upload zip.
|
||
_ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameZipName}\" RSL-gameuploads:");
|
||
|
||
if (game.isUpdate)
|
||
{
|
||
settings.SubmittedUpdates += game.Pckgcommand + "\n";
|
||
settings.Save();
|
||
}
|
||
|
||
// Delete files once uploaded.
|
||
if (File.Exists($"{settings.MainDir}\\{gameName}.txt"))
|
||
{
|
||
File.Delete($"{settings.MainDir}\\{gameName}.txt");
|
||
}
|
||
if (File.Exists($"{settings.MainDir}\\{gameZipName}"))
|
||
{
|
||
File.Delete($"{settings.MainDir}\\{gameZipName}");
|
||
}
|
||
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t3.Start();
|
||
while (t3.IsAlive)
|
||
{
|
||
isuploading = true;
|
||
await Task.Delay(100);
|
||
}
|
||
}
|
||
|
||
Program.form.gamesToUpload.Clear();
|
||
isworking = false;
|
||
isuploading = false;
|
||
Program.form.ULLabel.Visible = false;
|
||
Program.form.changeTitle("");
|
||
}
|
||
else
|
||
{
|
||
FlexibleMessageBox.Show(Program.form, $"You are attempting to upload from an unknown device, please connect a Meta Quest device to upload", "Unknown Device", MessageBoxButtons.OK);
|
||
}
|
||
}
|
||
|
||
public static async void newPackageUpload()
|
||
{
|
||
if (!string.IsNullOrEmpty(settings.NonAppPackages) && !settings.ListUpped)
|
||
{
|
||
Random r = new Random();
|
||
int x = r.Next(9999);
|
||
int y = x;
|
||
File.WriteAllText($"{settings.MainDir}\\FreeOrNonVR{y}.txt", settings.NonAppPackages);
|
||
string path = $"{settings.MainDir}\\rclone\\rclone.exe";
|
||
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
_ = ADB.RunCommandToString($"\"{settings.MainDir}\\rclone\\rclone.exe\" copy \"{settings.MainDir}\\FreeOrNonVR{y}.txt\" VRP-debuglogs:InstalledGamesList", path);
|
||
File.Delete($"{settings.MainDir}\\FreeOrNonVR{y}.txt");
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
settings.ListUpped = true;
|
||
settings.Save();
|
||
|
||
}
|
||
}
|
||
|
||
|
||
public async Task extractAndPrepareGameToUploadAsync(string GameName, string packagename, ulong installedVersionInt, bool isupdate)
|
||
{
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "";
|
||
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
_ = Sideloader.getApk(packagename);
|
||
});
|
||
changeTitle("Extracting APK file....");
|
||
t1.IsBackground = true;
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
changeTitle("Extracting OBB if it exists....");
|
||
Thread t2 = new Thread(() =>
|
||
{
|
||
_ = ADB.RunAdbCommandToString($"pull \"/sdcard/Android/obb/{packagename}\" \"{settings.MainDir}\\{packagename}\"");
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t2.Start();
|
||
|
||
while (t2.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
string HWID = SideloaderUtilities.UUID();
|
||
File.WriteAllText($"{settings.MainDir}\\{packagename}\\HWID.txt", HWID);
|
||
progressBar.IsIndeterminate = false;
|
||
UploadGame game = new UploadGame
|
||
{
|
||
isUpdate = isupdate,
|
||
Pckgcommand = packagename,
|
||
Uploadgamename = GameName,
|
||
Uploadversion = installedVersionInt
|
||
};
|
||
gamesToUpload.Add(game);
|
||
}
|
||
|
||
private async Task initMirrors()
|
||
{
|
||
_ = Logger.Log("Looking for Additional Mirrors...");
|
||
|
||
int index = 0;
|
||
await Task.Run(() => remotesList.Invoke(() =>
|
||
{
|
||
index = remotesList.SelectedIndex;
|
||
remotesList.Items.Clear();
|
||
}));
|
||
|
||
// Retry logic for RCLONE availability
|
||
string[] mirrors = null;
|
||
int maxRetries = 10;
|
||
int retryCount = 0;
|
||
|
||
while (retryCount < maxRetries)
|
||
{
|
||
try
|
||
{
|
||
mirrors = await Task.Run(() => RCLONE.runRcloneCommand_DownloadConfig("listremotes").Output.Split('\n'));
|
||
break; // Success, exit retry loop
|
||
}
|
||
catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 2) // File not found
|
||
{
|
||
retryCount++;
|
||
Logger.Log($"RCLONE not ready yet, attempt {retryCount}/{maxRetries}. Waiting...", LogLevel.WARNING);
|
||
|
||
if (retryCount >= maxRetries)
|
||
{
|
||
Logger.Log("RCLONE failed to initialize after multiple attempts", LogLevel.ERROR);
|
||
_ = FlexibleMessageBox.Show(Program.form,
|
||
"RCLONE could not be initialized. Please check your internet connection and restart the application.\n\nIf the problem persists, try running as Administrator.",
|
||
"RCLONE Initialization Failed",
|
||
MessageBoxButtons.OK,
|
||
MessageBoxIcon.Error);
|
||
|
||
// Fallback: Add only public mirror if available
|
||
if (hasPublicConfig)
|
||
{
|
||
await Task.Run(() => remotesList.Invoke(() =>
|
||
{
|
||
_ = remotesList.Items.Add("Public");
|
||
remotesList.SelectedIndex = 0;
|
||
}));
|
||
currentRemote = "Public";
|
||
UsingPublicConfig = true;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Wait before retry (exponential backoff: 500ms, 1s, 2s, 4s, ...)
|
||
await Task.Delay(Math.Min(500 * (int)Math.Pow(2, retryCount - 1), 5000));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Log($"Unexpected error initializing mirrors: {ex.Message}", LogLevel.ERROR);
|
||
_ = FlexibleMessageBox.Show(Program.form,
|
||
$"Error initializing mirrors: {ex.Message}",
|
||
"Mirror Initialization Error",
|
||
MessageBoxButtons.OK,
|
||
MessageBoxIcon.Error);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (mirrors == null)
|
||
{
|
||
Logger.Log("Failed to retrieve mirrors list", LogLevel.ERROR);
|
||
return;
|
||
}
|
||
|
||
_ = Logger.Log("Loaded following mirrors: ");
|
||
int itemsCount = 0;
|
||
|
||
if (hasPublicConfig)
|
||
{
|
||
_ = remotesList.Items.Add("Public");
|
||
itemsCount++;
|
||
}
|
||
|
||
foreach (string mirror in mirrors)
|
||
{
|
||
if (mirror.Contains("mirror"))
|
||
{
|
||
_ = Logger.Log(mirror.Remove(mirror.Length - 1));
|
||
await Task.Run(() => remotesList.Invoke(() =>
|
||
{
|
||
_ = remotesList.Items.Add(mirror.Remove(mirror.Length - 1).Replace("VRP-mirror", ""));
|
||
}));
|
||
itemsCount++;
|
||
}
|
||
}
|
||
|
||
if (itemsCount > 0)
|
||
{
|
||
await Task.Run(() => remotesList.Invoke(() =>
|
||
{
|
||
remotesList.SelectedIndex = 0;
|
||
string selectedRemote = remotesList.SelectedItem.ToString();
|
||
currentRemote = "";
|
||
|
||
if (selectedRemote != "Public")
|
||
{
|
||
currentRemote = "VRP-mirror";
|
||
}
|
||
currentRemote = string.Concat(currentRemote, selectedRemote);
|
||
}));
|
||
}
|
||
}
|
||
|
||
public static string processError = string.Empty;
|
||
public static string currentRemote = string.Empty;
|
||
private readonly string wrDelimiter = "-------";
|
||
|
||
private void deviceDropContainer_Click(object sender, EventArgs e)
|
||
{
|
||
ToggleContainer(deviceDropContainer, deviceDrop);
|
||
}
|
||
|
||
private void sideloadContainer_Click(object sender, EventArgs e)
|
||
{
|
||
ToggleContainer(sideloadContainer, sideloadDrop);
|
||
}
|
||
|
||
private void installedAppsMenuContainer_Click(object sender, EventArgs e)
|
||
{
|
||
ToggleContainer(installedAppsMenuContainer, installedAppsMenu);
|
||
}
|
||
|
||
private void backupDrop_Click(object sender, EventArgs e)
|
||
{
|
||
ToggleContainer(backupContainer, backupDrop);
|
||
}
|
||
|
||
private void otherDrop_Click(object sender, EventArgs e)
|
||
{
|
||
ToggleContainer(otherContainer, otherDrop);
|
||
}
|
||
|
||
private async void AnimateContainerHeight(Panel container, bool expand)
|
||
{
|
||
// Disable AutoSize during animation
|
||
container.AutoSize = false;
|
||
|
||
// Store the target height before any changes
|
||
int targetHeight = expand ? container.PreferredSize.Height : 0;
|
||
int startHeight = expand ? 0 : container.Height;
|
||
|
||
// For collapsing: hide immediately if already at 0
|
||
if (!expand && container.Height == 0)
|
||
{
|
||
container.Visible = false;
|
||
container.AutoSize = true;
|
||
return;
|
||
}
|
||
|
||
// Set height before making visible to prevent flicker on expand
|
||
container.Height = startHeight;
|
||
|
||
// Only show if expanding (collapsing container is already visible)
|
||
if (expand)
|
||
{
|
||
container.Visible = true;
|
||
}
|
||
|
||
// Suspend layout to prevent child controls from flickering
|
||
container.SuspendLayout();
|
||
|
||
// Stopwatch for consistent timing, 1.5ms per pixel height
|
||
int durationMs = (int)Math.Round(container.PreferredSize.Height * 1.5);
|
||
var stopwatch = Stopwatch.StartNew();
|
||
|
||
while (stopwatch.ElapsedMilliseconds < durationMs)
|
||
{
|
||
float progress = (float)stopwatch.ElapsedMilliseconds / durationMs;
|
||
progress = Math.Min(1f, progress);
|
||
|
||
// Ease-out curve
|
||
float easedProgress = 1f - (1f - progress) * (1f - progress);
|
||
|
||
int newHeight = (int)(startHeight + (targetHeight - startHeight) * easedProgress);
|
||
container.Height = Math.Max(0, newHeight);
|
||
|
||
// Yield to UI thread, but don't rely on delay accuracy
|
||
await Task.Delay(1);
|
||
}
|
||
|
||
// Ensure final state
|
||
container.Height = targetHeight;
|
||
|
||
// Resume layout before hiding to ensure clean state
|
||
container.ResumeLayout(false);
|
||
|
||
if (!expand)
|
||
{
|
||
container.Visible = false;
|
||
}
|
||
|
||
container.AutoSize = true;
|
||
}
|
||
|
||
private void CollapseAllContainersInstant(Panel exceptThis = null)
|
||
{
|
||
var containers = new[]
|
||
{
|
||
deviceDropContainer,
|
||
sideloadContainer,
|
||
installedAppsMenuContainer,
|
||
backupContainer,
|
||
otherContainer
|
||
};
|
||
|
||
foreach (var container in containers)
|
||
{
|
||
if (container != exceptThis && container.Visible)
|
||
{
|
||
// Hide before any layout changes to prevent flicker
|
||
container.Visible = false;
|
||
container.SuspendLayout();
|
||
container.AutoSize = false;
|
||
container.Height = 0;
|
||
container.ResumeLayout(false);
|
||
container.AutoSize = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ToggleContainer(Panel containerToToggle, Button dropButton)
|
||
{
|
||
// Collapse all other containers instantly
|
||
CollapseAllContainersInstant(containerToToggle);
|
||
|
||
// Check if we're collapsing (container is currently visible and has height)
|
||
bool isExpanding = !containerToToggle.Visible || containerToToggle.Height == 0;
|
||
|
||
if (isExpanding)
|
||
{
|
||
// Animate expansion
|
||
AnimateContainerHeight(containerToToggle, true);
|
||
}
|
||
else
|
||
{
|
||
// Close instantly without animation - hide before any layout changes
|
||
containerToToggle.Visible = false;
|
||
containerToToggle.SuspendLayout();
|
||
containerToToggle.AutoSize = false;
|
||
containerToToggle.Height = 0;
|
||
containerToToggle.ResumeLayout(false);
|
||
containerToToggle.AutoSize = true;
|
||
}
|
||
}
|
||
|
||
private void settingsButton_Click(object sender, EventArgs e)
|
||
{
|
||
SettingsForm settingsForm = new SettingsForm();
|
||
settingsForm.Show(Program.form);
|
||
}
|
||
|
||
private void aboutBtn_Click(object sender, EventArgs e)
|
||
{
|
||
string about = $@"Version: {Updater.LocalVersion}
|
||
|
||
This software is free.
|
||
{Updater.GitHubUrl}
|
||
|
||
Credits & Acknowledgements
|
||
-----------------------------------------
|
||
• Software originally developed by: rookie.wtf
|
||
• Special thanks to the VRP Mod Staff, Data Team, and all contributors
|
||
• VRP Staff (past & present):
|
||
fenopy, Maxine, JarJarBlinkz, pmow, SytheZN, Roma/Rookie,
|
||
Flow, Ivan, Kaladin, HarryEffinPotter, John, Sam Hoque, JP
|
||
|
||
Additional Thanks & Resources
|
||
-----------------------------------------
|
||
• rclone - https://rclone.org
|
||
• 7-Zip - https://www.7-zip.org
|
||
• ErikE - https://stackoverflow.com/users/57611/erike
|
||
• Serge Weinstock (SergeUtils)
|
||
• Mike Gold - https://www.c-sharpcorner.com/members/mike-gold2
|
||
";
|
||
|
||
_ = FlexibleMessageBox.Show(Program.form, about);
|
||
}
|
||
|
||
private async void listApkButton_Click(object sender, EventArgs e)
|
||
{
|
||
string titleMessage = "Refreshing devices, apps and update list...";
|
||
changeTitle(titleMessage);
|
||
if (isLoading) { return; }
|
||
isLoading = true;
|
||
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "Refreshing";
|
||
devicesbutton_Click(sender, e);
|
||
|
||
await initMirrors();
|
||
|
||
isLoading = false;
|
||
await refreshCurrentMirror(titleMessage);
|
||
}
|
||
|
||
private async Task refreshCurrentMirror(string titleMessage)
|
||
{
|
||
changeTitle(titleMessage);
|
||
if (isLoading) { return; }
|
||
isLoading = true;
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "Refreshing";
|
||
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
if (!UsingPublicConfig)
|
||
{
|
||
SideloaderRCLONE.initGames(currentRemote);
|
||
}
|
||
listAppsBtn();
|
||
})
|
||
{
|
||
IsBackground = false
|
||
};
|
||
t1.Start();
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
// Reset the initialized flag so initListView rebuilds _allItems with current install status
|
||
_allItemsInitialized = false;
|
||
_galleryDataSource = null;
|
||
|
||
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 (remotesList.SelectedIndex + 1 == remotesList.Items.Count)
|
||
{
|
||
reset = true;
|
||
for (int i = 0; i < steps; i++)
|
||
{
|
||
remotesList.SelectedIndex--;
|
||
}
|
||
}
|
||
if (reset)
|
||
{
|
||
remotesList.SelectedIndex--;
|
||
}
|
||
if (remotesList.Items.Count > remotesList.SelectedIndex && !reset)
|
||
{
|
||
remotesList.SelectedIndex++;
|
||
steps++;
|
||
}
|
||
});
|
||
}
|
||
catch
|
||
{
|
||
success = false;
|
||
}
|
||
|
||
// If we've tried all remotes and failed, show quota exceeded error
|
||
if (quotaTries > remotesList.Items.Count)
|
||
{
|
||
ShowError_QuotaExceeded();
|
||
|
||
if (Application.MessageLoop)
|
||
{
|
||
isOffline = true;
|
||
success = false;
|
||
return success;
|
||
}
|
||
}
|
||
|
||
return success;
|
||
}
|
||
|
||
private static void ShowError_QuotaExceeded()
|
||
{
|
||
string errorMessage =
|
||
$@"Rookie cannot reach our servers.
|
||
|
||
If this is your first time launching Rookie, please relaunch and try again.
|
||
|
||
If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.gg/tBKMZy7QDA) for troubleshooting steps.";
|
||
|
||
FlexibleMessageBox.Show(Program.form, errorMessage, "Unable to connect to remote server");
|
||
|
||
// Close application after showing the message
|
||
Application.Exit();
|
||
}
|
||
|
||
public async void cleanupActiveDownloadStatus()
|
||
{
|
||
speedLabel.Text = String.Empty;
|
||
progressBar.Value = 0;
|
||
gamesQueueList.RemoveAt(0);
|
||
}
|
||
|
||
public void SetProgress(int progress)
|
||
{
|
||
if (progressBar.InvokeRequired)
|
||
{
|
||
progressBar.Invoke(new Action(() => progressBar.Value = progress));
|
||
}
|
||
else
|
||
{
|
||
progressBar.Value = progress;
|
||
}
|
||
}
|
||
|
||
public bool isinstalling = false;
|
||
public static bool isInDownloadExtract = false;
|
||
public static bool removedownloading = false;
|
||
public async void downloadInstallGameButton_Click(object sender, EventArgs e)
|
||
{
|
||
{
|
||
if (!settings.CustomDownloadDir)
|
||
{
|
||
settings.DownloadDir = Environment.CurrentDirectory.ToString();
|
||
}
|
||
bool obbsMismatch = false;
|
||
if (nodeviceonstart && !updatesNotified)
|
||
{
|
||
_ = await CheckForDevice();
|
||
changeTitlebarToDevice();
|
||
showAvailableSpace();
|
||
listAppsBtn();
|
||
}
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "Downloading";
|
||
if (gamesListView.SelectedItems.Count == 0)
|
||
{
|
||
progressBar.IsIndeterminate = false;
|
||
changeTitle("You must select a game from the game list!");
|
||
return;
|
||
}
|
||
string namebox = gamesListView.SelectedItems[0].ToString();
|
||
string nameboxtranslated = Sideloader.gameNameToSimpleName(namebox);
|
||
int count = 0;
|
||
string[] gamesToDownload;
|
||
if (gamesListView.SelectedItems.Count > 0)
|
||
{
|
||
count = gamesListView.SelectedItems.Count;
|
||
gamesToDownload = new string[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
gamesToDownload[i] = gamesListView.SelectedItems[i].SubItems[SideloaderRCLONE.ReleaseNameIndex].Text;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
return;
|
||
}
|
||
|
||
progressBar.Value = 0;
|
||
progressBar.IsIndeterminate = false;
|
||
string game = gamesToDownload.Length == 1 ? $"\"{gamesToDownload[0]}\"" : "the selected games";
|
||
isinstalling = true;
|
||
//Add games to the queue
|
||
for (int i = 0; i < gamesToDownload.Length; i++)
|
||
{
|
||
gamesQueueList.Add(gamesToDownload[i]);
|
||
}
|
||
|
||
if (gamesAreDownloading)
|
||
{
|
||
return;
|
||
}
|
||
|
||
gamesAreDownloading = true;
|
||
|
||
|
||
//Do user json on firsttime
|
||
if (settings.UserJsonOnGameInstall)
|
||
{
|
||
Thread userJsonThread = new Thread(() => { changeTitle("Pushing user.json"); Sideloader.PushUserJsons(); })
|
||
{
|
||
IsBackground = true
|
||
};
|
||
userJsonThread.Start();
|
||
|
||
}
|
||
|
||
ProcessOutput output = new ProcessOutput("", "");
|
||
|
||
string gameName = "";
|
||
while (gamesQueueList.Count > 0)
|
||
{
|
||
gameName = gamesQueueList.ToArray()[0];
|
||
string packagename = Sideloader.gameNameToPackageName(gameName);
|
||
string versioncode = Sideloader.gameNameToVersionCode(gameName);
|
||
string dir = Path.GetDirectoryName(gameName);
|
||
string gameDirectory = Path.Combine(settings.DownloadDir, gameName);
|
||
string downloadDirectory = Path.Combine(settings.DownloadDir, gameName);
|
||
string path = gameDirectory;
|
||
|
||
string gameNameHash = string.Empty;
|
||
using (MD5 md5 = MD5.Create())
|
||
{
|
||
byte[] bytes = Encoding.UTF8.GetBytes(gameName + "\n");
|
||
byte[] hash = md5.ComputeHash(bytes);
|
||
StringBuilder sb = new StringBuilder();
|
||
foreach (byte b in hash)
|
||
{
|
||
_ = sb.Append(b.ToString("x2"));
|
||
}
|
||
|
||
gameNameHash = sb.ToString();
|
||
}
|
||
|
||
ProcessOutput gameDownloadOutput = new ProcessOutput("", "");
|
||
|
||
_ = Logger.Log($"Starting Game Download");
|
||
|
||
Thread t1;
|
||
string extraArgs = string.Empty;
|
||
if (settings.SingleThreadMode)
|
||
{
|
||
extraArgs = "--transfers 1 --multi-thread-streams 0";
|
||
}
|
||
string bandwidthLimit = string.Empty;
|
||
if (settings.BandwidthLimit > 0)
|
||
{
|
||
bandwidthLimit = $"--bwlimit={settings.BandwidthLimit}M";
|
||
}
|
||
if (UsingPublicConfig)
|
||
{
|
||
bool doDownload = true;
|
||
bool skipRedownload = false;
|
||
if (settings.UseDownloadedFiles == true)
|
||
{
|
||
skipRedownload = true;
|
||
}
|
||
|
||
if (Directory.Exists(gameDirectory))
|
||
{
|
||
if (skipRedownload == true)
|
||
{
|
||
if (Directory.Exists($"{settings.DownloadDir}\\{gameName}"))
|
||
{
|
||
doDownload = false;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
DialogResult res = FlexibleMessageBox.Show(Program.form,
|
||
$"{gameName} already exists in destination directory.\n\n" +
|
||
"Yes = Overwrite and re-download.\n" +
|
||
"No = Use existing files and install from them.",
|
||
"Download again?", MessageBoxButtons.YesNo);
|
||
|
||
doDownload = res == DialogResult.Yes;
|
||
}
|
||
|
||
if (doDownload)
|
||
{
|
||
// only delete after extraction; allows for resume if the fetch fails midway.
|
||
if (Directory.Exists($"{settings.DownloadDir}\\{gameName}"))
|
||
{
|
||
Directory.Delete($"{settings.DownloadDir}\\{gameName}", true);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (doDownload)
|
||
{
|
||
downloadDirectory = $"{settings.DownloadDir}\\{gameNameHash}";
|
||
_ = Logger.Log($"rclone copy \"Public:{SideloaderRCLONE.RcloneGamesFolder}/{gameName}\"");
|
||
t1 = new Thread(() =>
|
||
{
|
||
string rclonecommand =
|
||
$"copy \":http:/{gameNameHash}/\" \"{downloadDirectory}\" {extraArgs} --progress --rc {bandwidthLimit}";
|
||
gameDownloadOutput = RCLONE.runRcloneCommand_PublicConfig(rclonecommand);
|
||
});
|
||
Utilities.Metrics.CountDownload(packagename, versioncode);
|
||
}
|
||
else
|
||
{
|
||
t1 = new Thread(() => { gameDownloadOutput = new ProcessOutput("Download skipped."); });
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_ = Directory.CreateDirectory(gameDirectory);
|
||
downloadDirectory = $"{SideloaderRCLONE.RcloneGamesFolder}/{gameName}";
|
||
_ = Logger.Log($"rclone copy \"{currentRemote}:{downloadDirectory}\"");
|
||
t1 = new Thread(() =>
|
||
{
|
||
gameDownloadOutput = RCLONE.runRcloneCommand_DownloadConfig($"copy \"{currentRemote}:{downloadDirectory}\" \"{settings.DownloadDir}\\{gameName}\" {extraArgs} --progress --rc --retries 2 --low-level-retries 1 --check-first {bandwidthLimit}");
|
||
});
|
||
Utilities.Metrics.CountDownload(packagename, versioncode);
|
||
}
|
||
|
||
if (Directory.Exists(downloadDirectory))
|
||
{
|
||
string[] partialFiles = Directory.GetFiles($"{downloadDirectory}", "*.partial");
|
||
foreach (string file in partialFiles)
|
||
{
|
||
File.Delete(file);
|
||
_ = Logger.Log($"Deleted partial file: {file}");
|
||
}
|
||
}
|
||
|
||
t1.IsBackground = true;
|
||
t1.Start();
|
||
|
||
changeTitle("Downloading game " + gameName);
|
||
speedLabel.Text = "Starting download...";
|
||
|
||
// Track the highest valid progress to prevent brief progress bar flashes during multi-file transfers
|
||
int highestValidPercent = 0;
|
||
|
||
// Download
|
||
while (t1.IsAlive)
|
||
{
|
||
try
|
||
{
|
||
HttpResponseMessage response = await client.PostAsync("http://127.0.0.1:5572/core/stats", null);
|
||
string foo = await response.Content.ReadAsStringAsync();
|
||
dynamic results = JsonConvert.DeserializeObject<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;
|
||
|
||
progressBar.IsIndeterminate = false;
|
||
|
||
int percent = 0;
|
||
if (totalSize > 0)
|
||
{
|
||
percent = Convert.ToInt32((downloadedSize / totalSize) * 100);
|
||
}
|
||
|
||
// Clamp to 0-99 while download is in progress to prevent brief 100% flashes
|
||
percent = Math.Max(0, Math.Min(99, percent));
|
||
|
||
// Only allow progress to increase
|
||
if (percent >= highestValidPercent)
|
||
{
|
||
highestValidPercent = percent;
|
||
}
|
||
else
|
||
{
|
||
// Progress went backwards? Keep showing the highest valid percent we've seen
|
||
percent = highestValidPercent;
|
||
}
|
||
|
||
progressBar.Value = percent;
|
||
|
||
TimeSpan time = TimeSpan.FromSeconds(globalEta);
|
||
|
||
UpdateProgressStatus(
|
||
"Downloading",
|
||
(int)transfersComplete + 1,
|
||
(int)fileCount,
|
||
percent,
|
||
time,
|
||
downloadSpeed);
|
||
}
|
||
}
|
||
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.")
|
||
{
|
||
// ETA tracking for extraction
|
||
DateTime extractStart = DateTime.UtcNow;
|
||
string currentExtractFile = "";
|
||
|
||
Thread extractionThread = new Thread(() =>
|
||
{
|
||
Invoke(new Action(() =>
|
||
{
|
||
speedLabel.Text = "Extracting...";
|
||
progressBar.IsIndeterminate = false;
|
||
progressBar.Value = 0;
|
||
progressBar.OperationType = "Extracting";
|
||
isInDownloadExtract = true;
|
||
}));
|
||
|
||
// Set up extraction callbacks
|
||
Zip.ExtractionProgressCallback = (percent, eta) =>
|
||
{
|
||
this.Invoke(() =>
|
||
{
|
||
progressBar.Value = percent;
|
||
UpdateProgressStatus("Extracting", percent: percent, eta: eta);
|
||
|
||
progressBar.StatusText = !string.IsNullOrEmpty(currentExtractFile)
|
||
? $"{currentExtractFile} · {percent}%"
|
||
: $"{percent}%";
|
||
});
|
||
};
|
||
|
||
Zip.ExtractionStatusCallback = (fileName) =>
|
||
{
|
||
currentExtractFile = fileName ?? "";
|
||
};
|
||
|
||
try
|
||
{
|
||
changeTitle("Extracting " + gameName);
|
||
Zip.ExtractFile($"{settings.DownloadDir}\\{gameNameHash}\\{gameNameHash}.7z.001", $"{settings.DownloadDir}", PublicConfigFile.Password);
|
||
changeTitle("");
|
||
}
|
||
catch (ExtractionException ex)
|
||
{
|
||
Invoke(new Action(() =>
|
||
{
|
||
cleanupActiveDownloadStatus();
|
||
}));
|
||
otherError = true;
|
||
this.Invoke(() => _ = FlexibleMessageBox.Show(Program.form, $"7zip error: {ex.Message}"));
|
||
output += new ProcessOutput("", "Extract Failed");
|
||
}
|
||
finally
|
||
{
|
||
// Clear callbacks
|
||
Zip.ExtractionProgressCallback = null;
|
||
Zip.ExtractionStatusCallback = null;
|
||
}
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
extractionThread.Start();
|
||
|
||
while (extractionThread.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
progressBar.StatusText = ""; // Clear status after extraction
|
||
|
||
if (Directory.Exists($"{settings.DownloadDir}\\{gameNameHash}"))
|
||
{
|
||
Directory.Delete($"{settings.DownloadDir}\\{gameNameHash}", true);
|
||
}
|
||
}
|
||
|
||
if (quotaError == false && otherError == false)
|
||
{
|
||
ADB.DeviceID = GetDeviceID();
|
||
quotaTries = 0;
|
||
progressBar.Value = 0;
|
||
progressBar.IsIndeterminate = false;
|
||
changeTitle("Installing game APK " + gameName);
|
||
if (File.Exists(Path.Combine(settings.DownloadDir, gameName, "install.txt")))
|
||
{
|
||
isinstalltxt = true;
|
||
installTxtPath = Path.Combine(settings.DownloadDir, gameName, "install.txt");
|
||
}
|
||
else if (File.Exists(Path.Combine(settings.DownloadDir, gameName, "Install.txt")))
|
||
{
|
||
isinstalltxt = true;
|
||
installTxtPath = Path.Combine(settings.DownloadDir, gameName, "Install.txt");
|
||
}
|
||
|
||
string[] files = Directory.GetFiles(settings.DownloadDir + "\\" + gameName);
|
||
|
||
Debug.WriteLine("Game Folder is: " + settings.DownloadDir + "\\" + gameName);
|
||
Debug.WriteLine("FILES IN GAME FOLDER: ");
|
||
|
||
if (isinstalltxt)
|
||
{
|
||
// Only sideload if device is connected and sideloading not disabled
|
||
if (!settings.NodeviceMode && !nodeviceonstart && DeviceConnected)
|
||
{
|
||
Thread installtxtThread = new Thread(() =>
|
||
{
|
||
output += Sideloader.RunADBCommandsFromFile(installTxtPath);
|
||
changeTitle("");
|
||
});
|
||
installtxtThread.Start();
|
||
while (installtxtThread.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
output.Output = "Download complete (installation skipped).\n\nConnect a device or enable sideloading to install.";
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Only sideload if device is connected and sideloading not disabled
|
||
if (!settings.NodeviceMode && !nodeviceonstart && DeviceConnected)
|
||
{
|
||
// Find the APK file to install
|
||
string apkFile = files.FirstOrDefault(file => Path.GetExtension(file) == ".apk");
|
||
|
||
if (apkFile != null)
|
||
{
|
||
CurrAPK = apkFile;
|
||
CurrPCKG = packagename;
|
||
System.Windows.Forms.Timer t = new System.Windows.Forms.Timer
|
||
{
|
||
Interval = 150000 // 150 seconds to fail
|
||
};
|
||
t.Tick += new EventHandler(timer_Tick4);
|
||
t.Start();
|
||
|
||
changeTitle($"Sideloading APK...");
|
||
progressBar.IsIndeterminate = false;
|
||
progressBar.OperationType = "Installing";
|
||
progressBar.Value = 0;
|
||
|
||
// Use async method with progress
|
||
output += await ADB.SideloadWithProgressAsync(
|
||
apkFile,
|
||
(progress, eta) => this.Invoke(() => {
|
||
if (progress == 0)
|
||
{
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "Installing";
|
||
}
|
||
else
|
||
{
|
||
progressBar.IsIndeterminate = false;
|
||
progressBar.Value = progress;
|
||
}
|
||
UpdateProgressStatus("Installing APK", percent: progress, eta: eta);
|
||
progressBar.StatusText = $"Installing · {progress}%";
|
||
}),
|
||
status => this.Invoke(() => {
|
||
if (!string.IsNullOrEmpty(status))
|
||
{
|
||
// "Completing Installation..."
|
||
speedLabel.Text = status;
|
||
progressBar.StatusText = status;
|
||
}
|
||
}),
|
||
packagename,
|
||
Sideloader.gameNameToSimpleName(gameName));
|
||
|
||
t.Stop();
|
||
progressBar.IsIndeterminate = false;
|
||
progressBar.StatusText = ""; // Clear status after APK install
|
||
|
||
Debug.WriteLine(wrDelimiter);
|
||
if (Directory.Exists($"{settings.DownloadDir}\\{gameName}\\{packagename}"))
|
||
{
|
||
deleteOBB(packagename);
|
||
|
||
changeTitle($"Copying {packagename} OBB to device...");
|
||
progressBar.Value = 0;
|
||
progressBar.OperationType = "Copying OBB";
|
||
|
||
// Use async method with progress for OBB
|
||
string currentObbStatusBase = string.Empty; // phase or filename
|
||
|
||
output += await ADB.CopyOBBWithProgressAsync(
|
||
$"{settings.DownloadDir}\\{gameName}\\{packagename}",
|
||
(progress, eta) => this.Invoke(() =>
|
||
{
|
||
progressBar.Value = progress;
|
||
UpdateProgressStatus("Copying OBB", percent: progress, eta: eta);
|
||
|
||
if (!string.IsNullOrEmpty(currentObbStatusBase))
|
||
{
|
||
progressBar.StatusText = $"{currentObbStatusBase} · {progress}%";
|
||
}
|
||
else
|
||
{
|
||
progressBar.StatusText = $"{progress}%";
|
||
}
|
||
}),
|
||
status => this.Invoke(() =>
|
||
{
|
||
currentObbStatusBase = status ?? string.Empty;
|
||
}),
|
||
Sideloader.gameNameToSimpleName(gameName));
|
||
|
||
progressBar.StatusText = ""; // Clear status after OBB copy
|
||
changeTitle("");
|
||
|
||
if (!nodeviceonstart | DeviceConnected)
|
||
{
|
||
if (!output.Output.Contains("offline"))
|
||
{
|
||
try
|
||
{
|
||
obbsMismatch = await compareOBBSizes(packagename, gameName, output);
|
||
}
|
||
catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error comparing OBB sizes: {ex.Message}"); }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
output.Output = "Download complete (installation skipped).\n\nConnect a device or enable sideloading to install.";
|
||
}
|
||
changeTitle($"Installation of {gameName} completed.");
|
||
}
|
||
// Only delete if setting enabled and device was connected (so we actually installed)
|
||
if (settings.DeleteAllAfterInstall && !nodeviceonstart && DeviceConnected)
|
||
{
|
||
changeTitle("Deleting game files");
|
||
try { Directory.Delete(settings.DownloadDir + "\\" + gameName, true); } catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}"); }
|
||
}
|
||
// Remove current game
|
||
cleanupActiveDownloadStatus();
|
||
}
|
||
}
|
||
}
|
||
if (removedownloading)
|
||
{
|
||
removedownloading = false;
|
||
gamesAreDownloading = false;
|
||
isinstalling = false;
|
||
return;
|
||
}
|
||
if (!obbsMismatch)
|
||
{
|
||
changeTitle("Refreshing games list, please wait...\n");
|
||
showAvailableSpace();
|
||
listAppsBtn();
|
||
|
||
if (!updateAvailableClicked && !upToDate_Clicked && !NeedsDonation_Clicked && !settings.NodeviceMode && !gamesQueueList.Any())
|
||
{
|
||
// Reset the initialized flag so initListView rebuilds _allItems with current install status
|
||
_allItemsInitialized = false;
|
||
_galleryDataSource = null;
|
||
initListView(false);
|
||
}
|
||
if (settings.EnableMessageBoxes)
|
||
{
|
||
ShowPrcOutput(output);
|
||
}
|
||
progressBar.IsIndeterminate = false;
|
||
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}\"");
|
||
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())
|
||
{
|
||
// Reset the initialized flag so initListView rebuilds _allItems with current install status
|
||
_allItemsInitialized = false;
|
||
_galleryDataSource = null;
|
||
initListView(false);
|
||
}
|
||
if (settings.EnableMessageBoxes)
|
||
{
|
||
ShowPrcOutput(output);
|
||
}
|
||
progressBar.IsIndeterminate = false;
|
||
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)
|
||
{
|
||
// Cleanup DNS helper (stops proxy)
|
||
DnsHelper.Cleanup();
|
||
|
||
if (isinstalling)
|
||
{
|
||
DialogResult res1 = FlexibleMessageBox.Show(Program.form, "There are downloads and/or installations in progress,\nif you exit now you'll have to start the entire process over again.\nAre you sure you want to exit?", "Still downloading/installing.",
|
||
MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
|
||
if (res1 != DialogResult.Yes)
|
||
{
|
||
e.Cancel = true;
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
RCLONE.killRclone();
|
||
}
|
||
}
|
||
else if (isuploading)
|
||
{
|
||
DialogResult res = FlexibleMessageBox.Show(Program.form, "There is an upload still in progress, if you exit now\nyou'll have to start the entire process over again.\nAre you sure you want to exit?", "Still uploading.",
|
||
MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
|
||
if (res != DialogResult.Yes)
|
||
{
|
||
e.Cancel = true;
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
RCLONE.killRclone();
|
||
_ = ADB.RunAdbCommandToString("kill-server");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
RCLONE.killRclone();
|
||
_ = ADB.RunAdbCommandToString("kill-server");
|
||
}
|
||
|
||
}
|
||
|
||
private async void ADBWirelessToggle_Click(object sender, EventArgs e)
|
||
{
|
||
// Check if wireless ADB is currently enabled
|
||
bool isWirelessEnabled = File.Exists(storedIpPath) && !string.IsNullOrEmpty(settings.IPAddress);
|
||
|
||
// If enabled, offer to disable or switch device
|
||
if (isWirelessEnabled)
|
||
{
|
||
DialogResult dialogResult = FlexibleMessageBox.Show(
|
||
Program.form,
|
||
"Wireless ADB is currently enabled.\n\n" +
|
||
"Yes = Connect to a different device\n" +
|
||
"No = Disable wireless ADB completely",
|
||
"Wireless ADB Options",
|
||
MessageBoxButtons.YesNoCancel);
|
||
|
||
if (dialogResult == DialogResult.Cancel)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Disable wireless ADB completely
|
||
if (dialogResult == DialogResult.No)
|
||
{
|
||
ADB.wirelessadbON = false;
|
||
changeTitle("Disabling wireless ADB...");
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "";
|
||
|
||
await Task.Run(() =>
|
||
{
|
||
ADB.RunAdbCommandToString("disconnect");
|
||
ADB.RunAdbCommandToString("kill-server");
|
||
ADB.RunAdbCommandToString("start-server");
|
||
});
|
||
|
||
settings.IPAddress = string.Empty;
|
||
settings.Save();
|
||
|
||
if (File.Exists(storedIpPath))
|
||
{
|
||
try { File.Delete(storedIpPath); } catch { }
|
||
}
|
||
|
||
progressBar.IsIndeterminate = false;
|
||
_ = await CheckForDevice();
|
||
changeTitlebarToDevice();
|
||
changeTitle("Wireless ADB disabled.", true);
|
||
|
||
UpdateWirelessADBButtonText();
|
||
UpdateStatusLabels();
|
||
return;
|
||
}
|
||
|
||
// User chose "Yes" – switch device: disconnect current wireless connection
|
||
changeTitle("Disconnecting current device...");
|
||
await Task.Run(() => ADB.RunAdbCommandToString("disconnect"));
|
||
}
|
||
|
||
// Enable or switch wireless ADB - offer scan or manual entry
|
||
DialogResult res = FlexibleMessageBox.Show(
|
||
Program.form,
|
||
"How would you like to connect?\n\n" +
|
||
"Yes = Automatic (scans network to find device)\n" +
|
||
"No = Manual (enter IP address)",
|
||
"Connection Method",
|
||
MessageBoxButtons.YesNoCancel);
|
||
|
||
if (res == DialogResult.Cancel)
|
||
{
|
||
changeTitle("");
|
||
return;
|
||
}
|
||
|
||
string ipAddress = null;
|
||
|
||
if (res == DialogResult.Yes)
|
||
{
|
||
// Network scan
|
||
ipAddress = await ShowNetworkScanDialogAsync();
|
||
}
|
||
else
|
||
{
|
||
// Manual IP entry
|
||
ipAddress = ShowManualIPDialog();
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(ipAddress))
|
||
{
|
||
changeTitle("");
|
||
return;
|
||
}
|
||
|
||
// Connect to the device
|
||
changeTitle($"Connecting to {ipAddress}...");
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "";
|
||
|
||
string ipCommand = $"connect {ipAddress}:5555";
|
||
string connectResult = await Task.Run(() => ADB.RunAdbCommandToString(ipCommand).Output);
|
||
|
||
progressBar.IsIndeterminate = false;
|
||
|
||
if (connectResult.Contains("cannot resolve host") ||
|
||
connectResult.Contains("cannot connect to") ||
|
||
connectResult.Contains("failed") ||
|
||
connectResult.Contains("unable"))
|
||
{
|
||
changeTitle("");
|
||
_ = FlexibleMessageBox.Show(
|
||
Program.form,
|
||
$"Failed to connect to {ipAddress}\n\nPlease verify:\n" +
|
||
"- The IP address is correct\n" +
|
||
"- The device is on the same network\n" +
|
||
"- Developer mode is enabled\n" +
|
||
"- Wireless ADB with tcpip 5555 enabled (requires one-time setup*)\n\n" +
|
||
"* Connect device via USB and run ADB command: tcpip 5555",
|
||
"Connection Failed",
|
||
MessageBoxButtons.OK);
|
||
|
||
UpdateWirelessADBButtonText();
|
||
UpdateStatusLabels();
|
||
return;
|
||
}
|
||
|
||
// Success - save settings and configure device
|
||
_ = await CheckForDevice();
|
||
changeTitlebarToDevice();
|
||
showAvailableSpace();
|
||
|
||
settings.IPAddress = ipCommand;
|
||
settings.WirelessADB = true;
|
||
settings.Save();
|
||
|
||
try
|
||
{
|
||
File.WriteAllText(storedIpPath, ipCommand);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Log($"Unable to write to StoredIP.txt: {ex.Message}", LogLevel.ERROR);
|
||
}
|
||
|
||
ADB.wirelessadbON = true;
|
||
|
||
// Configure wake settings in background
|
||
_ = Task.Run(() =>
|
||
{
|
||
ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_available 1");
|
||
ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_enabled 1");
|
||
});
|
||
|
||
changeTitle("Connected successfully!", true);
|
||
UpdateWirelessADBButtonText();
|
||
UpdateStatusLabels();
|
||
}
|
||
|
||
private string ShowManualIPDialog()
|
||
{
|
||
// Get local subnet prefix to pre-fill
|
||
string subnetPrefix = GetLocalIPv4();
|
||
|
||
using (Form dialog = new Form())
|
||
{
|
||
dialog.Text = "Enter Quest IP Address";
|
||
dialog.Size = new Size(350, 150);
|
||
dialog.StartPosition = FormStartPosition.CenterParent;
|
||
dialog.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||
dialog.MaximizeBox = false;
|
||
dialog.MinimizeBox = false;
|
||
dialog.BackColor = Color.FromArgb(20, 24, 29);
|
||
dialog.ForeColor = Color.White;
|
||
|
||
var label = new Label
|
||
{
|
||
Text = "Enter your Quest's IP Address:",
|
||
ForeColor = Color.White,
|
||
AutoSize = true,
|
||
Location = new Point(15, 15)
|
||
};
|
||
|
||
var textBox = new TextBox
|
||
{
|
||
Location = new Point(15, 40),
|
||
Size = new Size(300, 24),
|
||
BackColor = Color.FromArgb(40, 44, 52),
|
||
ForeColor = Color.White,
|
||
BorderStyle = BorderStyle.FixedSingle,
|
||
Text = subnetPrefix // Pre-fill with subnet prefix
|
||
};
|
||
|
||
// Position cursor at end of pre-filled text
|
||
textBox.SelectionStart = textBox.Text.Length;
|
||
|
||
var okButton = CreateStyledButton("OK", DialogResult.OK, new Point(155, 75));
|
||
var cancelButton = CreateStyledButton("Cancel", DialogResult.Cancel, new Point(240, 75), false);
|
||
|
||
dialog.Controls.AddRange(new Control[] { label, textBox, okButton, cancelButton });
|
||
dialog.AcceptButton = okButton;
|
||
dialog.CancelButton = cancelButton;
|
||
|
||
// Focus the textbox when dialog shows
|
||
dialog.Shown += (s, e) =>
|
||
{
|
||
textBox.Focus();
|
||
textBox.SelectionStart = textBox.Text.Length;
|
||
};
|
||
|
||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||
{
|
||
return textBox.Text.Trim();
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private string GetLocalIPv4()
|
||
{
|
||
try
|
||
{
|
||
foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces())
|
||
{
|
||
if (ni.OperationalStatus != OperationalStatus.Up)
|
||
continue;
|
||
|
||
var ipProps = ni.GetIPProperties();
|
||
foreach (var ua in ipProps.UnicastAddresses)
|
||
{
|
||
if (ua.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||
!IPAddress.IsLoopback(ua.Address))
|
||
{
|
||
|
||
string localIp = ua.Address.ToString();
|
||
string localPrefix = null;
|
||
|
||
if (!string.IsNullOrEmpty(localIp))
|
||
{
|
||
var o = localIp.Split('.');
|
||
if (o.Length == 4)
|
||
{
|
||
localPrefix = $"{o[0]}.{o[1]}.{o[2]}.";
|
||
}
|
||
}
|
||
|
||
return localPrefix;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Log($"Unable to get local IPv4: {ex.Message}", LogLevel.WARNING);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private async Task<List<string>> ScanNetworkForAdbDevicesAsync()
|
||
{
|
||
var foundDevices = new List<string>();
|
||
string localSubnet = GetLocalIPv4();
|
||
|
||
if (string.IsNullOrEmpty(localSubnet))
|
||
{
|
||
Logger.Log("Could not determine local subnet for scanning", LogLevel.WARNING);
|
||
return foundDevices;
|
||
}
|
||
|
||
changeTitle("Scanning network for ADB devices...");
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "";
|
||
|
||
// Scan common IP range (1-254) on port 5555
|
||
var tasks = new List<Task<string>>();
|
||
|
||
for (int i = 1; i <= 254; i++)
|
||
{
|
||
string ip = $"{localSubnet}{i}";
|
||
tasks.Add(CheckAdbDeviceAsync(ip, 5555));
|
||
}
|
||
|
||
var results = await Task.WhenAll(tasks);
|
||
foundDevices.AddRange(results.Where(r => !string.IsNullOrEmpty(r)));
|
||
|
||
progressBar.IsIndeterminate = false;
|
||
changeTitle("");
|
||
|
||
return foundDevices;
|
||
}
|
||
|
||
private async Task<string> CheckAdbDeviceAsync(string ip, int port)
|
||
{
|
||
try
|
||
{
|
||
using (var client = new TcpClient())
|
||
{
|
||
// Timeout, 1000ms should be enough for local network
|
||
var connectTask = client.ConnectAsync(ip, port);
|
||
if (await Task.WhenAny(connectTask, Task.Delay(1000)) == connectTask)
|
||
{
|
||
if (client.Connected)
|
||
{
|
||
// Port is open, try ADB connect to verify it's actually an ADB device
|
||
string result = ADB.RunAdbCommandToString($"connect {ip}:{port}").Output;
|
||
if (result.Contains("connected") || result.Contains("already"))
|
||
{
|
||
// Disconnect immediately, we're just scanning
|
||
ADB.RunAdbCommandToString($"disconnect {ip}:{port}");
|
||
return ip;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// Ignore connection failures, device not present or not ADB
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private async Task<string> ShowNetworkScanDialogAsync()
|
||
{
|
||
var devices = await ScanNetworkForAdbDevicesAsync();
|
||
|
||
if (devices.Count == 0)
|
||
{
|
||
FlexibleMessageBox.Show(Program.form,
|
||
"No ADB devices found on the network.\n\n" +
|
||
"Please verify:\n" +
|
||
"- The device is on the same network\n" +
|
||
"- Developer mode is enabled\n" +
|
||
"- Wireless ADB with tcpip 5555 enabled (requires one-time setup*)\n\n" +
|
||
"* Connect device via USB and run ADB command: tcpip 5555",
|
||
"No Devices Found");
|
||
return null;
|
||
}
|
||
|
||
if (devices.Count == 1)
|
||
{
|
||
return devices[0];
|
||
}
|
||
|
||
// Multiple devices found - let user choose
|
||
using (Form dialog = new Form())
|
||
{
|
||
dialog.Text = "Select ADB Device";
|
||
dialog.Size = new Size(350, 150);
|
||
dialog.StartPosition = FormStartPosition.CenterParent;
|
||
dialog.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||
dialog.MaximizeBox = false;
|
||
dialog.MinimizeBox = false;
|
||
dialog.BackColor = Color.FromArgb(20, 24, 29);
|
||
dialog.ForeColor = Color.White;
|
||
|
||
var label = new Label
|
||
{
|
||
Text = $"Found {devices.Count} ADB devices:",
|
||
ForeColor = Color.White,
|
||
AutoSize = true,
|
||
Location = new Point(15, 15)
|
||
};
|
||
|
||
var comboBox = new ComboBox
|
||
{
|
||
Location = new Point(15, 40),
|
||
Size = new Size(300, 24),
|
||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||
BackColor = Color.FromArgb(42, 45, 58),
|
||
ForeColor = Color.White
|
||
};
|
||
|
||
foreach (var device in devices)
|
||
comboBox.Items.Add(device);
|
||
comboBox.SelectedIndex = 0;
|
||
|
||
var okButton = CreateStyledButton("Connect", DialogResult.OK, new Point(155, 75));
|
||
var cancelButton = CreateStyledButton("Cancel", DialogResult.Cancel, new Point(240, 75), false);
|
||
|
||
dialog.Controls.AddRange(new Control[] { label, comboBox, okButton, cancelButton });
|
||
dialog.AcceptButton = okButton;
|
||
dialog.CancelButton = cancelButton;
|
||
|
||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||
return comboBox.SelectedItem.ToString();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private void UpdateWirelessADBButtonText()
|
||
{
|
||
bool isWirelessEnabled = File.Exists(storedIpPath) && !string.IsNullOrEmpty(settings.IPAddress);
|
||
ADBWirelessToggle.Text = isWirelessEnabled ? "WIRELESS ADB" : "ENABLE WIRELESS ADB";
|
||
}
|
||
|
||
private void 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...");
|
||
UpdateStatusLabels();
|
||
}
|
||
}
|
||
|
||
private void QuestOptionsButton_Click(object sender, EventArgs e)
|
||
{
|
||
QuestForm Form = new QuestForm();
|
||
Form.Show(Program.form);
|
||
}
|
||
|
||
private void listView1_ColumnClick(object sender, ColumnClickEventArgs e)
|
||
{
|
||
// Determine 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...");
|
||
|
||
// Reset the initialized flag so initListView rebuilds _allItems with current install status
|
||
_allItemsInitialized = false;
|
||
_galleryDataSource = null;
|
||
|
||
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
|
||
// (downloadFiles() should have already downloaded them, but check anyway)
|
||
string runtimesPath = Path.Combine(Environment.CurrentDirectory, "runtimes");
|
||
string webView2LoaderArm64 = Path.Combine(runtimesPath, "win-arm64", "native", "WebView2Loader.dll");
|
||
string webView2LoaderX86 = Path.Combine(runtimesPath, "win-x86", "native", "WebView2Loader.dll");
|
||
string webView2LoaderX64 = Path.Combine(runtimesPath, "win-x64", "native", "WebView2Loader.dll");
|
||
|
||
bool runtimeExists = File.Exists(webView2LoaderX86) || File.Exists(webView2LoaderX64) || File.Exists(webView2LoaderArm64);
|
||
|
||
if (!runtimeExists)
|
||
{
|
||
// Runtime wasn't downloaded during startup - disable trailers
|
||
Logger.Log("WebView2 runtime not found, disabling trailer playback", LogLevel.WARNING);
|
||
enviromentCreated = true;
|
||
webView21.Hide();
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var appDataFolder = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "RSL");
|
||
var env = await CoreWebView2Environment.CreateAsync(userDataFolder: appDataFolder);
|
||
|
||
await webView21.EnsureCoreWebView2Async(env);
|
||
|
||
// Map local folder to a trusted origin (https://app.local)
|
||
var webroot = Path.Combine(Environment.CurrentDirectory, "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 */)
|
||
{
|
||
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;
|
||
|
||
_listViewUninstallButton.Visible = false;
|
||
await UninstallGameAsync(item);
|
||
}
|
||
|
||
public void UpdateGamesButton_Click(object sender, EventArgs e)
|
||
{
|
||
if (!DeviceConnected && Devices.Count == 0)
|
||
{
|
||
FlexibleMessageBox.Show(Program.form,
|
||
"No device connected. Please connect your Quest and click 'RECONNECT DEVICE' first.",
|
||
"Device Required",
|
||
MessageBoxButtons.OK);
|
||
return;
|
||
}
|
||
|
||
changeTitle("Refreshing installed apps and checking for updates...");
|
||
|
||
// Reset the initialized flag so initListView rebuilds _allItems with current install status
|
||
_allItemsInitialized = false;
|
||
_galleryDataSource = null;
|
||
|
||
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.IsIndeterminate = true;
|
||
progressBar.OperationType = "Loading";
|
||
string HWID = SideloaderUtilities.UUID();
|
||
string GameName = selectedApp;
|
||
string packageName = Sideloader.gameNameToPackageName(GameName);
|
||
string InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {packageName} | grep versionCode -F\"").Output;
|
||
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingBeforeFirst(InstalledVersionCode, "versionCode=");
|
||
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingAfterFirst(InstalledVersionCode, " ");
|
||
ulong VersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode));
|
||
if (Directory.Exists($"{settings.MainDir}\\{packageName}"))
|
||
{
|
||
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
|
||
}
|
||
|
||
ProcessOutput output = new ProcessOutput("", "");
|
||
changeTitle("Extracting APK....");
|
||
|
||
_ = Directory.CreateDirectory($"{settings.MainDir}\\{packageName}");
|
||
|
||
Thread t1 = new Thread(() =>
|
||
{
|
||
output = Sideloader.getApk(GameName);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t1.Start();
|
||
|
||
while (t1.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
changeTitle("Extracting OBB if it exists....");
|
||
Thread t2 = new Thread(() =>
|
||
{
|
||
output += ADB.RunAdbCommandToString($"pull \"/sdcard/Android/obb/{packageName}\" \"{settings.MainDir}\\{packageName}\"");
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t2.Start();
|
||
|
||
while (t2.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
if (File.Exists($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip"))
|
||
{
|
||
File.Delete($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip");
|
||
}
|
||
|
||
string path = $"{settings.MainDir}\\7z.exe";
|
||
string cmd = $"7z a -mx1 \"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip\" .\\{packageName}\\*";
|
||
changeTitle("Zipping extracted application...");
|
||
Thread t3 = new Thread(() =>
|
||
{
|
||
_ = ADB.RunCommandToString(cmd, path);
|
||
})
|
||
{
|
||
IsBackground = true
|
||
};
|
||
t3.Start();
|
||
|
||
while (t3.IsAlive)
|
||
{
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
if (File.Exists($"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip"))
|
||
{
|
||
File.Delete($"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip");
|
||
}
|
||
|
||
File.Copy($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip", $"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip");
|
||
File.Delete($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip");
|
||
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
|
||
isworking = false;
|
||
changeTitle("");
|
||
progressBar.IsIndeterminate = false;
|
||
_ = FlexibleMessageBox.Show(Program.form, $"{GameName} pulled to:\n\n{GameName} v{VersionInt} {packageName}.zip\n\nOn your desktop!");
|
||
}
|
||
}
|
||
|
||
bool upToDate_Clicked = false;
|
||
private void btnInstalled_Click(object sender, EventArgs e)
|
||
{
|
||
btnInstalled.Click -= btnInstalled_Click;
|
||
btnUpdateAvailable.Click -= btnUpdateAvailable_Click;
|
||
btnNewerThanList.Click -= btnNewerThanList_Click;
|
||
|
||
if (updateAvailableClicked || NeedsDonation_Clicked)
|
||
{
|
||
updateAvailableClicked = false;
|
||
NeedsDonation_Clicked = false;
|
||
upToDate_Clicked = false;
|
||
}
|
||
|
||
if (!upToDate_Clicked)
|
||
{
|
||
upToDate_Clicked = true;
|
||
// Filter to show installed, update available and newer than list entries
|
||
FilterListByColors(new[] { ColorInstalled, ColorUpdateAvailable, ColorDonateGame });
|
||
}
|
||
else
|
||
{
|
||
upToDate_Clicked = false;
|
||
RestoreFullList();
|
||
}
|
||
|
||
// Update button visual states
|
||
UpdateFilterButtonStates();
|
||
|
||
// Refresh gallery view if active
|
||
if (isGalleryView)
|
||
{
|
||
PopulateGalleryView();
|
||
}
|
||
|
||
btnInstalled.Click += btnInstalled_Click;
|
||
btnUpdateAvailable.Click += btnUpdateAvailable_Click;
|
||
btnNewerThanList.Click += btnNewerThanList_Click;
|
||
}
|
||
|
||
bool NeedsDonation_Clicked = false;
|
||
private void btnNewerThanList_Click(object sender, EventArgs e)
|
||
{
|
||
btnInstalled.Click -= btnInstalled_Click;
|
||
btnUpdateAvailable.Click -= btnUpdateAvailable_Click;
|
||
btnNewerThanList.Click -= btnNewerThanList_Click;
|
||
|
||
if (updateAvailableClicked || upToDate_Clicked)
|
||
{
|
||
updateAvailableClicked = false;
|
||
upToDate_Clicked = false;
|
||
NeedsDonation_Clicked = false;
|
||
}
|
||
|
||
if (!NeedsDonation_Clicked)
|
||
{
|
||
NeedsDonation_Clicked = true;
|
||
FilterListByColor(ColorDonateGame); // Needs donation color
|
||
}
|
||
else
|
||
{
|
||
NeedsDonation_Clicked = false;
|
||
RestoreFullList();
|
||
}
|
||
|
||
// Update button visual states
|
||
UpdateFilterButtonStates();
|
||
|
||
// Refresh gallery view if active
|
||
if (isGalleryView)
|
||
{
|
||
PopulateGalleryView();
|
||
}
|
||
|
||
btnInstalled.Click += btnInstalled_Click;
|
||
btnUpdateAvailable.Click += btnUpdateAvailable_Click;
|
||
btnNewerThanList.Click += btnNewerThanList_Click;
|
||
}
|
||
|
||
private void FilterListByColors(Color[] targetColors)
|
||
{
|
||
changeTitle("Filtering Game List...");
|
||
|
||
if (_allItems == null || _allItems.Count == 0)
|
||
{
|
||
changeTitle("No games to filter");
|
||
return;
|
||
}
|
||
|
||
var targetArgbs = new HashSet<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)
|
||
{
|
||
settings.NodeviceMode = false;
|
||
btnNoDevice.Text = "DISABLE SIDELOADING";
|
||
UpdateStatusLabels();
|
||
|
||
// No Device Mode is currently On. Toggle it Off (enable sideloading)
|
||
// Ask user about delete after install preference
|
||
DialogResult deleteResult = FlexibleMessageBox.Show(Program.form,
|
||
"Delete game files after install?",
|
||
"Sideloading enabled",
|
||
MessageBoxButtons.YesNo);
|
||
|
||
settings.DeleteAllAfterInstall = (deleteResult == DialogResult.Yes);
|
||
}
|
||
else
|
||
{
|
||
settings.NodeviceMode = true;
|
||
settings.DeleteAllAfterInstall = false;
|
||
btnNoDevice.Text = "ENABLE SIDELOADING";
|
||
UpdateStatusLabels();
|
||
}
|
||
|
||
settings.Save();
|
||
}
|
||
|
||
private ListViewItem _rightClickedItem;
|
||
private void gamesListView_MouseClick(object sender, MouseEventArgs e)
|
||
{
|
||
if (e.Button != MouseButtons.Right) return;
|
||
|
||
_rightClickedItem = gamesListView.GetItemAt(e.X, e.Y);
|
||
if (_rightClickedItem == null) return;
|
||
|
||
gamesListView.SelectedItems.Clear();
|
||
_rightClickedItem.Selected = true;
|
||
|
||
UpdateFavoriteMenuItemText();
|
||
favoriteGame.Show(gamesListView, e.Location);
|
||
}
|
||
|
||
private void favoriteButton_Click(object sender, EventArgs e)
|
||
{
|
||
if (_rightClickedItem == null) return;
|
||
|
||
string packageName = _rightClickedItem.SubItems[1].Text;
|
||
|
||
if (settings.FavoritedGames.Contains(packageName))
|
||
settings.RemoveFavoriteGame(packageName);
|
||
else
|
||
settings.AddFavoriteGame(packageName);
|
||
|
||
UpdateFavoriteMenuItemText();
|
||
}
|
||
|
||
private void UpdateFavoriteMenuItemText()
|
||
{
|
||
if (_rightClickedItem == null) return;
|
||
string packageName = _rightClickedItem.SubItems[1].Text;
|
||
favoriteButton.Text = settings.FavoritedGames.Contains(packageName) ? "Remove from Favorites" : "★ Add to Favorites";
|
||
}
|
||
|
||
private void favoriteSwitcher_Click(object sender, EventArgs e)
|
||
{
|
||
// Guard: ensure _allItems is populated
|
||
if (_allItems == null || _allItems.Count == 0)
|
||
{
|
||
Logger.Log("favoriteSwitcher_Click: _allItems is null or empty");
|
||
return;
|
||
}
|
||
|
||
bool showFavoritesOnly = favoriteSwitcher.Text == "FAVORITES";
|
||
|
||
if (showFavoritesOnly)
|
||
{
|
||
favoriteSwitcher.Text = "ALL";
|
||
|
||
var favSet = new HashSet<string>(settings.FavoritedGames, StringComparer.OrdinalIgnoreCase);
|
||
|
||
var favoriteItems = _allItems
|
||
.Where(item => item.SubItems.Count > 1 && favSet.Contains(item.SubItems[1].Text))
|
||
.ToList();
|
||
|
||
gamesListView.BeginUpdate();
|
||
gamesListView.Items.Clear();
|
||
gamesListView.Items.AddRange(favoriteItems.ToArray());
|
||
gamesListView.EndUpdate();
|
||
|
||
_galleryDataSource = favoriteItems;
|
||
if (isGalleryView && _fastGallery != null)
|
||
{
|
||
_fastGallery.RefreshFavoritesCache();
|
||
_fastGallery.UpdateItems(favoriteItems);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
favoriteSwitcher.Text = "FAVORITES";
|
||
|
||
gamesListView.BeginUpdate();
|
||
gamesListView.Items.Clear();
|
||
gamesListView.Items.AddRange(_allItems.ToArray());
|
||
gamesListView.EndUpdate();
|
||
|
||
_galleryDataSource = _allItems;
|
||
if (isGalleryView && _fastGallery != null)
|
||
{
|
||
_fastGallery.RefreshFavoritesCache();
|
||
_fastGallery.UpdateItems(_allItems);
|
||
}
|
||
}
|
||
}
|
||
|
||
public async void UpdateQuestInfoPanel()
|
||
{
|
||
// Check if device is actually connected by checking Devices list
|
||
bool hasDevice = Devices != null && Devices.Count > 0 && !Devices.Contains("unauthorized");
|
||
bool bShowStatus = true;
|
||
|
||
if ((!settings.NodeviceMode && hasDevice) || DeviceConnected)
|
||
{
|
||
try
|
||
{
|
||
ADB.DeviceID = GetDeviceID();
|
||
|
||
// Get device model
|
||
string deviceModel = ADB.RunAdbCommandToString("shell getprop ro.product.model").Output.Trim();
|
||
if (string.IsNullOrEmpty(deviceModel))
|
||
{
|
||
deviceModel = "No Device Found";
|
||
SetQuestStorageProgress(0);
|
||
bShowStatus = false;
|
||
}
|
||
|
||
string firmware = ADB.RunAdbCommandToString("shell getprop ro.build.branch").Output.Trim(); // releases-oculus-14.0-v78
|
||
if (string.IsNullOrEmpty(firmware))
|
||
{
|
||
firmware = string.Empty;
|
||
} else {
|
||
firmware = Utilities.StringUtilities.RemoveEverythingBeforeFirst(firmware, "-v");
|
||
firmware = Utilities.StringUtilities.KeepOnlyNumbers(firmware);
|
||
}
|
||
|
||
// Get storage info
|
||
string storageOutput = ADB.RunAdbCommandToString("shell df /sdcard").Output;
|
||
string[] lines = storageOutput.Split('\n');
|
||
|
||
long totalSpace = 0;
|
||
long usedSpace = 0;
|
||
long freeSpace = 0;
|
||
|
||
if (lines.Length > 1)
|
||
{
|
||
string[] parts = lines[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||
if (parts.Length >= 4)
|
||
{
|
||
if (long.TryParse(parts[1], out totalSpace) &&
|
||
long.TryParse(parts[2], out usedSpace) &&
|
||
long.TryParse(parts[3], out freeSpace))
|
||
{
|
||
totalSpace = totalSpace / 1024; // Convert to MB
|
||
usedSpace = usedSpace / 1024;
|
||
freeSpace = freeSpace / 1024;
|
||
|
||
// Format free space display
|
||
if (freeSpace > 1024)
|
||
{
|
||
freeSpaceText = $"{(freeSpace / 1024.0):F2} GB AVAILABLE";
|
||
}
|
||
else
|
||
{
|
||
freeSpaceText = $"{freeSpace} MB AVAILABLE";
|
||
}
|
||
|
||
freeSpaceTextDetailed = $"{(usedSpace / 1024.0):F0} GB OF {(totalSpace / 1024.0):F0} GB USED";
|
||
}
|
||
}
|
||
}
|
||
|
||
// Calculate storage percentage used - clamped to 1%..100%
|
||
int storagePercentUsed = Math.Min(100, Math.Max(1, (100 - (totalSpace > 0 ? (int)((usedSpace * 100) / totalSpace) : 0))));
|
||
|
||
// Update UI on main thread
|
||
questInfoPanel.Invoke(() =>
|
||
{
|
||
string qinfo = deviceModel;
|
||
if (!string.IsNullOrEmpty(firmware))
|
||
{
|
||
qinfo = $"{qinfo} (v{firmware})";
|
||
}
|
||
questInfoLabel.Text = qinfo;
|
||
diskLabel.Text = freeSpaceText;
|
||
SetQuestStorageProgress(storagePercentUsed);
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Log($"Unable to update quest info panel: {ex.Message}", LogLevel.ERROR);
|
||
questInfoPanel.Invoke(() =>
|
||
{
|
||
questInfoLabel.Text = "No Device Found";
|
||
SetQuestStorageProgress(0);
|
||
bShowStatus = false;
|
||
});
|
||
}
|
||
}
|
||
else
|
||
{
|
||
questInfoPanel.Invoke(() =>
|
||
{
|
||
questInfoLabel.Text = "No Device Found";
|
||
SetQuestStorageProgress(0);
|
||
bShowStatus = false;
|
||
});
|
||
}
|
||
|
||
// Toggle visibility atomically on UI thread, based on device status
|
||
questInfoPanel.Invoke(() =>
|
||
{
|
||
questStorageProgressBar.Visible = true;
|
||
batteryLevImg.Visible = bShowStatus;
|
||
batteryLabel.Visible = bShowStatus;
|
||
questInfoLabel.Visible = true;
|
||
diskLabel.Visible = bShowStatus;
|
||
});
|
||
}
|
||
|
||
private void QuestInfoHoverEnter(object sender, EventArgs e)
|
||
{
|
||
// Only react when device info is shown
|
||
if (!questStorageProgressBar.Visible) return;
|
||
|
||
diskLabel.Text = freeSpaceTextDetailed;
|
||
}
|
||
|
||
// Restore the original baseline text ("XX GB FREE")
|
||
private void QuestInfoHoverLeave(object sender, EventArgs e)
|
||
{
|
||
// Only react when device info is shown
|
||
if (!questStorageProgressBar.Visible) return;
|
||
|
||
// Ignore leave fired when moving between child controls inside the container
|
||
var panel = questInfoPanel;
|
||
var mouse = panel.PointToClient(MousePosition);
|
||
if (panel.ClientRectangle.Contains(mouse)) return;
|
||
|
||
diskLabel.Text = freeSpaceText;
|
||
}
|
||
|
||
private void questStorageProgressBar_Paint(object sender, PaintEventArgs e)
|
||
{
|
||
var g = e.Graphics;
|
||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||
g.Clear(questStorageProgressBar.BackColor);
|
||
|
||
int w = questStorageProgressBar.ClientSize.Width;
|
||
int h = questStorageProgressBar.ClientSize.Height;
|
||
if (w <= 0 || h <= 0) return;
|
||
|
||
// Rounded rectangle parameters (outer + fill share the same geometry).
|
||
int radius = 10;
|
||
Color bgColor = Color.FromArgb(28, 32, 38);
|
||
Color borderColor = Color.FromArgb(60, 65, 75);
|
||
|
||
// Modern fill gradient (adjust as desired)
|
||
Color progressStart = Color.FromArgb(43, 160, 140);
|
||
Color progressEnd = Color.FromArgb(30, 110, 95);
|
||
|
||
// Build the rounded outer path (used for background and clipping).
|
||
var outer = new RoundedRectangleF(w, h, radius);
|
||
|
||
// Paint background
|
||
using (var bgBrush = new SolidBrush(bgColor))
|
||
using (var borderPen = new Pen(borderColor, 1f))
|
||
{
|
||
g.FillPath(bgBrush, outer.Path);
|
||
g.DrawPath(borderPen, outer.Path);
|
||
}
|
||
|
||
// Progress fraction and width
|
||
float p = Math.Max(0f, Math.Min(1f, _questStorageProgress / 100f));
|
||
int progressWidth = Math.Max(0, (int)(w * p));
|
||
if (progressWidth <= 0) return;
|
||
|
||
// Near-full rounding behavior:
|
||
// As progress approaches 100%, progressively include the outer right-rounded corners.
|
||
// Threshold start at 97%. At 97% -> straight cut; at 100% -> fully rounded outer corners.
|
||
float t = 0f;
|
||
if (p > 0.97f)
|
||
{
|
||
t = Math.Min(1f, (p - 0.97f) / 0.03f); // 0..1 over last 3%
|
||
}
|
||
|
||
// Build a clipping region for the fill: intersection of outer rounded rect with
|
||
// the left rectangular portion [0..progressWidth]
|
||
// plus a progressive right-cap region that extends into the rounded corners
|
||
// with width up to 2*radius, scaled by t.
|
||
Region fillClip = new Region(new Rectangle(0, 0, progressWidth, h));
|
||
if (t > 0f)
|
||
{
|
||
int capWidth = (int)(t * (2 * radius));
|
||
if (capWidth > 0)
|
||
{
|
||
// This rectangle sits inside the area of the right rounded corners,
|
||
// so union-ing it with the rectangular clip allows the fill to
|
||
// progressively "wrap" into the curvature.
|
||
var rightCapRect = new Rectangle(w - (2 * radius), 0, capWidth, h);
|
||
fillClip.Union(rightCapRect);
|
||
}
|
||
}
|
||
|
||
Region prevClip = g.Clip;
|
||
try
|
||
{
|
||
// Final fill region = outer rounded path ∩ fillClip
|
||
using (var outerRegion = new Region(outer.Path))
|
||
{
|
||
outerRegion.Intersect(fillClip);
|
||
g.SetClip(outerRegion, CombineMode.Replace);
|
||
|
||
using (var progressBrush = new LinearGradientBrush(
|
||
new Rectangle(0, 0, Math.Max(1, progressWidth), h),
|
||
progressStart,
|
||
progressEnd,
|
||
LinearGradientMode.Horizontal))
|
||
{
|
||
// Fill the outer path; clipping ensures the fill grows left to right,
|
||
// stays fully flush to the outer geometry, and never exceeds it.
|
||
g.FillPath(progressBrush, outer.Path);
|
||
}
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
// Restore clip and re-stroke border to keep outline crisp
|
||
g.Clip = prevClip;
|
||
using (var borderPen = new Pen(borderColor, 1f))
|
||
{
|
||
g.DrawPath(borderPen, outer.Path);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void SetQuestStorageProgress(int percentage)
|
||
{
|
||
_questStorageProgress = Math.Max(0, Math.Min(100, percentage));
|
||
|
||
if (questStorageProgressBar.InvokeRequired)
|
||
{
|
||
questStorageProgressBar.Invoke(new Action(() => questStorageProgressBar.Invalidate()));
|
||
}
|
||
else
|
||
{
|
||
questStorageProgressBar.Invalidate();
|
||
}
|
||
}
|
||
|
||
private void btnViewToggle_Click(object sender, EventArgs e)
|
||
{
|
||
// Capture currently selected item before switching views
|
||
string selectedPackageName = null;
|
||
if (gamesListView.SelectedItems.Count > 0)
|
||
{
|
||
var selectedItem = gamesListView.SelectedItems[0];
|
||
if (selectedItem.SubItems.Count > 2)
|
||
{
|
||
selectedPackageName = selectedItem.SubItems[SideloaderRCLONE.PackageNameIndex].Text;
|
||
}
|
||
}
|
||
|
||
isGalleryView = !isGalleryView;
|
||
|
||
// Save user preference
|
||
settings.UseGalleryView = isGalleryView;
|
||
settings.Save();
|
||
|
||
if (isGalleryView)
|
||
{
|
||
btnViewToggle.Text = "LIST";
|
||
gamesListView.Visible = false;
|
||
gamesGalleryView.Visible = true;
|
||
|
||
// Only populate if data is available, otherwise it will be populated when initListView completes
|
||
if (_allItems != null && _allItems.Count > 0)
|
||
{
|
||
PopulateGalleryView();
|
||
|
||
// Scroll to the previously selected item in gallery view
|
||
if (!string.IsNullOrEmpty(selectedPackageName) && _fastGallery != null)
|
||
{
|
||
_fastGallery.ScrollToPackage(selectedPackageName);
|
||
}
|
||
}
|
||
}
|
||
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;
|
||
|
||
var item = _fastGallery.GetItemAtIndex(index);
|
||
if (item == null) return;
|
||
|
||
await UninstallGameAsync(item);
|
||
}
|
||
|
||
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.Standard
|
||
};
|
||
|
||
// Copy items from m_combo
|
||
foreach (var item in m_combo.Items)
|
||
{
|
||
comboBox.Items.Add(item);
|
||
}
|
||
|
||
var okButton = CreateStyledButton("OK", DialogResult.OK, new Point(255, 75));
|
||
var cancelButton = CreateStyledButton("Cancel", DialogResult.Cancel, new Point(340, 75), false);
|
||
|
||
dialog.Controls.AddRange(new Control[] { label, comboBox, okButton, cancelButton });
|
||
dialog.AcceptButton = okButton;
|
||
dialog.CancelButton = cancelButton;
|
||
|
||
if (dialog.ShowDialog(this) == DialogResult.OK && comboBox.SelectedIndex != -1)
|
||
{
|
||
return comboBox.SelectedItem.ToString();
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private string ShowDeviceSelector(string promptText = "Select a device")
|
||
{
|
||
// Refresh the list of devices first
|
||
string output = ADB.RunAdbCommandToString("devices").Output;
|
||
string[] lines = output.Split('\n');
|
||
|
||
var deviceList = new List<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;
|
||
FlexibleMessageBox.Show(this, $"Selected device: {deviceList[0]}\n\nNo other devices detected");
|
||
return deviceList[0];
|
||
}
|
||
|
||
// Create a dialog to show the device selection
|
||
using (Form dialog = new Form())
|
||
{
|
||
dialog.Text = promptText;
|
||
dialog.Size = new Size(400, 150);
|
||
dialog.StartPosition = FormStartPosition.CenterParent;
|
||
dialog.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||
dialog.MaximizeBox = false;
|
||
dialog.MinimizeBox = false;
|
||
dialog.BackColor = Color.FromArgb(20, 24, 29);
|
||
dialog.ForeColor = Color.White;
|
||
|
||
var label = new Label
|
||
{
|
||
Text = promptText,
|
||
ForeColor = Color.White,
|
||
AutoSize = true,
|
||
Location = new Point(15, 15)
|
||
};
|
||
|
||
var comboBox = new ComboBox
|
||
{
|
||
Location = new Point(15, 40),
|
||
Size = new Size(350, 24),
|
||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||
BackColor = Color.FromArgb(42, 45, 58),
|
||
ForeColor = Color.White,
|
||
FlatStyle = FlatStyle.Standard
|
||
};
|
||
|
||
// Add devices to combo
|
||
foreach (var device in deviceList)
|
||
{
|
||
comboBox.Items.Add(device);
|
||
}
|
||
|
||
if (comboBox.Items.Count > 0)
|
||
{
|
||
comboBox.SelectedIndex = 0;
|
||
}
|
||
|
||
var okButton = CreateStyledButton("OK", DialogResult.OK, new Point(205, 75));
|
||
var cancelButton = CreateStyledButton("Cancel", DialogResult.Cancel, new Point(290, 75), false);
|
||
|
||
dialog.Controls.AddRange(new Control[] { label, comboBox, okButton, cancelButton });
|
||
dialog.AcceptButton = okButton;
|
||
dialog.CancelButton = cancelButton;
|
||
|
||
if (dialog.ShowDialog(this) == DialogResult.OK && comboBox.SelectedIndex != -1)
|
||
{
|
||
string selectedDevice = comboBox.SelectedItem.ToString();
|
||
|
||
// Update internal combo for compatibility
|
||
devicesComboBox.Items.Clear();
|
||
foreach (var device in deviceList)
|
||
{
|
||
devicesComboBox.Items.Add(device);
|
||
}
|
||
devicesComboBox.SelectedItem = selectedDevice;
|
||
|
||
return selectedDevice;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private void selectDeviceButton_Click(object sender, EventArgs e)
|
||
{
|
||
string selectedDevice = ShowDeviceSelector("Select a device");
|
||
if (selectedDevice != null)
|
||
{
|
||
ADB.DeviceID = selectedDevice;
|
||
changeTitlebarToDevice();
|
||
showAvailableSpace();
|
||
changeTitle($"Selected device: {selectedDevice}", true);
|
||
}
|
||
}
|
||
|
||
private void selectMirrorButton_Click(object sender, EventArgs e)
|
||
{
|
||
string selectedMirror = ShowMirrorSelector("Select a mirror");
|
||
if (selectedMirror != null)
|
||
{
|
||
// Find and select the mirror in the hidden remotesList
|
||
for (int i = 0; i < remotesList.Items.Count; i++)
|
||
{
|
||
if (remotesList.Items[i].ToString() == selectedMirror)
|
||
{
|
||
remotesList.SelectedIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private string ShowMirrorSelector(string promptText = "Select a mirror")
|
||
{
|
||
if (remotesList.Items.Count == 0)
|
||
{
|
||
FlexibleMessageBox.Show(this, "No mirrors available.");
|
||
return null;
|
||
}
|
||
|
||
// If only one mirror, just inform the user
|
||
if (remotesList.Items.Count == 1)
|
||
{
|
||
string onlyMirror = remotesList.Items[0].ToString();
|
||
FlexibleMessageBox.Show(this, $"Currently using mirror: {onlyMirror}\n\nNo other mirrors available");
|
||
return onlyMirror;
|
||
}
|
||
|
||
using (Form dialog = new Form())
|
||
{
|
||
dialog.Text = promptText;
|
||
dialog.Size = new Size(350, 150);
|
||
dialog.StartPosition = FormStartPosition.CenterParent;
|
||
dialog.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||
dialog.MaximizeBox = false;
|
||
dialog.MinimizeBox = false;
|
||
dialog.BackColor = Color.FromArgb(20, 24, 29);
|
||
dialog.ForeColor = Color.White;
|
||
|
||
var label = new Label
|
||
{
|
||
Text = $"{promptText} (Current: {remotesList.SelectedItem ?? "None"})",
|
||
ForeColor = Color.White,
|
||
AutoSize = true,
|
||
Location = new Point(15, 15)
|
||
};
|
||
|
||
var comboBox = new ComboBox
|
||
{
|
||
Location = new Point(15, 40),
|
||
Size = new Size(300, 24),
|
||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||
BackColor = Color.FromArgb(42, 45, 58),
|
||
ForeColor = Color.White,
|
||
FlatStyle = FlatStyle.Standard
|
||
};
|
||
|
||
// Add mirrors to combo
|
||
foreach (var item in remotesList.Items)
|
||
{
|
||
comboBox.Items.Add(item.ToString());
|
||
}
|
||
|
||
// Select current mirror
|
||
if (remotesList.SelectedIndex >= 0)
|
||
{
|
||
comboBox.SelectedIndex = remotesList.SelectedIndex;
|
||
}
|
||
else if (comboBox.Items.Count > 0)
|
||
{
|
||
comboBox.SelectedIndex = 0;
|
||
}
|
||
|
||
var okButton = CreateStyledButton("OK", DialogResult.OK, new Point(155, 75));
|
||
var cancelButton = CreateStyledButton("Cancel", DialogResult.Cancel, new Point(240, 75), false);
|
||
|
||
dialog.Controls.AddRange(new Control[] { label, comboBox, okButton, cancelButton });
|
||
dialog.AcceptButton = okButton;
|
||
dialog.CancelButton = cancelButton;
|
||
|
||
if (dialog.ShowDialog(this) == DialogResult.OK && comboBox.SelectedIndex != -1)
|
||
{
|
||
return comboBox.SelectedItem.ToString();
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private Button CreateStyledButton(string text, DialogResult dialogResult, Point location, bool isPrimary = true)
|
||
{
|
||
var button = new Button
|
||
{
|
||
Text = text,
|
||
DialogResult = dialogResult,
|
||
Location = location,
|
||
Size = new Size(75, 28),
|
||
FlatStyle = FlatStyle.Flat,
|
||
BackColor = Color.FromArgb(42, 45, 58),
|
||
ForeColor = Color.White,
|
||
Font = new Font("Segoe UI", 9F),
|
||
Cursor = Cursors.Hand
|
||
};
|
||
|
||
button.FlatAppearance.BorderSize = 0;
|
||
|
||
// Track hover state
|
||
bool isHovered = false;
|
||
|
||
button.MouseEnter += (s, e) => { isHovered = true; button.Invalidate(); };
|
||
button.MouseLeave += (s, e) => { isHovered = false; button.Invalidate(); };
|
||
|
||
button.Paint += (s, e) =>
|
||
{
|
||
var btn = s as Button;
|
||
var g = e.Graphics;
|
||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||
|
||
int radius = 4;
|
||
Rectangle drawRect = new Rectangle(1, 1, btn.Width - 2, btn.Height - 2);
|
||
|
||
// Clear with parent background
|
||
using (SolidBrush clearBrush = new SolidBrush(btn.Parent?.BackColor ?? Color.FromArgb(20, 24, 29)))
|
||
{
|
||
g.FillRectangle(clearBrush, 0, 0, btn.Width, btn.Height);
|
||
}
|
||
|
||
using (GraphicsPath path = CreateRoundedRectPath(drawRect, radius))
|
||
{
|
||
// Hover: accent color, Normal: dark button color
|
||
Color bgColor = isHovered
|
||
? Color.FromArgb(93, 203, 173)
|
||
: btn.BackColor;
|
||
|
||
Color textColor = isHovered
|
||
? Color.FromArgb(20, 20, 20)
|
||
: btn.ForeColor;
|
||
|
||
using (SolidBrush brush = new SolidBrush(bgColor))
|
||
{
|
||
g.FillPath(brush, path);
|
||
}
|
||
|
||
// Subtle border on normal state
|
||
if (!isHovered)
|
||
{
|
||
using (Pen borderPen = new Pen(Color.FromArgb(70, 75, 90), 1))
|
||
{
|
||
g.DrawPath(borderPen, path);
|
||
}
|
||
}
|
||
|
||
TextRenderer.DrawText(g, btn.Text, btn.Font,
|
||
new Rectangle(0, 0, btn.Width, btn.Height), textColor,
|
||
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
|
||
}
|
||
|
||
// Set rounded region
|
||
using (GraphicsPath regionPath = CreateRoundedRectPath(new Rectangle(0, 0, btn.Width, btn.Height), radius))
|
||
{
|
||
btn.Region = new Region(regionPath);
|
||
}
|
||
};
|
||
|
||
return button;
|
||
}
|
||
|
||
private GraphicsPath CreateRoundedRectPath(Rectangle rect, int radius)
|
||
{
|
||
GraphicsPath path = new GraphicsPath();
|
||
|
||
if (radius <= 0)
|
||
{
|
||
path.AddRectangle(rect);
|
||
return path;
|
||
}
|
||
|
||
int diameter = radius * 2;
|
||
diameter = Math.Min(diameter, Math.Min(rect.Width, rect.Height));
|
||
radius = diameter / 2;
|
||
|
||
Rectangle arcRect = new Rectangle(rect.Location, new Size(diameter, diameter));
|
||
|
||
path.AddArc(arcRect, 180, 90);
|
||
arcRect.X = rect.Right - diameter;
|
||
path.AddArc(arcRect, 270, 90);
|
||
arcRect.Y = rect.Bottom - diameter;
|
||
path.AddArc(arcRect, 0, 90);
|
||
arcRect.X = rect.Left;
|
||
path.AddArc(arcRect, 90, 90);
|
||
|
||
path.CloseFigure();
|
||
return path;
|
||
}
|
||
|
||
private void UpdateFilterButtonStates()
|
||
{
|
||
Color inactiveStroke = Color.FromArgb(74, 74, 74);
|
||
Color activeBg = Color.FromArgb(40, 45, 55);
|
||
Color inactiveBg = Color.FromArgb(32, 35, 45);
|
||
|
||
// btnInstalled state
|
||
if (upToDate_Clicked)
|
||
{
|
||
btnInstalled.StrokeColor = ColorInstalled;
|
||
btnInstalled.Inactive1 = activeBg;
|
||
btnInstalled.Inactive2 = activeBg;
|
||
}
|
||
else
|
||
{
|
||
btnInstalled.StrokeColor = inactiveStroke;
|
||
btnInstalled.Inactive1 = inactiveBg;
|
||
btnInstalled.Inactive2 = inactiveBg;
|
||
}
|
||
|
||
// btnUpdateAvailable state
|
||
if (updateAvailableClicked)
|
||
{
|
||
btnUpdateAvailable.StrokeColor = ColorUpdateAvailable;
|
||
btnUpdateAvailable.Inactive1 = activeBg;
|
||
btnUpdateAvailable.Inactive2 = activeBg;
|
||
}
|
||
else
|
||
{
|
||
btnUpdateAvailable.StrokeColor = inactiveStroke;
|
||
btnUpdateAvailable.Inactive1 = inactiveBg;
|
||
btnUpdateAvailable.Inactive2 = inactiveBg;
|
||
}
|
||
|
||
// btnNewerThanList state
|
||
if (NeedsDonation_Clicked)
|
||
{
|
||
btnNewerThanList.StrokeColor = ColorDonateGame;
|
||
btnNewerThanList.Inactive1 = activeBg;
|
||
btnNewerThanList.Inactive2 = activeBg;
|
||
}
|
||
else
|
||
{
|
||
btnNewerThanList.StrokeColor = inactiveStroke;
|
||
btnNewerThanList.Inactive1 = inactiveBg;
|
||
btnNewerThanList.Inactive2 = inactiveBg;
|
||
}
|
||
|
||
// Force repaint
|
||
btnInstalled.Invalidate();
|
||
btnUpdateAvailable.Invalidate();
|
||
btnNewerThanList.Invalidate();
|
||
}
|
||
|
||
private void UnfocusSearchTextBox(object sender, EventArgs e)
|
||
{
|
||
// Only unfocus if the search text box currently has focus
|
||
if (searchTextBox.Focused)
|
||
{
|
||
// Move focus to the appropriate view
|
||
if (isGalleryView && gamesGalleryView.Visible)
|
||
{
|
||
gamesGalleryView.Focus();
|
||
}
|
||
else
|
||
{
|
||
gamesListView.Focus();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateNotesScrollBar()
|
||
{
|
||
// Check if content height exceeds visible height
|
||
int contentHeight = notesRichTextBox.GetPositionFromCharIndex(notesRichTextBox.TextLength).Y
|
||
+ notesRichTextBox.Font.Height;
|
||
|
||
if (contentHeight > notesRichTextBox.ClientSize.Height)
|
||
{
|
||
notesRichTextBox.ScrollBars = RichTextBoxScrollBars.Vertical;
|
||
}
|
||
else
|
||
{
|
||
notesRichTextBox.ScrollBars = RichTextBoxScrollBars.None;
|
||
}
|
||
}
|
||
|
||
public class CenteredMenuRenderer : ToolStripProfessionalRenderer
|
||
{
|
||
protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e)
|
||
{
|
||
var rect = new Rectangle(Point.Empty, e.Item.Size);
|
||
Color bgColor = e.Item.Selected ? Color.FromArgb(55, 58, 65) : Color.FromArgb(40, 42, 48);
|
||
using (var brush = new SolidBrush(bgColor))
|
||
e.Graphics.FillRectangle(brush, rect);
|
||
}
|
||
|
||
protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
|
||
{
|
||
// Use the full item bounds for centered text
|
||
var textRect = new Rectangle(0, 0, e.Item.Width, e.Item.Height);
|
||
TextRenderer.DrawText(e.Graphics, e.Text, e.TextFont, textRect, e.TextColor,
|
||
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
|
||
}
|
||
|
||
protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e)
|
||
{
|
||
using (var brush = new SolidBrush(Color.FromArgb(40, 42, 48)))
|
||
e.Graphics.FillRectangle(brush, e.AffectedBounds);
|
||
}
|
||
|
||
protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e)
|
||
{
|
||
using (var pen = new Pen(Color.FromArgb(60, 63, 70)))
|
||
e.Graphics.DrawRectangle(pen, 0, 0, e.AffectedBounds.Width - 1, e.AffectedBounds.Height - 1);
|
||
}
|
||
}
|
||
|
||
public void SetTrailerVisibility(bool visible)
|
||
{
|
||
webView21.Enabled = visible;
|
||
webView21.Visible = visible;
|
||
|
||
if (!visible) ShowVideoPlaceholder();
|
||
}
|
||
|
||
private void InitializeModernPanels()
|
||
{
|
||
Color panelColor = Color.FromArgb(24, 26, 30);
|
||
|
||
// 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();
|
||
}
|
||
|
||
private async Task UninstallGameAsync(ListViewItem item)
|
||
{
|
||
string packageName = item.SubItems.Count > 2 ? item.SubItems[2].Text : "";
|
||
string gameName = item.Text;
|
||
|
||
if (string.IsNullOrEmpty(packageName))
|
||
return;
|
||
|
||
// Confirm uninstall
|
||
DialogResult dialogresult = FlexibleMessageBox.Show(
|
||
$"Are you sure you want to uninstall {gameName}?",
|
||
"Proceed with uninstall?", MessageBoxButtons.YesNo);
|
||
|
||
if (dialogresult == DialogResult.No)
|
||
return;
|
||
|
||
// Ask about backup
|
||
if (!settings.CustomBackupDir)
|
||
{
|
||
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||
}
|
||
else
|
||
{
|
||
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||
}
|
||
|
||
DialogResult dialogresult2 = FlexibleMessageBox.Show(
|
||
$"Do you want to attempt to automatically backup any saves to {backupFolder}\\{DateTime.Today.ToString("yyyy.MM.dd")}\\",
|
||
"Attempt Game Backup?", MessageBoxButtons.YesNo);
|
||
|
||
if (dialogresult2 == DialogResult.Yes)
|
||
{
|
||
Sideloader.BackupGame(packageName);
|
||
}
|
||
|
||
// Perform uninstall
|
||
ProcessOutput output = new ProcessOutput("", "");
|
||
progressBar.IsIndeterminate = true;
|
||
progressBar.OperationType = "";
|
||
|
||
await Task.Run(() => {
|
||
output += Sideloader.UninstallGame(packageName);
|
||
});
|
||
|
||
ShowPrcOutput(output);
|
||
showAvailableSpace();
|
||
progressBar.IsIndeterminate = false;
|
||
|
||
// Remove from combo box
|
||
for (int i = 0; i < m_combo.Items.Count; i++)
|
||
{
|
||
string comboItem = m_combo.Items[i].ToString();
|
||
if (comboItem.Equals(gameName, StringComparison.OrdinalIgnoreCase) ||
|
||
comboItem.Equals(packageName, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
m_combo.Items.RemoveAt(i);
|
||
break;
|
||
}
|
||
}
|
||
|
||
await RefreshGameListAsync();
|
||
}
|
||
|
||
private async Task RefreshGameListAsync()
|
||
{
|
||
// Save current filter state before refreshing
|
||
bool wasUpdateAvailableClicked = updateAvailableClicked;
|
||
bool wasUpToDateClicked = upToDate_Clicked;
|
||
bool wasNeedsDonationClicked = NeedsDonation_Clicked;
|
||
|
||
// Temporarily clear filter states
|
||
updateAvailableClicked = false;
|
||
upToDate_Clicked = false;
|
||
NeedsDonation_Clicked = false;
|
||
|
||
// Refresh the list to update installed status
|
||
_allItemsInitialized = false;
|
||
_galleryDataSource = null;
|
||
listAppsBtn();
|
||
|
||
bool wasGalleryView = isGalleryView;
|
||
isGalleryView = false;
|
||
|
||
initListView(false);
|
||
|
||
// Wait for initListView to finish rebuilding _allItems
|
||
while (!_allItemsInitialized || !loaded)
|
||
{
|
||
await Task.Delay(50);
|
||
}
|
||
|
||
isGalleryView = wasGalleryView;
|
||
|
||
// Reapply the active filter
|
||
if (wasUpToDateClicked)
|
||
{
|
||
upToDate_Clicked = true;
|
||
FilterListByColors(new[] { ColorInstalled, ColorUpdateAvailable, ColorDonateGame });
|
||
}
|
||
else if (wasUpdateAvailableClicked)
|
||
{
|
||
updateAvailableClicked = true;
|
||
FilterListByColor(ColorUpdateAvailable);
|
||
}
|
||
else if (wasNeedsDonationClicked)
|
||
{
|
||
NeedsDonation_Clicked = true;
|
||
FilterListByColor(ColorDonateGame);
|
||
}
|
||
else if (isGalleryView)
|
||
{
|
||
gamesListView.Visible = false;
|
||
gamesGalleryView.Visible = true;
|
||
PopulateGalleryView();
|
||
}
|
||
}
|
||
|
||
private async void timer_DeviceCheck(object sender, EventArgs e)
|
||
{
|
||
// Skip if a device is connected, we're in the middle of loading or other operations
|
||
if (DeviceConnected || isLoading || isinstalling || isuploading) return;
|
||
|
||
// Run a quick device check in background
|
||
try
|
||
{
|
||
string output = await Task.Run(() => ADB.RunAdbCommandToString("devices").Output);
|
||
|
||
string[] lines = output.Split('\n');
|
||
bool hasDeviceNow = false;
|
||
|
||
for (int i = 1; i < lines.Length; i++)
|
||
{
|
||
string line = lines[i].Trim();
|
||
if (line.Length > 0 && !string.IsNullOrWhiteSpace(line) && !line.Contains("unauthorized"))
|
||
{
|
||
hasDeviceNow = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Device state changed
|
||
if (hasDeviceNow)
|
||
{
|
||
// Device connected - do a full refresh
|
||
await CheckForDevice();
|
||
changeTitlebarToDevice();
|
||
showAvailableSpace();
|
||
|
||
// Reset the initialized flag so initListView rebuilds _allItems with current install status
|
||
_allItemsInitialized = false;
|
||
_galleryDataSource = null;
|
||
|
||
listAppsBtn();
|
||
initListView(false);
|
||
UpdateStatusLabels();
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// Silently catch errors
|
||
}
|
||
}
|
||
|
||
private void UpdateStatusLabels()
|
||
{
|
||
// Device ID
|
||
if (DeviceConnected && Devices.Count > 0 && !string.IsNullOrEmpty(Devices[0]))
|
||
{
|
||
string deviceId = Devices[0].Replace("device", "").Trim();
|
||
// Truncate if too long
|
||
if (deviceId.Length > 20)
|
||
deviceId = deviceId.Substring(0, 17) + "...";
|
||
deviceIdLabel.Text = $"Device: {deviceId}";
|
||
}
|
||
else
|
||
{
|
||
deviceIdLabel.Text = "Device: Not connected";
|
||
}
|
||
|
||
// Active Mirror
|
||
string mirrorName = "None";
|
||
if (UsingPublicConfig)
|
||
{
|
||
mirrorName = "Public";
|
||
}
|
||
else if (remotesList.SelectedItem != null)
|
||
{
|
||
mirrorName = remotesList.SelectedItem.ToString();
|
||
}
|
||
activeMirrorLabel.Text = $"Mirror: {mirrorName}";
|
||
|
||
UpdateSideloadingUI();
|
||
}
|
||
|
||
public void UpdateSideloadingUI()
|
||
{
|
||
// Update the sideload button text
|
||
if (settings.NodeviceMode)
|
||
{
|
||
btnNoDevice.Text = "ENABLE SIDELOADING";
|
||
}
|
||
else
|
||
{
|
||
btnNoDevice.Text = "DISABLE SIDELOADING";
|
||
}
|
||
|
||
// Sideloading Status
|
||
if (settings.NodeviceMode)
|
||
{
|
||
sideloadingStatusLabel.Text = "Sideloading: Disabled";
|
||
sideloadingStatusLabel.ForeColor = Color.FromArgb(255, 100, 100); // Red-ish for disabled
|
||
}
|
||
else
|
||
{
|
||
sideloadingStatusLabel.Text = "Sideloading: Enabled";
|
||
sideloadingStatusLabel.ForeColor = Color.FromArgb(93, 203, 173); // Accent green for enabled
|
||
}
|
||
}
|
||
|
||
public void UpdateProgressStatus(string operation,
|
||
int current = 0,
|
||
int total = 0,
|
||
int percent = 0,
|
||
TimeSpan? eta = null,
|
||
double? speedMBps = null)
|
||
{
|
||
if (this.InvokeRequired)
|
||
{
|
||
this.Invoke(() => UpdateProgressStatus(operation, current, total, percent, eta, speedMBps));
|
||
return;
|
||
}
|
||
|
||
// Sync the progress bar's operation type to the current operation
|
||
progressBar.OperationType = operation;
|
||
|
||
var sb = new StringBuilder();
|
||
|
||
// Operation name
|
||
sb.Append(operation);
|
||
|
||
// Percentage
|
||
if (percent > 0)
|
||
{
|
||
sb.Append($" ({percent}%)");
|
||
}
|
||
|
||
// Speed
|
||
if (speedMBps.HasValue && speedMBps.Value > 0)
|
||
{
|
||
sb.Append($" @ {speedMBps.Value:F1} MB/s");
|
||
}
|
||
|
||
// File count if applicable
|
||
if (total > 1)
|
||
{
|
||
sb.Append($" ({current}/{total})");
|
||
}
|
||
|
||
// ETA
|
||
if (eta.HasValue && eta.Value.TotalSeconds > 0)
|
||
{
|
||
sb.Append($" • ETA: {eta.Value:hh\\:mm\\:ss}");
|
||
}
|
||
|
||
speedLabel.Text = sb.ToString();
|
||
}
|
||
|
||
public void ClearProgressStatus()
|
||
{
|
||
if (this.InvokeRequired)
|
||
{
|
||
this.Invoke(ClearProgressStatus);
|
||
return;
|
||
}
|
||
|
||
speedLabel.Text = "";
|
||
}
|
||
}
|
||
|
||
public static class ControlExtensions
|
||
{
|
||
public static void Invoke(this Control control, Action action)
|
||
{
|
||
if (control.InvokeRequired)
|
||
{
|
||
_ = control.Invoke(new MethodInvoker(action), null);
|
||
}
|
||
else
|
||
{
|
||
action.Invoke();
|
||
}
|
||
}
|
||
}
|
||
}
|