Compare commits

...

87 Commits

Author SHA1 Message Date
Fenopy
15efd91769 Merge pull request #300 from VRPirates/beta/RSL-3.0.1
Beta/rsl 3.0.1
2026-03-18 12:58:00 -05:00
fenopy
28fe132012 fix: better handling when no config files are available 2026-03-18 12:55:26 -05:00
Fenopy
db93c7b610 Merge pull request #292 from jp64k/RSL-3.0.5
Bunch of changes, look inside
2026-02-06 06:22:15 -06:00
jp64k
3bae614d7e Fixed another regression (release notes placeholder not being centered on launch) 2026-02-04 04:35:27 +01:00
jp64k
3e3a452ceb Updated changelog.txt 2026-02-04 04:11:48 +01:00
jp64k
4dc11191f0 Implemented async real-time progress updates for drag and drop. Removed deprecated Sideload(...) and CopyOBB(...) functions
Replaced the old sideload/OBB operations with our newer async, progress-aware flows to enable real-time progress feedback in the UI for non-blocking drag and drop operations. Also cleaned up a bit code and made minor logging adjustments. Updated "No Trailer found" message to disappear after 5 seconds
2026-02-04 03:47:28 +01:00
jp64k
3e626c6390 Refined backup messages and updated backup button labels
Changed button texts for clarity: 'BACKUP WITH ADB' to 'BACKUP GAMESAVE WITH ADB', 'BACKUP GAMESAVES' to 'BACKUP ALL GAMESAVES'. Updated dialog and message box titles to match. Added explanatory notes to the bulk backup summary, guiding users to use ADB backup for games requiring special permissions
2026-02-02 04:04:13 +01:00
jp64k
5b44295418 Improved Rookie connection status feedback and fixed typo
Updated the sideloading status label and download button to provide clearer feedback when no device is connected or sideloading is disabled. The download button now shows "DOWNLOAD AND INSTALL" only when sideloading is enabled and a device is connected, otherwise it shows "DOWNLOAD". Meanwhile the sideloading status label now shows "Sideloading: No Device Connected" in orange while no device is connected. Also added missing </Setting> tag in Settings.settings
2026-02-02 03:51:39 +01:00
jp64k
95b9059bed ListView: Show uninstall button on hover
Changed uninstall button logic to display on mouse hover over a ListView item instead of on selection (click) to match gallery view behavior
2026-02-02 02:48:21 +01:00
jp64k
163a0d9fce Bumped version to 3.0.1 and updated changelogs
Increased the version to 3.0.1. New changelog reflects fixes for popularity ranking, favorites updating, improved YouTube trailer matching, automatic public config creation, and new grouped tile logic in Gallery View
2026-01-30 04:49:56 +01:00
jp64k
af84f2cf8c Improvd base game name derivation for grouped tiles
Refactored the logic to strip parentheses from all version names before selecting the shortest, instead of selecting the shortest name first and then stripping
2026-01-30 04:40:59 +01:00
jp64k
9f2e824df2 Gallery View: Added logic to remove unfavorited subitems of grouped tiles in favorites-view
In gallery view, when a game version of a grouped tile is unfavorited in favorites-view, it is now
automatically removed from the display. Added RemoveVersionFromDisplay
method to handle item removal, tile updates and layout
recalculation. The grouped-view is automatically closed
when only one version remains after unfavoriting
2026-01-30 04:33:51 +01:00
jp64k
f21efcd476 Fixed group titles to show actual name when only one version is favorited
Updated the version name logic in gallery view to display the actual game name instead of the simplified base name when only one version of a grouped game is present in favorites
2026-01-30 04:17:38 +01:00
jp64k
50b52d963b Fixed favorites not refreshing when removing entries
Fixed favorites not refreshing when removing entries while in favorites view. Previously, removed items would only disappear after switching to regular view and back. The favorites list now updates immediately upon removal
2026-01-30 04:07:30 +01:00
jp64k
61e6c143fa Fixed regression from earlier commit (f91ee27)
Corrected the index used to parse the size in MB from index 6 to 5
2026-01-30 03:55:35 +01:00
jp64k
60d68cbc8c Improved YouTube trailer matching accuracy
Improved the trailer selection algorithm by requiring all game name words (normalized by removing apostrophes) to be present in the video title. Increased the number of processed matches from 5 to 10 and refined scoring bonuses and penalties, enhancing the accuracy for selecting the most relevant clip
2026-01-30 03:50:24 +01:00
jp64k
93ed359ed1 Auto-create public config file without prompt
Removed the user prompt for creating the public config file when missing. The file is now created automatically when AutoUpdateConfig is enabled, streamlining the process similar to how Rookie handles other required files
2026-01-30 03:13:13 +01:00
jp64k
8bd7efc22e Gallery View: Added grouped tile logic for packages with multiple versions
Implemented grouping of gallery tiles by package with a cleaned base name and an overlay UI for selection among multiple versions. Added new rendering logic, sorting and interaction for grouped tiles, including badges, a scrollable overlay, favorite context-menu support and release-notes updates on hover. Refactored a bit of code in GalleryView.
2026-01-29 18:47:08 +01:00
fenopy
07957d93fe Merge branch 'master' of https://github.com/VRPirates/rookie 2026-01-28 06:21:45 -06:00
jp64k
f91ee27c35 Fixed culture-specific decimal separator problem
Replaced all direct double.TryParse calls with new StringUtilities.TryParseDouble helper method to ensure consistent, culture-invariant parsing of numeric values
2026-01-24 16:57:34 +01:00
Fenopy
d8cc5dc539 Merge pull request #283 from jp64k/RSL-3.0.4
Improved YouTube trailer search accuracy, added TryDeleteDirectory utility for all directory deletions, moved WebView2 cache from hardcoded path to app directory, replaced dated folder dialogs with modern FolderSelectDialog, improved backup/restore logic
2026-01-08 06:06:21 -06:00
jp64k
ed17322983 Refactored and fixed backup directory handling, refactored and fixed backup/restore logic and improved UX
Centralized backup directory logic via SettingsManager.GetEffectiveBackupDir(), replacing repeated path construction throughout the codebase. Fixed backup and restore logic aborting on error, streamlined backup/restore workflows for both individual apps and bulk operations, improved UI text for clarity, improved error handling and improved progress feedback. Removed redundant code and ensured consistent backup directory usage in MainForm, Sideloader, and SettingsForm
2026-01-08 01:46:11 +01:00
jp64k
0970b73fe7 Replaced dated download/backup directory set dialogs with modern FolderSelectDialog
Updated download and backup directory selection to use modern FolderSelectDialog for better folder browsing with initial directories based on user settings, improving UX
2026-01-07 20:32:26 +01:00
jp64k
7cc437309a Moved hardcoded WebView2 path from "C:\RSL" to the application's current directory
Changed the WebView2 cache directory from system root path "C:\RSL" to the application's current directory for improved portability and reliability. Ensured the cache directory is created before initializing the WebView2 environment
2026-01-07 20:16:44 +01:00
jp64k
a050d82c8b Refactored all directory deletion calls to use new TryDeleteDirectory utility
Replaced all direct Directory.Delete calls by our new custom FileSystemUtilities.TryDeleteDirectory class and method for safer and more robust directory deletion throughout the codebase with automatic retries (3x 150ms), recursive deletion, handling of read-only attributes, rename-then-delete fallback, and better error handling. Also moved the WebView cleanup to happen earlier on initialization, before creation of the WebView environment
2026-01-07 19:46:35 +01:00
jp64k
4c5a651b2b Removed deprecated multi-strategy approach 2026-01-07 17:39:28 +01:00
jp64k
af17f5bb12 Improved YouTube trailer search accuracy
Improved YouTube trailer search algorithm by cleaning game names, having 2 search strategies for higher hit accuracy, and adding a scoring system that selects the most relevant clip (trailer) based on title similarity and result position (respecting YouTube's sort order)
2026-01-07 03:55:52 +01:00
Fenopy
670ace1963 Merge pull request #282 from jp64k/RSL-3.0.4
Feat: Rookie now flashes in taskbar when FlexibleMessageBox is thrown and window is inactive, Fix: FlexibleMessageBox positioning for inactive/minimized window
2026-01-06 12:03:22 -06:00
jp64k
77cfb81545 Feat: Rookie now flashes in taskbar when FlexibleMessageBox is thrown and window is inactive, Fix: FlexibleMessageBox positioning for inactive/minimized window
Added taskbar flashing to notify users when a task is complete or requires user input (= when FlexibleMessageBox is thrown) while the application is inactive or minimized. Enhanced FlexibleMessageBox to better handle when the application is minimized by centering the dialog on the screen and repositioning it when the form is restored.
2026-01-06 17:51:36 +01:00
fenopy
3d241cc634 fix: issue with selecting the default mirror 2026-01-06 08:50:34 -06:00
Fenopy
187f1d3689 Merge pull request #279 from jp64k/RSL-3.0-2
Fixes, QoL and new features... Enhanced DNS/ADB handling, fixed gallery selection, fixed newer than list/blacklist logic, synchronized gallery/list view sorting, modernized download queue UI, added session persistence
2026-01-05 06:53:11 -06:00
jp64k
6ccaf06ab4 Fixed broken shortcuts (CTRL+P, CTRL+L, ALT+L)
Fixed broken shortcuts (CTRL+P behavior was already broken on 2.34.1), updated to use FlexibleMessageBox and changed CTRL+P logic to immediately copy the selected game's package name without confirmation, supporting both list and gallery views, with a neater notification
2026-01-03 05:41:01 +01:00
jp64k
f289026438 Added game name to disk/device space error messages
Updated disk and device space error messages to show the game name
2026-01-03 05:14:32 +01:00
jp64k
de6b0e4c70 Added download queue space checks with new error messages
Implemented disk and device space validation for the download/install queue with per-game and total queue size checks. The UI displays total queue size and updates available device space after each install. Improved error handling and user prompts for insufficient space.

When adding an entry to the queue, we check:
- Free disk space in the current download directory
- Device free space (if sideloading is enabled)
- Whether the download is already queued
- Total queue size against download directory and device free space to prevent over-queuing

When a download starts (e.g., when resuming downloads after a restart), we recheck disk and device space for that download and allow the user to skip it or clear the queue if not.

Space checks are intentionally lenient. We only block downloads when we're confident the download won't fit. If space is close, users will proceed since they'll receive an error during the actual installation when it fails anyway.
2026-01-03 05:05:43 +01:00
jp64k
debb204719 Improved YouTube search query accuracy
Wrapped the game name in quotes in the YouTube search query to yield more accurate results for VR trailers
2026-01-02 16:09:45 +01:00
jp64k
f80d5f841c Removed dated drag-and-drop label, updated notes display
Removed DragDropLbl label and its related logic, consolidating drag-and-drop tips into the notesRichTextBox with improved styling. Added UpdateReleaseNotes method to handle dynamic notes display and placeholer styling
2026-01-02 15:43:33 +01:00
jp64k
38a1968c42 Refactored gallery view tile rendering for better performance and increased hover effect animation speed
Simplified and unified tile and thumbnail drawing, reduced gallery image filter quality (interpolation) for better performance with negligible visual quality loss while maintaining the best filter quality (HighQualityBicubic) for the currently hovered tile, and adjusted hover and animation constants. Tile borders are now drawn after the image. Removed no longer used FormatSize method
2026-01-02 15:20:30 +01:00
jp64k
b92def0283 Added persistent download queue with resume support
Implemented automatic saving and loading of the download queue, allowing users to resume unfinished downloads after restarting the application. Added methods to persist the queue state, prompting users to ask if they want to resume queued downloads on startup, and updated queue management logic to support resumed downloads
2025-12-31 05:37:47 +01:00
jp64k
62343f8cf6 Implemented minor Copilot changes 2025-12-31 05:11:58 +01:00
jp64k
fdb091cee6 Persist window size, position, state and sort order between sessions
Added functionality to automatically save and restore the main window's position, size, (maximized) state, and sort order for list and gallery views between sessions. Introduced new properties in SettingsManager for window and sort state persistence. Updated MainForm to load and save these settings on startup, close, and sort changes, and added default/fallback behavior. Modified ModernListView to suppress header drawing and interaction during initialization
2025-12-31 04:54:11 +01:00
jp64k
3993e574a8 Replaced dated download queue ListBox with custom ModernQueuePanel
Introduced ModernQueuePanel as a modern control for the download queue, supporting
drag-and-drop reordering, auto-scroll, and entry removal via close buttons (replaced
double-click) in a modernized look. Removed legacy ListBox event handlers and updated
MainForm to use the new panel, synchronizing it with the queue's binding list and
refactored UI logic for queue management and download cancellation
2025-12-31 03:44:46 +01:00
jp64k
fb36826091 Fixed incorrect NEWER THAN LIST logic/behavior for blacklisted apps
Moved increment of newerThanListCount and setting of item.ForeColor outside the blacklist check. Now, only the prompt for upload is skipped for blacklisted packages, while the UI still reflects that a newer version is installed
2025-12-30 22:24:14 +01:00
jp64k
84ddce1423 Synchronized sorting states between list view and gallery view
Introduced shared sorting state logic to maintain consistent sorting order when switching between gallery and list views (game name, last updated, size, popularity, asc and desc)
2025-12-30 22:12:29 +01:00
jp64k
f363550351 Fixed incorrect item selection in gallery view for entries with duplicate package names
Updated GalleryView selection logic to consistently use unique release name (SubItems[ReleaseNameIndex]) instead of non-unique package name, which fixes incorrect item selections in gallery view for entries with duplicate package names
2025-12-30 21:50:47 +01:00
jp64k
02ab0f0e2a Improved timeout handling for ADB connect command and suppressed devices command log spam
Enhanced RunAdbCommandToString to use a 5-second timeout for connect commands, preventing prolonged blocking during startup/initialization when an ADB wireless config exists but fails to connect to the device. Also added a suppressLogging parameter to control 'devices' command output logging, which is used for our quick 'devices' check command that runs every second while no devices are connected
2025-12-30 17:23:00 +01:00
jp64k
5819bc8083 Refactored DnsHelper and added DNS testing for the public config hostname instead of a hardcoded URL
Refactored DnsHelper for maintainability and added automatic DNS testing for the public config hostname after creation/update of vrp-public.json to automatically enable fallback DNS and proxy if system DNS fails for that
2025-12-30 16:54:00 +01:00
fenopy
311f0cfb7e feat: save selected mirror 2025-12-23 09:26:35 -06:00
Fenopy
58cb75c38c Merge pull request #276 from jp64k/RSL-3.0-2
Several changes and fixes, see notes below
2025-12-23 07:07:17 -06:00
Fenopy
4383b9d398 store deviceId in variable instead of line-splitting
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 07:06:33 -06:00
jp64k
b3ce3ab214 Adjusted wireless ADB dialog widths to fix layout
Increased width of 'Wireless ADB Options' and 'Wireless ADB Connection' dialogs by 6 pixels each to improve layout centering
2025-12-22 03:47:53 +01:00
jp64k
ae72432aee Improved FlexibleMessageBox button and text placement
Reduced left padding for the title and text labels, reduced right padding of buttons for better alignment, added code to resize the title panel to fix the close button position - https://puu.sh/KFOEG/5c3d72aaee.png
2025-12-22 03:43:48 +01:00
jp64k
5a939d6234 Refactored ADBWirelessToggle and device detection
Replaced message box prompts with custom dialogs for wireless ADB options and connection methods, providing clearer choices for users. Enhanced wireless ADB state detection by verifying actual device connection, and readded old automatic USB setup option as third option for wireless ADB ("Automatic (USB)"). Also refactored CheckForDevice() to use Task.Run/await pattern and optimized battery check
2025-12-22 02:42:34 +01:00
jp64k
e6ce947700 Refactored filter/selection logic, now preserves filter and selection state on game list refresh
Refactored methods to use RefreshGameListAsync() instead of reinitializing the list view, ensuring filter and selection state are preserved after refreshes. Added logic to restore the last selected item by package name in both list and gallery views. Also refactored ModernListView to simplify marquee calculation logic.
2025-12-22 01:25:50 +01:00
jp64k
d24df061df Minor ModernListView.cs cleanup and fixed hovered text disappearing after scroll
Removed MarqueeEnabled and MarqueeOnlyWhenFocused fields and related conditional logic, as they were no longer used. Also fixed hover state updates after scrolling
2025-12-22 00:24:23 +01:00
Fenopy
1b06ab7981 Merge pull request #274 from jp64k/RSL-3.0-2
Implemented custom ModernListView class, added modern scrollbar to gallery view, reworked list view columns and sorting, added fix to skip 0 MB entries when MR-Fix version exists
2025-12-18 11:29:02 -06:00
jp64k
5f4cfc09fe Fixed "&" not being rendered in new list view
Included TextFormatFlags.NoPrefix in text rendering flags for both header and cell drawing to fix & rendering.
2025-12-18 16:11:37 +01:00
jp64k
1de339da75 Updated ModernListView.cs
Changed several public properties and enums in ModernListView to private, removed unused code, reduced text scroll speed (marquee)
2025-12-18 06:09:50 +01:00
jp64k
4f653f2131 Implemented Copilot suggestions 2025-12-18 05:53:52 +01:00
jp64k
3148ddcfa3 Implemented custom ModernListView class, added modern scrollbar to gallery view, reworked list view columns and sorting, added fix to skip 0 MB entries when MR-Fix version exists
Added custom ModernListView class with modern dark theme appearance and refined behavior with smooth text scrolling support. Required a lot of finicking to get the details right. Reworked ListView columns and sorting for size and popularity, including parsing with reformatted GB/MB size and popularity ranking. Updated GalleryView to support new formats and implemented modern scrollbars. Also added logic to skip 0 MB entries when an MR-Fix version of same game exists
2025-12-18 05:44:54 +01:00
fenopy
b793d2a140 bump rclone to 1.72.1 2025-12-17 15:36:55 -06:00
Fenopy
5f16ad13e2 Merge pull request #273 from jp64k/RSL-3.0-2
Fixed update sorting for gallery view to consider exact date+time (and not just date)
2025-12-17 15:20:01 -06:00
Fenopy
ddb503feec Update to handle UTC timestamps
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-17 15:19:44 -06:00
jp64k
1bcfbd132d Updated icon in forms I missed earlier; now reusing existing icon; dropping .exe size from 3,38 MB to 1,03 MB
Updated icon in forms I missed earlier; now reusing existing icon; dropping .exe size from 3,38 MB to 1,03 MB
2025-12-17 22:18:19 +01:00
jp64k
9862a6a8ca Fixed update sorting for gallery view to consider exact date+time (and not just date)
Updated ParseDate to use DateTime.TryParseExact with a specific format handling ' UTC' suffix
2025-12-17 19:43:39 +01:00
Fenopy
9ef3c9264e Merge pull request #272 from jp64k/RSL-3.0-2
Fixed UI lag during progress updates, unified DLS+ETA progress labels and implemented ETA tracking for extraction, installation, and copy operations
2025-12-17 06:11:27 -06:00
jp64k
12c371da84 Fixed gallery view placeholder text alignment for missing thumbnails
Adjusted the rectangle used for placeholder text when a thumbnail is missing to use a stable, non-scaled base thumbnail size to ensure consistent text layout regardless of thumbnail scaling
2025-12-17 05:13:10 +01:00
jp64k
782ead1c1e Remove InstalledVersion column, now showing it unified in single version tab
Removed the InstalledVersion column from the ListView and its designer references. Installed version information is now appended to the Version column text "Version (vs Installed)" when installed, improving clarity and reducing column clutter.
2025-12-17 05:07:08 +01:00
jp64k
e9f77449f0 Added 'Installed Version' column to games list
Introduces a 'Installed' column to the games ListView, displaying the installed version for each game. Updates sorting logic to handle the new column numerically and defines the corresponding index in RCLONE.cs.
2025-12-17 04:51:57 +01:00
jp64k
fd77c4db8b Reworked tile design in game gallery
For a cleaner UI and to allow larger thumbnails, game names are now shown on hover, except for tiles that don't have thumbnails. Adjusted thumbnail padding and corner radius to further increase thumbnail size and keep rounding consistent. Refined positioning of the delete button to align with the new tile layout
2025-12-17 04:18:36 +01:00
jp64k
d9a8d1c460 Fixed trailer player regression
For some reason this fixes trailers sometimes not playing correctly
2025-12-17 02:53:57 +01:00
jp64k
c5b151471a Refactored progress bar to show fractional percentages and improved ETA smoothing
Progress values and callbacks (labels, progressbar) now use float instead of int to reflect fractional percentages for a better / more responsive user experience. Improved ETA display for APK install, OBB copy and ZIP extraction operations by introducing a reusable EtaEstimator class for smoother and more accurate ETA calculations
2025-12-17 02:20:40 +01:00
jp64k
2f843bc458 Refactored game list refresh logic to use existing RefreshGameListAsync() method
Replaced manual list view initialization and checks with a call to the existing RefreshGameListAsync() method, simplifying the refresh logic and ensuring that the game list is properly refreshed when a filter is active and a game is updated (e.g., when installing an already installed game (updating) while in 'update available' view, the game status now correctly changes to reflect that it has been updated)
2025-12-17 00:31:25 +01:00
jp64k
f528520024 Simplified extraction and installation progress labels in inner progress bar
- Removed extraction filenames from progress status label in inner progress bar to keep it clean and simple, and to avoid confusion with OBB file copy operations
- Removed redundant ETA text for install progress in inner progress bar to match the style of other progress labels
- Changed the trailer player.html directory from 'webroot' to 'trailer', shortened YouTube player initialization script, added fs: 0 to hide unnecessary fullscreen toggle, iv_load_policy: 3 to hide video annotations
2025-12-17 00:05:03 +01:00
jp64k
a92d4c0267 Unified DLS+ETA progress labels and implemented ETA tracking for extraction, installation, and copy operations
- 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
2025-12-16 22:50:55 +01:00
jp64k
acaea1d243 Fixed UI lag during progress updates
Added throttling to progress and status callbacks during APK installation and OBB copy operations. UI updates are now limited to at most once every 100 ms or when the progress percentage changes, preventing excessive UI thread updates and associated lag.
2025-12-16 21:10:53 +01:00
Maxine
2b770b30a8 Revert "Restructure and project file organization"
This reverts commit 0edc1deed0.
2025-09-14 08:56:11 -07:00
Maxine
e6d178cb2a Revert "Minor MainForm field cleanup"
This reverts commit 630fe98d31.
2025-09-14 08:56:11 -07:00
Maxine
c2f5b20e83 Revert "Minor Main file organization"
This reverts commit bd193c0d3f.
2025-09-14 08:56:11 -07:00
Maxine
952251be35 Revert "Renamed 'ADB' to 'AdbManager'"
This reverts commit 252955da0a.
2025-09-14 08:56:11 -07:00
Maxine
385fe45d5d Revert "Fix WebView2 'too many automatic redirections' error"
This reverts commit 06dc91f130.
2025-09-14 08:56:11 -07:00
Maxine
5db0ea301d Revert "Fix unmoved line"
This reverts commit 523efba81c.
2025-09-14 08:56:11 -07:00
Sombody101
523efba81c Fix unmoved line 2025-09-14 08:43:59 -07:00
Sombody101
06dc91f130 Fix WebView2 'too many automatic redirections' error 2025-09-14 08:43:59 -07:00
Sombody101
252955da0a Renamed 'ADB' to 'AdbManager' 2025-09-14 08:43:59 -07:00
Sombody101
bd193c0d3f Minor Main file organization 2025-09-14 08:43:59 -07:00
Sombody101
630fe98d31 Minor MainForm field cleanup 2025-09-14 08:43:59 -07:00
Sombody101
0edc1deed0 Restructure and project file organization 2025-09-14 08:43:59 -07:00
44 changed files with 6860 additions and 43301 deletions

383
ADB.cs
View File

@@ -72,7 +72,7 @@ namespace AndroidSideloader
return _currentDevice;
}
public static ProcessOutput RunAdbCommandToString(string command)
public static ProcessOutput RunAdbCommandToString(string command, bool suppressLogging = false)
{
command = command.Replace("adb", "");
@@ -85,7 +85,7 @@ namespace AndroidSideloader
command = $" -s {DeviceID} {command}";
}
if (!command.Contains("dumpsys") && !command.Contains("shell pm list packages") && !command.Contains("KEYCODE_WAKEUP"))
if (!suppressLogging && !command.Contains("dumpsys") && !command.Contains("shell pm list packages") && !command.Contains("KEYCODE_WAKEUP"))
{
string logcmd = command;
if (logcmd.Contains(Environment.CurrentDirectory))
@@ -95,6 +95,9 @@ namespace AndroidSideloader
_ = Logger.Log($"Running command: {logcmd}");
}
bool isConnectCommand = command.Contains("connect");
int timeoutMs = isConnectCommand ? 5000 : -1; // 5 second timeout for connect commands
using (Process adb = new Process())
{
adb.StartInfo.FileName = adbFilePath;
@@ -111,19 +114,39 @@ namespace AndroidSideloader
try
{
output = adb.StandardOutput.ReadToEnd();
error = adb.StandardError.ReadToEnd();
}
catch { }
if (command.Contains("connect"))
{
bool graceful = adb.WaitForExit(3000);
if (!graceful)
if (isConnectCommand)
{
adb.Kill();
adb.WaitForExit();
// For connect commands, we use async reading with timeout to avoid blocking on TCP timeout
var outputTask = adb.StandardOutput.ReadToEndAsync();
var errorTask = adb.StandardError.ReadToEndAsync();
bool exited = adb.WaitForExit(timeoutMs);
if (!exited)
{
try { adb.Kill(); } catch { }
adb.WaitForExit(1000);
output = "Connection timed out";
error = "cannot connect: Connection timed out";
Logger.Log($"ADB connect command timed out after {timeoutMs}ms", LogLevel.WARNING);
}
else
{
// Process exited within timeout, safe to read output
output = outputTask.Result;
error = errorTask.Result;
}
}
else
{
// For non-connect commands, read output normally
output = adb.StandardOutput.ReadToEnd();
error = adb.StandardError.ReadToEnd();
}
}
catch (Exception ex)
{
Logger.Log($"Error reading ADB output: {ex.Message}", LogLevel.WARNING);
}
if (error.Contains("ADB_VENDOR_KEYS") && !settings.AdbDebugWarned)
@@ -134,7 +157,7 @@ namespace AndroidSideloader
{
_ = FlexibleMessageBox.Show(Program.form, "There is not enough room on your device to install this package. Please clear AT LEAST 2x the amount of the app you are trying to install.");
}
if (!output.Contains("version") && !output.Contains("KEYCODE_WAKEUP") && !output.Contains("Filesystem") && !output.Contains("package:") && !output.Equals(null))
if (!suppressLogging && !output.Contains("version") && !output.Contains("KEYCODE_WAKEUP") && !output.Contains("Filesystem") && !output.Contains("package:") && !output.Equals(null))
{
_ = Logger.Log(output);
}
@@ -154,13 +177,13 @@ namespace AndroidSideloader
// Copies and installs an APK with real-time progress reporting using AdvancedSharpAdbClient
public static async Task<ProcessOutput> SideloadWithProgressAsync(
string path,
Action<int> progressCallback = null,
Action<float, TimeSpan?> progressCallback = null,
Action<string> statusCallback = null,
string packagename = "",
string gameName = "")
{
statusCallback?.Invoke("Installing APK...");
progressCallback?.Invoke(0);
progressCallback?.Invoke(0, null);
try
{
@@ -175,43 +198,87 @@ namespace AndroidSideloader
statusCallback?.Invoke("Installing APK...");
// Throttle UI updates to prevent lag
DateTime lastProgressUpdate = DateTime.MinValue;
float lastReportedPercent = -1;
const int ThrottleMs = 100; // Update UI every 100ms
// Shared ETA engine (percent-units)
var eta = new EtaEstimator(alpha: 0.05, reanchorThreshold: 0.20);
// Create install progress handler
Action<InstallProgressEventArgs> installProgress = (args) =>
{
// Map PackageInstallProgressState to percentage
int percent = 0;
float percent = 0;
string status = null;
TimeSpan? displayEta = null;
switch (args.State)
{
case PackageInstallProgressState.Preparing:
percent = 0;
statusCallback?.Invoke("Preparing...");
status = "Preparing...";
eta.Reset();
break;
case PackageInstallProgressState.Uploading:
percent = (int)Math.Round(args.UploadProgress);
statusCallback?.Invoke($"Installing · {args.UploadProgress:F0}%");
percent = (float)args.UploadProgress;
// Update ETA engine using percent as units (0..100)
if (percent > 0 && percent < 100)
{
eta.Update(totalUnits: 100, doneUnits: (long)Math.Round(percent));
displayEta = eta.GetDisplayEta();
}
else
{
displayEta = eta.GetDisplayEta();
}
status = $"Installing · {percent:0.0}%";
break;
case PackageInstallProgressState.Installing:
percent = 100;
statusCallback?.Invoke("Completing Installation...");
status = "Completing Installation...";
displayEta = null;
break;
case PackageInstallProgressState.Finished:
percent = 100;
statusCallback?.Invoke("");
status = "";
displayEta = null;
break;
default:
percent = 50;
percent = 100;
status = "";
displayEta = null;
break;
}
progressCallback?.Invoke(percent);
var updateNow = DateTime.UtcNow;
bool shouldUpdate = (updateNow - lastProgressUpdate).TotalMilliseconds >= ThrottleMs
|| Math.Abs(percent - lastReportedPercent) >= 0.1f
|| args.State != PackageInstallProgressState.Uploading;
if (shouldUpdate)
{
lastProgressUpdate = updateNow;
lastReportedPercent = percent;
// ETA goes back via progress callback (label); status remains percent-only string for inner bar
progressCallback?.Invoke(percent, displayEta);
if (status != null) statusCallback?.Invoke(status);
}
};
// Install the package with progress
await Task.Run(() =>
{
packageManager.InstallPackage(path, installProgress);
});
progressCallback?.Invoke(100);
progressCallback?.Invoke(100, null);
statusCallback?.Invoke("");
return new ProcessOutput($"{gameName}: Success\n");
@@ -220,19 +287,38 @@ namespace AndroidSideloader
{
Logger.Log($"SideloadWithProgressAsync error: {ex.Message}", LogLevel.ERROR);
// Check for signature mismatch errors
if (ex.Message.Contains("INSTALL_FAILED") ||
ex.Message.Contains("signatures do not match"))
// Signature mismatches and version downgrades can be fixed by reinstalling
bool isReinstallEligible = ex.Message.Contains("signatures do not match") ||
ex.Message.Contains("INSTALL_FAILED_VERSION_DOWNGRADE") ||
ex.Message.Contains("failed to install");
// For insufficient storage, offer reinstall if it's an upgrade
// As uninstalling old version frees space for the new one
bool isStorageIssue = ex.Message.Contains("INSUFFICIENT_STORAGE");
bool isUpgrade = !string.IsNullOrEmpty(packagename) &&
settings.InstalledApps.Contains(packagename);
if (isStorageIssue && isUpgrade)
{
isReinstallEligible = true;
}
if (isReinstallEligible)
{
bool cancelClicked = false;
if (!settings.AutoReinstall)
{
string message = isStorageIssue
? "Installation failed due to insufficient storage. Since this is an upgrade, Rookie can uninstall the old version first to free up space, then install the new version.\n\nRookie will also attempt to backup your save data and reinstall the game automatically, however some games do not store their saves in an accessible location (less than 5%). Continue with reinstall?"
: "In place upgrade has failed. Rookie will attempt to backup your save data and reinstall the game automatically, however some games do not store their saves in an accessible location (less than 5%). Continue with reinstall?";
string title = isStorageIssue ? "Insufficient Storage" : "In place upgrade failed";
Program.form.Invoke(() =>
{
DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form,
"In place upgrade has failed. Rookie can attempt to backup your save data and reinstall the game automatically, however some games do not store their saves in an accessible location (less than 5%). Continue with reinstall?",
"In place upgrade failed.", MessageBoxButtons.OKCancel);
message, title, MessageBoxButtons.OKCancel);
if (dialogResult1 == DialogResult.Cancel)
cancelClicked = true;
});
@@ -241,7 +327,6 @@ namespace AndroidSideloader
if (cancelClicked)
return new ProcessOutput("", "Installation cancelled by user");
// Perform reinstall
statusCallback?.Invoke("Performing reinstall...");
try
@@ -250,52 +335,49 @@ namespace AndroidSideloader
var client = GetAdbClient();
var packageManager = new PackageManager(client, device);
// Backup save data
statusCallback?.Invoke("Backing up save data...");
_ = RunAdbCommandToString($"pull \"/sdcard/Android/data/{MainForm.CurrPCKG}\" \"{Environment.CurrentDirectory}\"");
_ = RunAdbCommandToString($"pull \"/sdcard/Android/data/{packagename}\" \"{Environment.CurrentDirectory}\"");
// Uninstall
statusCallback?.Invoke("Uninstalling old version...");
packageManager.UninstallPackage(packagename);
// Reinstall with progress
statusCallback?.Invoke("Reinstalling game...");
Action<InstallProgressEventArgs> reinstallProgress = (args) =>
{
if (args.State == PackageInstallProgressState.Uploading)
{
progressCallback?.Invoke((int)Math.Round(args.UploadProgress));
progressCallback?.Invoke((float)args.UploadProgress, null);
}
};
packageManager.InstallPackage(path, reinstallProgress);
// Restore save data
statusCallback?.Invoke("Restoring save data...");
_ = RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{MainForm.CurrPCKG}\" /sdcard/Android/data/");
_ = RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{packagename}\" /sdcard/Android/data/");
string directoryToDelete = Path.Combine(Environment.CurrentDirectory, MainForm.CurrPCKG);
string directoryToDelete = Path.Combine(Environment.CurrentDirectory, packagename);
if (Directory.Exists(directoryToDelete) && directoryToDelete != Environment.CurrentDirectory)
{
Directory.Delete(directoryToDelete, true);
FileSystemUtilities.TryDeleteDirectory(directoryToDelete);
}
progressCallback?.Invoke(100);
progressCallback?.Invoke(100, null);
return new ProcessOutput($"{gameName}: Reinstall: Success\n", "");
}
catch (Exception reinstallEx)
{
return new ProcessOutput($"{gameName}: Reinstall: Failed: {reinstallEx.Message}\n");
return new ProcessOutput("", $"{gameName}: Reinstall Failed: {reinstallEx.Message}\n");
}
}
return new ProcessOutput("", ex.Message);
// Return the error message so it's displayed to the user
return new ProcessOutput("", $"\n{gameName}: {ex.Message}");
}
}
// Copies OBB folder with real-time progress reporting using AdvancedSharpAdbClient
public static async Task<ProcessOutput> CopyOBBWithProgressAsync(
string localPath,
Action<int> progressCallback = null,
Action<float, TimeSpan?> progressCallback = null,
Action<string> statusCallback = null,
string gameName = "")
{
@@ -318,7 +400,7 @@ namespace AndroidSideloader
string remotePath = $"/sdcard/Android/obb/{folderName}";
statusCallback?.Invoke($"Preparing: {folderName}");
progressCallback?.Invoke(0);
progressCallback?.Invoke(0, null);
// Delete existing OBB folder and create new one
ExecuteShellCommand(client, device, $"rm -rf \"{remotePath}\"");
@@ -329,6 +411,14 @@ namespace AndroidSideloader
long totalBytes = files.Sum(f => new FileInfo(f).Length);
long transferredBytes = 0;
// Throttle UI updates to prevent lag
DateTime lastProgressUpdate = DateTime.MinValue;
float lastReportedPercent = -1;
const int ThrottleMs = 100; // Update UI every 100ms
// Shared ETA engine (bytes-units)
var eta = new EtaEstimator(alpha: 0.10, reanchorThreshold: 0.20);
statusCallback?.Invoke($"Copying: {folderName}");
using (var syncService = new SyncService(client, device))
@@ -341,9 +431,6 @@ namespace AndroidSideloader
string remoteFilePath = $"{remotePath}/{relativePath}";
string fileName = Path.GetFileName(file);
// Let UI know which file we're currently on
statusCallback?.Invoke(fileName);
// Ensure remote directory exists
string remoteDir = remoteFilePath.Substring(0, remoteFilePath.LastIndexOf('/'));
ExecuteShellCommand(client, device, $"mkdir -p \"{remoteDir}\"");
@@ -352,23 +439,37 @@ namespace AndroidSideloader
long fileSize = fileInfo.Length;
long capturedTransferredBytes = transferredBytes;
// Progress handler for this file
Action<SyncProgressChangedEventArgs> progressHandler = (args) =>
{
long totalProgressBytes = capturedTransferredBytes + args.ReceivedBytesSize;
double overallPercent = totalBytes > 0
? (totalProgressBytes * 100.0) / totalBytes
: 0.0;
float overallPercent = totalBytes > 0
? (float)(totalProgressBytes * 100.0 / totalBytes)
: 0f;
int overallPercentInt = (int)Math.Round(overallPercent);
overallPercentInt = Math.Max(0, Math.Min(100, overallPercentInt));
overallPercent = Math.Max(0, Math.Min(100, overallPercent));
// Single source of truth for UI (bar + label + text)
progressCallback?.Invoke(overallPercentInt);
// Update ETA engine in bytes
if (totalBytes > 0 && totalProgressBytes > 0 && overallPercent < 100)
{
eta.Update(totalUnits: totalBytes, doneUnits: totalProgressBytes);
}
TimeSpan? displayEta = eta.GetDisplayEta();
var now2 = DateTime.UtcNow;
bool shouldUpdate = (now2 - lastProgressUpdate).TotalMilliseconds >= ThrottleMs
|| Math.Abs(overallPercent - lastReportedPercent) >= 0.1f;
if (shouldUpdate)
{
lastProgressUpdate = now2;
lastReportedPercent = overallPercent;
progressCallback?.Invoke(overallPercent, displayEta);
statusCallback?.Invoke(fileName);
}
};
// Push the file with progress
using (var stream = File.OpenRead(file))
{
await Task.Run(() =>
@@ -383,13 +484,11 @@ namespace AndroidSideloader
});
}
// Mark this file as fully transferred
transferredBytes += fileSize;
}
}
// Ensure final 100% and clear status
progressCallback?.Invoke(100);
progressCallback?.Invoke(100, null);
statusCallback?.Invoke("");
return new ProcessOutput($"{gameName}: OBB transfer: Success\n", "");
@@ -397,7 +496,6 @@ namespace AndroidSideloader
catch (Exception ex)
{
Logger.Log($"CopyOBBWithProgressAsync error: {ex.Message}", LogLevel.ERROR);
return new ProcessOutput("", $"{gameName}: OBB transfer: Failed: {ex.Message}\n");
}
}
@@ -582,78 +680,103 @@ namespace AndroidSideloader
return $"Total space: {string.Format("{0:0.00}", (double)totalSize / 1000)}GB\nUsed space: {string.Format("{0:0.00}", (double)usedSize / 1000)}GB\nFree space: {string.Format("{0:0.00}", (double)freeSize / 1000)}GB";
}
}
public static ProcessOutput Sideload(string path, string packagename = "")
internal class EtaEstimator
{
private readonly double _alpha; // EWMA smoothing
private readonly double _reanchorThreshold; // % difference required to re-anchor
private readonly double _minSampleSeconds; // ignore too-short dt
private DateTime _lastSampleTimeUtc;
private long _lastSampleDoneUnits;
private double _smoothedUnitsPerSecond;
private TimeSpan? _etaAnchorValue;
private DateTime _etaAnchorTimeUtc;
public EtaEstimator(double alpha, double reanchorThreshold, double minSampleSeconds = 0.15)
{
ProcessOutput ret = new ProcessOutput();
ret += RunAdbCommandToString($"install -g \"{path}\"");
string out2 = ret.Output + ret.Error;
if (out2.Contains("failed"))
{
_ = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
_ = Logger.Log(out2);
if (out2.Contains("offline") && !settings.NodeviceMode)
{
DialogResult dialogResult2 = FlexibleMessageBox.Show(Program.form, "Device is offline. Press Yes to reconnect, or if you don't wish to connect and just want to download the game (requires unchecking \"Delete games after install\" from settings menu) then press No.", "Device offline.", MessageBoxButtons.YesNoCancel);
}
if (out2.Contains($"signatures do not match previously") || out2.Contains("INSTALL_FAILED_VERSION_DOWNGRADE") || out2.Contains("signatures do not match") || out2.Contains("failed to install"))
{
ret.Error = string.Empty;
ret.Output = string.Empty;
bool cancelClicked = false;
if (!settings.AutoReinstall)
{
Program.form.Invoke((MethodInvoker)(() =>
{
DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, "In place upgrade has failed. Rookie can attempt to backup your save data and reinstall the game automatically, however some games do not store their saves in an accessible location (less than 5%). Continue with reinstall?", "In place upgrade failed.", MessageBoxButtons.OKCancel);
if (dialogResult1 == DialogResult.Cancel)
cancelClicked = true;
}));
}
if (cancelClicked)
return ret;
Program.form.changeTitle("Performing reinstall, please wait...");
_ = RunAdbCommandToString("kill-server");
_ = RunAdbCommandToString("devices");
_ = RunAdbCommandToString($"pull \"/sdcard/Android/data/{MainForm.CurrPCKG}\" \"{Environment.CurrentDirectory}\"");
Program.form.changeTitle("Uninstalling game...");
_ = Sideloader.UninstallGame(MainForm.CurrPCKG);
Program.form.changeTitle("Reinstalling game...");
ret += RunAdbCommandToString($"install -g \"{path}\"");
_ = RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{MainForm.CurrPCKG}\" /sdcard/Android/data/");
string directoryToDelete = Path.Combine(Environment.CurrentDirectory, MainForm.CurrPCKG);
if (Directory.Exists(directoryToDelete))
{
if (directoryToDelete != Environment.CurrentDirectory)
{
Directory.Delete(directoryToDelete, true);
}
}
Program.form.changeTitle("");
return ret;
}
}
Program.form.changeTitle("");
return ret;
_alpha = alpha;
_reanchorThreshold = reanchorThreshold;
_minSampleSeconds = minSampleSeconds;
Reset();
}
public static ProcessOutput CopyOBB(string path)
public void Reset()
{
string folder = Path.GetFileName(path);
string lastFolder = Path.GetFileName(path);
return folder.Contains(".")
? RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{lastFolder}\" && mkdir \"/sdcard/Android/obb/{lastFolder}\"") + RunAdbCommandToString($"push \"{path}\" \"/sdcard/Android/obb\"")
: new ProcessOutput("No OBB Folder found");
_lastSampleTimeUtc = DateTime.UtcNow;
_lastSampleDoneUnits = 0;
_smoothedUnitsPerSecond = 0;
_etaAnchorValue = null;
_etaAnchorTimeUtc = DateTime.UtcNow;
}
// Updates internal rate estimate and re-anchors ETA
// totalUnits: total work units (e.g., 100 for percent, or totalBytes for bytes)
// doneUnits: completed work units so far (e.g., percent, or bytes transferred)
public void Update(long totalUnits, long doneUnits)
{
var now = DateTime.UtcNow;
if (totalUnits <= 0) return;
doneUnits = Math.Max(0, Math.Min(totalUnits, doneUnits));
long remainingUnits = Math.Max(0, totalUnits - doneUnits);
double dt = (now - _lastSampleTimeUtc).TotalSeconds;
long dUnits = doneUnits - _lastSampleDoneUnits;
if (dt >= _minSampleSeconds && dUnits > 0)
{
double instUnitsPerSecond = dUnits / dt;
if (_smoothedUnitsPerSecond <= 0)
_smoothedUnitsPerSecond = instUnitsPerSecond;
else
_smoothedUnitsPerSecond = _alpha * instUnitsPerSecond + (1 - _alpha) * _smoothedUnitsPerSecond;
_lastSampleTimeUtc = now;
_lastSampleDoneUnits = doneUnits;
}
if (_smoothedUnitsPerSecond > 1e-6 && remainingUnits > 0)
{
var newEta = TimeSpan.FromSeconds(remainingUnits / _smoothedUnitsPerSecond);
if (newEta < TimeSpan.Zero) newEta = TimeSpan.Zero;
if (!_etaAnchorValue.HasValue)
{
_etaAnchorValue = newEta;
_etaAnchorTimeUtc = now;
}
else
{
// What countdown would currently show
var predictedNow = _etaAnchorValue.Value - (now - _etaAnchorTimeUtc);
if (predictedNow < TimeSpan.Zero) predictedNow = TimeSpan.Zero;
double baseSeconds = Math.Max(1, predictedNow.TotalSeconds);
double diffRatio = Math.Abs(newEta.TotalSeconds - predictedNow.TotalSeconds) / baseSeconds;
if (diffRatio > _reanchorThreshold)
{
_etaAnchorValue = newEta;
_etaAnchorTimeUtc = now;
}
}
}
}
// Returns a countdown ETA for UI display
public TimeSpan? GetDisplayEta()
{
if (!_etaAnchorValue.HasValue) return null;
var remaining = _etaAnchorValue.Value - (DateTime.UtcNow - _etaAnchorTimeUtc);
if (remaining < TimeSpan.Zero) remaining = TimeSpan.Zero;
return TimeSpan.FromSeconds(Math.Ceiling(remaining.TotalSeconds));
}
}
}

View File

@@ -13,6 +13,10 @@ namespace AndroidSideloader
public AdbCommandForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
this.ShowIcon = true; // Enable icon
}
private void InitializeComponent()

View File

@@ -190,9 +190,13 @@
<Compile Include="GalleryView.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="ModernListView.cs" />
<Compile Include="ModernProgessBar.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="ModernQueuePanel.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
@@ -240,6 +244,7 @@
<Compile Include="Sideloader\RCLONE.cs" />
<Compile Include="Sideloader\Utilities.cs" />
<Compile Include="Utilities\DnsHelper.cs" />
<Compile Include="Utilities\FileSystemUtilities.cs" />
<Compile Include="Utilities\Logger.cs" />
<Compile Include="QuestForm.cs">
<SubType>Form</SubType>

View File

@@ -200,6 +200,9 @@
<setting name="proxyPort" serializeAs="String">
<value />
</setting>
<setting name="selectedMirror" serializeAs="String">
<value />
</setting>
</AndroidSideloader.Properties.Settings>
<AndroidADB.Sideloader.Properties.Settings>
<setting name="checkForUpdates" serializeAs="String">

View File

@@ -1,4 +1,21 @@
RSL 2.34
RSL 3.0
Major Rookie overhaul with modernized UI, significant performance improvements and upgraded UX.
- Added high-performance Gallery View with search, filters, sorting, favorites, hover animations, smooth scrolling and uninstall buttons
- Toggle seamlessly between List and Gallery views with your preference remembered across launches
- Complete UI redesign with new dark theme, modernized components and subtle animations throughout
- Refined navigation, layouts, sizing and color consistency across the entire application
- Added uninstall buttons directly in List and Gallery views for quicker app management
- Improved startup performance through overhaul of initialization logic, removal of splash screen, parallelized async loading, batched version retrieval, optimized metadata extraction and game list initialization
- Instant list filtering through caching and streamlined filter logic (INSTALLED / UPDATE AVAILABLE / NEWER THAN LIST)
- Improved search speed and responsiveness
- Fixed and improved trailer handling with faster trailer loading
- Fixed multiple startup issues including connection errors and zombie ADB instances
- Added local blacklist support allowing users to permanently suppress donation prompts for specific apps
- Reduced application size by removal of now unused assets
RSL 2.34
- Feature: Allow users to favorite games (right click on game)
- Fix: Release Notes not showing with trailers enabled

View File

@@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
using System.Windows.Forms;
/// <summary>
@@ -41,23 +42,41 @@ public class ListViewColumnSorter : IComparer
ListViewItem listviewX = (ListViewItem)x;
ListViewItem listviewY = (ListViewItem)y;
// Determine if the column requires numeric comparison
if (SortColumn == 3 || SortColumn == 5) // Numeric columns: VersionCodeIndex, VersionNameIndex
// Special handling for column 6 (Popularity ranking)
if (SortColumn == 6)
{
try
{
// Parse and compare numeric values directly
int xNum = ParseNumber(listviewX.SubItems[SortColumn].Text);
int yNum = ParseNumber(listviewY.SubItems[SortColumn].Text);
string textX = listviewX.SubItems[SortColumn].Text;
string textY = listviewY.SubItems[SortColumn].Text;
// Compare numerically
compareResult = xNum.CompareTo(yNum);
}
catch
// Extract numeric values from "#1", "#10", etc.
// "-" represents unranked and should go to the end
int rankX = int.MaxValue; // Default for unranked (-)
int rankY = int.MaxValue;
if (textX.StartsWith("#") && int.TryParse(textX.Substring(1), out int parsedX))
{
// Fallback to string comparison if parsing fails
compareResult = ObjectCompare.Compare(listviewX.SubItems[SortColumn].Text, listviewY.SubItems[SortColumn].Text);
rankX = parsedX;
}
if (textY.StartsWith("#") && int.TryParse(textY.Substring(1), out int parsedY))
{
rankY = parsedY;
}
// Compare the numeric ranks
compareResult = rankX.CompareTo(rankY);
}
// Special handling for column 5 (Size)
else if (SortColumn == 5)
{
string textX = listviewX.SubItems[SortColumn].Text;
string textY = listviewY.SubItems[SortColumn].Text;
double sizeX = ParseSize(textX);
double sizeY = ParseSize(textY);
// Compare the numeric sizes
compareResult = sizeX.CompareTo(sizeY);
}
else
{
@@ -91,6 +110,49 @@ public class ListViewColumnSorter : IComparer
return int.TryParse(text, out int result) ? result : 0;
}
/// <summary>
/// Parses size strings with units (GB/MB) and converts them to MB for comparison.
/// </summary>
/// <param name="sizeStr">Size string (e.g., "1.23 GB", "123 MB")</param>
/// <returns>Size in MB as a double</returns>
private double ParseSize(string sizeStr)
{
if (string.IsNullOrEmpty(sizeStr))
return 0;
// Remove whitespace
sizeStr = sizeStr.Trim();
// Handle new format: "1.23 GB" or "123 MB"
if (sizeStr.EndsWith(" GB", StringComparison.OrdinalIgnoreCase))
{
string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim();
if (double.TryParse(numPart, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double gb))
{
return gb * 1024.0; // Convert GB to MB for consistent sorting
}
}
else if (sizeStr.EndsWith(" MB", StringComparison.OrdinalIgnoreCase))
{
string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim();
if (double.TryParse(numPart, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double mb))
{
return mb;
}
}
// Fallback: try parsing as raw number
if (double.TryParse(sizeStr, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double rawMb))
{
return rawMb;
}
return 0;
}
/// <summary>
/// Gets or sets the index of the column to be sorted (default is '0').
/// </summary>

View File

@@ -30,7 +30,6 @@ namespace AndroidSideloader
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DonorsListViewForm));
this.DonationTimer = new System.Windows.Forms.Timer(this.components);
this.panel1 = new System.Windows.Forms.Panel();
this.skip_forever = new AndroidSideloader.RoundButton();
@@ -310,7 +309,6 @@ namespace AndroidSideloader
this.Controls.Add(this.panel1);
this.ForeColor = System.Drawing.Color.White;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "DonorsListViewForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Load += new System.EventHandler(this.DonorsListViewForm_Load);

View File

@@ -42,6 +42,10 @@ namespace AndroidSideloader
public DonorsListViewForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
ApplyModernTheme();
CenterToScreen();

File diff suppressed because it is too large Load Diff

View File

@@ -83,7 +83,18 @@ namespace JR.Utils.GUI.Forms
private const int CS_DROPSHADOW = 0x00020000;
private const int WM_NCLBUTTONDOWN = 0xA1;
private const int HT_CAPTION = 0x2;
private const int BORDER_RADIUS = 12;
private const uint FLASHW_TRAY = 0x00000002;
private const uint FLASHW_TIMERNOFG = 0x0000000C;
[StructLayout(LayoutKind.Sequential)]
private struct FLASHWINFO
{
public uint cbSize;
public IntPtr hwnd;
public uint dwFlags;
public uint uCount;
public uint dwTimeout;
}
[DllImport("user32.dll")]
private static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
@@ -91,6 +102,9 @@ namespace JR.Utils.GUI.Forms
[DllImport("user32.dll")]
private static extern bool ReleaseCapture();
[DllImport("user32.dll")]
private static extern bool FlashWindowEx(ref FLASHWINFO pwfi);
protected override CreateParams CreateParams
{
get
@@ -170,7 +184,7 @@ namespace JR.Utils.GUI.Forms
titleLabel.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
titleLabel.Location = new System.Drawing.Point(0, 0);
titleLabel.Name = "titleLabel";
titleLabel.Padding = new System.Windows.Forms.Padding(18, 0, 0, 0);
titleLabel.Padding = new System.Windows.Forms.Padding(12, 0, 0, 0);
titleLabel.Size = new System.Drawing.Size(218, 28);
titleLabel.TabIndex = 0;
titleLabel.Text = "<Caption>";
@@ -198,7 +212,7 @@ namespace JR.Utils.GUI.Forms
//
button1.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
button1.DialogResult = System.Windows.Forms.DialogResult.OK;
button1.Location = new System.Drawing.Point(16, 80);
button1.Location = new System.Drawing.Point(26, 80);
button1.Name = "button1";
button1.Size = new System.Drawing.Size(75, 28);
button1.TabIndex = 2;
@@ -222,7 +236,7 @@ namespace JR.Utils.GUI.Forms
richTextBoxMessage.BorderStyle = System.Windows.Forms.BorderStyle.None;
richTextBoxMessage.DataBindings.Add(new System.Windows.Forms.Binding("Text", FlexibleMessageBoxFormBindingSource, "MessageText", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged));
richTextBoxMessage.Font = new System.Drawing.Font("Microsoft Sans Serif", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
richTextBoxMessage.Location = new System.Drawing.Point(52, 6);
richTextBoxMessage.Location = new System.Drawing.Point(46, 6);
richTextBoxMessage.Margin = new System.Windows.Forms.Padding(0);
richTextBoxMessage.Name = "richTextBoxMessage";
richTextBoxMessage.ReadOnly = true;
@@ -259,7 +273,7 @@ namespace JR.Utils.GUI.Forms
//
button2.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
button2.DialogResult = System.Windows.Forms.DialogResult.OK;
button2.Location = new System.Drawing.Point(97, 80);
button2.Location = new System.Drawing.Point(107, 80);
button2.Name = "button2";
button2.Size = new System.Drawing.Size(75, 28);
button2.TabIndex = 3;
@@ -277,7 +291,7 @@ namespace JR.Utils.GUI.Forms
//
button3.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
button3.DialogResult = System.Windows.Forms.DialogResult.OK;
button3.Location = new System.Drawing.Point(178, 80);
button3.Location = new System.Drawing.Point(188, 80);
button3.Name = "button3";
button3.Size = new System.Drawing.Size(75, 28);
button3.TabIndex = 0;
@@ -604,7 +618,7 @@ namespace JR.Utils.GUI.Forms
ownerForm = Form.ActiveForm;
}
if (ownerForm != null && ownerForm.Visible)
if (ownerForm != null && ownerForm.Visible && ownerForm.WindowState != FormWindowState.Minimized)
{
// Center relative to owner window
int x = ownerForm.Left + (ownerForm.Width - flexibleMessageBoxForm.Width) / 2;
@@ -620,7 +634,7 @@ namespace JR.Utils.GUI.Forms
}
else
{
// No owner found: center on current screen
// No owner found or minimized: center on current screen
CenterOnScreen(flexibleMessageBoxForm);
}
}
@@ -843,8 +857,50 @@ namespace JR.Utils.GUI.Forms
flexibleMessageBoxForm.richTextBoxMessage.Font = FONT;
SetDialogSizes(flexibleMessageBoxForm, text, caption);
// Force panel resize to fix closebutton position
int contentWidth = flexibleMessageBoxForm.ClientSize.Width - 16; // 8px padding
flexibleMessageBoxForm.titlePanel.Width = contentWidth;
// Get owner form
Form ownerForm = owner as Form ?? Form.ActiveForm;
bool ownerWasMinimized = ownerForm != null && ownerForm.WindowState == FormWindowState.Minimized;
SetDialogStartPosition(flexibleMessageBoxForm, owner);
// If owner was minimized, reposition dialog when owner is restored
if (ownerWasMinimized && ownerForm != null)
{
EventHandler resizeHandler = null;
resizeHandler = (s, e) =>
{
if (ownerForm.WindowState != FormWindowState.Minimized && flexibleMessageBoxForm.Visible && !flexibleMessageBoxForm.IsDisposed)
{
SetDialogStartPosition(flexibleMessageBoxForm, owner);
ownerForm.Resize -= resizeHandler;
}
};
ownerForm.Resize += resizeHandler;
}
// Flash taskbar if application is inactive or minimized
if (Form.ActiveForm == null || ownerWasMinimized)
{
Form targetForm = ownerForm ?? Application.OpenForms.Cast<Form>().FirstOrDefault(f => f != null && !f.IsDisposed && f != flexibleMessageBoxForm && f.Visible);
if (targetForm != null && !targetForm.IsDisposed)
{
FLASHWINFO info = new FLASHWINFO
{
cbSize = (uint)Marshal.SizeOf(typeof(FLASHWINFO)),
hwnd = targetForm.Handle,
dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG,
uCount = 5,
dwTimeout = 0
};
_ = FlashWindowEx(ref info);
}
}
return flexibleMessageBoxForm.ShowDialog(owner);
}

File diff suppressed because it is too large Load Diff

324
MainForm.Designer.cs generated
View File

@@ -34,9 +34,7 @@ namespace AndroidSideloader
{
this.components = new System.ComponentModel.Container();
this.m_combo = new SergeUtils.EasyCompletionComboBox();
this.progressBar = new AndroidSideloader.ModernProgressBar();
this.speedLabel = new System.Windows.Forms.Label();
this.etaLabel = new System.Windows.Forms.Label();
this.freeDisclaimer = new System.Windows.Forms.Label();
this.gamesQueListBox = new System.Windows.Forms.ListBox();
this.devicesComboBox = new System.Windows.Forms.ComboBox();
@@ -51,7 +49,6 @@ namespace AndroidSideloader
this.DownloadsIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.gamesQueueLabel = new System.Windows.Forms.Label();
this.notesRichTextBox = new System.Windows.Forms.RichTextBox();
this.DragDropLbl = new System.Windows.Forms.Label();
this.lblNotes = new System.Windows.Forms.Label();
this.gamesPictureBox = new System.Windows.Forms.PictureBox();
this.startsideloadbutton_Tooltip = new System.Windows.Forms.ToolTip(this.components);
@@ -121,9 +118,14 @@ namespace AndroidSideloader
this.deviceIdLabel = new System.Windows.Forms.Label();
this.rookieStatusLabel = new System.Windows.Forms.Label();
this.sidebarMediaPanel = new System.Windows.Forms.Panel();
this.downloadInstallGameButton = new AndroidSideloader.RoundButton();
this.selectedGameLabel = new System.Windows.Forms.Label();
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
this.webView21 = new Microsoft.Web.WebView2.WinForms.WebView2();
this.favoriteGame = new System.Windows.Forms.ContextMenuStrip(this.components);
this.favoriteButton = new System.Windows.Forms.ToolStripMenuItem();
this.gamesGalleryView = new System.Windows.Forms.FlowLayoutPanel();
this.btnViewToggle_Tooltip = new System.Windows.Forms.ToolTip(this.components);
this.webViewPlaceholderPanel = new System.Windows.Forms.Panel();
this.searchPanel = new AndroidSideloader.RoundButton();
this.searchIconPictureBox = new System.Windows.Forms.PictureBox();
this.searchTextBox = new System.Windows.Forms.TextBox();
@@ -132,12 +134,8 @@ namespace AndroidSideloader
this.btnInstalled = new AndroidSideloader.RoundButton();
this.btnUpdateAvailable = new AndroidSideloader.RoundButton();
this.btnNewerThanList = new AndroidSideloader.RoundButton();
this.webView21 = new Microsoft.Web.WebView2.WinForms.WebView2();
this.favoriteGame = new System.Windows.Forms.ContextMenuStrip(this.components);
this.favoriteButton = new System.Windows.Forms.ToolStripMenuItem();
this.gamesGalleryView = new System.Windows.Forms.FlowLayoutPanel();
this.btnViewToggle_Tooltip = new System.Windows.Forms.ToolTip(this.components);
this.webViewPlaceholderPanel = new System.Windows.Forms.Panel();
this.progressBar = new AndroidSideloader.ModernProgressBar();
this.downloadInstallGameButton = new AndroidSideloader.RoundButton();
((System.ComponentModel.ISupportInitialize)(this.gamesPictureBox)).BeginInit();
this.gamesPictureBox.SuspendLayout();
this.progressDLbtnContainer.SuspendLayout();
@@ -153,10 +151,10 @@ namespace AndroidSideloader
this.statusInfoPanel.SuspendLayout();
this.sidebarMediaPanel.SuspendLayout();
this.tableLayoutPanel1.SuspendLayout();
this.searchPanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.webView21)).BeginInit();
this.favoriteGame.SuspendLayout();
this.searchPanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).BeginInit();
this.SuspendLayout();
//
// m_combo
@@ -167,65 +165,24 @@ namespace AndroidSideloader
this.m_combo.ForeColor = global::AndroidSideloader.Properties.Settings.Default.FontColor;
this.m_combo.Location = new System.Drawing.Point(253, 9);
this.m_combo.Name = "m_combo";
this.m_combo.Size = new System.Drawing.Size(374, 25);
this.m_combo.Size = new System.Drawing.Size(374, 24);
this.m_combo.TabIndex = 0;
this.m_combo.Text = "Select an Installed App...";
this.m_combo.Visible = false;
//
// progressBar
//
this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.progressBar.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(35)))), ((int)(((byte)(45)))));
this.progressBar.BackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(28)))), ((int)(((byte)(32)))), ((int)(((byte)(38)))));
this.progressBar.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
this.progressBar.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
this.progressBar.IndeterminateColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
this.progressBar.IsIndeterminate = false;
this.progressBar.Location = new System.Drawing.Point(1, 18);
this.progressBar.Maximum = 100;
this.progressBar.Minimum = 0;
this.progressBar.MinimumSize = new System.Drawing.Size(200, 13);
this.progressBar.Name = "progressBar";
this.progressBar.OperationType = "";
this.progressBar.ProgressEndColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(160)))), ((int)(((byte)(130)))));
this.progressBar.ProgressStartColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(220)))), ((int)(((byte)(190)))));
this.progressBar.Radius = 6;
this.progressBar.Size = new System.Drawing.Size(983, 13);
this.progressBar.StatusText = "";
this.progressBar.TabIndex = 7;
this.progressBar.TextColor = System.Drawing.Color.FromArgb(((int)(((byte)(230)))), ((int)(((byte)(230)))), ((int)(((byte)(230)))));
this.progressBar.Value = 0;
//
// speedLabel
//
this.speedLabel.AutoSize = true;
this.speedLabel.BackColor = System.Drawing.Color.Transparent;
this.speedLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Bold);
this.speedLabel.ForeColor = System.Drawing.Color.White;
this.speedLabel.Location = new System.Drawing.Point(-2, -3);
this.speedLabel.Location = new System.Drawing.Point(-1, 3);
this.speedLabel.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
this.speedLabel.Name = "speedLabel";
this.speedLabel.Size = new System.Drawing.Size(152, 16);
this.speedLabel.TabIndex = 76;
this.speedLabel.Text = "DLS: Speed in MBPS";
this.speedLabel_Tooltip.SetToolTip(this.speedLabel, "Current download speed, updates every second, in mbps");
//
// etaLabel
//
this.etaLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.etaLabel.BackColor = System.Drawing.Color.Transparent;
this.etaLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.etaLabel.ForeColor = System.Drawing.Color.White;
this.etaLabel.Location = new System.Drawing.Point(790, -3);
this.etaLabel.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
this.etaLabel.Name = "etaLabel";
this.etaLabel.Size = new System.Drawing.Size(196, 18);
this.etaLabel.TabIndex = 75;
this.etaLabel.Text = "ETA: HH:MM:SS Left";
this.etaLabel.TextAlign = System.Drawing.ContentAlignment.TopRight;
this.etaLabel_Tooltip.SetToolTip(this.etaLabel, "Estimated time when game will finish download, updates every 5 seconds, format is" +
" HH:MM:SS");
//
// freeDisclaimer
//
@@ -257,10 +214,6 @@ namespace AndroidSideloader
this.gamesQueListBox.Name = "gamesQueListBox";
this.gamesQueListBox.Size = new System.Drawing.Size(266, 192);
this.gamesQueListBox.TabIndex = 9;
this.gamesQueListBox.MouseClick += new System.Windows.Forms.MouseEventHandler(this.gamesQueListBox_MouseClick);
this.gamesQueListBox.DrawItem += new System.Windows.Forms.DrawItemEventHandler(this.gamesQueListBox_DrawItem);
this.gamesQueListBox.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop);
this.gamesQueListBox.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter);
//
// devicesComboBox
//
@@ -272,7 +225,7 @@ namespace AndroidSideloader
this.devicesComboBox.Location = new System.Drawing.Point(253, 39);
this.devicesComboBox.Margin = new System.Windows.Forms.Padding(2);
this.devicesComboBox.Name = "devicesComboBox";
this.devicesComboBox.Size = new System.Drawing.Size(164, 25);
this.devicesComboBox.Size = new System.Drawing.Size(164, 24);
this.devicesComboBox.TabIndex = 1;
this.devicesComboBox.Text = "Select your device";
this.devicesComboBox.Visible = false;
@@ -288,7 +241,7 @@ namespace AndroidSideloader
this.remotesList.Location = new System.Drawing.Point(567, 40);
this.remotesList.Margin = new System.Windows.Forms.Padding(2);
this.remotesList.Name = "remotesList";
this.remotesList.Size = new System.Drawing.Size(67, 25);
this.remotesList.Size = new System.Drawing.Size(67, 24);
this.remotesList.TabIndex = 3;
this.remotesList.Visible = false;
this.remotesList.SelectedIndexChanged += new System.EventHandler(this.remotesList_SelectedIndexChanged);
@@ -310,10 +263,12 @@ namespace AndroidSideloader
this.DownloadsIndex});
this.gamesListView.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.gamesListView.ForeColor = System.Drawing.Color.White;
this.gamesListView.FullRowSelect = true;
this.gamesListView.HideSelection = false;
this.gamesListView.ImeMode = System.Windows.Forms.ImeMode.Off;
this.gamesListView.Location = new System.Drawing.Point(258, 44);
this.gamesListView.Name = "gamesListView";
this.gamesListView.OwnerDraw = true;
this.gamesListView.ShowGroups = false;
this.gamesListView.Size = new System.Drawing.Size(984, 409);
this.gamesListView.TabIndex = 6;
@@ -329,39 +284,41 @@ namespace AndroidSideloader
// GameNameIndex
//
this.GameNameIndex.Text = "Game Name";
this.GameNameIndex.Width = 158;
this.GameNameIndex.Width = 160;
//
// ReleaseNameIndex
//
this.ReleaseNameIndex.Text = "Release Name";
this.ReleaseNameIndex.Width = 244;
this.ReleaseNameIndex.Width = 220;
//
// PackageNameIndex
//
this.PackageNameIndex.Text = "Package Name";
this.PackageNameIndex.Width = 87;
this.PackageNameIndex.Width = 120;
//
// VersionCodeIndex
//
this.VersionCodeIndex.Text = "Version";
this.VersionCodeIndex.Width = 75;
this.VersionCodeIndex.Text = "Version (Rookie/Local)";
this.VersionCodeIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.VersionCodeIndex.Width = 164;
//
// ReleaseAPKPathIndex
//
this.ReleaseAPKPathIndex.Text = "Last Updated";
this.ReleaseAPKPathIndex.Width = 145;
this.ReleaseAPKPathIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.ReleaseAPKPathIndex.Width = 135;
//
// VersionNameIndex
//
this.VersionNameIndex.Text = "Size (MB)";
this.VersionNameIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
this.VersionNameIndex.Width = 66;
this.VersionNameIndex.Text = "Size";
this.VersionNameIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.VersionNameIndex.Width = 85;
//
// DownloadsIndex
//
this.DownloadsIndex.Text = "Popularity";
this.DownloadsIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
this.DownloadsIndex.Width = 80;
this.DownloadsIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.DownloadsIndex.Width = 100;
//
// gamesQueueLabel
//
@@ -382,8 +339,8 @@ namespace AndroidSideloader
| System.Windows.Forms.AnchorStyles.Right)));
this.notesRichTextBox.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
this.notesRichTextBox.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.notesRichTextBox.Font = new System.Drawing.Font("Microsoft Sans Serif", 10F);
this.notesRichTextBox.ForeColor = System.Drawing.Color.White;
this.notesRichTextBox.Font = new System.Drawing.Font("Segoe UI", 8.5F, System.Drawing.FontStyle.Italic);
this.notesRichTextBox.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(140)))), ((int)(((byte)(140)))), ((int)(((byte)(140)))));
this.notesRichTextBox.HideSelection = false;
this.notesRichTextBox.Location = new System.Drawing.Point(954, 496);
this.notesRichTextBox.Name = "notesRichTextBox";
@@ -392,24 +349,11 @@ namespace AndroidSideloader
this.notesRichTextBox.ShowSelectionMargin = true;
this.notesRichTextBox.Size = new System.Drawing.Size(265, 192);
this.notesRichTextBox.TabIndex = 10;
this.notesRichTextBox.Text = "Tip: Press F1 to see all shortcuts";
this.notesRichTextBox.SelectionAlignment = System.Windows.Forms.HorizontalAlignment.Center;
this.notesRichTextBox.Text = "\n\n\n\n\nTip: Press F1 to see all shortcuts\n\nDrag and drop APKs or folders to install" +
"";
this.notesRichTextBox.LinkClicked += new System.Windows.Forms.LinkClickedEventHandler(this.notesRichTextBox_LinkClicked);
//
// DragDropLbl
//
this.DragDropLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.DragDropLbl.AutoSize = true;
this.DragDropLbl.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(20)))), ((int)(((byte)(20)))));
this.DragDropLbl.DataBindings.Add(new System.Windows.Forms.Binding("ForeColor", global::AndroidSideloader.Properties.Settings.Default, "FontColor", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged));
this.DragDropLbl.Font = new System.Drawing.Font("Microsoft Sans Serif", 36F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.DragDropLbl.ForeColor = global::AndroidSideloader.Properties.Settings.Default.FontColor;
this.DragDropLbl.Location = new System.Drawing.Point(620, 561);
this.DragDropLbl.Name = "DragDropLbl";
this.DragDropLbl.Size = new System.Drawing.Size(320, 55);
this.DragDropLbl.TabIndex = 25;
this.DragDropLbl.Text = "DragDropLBL";
this.DragDropLbl.Visible = false;
//
// lblNotes
//
this.lblNotes.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
@@ -514,7 +458,7 @@ namespace AndroidSideloader
this.backupadbbutton.Padding = new System.Windows.Forms.Padding(30, 0, 0, 0);
this.backupadbbutton.Size = new System.Drawing.Size(233, 28);
this.backupadbbutton.TabIndex = 1;
this.backupadbbutton.Text = "BACKUP WITH ADB";
this.backupadbbutton.Text = "BACKUP GAMESAVE WITH ADB";
this.backupadbbutton.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
this.backupadbbutton_Tooltip.SetToolTip(this.backupadbbutton, "Save game data via ADB-Backup");
this.backupadbbutton.UseVisualStyleBackColor = false;
@@ -534,9 +478,9 @@ namespace AndroidSideloader
this.backupbutton.Padding = new System.Windows.Forms.Padding(30, 0, 0, 0);
this.backupbutton.Size = new System.Drawing.Size(233, 28);
this.backupbutton.TabIndex = 1;
this.backupbutton.Text = "BACKUP GAMEDATA";
this.backupbutton.Text = "BACKUP ALL GAMESAVES";
this.backupbutton.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
this.backupbutton_Tooltip.SetToolTip(this.backupbutton, "Save game and apps data to the sideloader folder (Does not save APKs or OBBs)");
this.backupbutton_Tooltip.SetToolTip(this.backupbutton, "Save game and apps data to the backup folder (Does not save APKs or OBBs)");
this.backupbutton.UseVisualStyleBackColor = false;
this.backupbutton.Click += new System.EventHandler(this.backupbutton_Click);
//
@@ -554,9 +498,9 @@ namespace AndroidSideloader
this.restorebutton.Padding = new System.Windows.Forms.Padding(30, 0, 0, 0);
this.restorebutton.Size = new System.Drawing.Size(233, 28);
this.restorebutton.TabIndex = 0;
this.restorebutton.Text = "RESTORE GAMEDATA";
this.restorebutton.Text = "RESTORE GAMESAVES";
this.restorebutton.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
this.restorebutton_Tooltip.SetToolTip(this.restorebutton, "Restore game and apps data to the device (Use BACKUP GAMEDATA first)");
this.restorebutton_Tooltip.SetToolTip(this.restorebutton, "Restore game and apps data to the device");
this.restorebutton.UseVisualStyleBackColor = false;
this.restorebutton.Click += new System.EventHandler(this.restorebutton_Click);
//
@@ -808,12 +752,11 @@ namespace AndroidSideloader
this.progressDLbtnContainer.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.progressDLbtnContainer.BackColor = System.Drawing.Color.Transparent;
this.progressDLbtnContainer.Controls.Add(this.progressBar);
this.progressDLbtnContainer.Controls.Add(this.etaLabel);
this.progressDLbtnContainer.Controls.Add(this.speedLabel);
this.progressDLbtnContainer.Location = new System.Drawing.Point(258, 459);
this.progressDLbtnContainer.Location = new System.Drawing.Point(258, 453);
this.progressDLbtnContainer.MinimumSize = new System.Drawing.Size(600, 34);
this.progressDLbtnContainer.Name = "progressDLbtnContainer";
this.progressDLbtnContainer.Size = new System.Drawing.Size(984, 34);
this.progressDLbtnContainer.Size = new System.Drawing.Size(984, 40);
this.progressDLbtnContainer.TabIndex = 96;
//
// diskLabel
@@ -1281,35 +1224,6 @@ namespace AndroidSideloader
this.sidebarMediaPanel.Size = new System.Drawing.Size(233, 214);
this.sidebarMediaPanel.TabIndex = 101;
//
// downloadInstallGameButton
//
this.downloadInstallGameButton.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190)))));
this.downloadInstallGameButton.Active2 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190)))));
this.downloadInstallGameButton.BackColor = System.Drawing.Color.Transparent;
this.downloadInstallGameButton.Cursor = System.Windows.Forms.Cursors.Hand;
this.downloadInstallGameButton.DialogResult = System.Windows.Forms.DialogResult.OK;
this.downloadInstallGameButton.Disabled1 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22)))));
this.downloadInstallGameButton.Disabled2 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22)))));
this.downloadInstallGameButton.DisabledStrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(55)))), ((int)(((byte)(65)))));
this.downloadInstallGameButton.Enabled = false;
this.downloadInstallGameButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
this.downloadInstallGameButton.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(59)))), ((int)(((byte)(67)))), ((int)(((byte)(82)))));
this.downloadInstallGameButton.Inactive1 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
this.downloadInstallGameButton.Inactive2 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
this.downloadInstallGameButton.Location = new System.Drawing.Point(6, 177);
this.downloadInstallGameButton.Margin = new System.Windows.Forms.Padding(0);
this.downloadInstallGameButton.Name = "downloadInstallGameButton";
this.downloadInstallGameButton.Radius = 4;
this.downloadInstallGameButton.Size = new System.Drawing.Size(238, 30);
this.downloadInstallGameButton.Stroke = true;
this.downloadInstallGameButton.StrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
this.downloadInstallGameButton.TabIndex = 94;
this.downloadInstallGameButton.Text = "DOWNLOAD AND INSTALL";
this.downloadInstallGameButton.Transparency = false;
this.downloadInstallGameButton.Click += new System.EventHandler(this.downloadInstallGameButton_Click);
this.downloadInstallGameButton.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop);
this.downloadInstallGameButton.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter);
//
// selectedGameLabel
//
this.selectedGameLabel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(24)))), ((int)(((byte)(29)))));
@@ -1348,6 +1262,58 @@ namespace AndroidSideloader
this.tableLayoutPanel1.Size = new System.Drawing.Size(984, 34);
this.tableLayoutPanel1.TabIndex = 97;
//
// webView21
//
this.webView21.AllowExternalDrop = true;
this.webView21.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.webView21.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
this.webView21.CreationProperties = null;
this.webView21.DefaultBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
this.webView21.Location = new System.Drawing.Point(259, 496);
this.webView21.Name = "webView21";
this.webView21.Size = new System.Drawing.Size(384, 216);
this.webView21.TabIndex = 98;
this.webView21.ZoomFactor = 1D;
//
// favoriteGame
//
this.favoriteGame.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48)))));
this.favoriteGame.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.favoriteButton});
this.favoriteGame.Name = "favoriteGame";
this.favoriteGame.ShowImageMargin = false;
this.favoriteGame.Size = new System.Drawing.Size(149, 26);
//
// favoriteButton
//
this.favoriteButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48)))));
this.favoriteButton.ForeColor = System.Drawing.Color.White;
this.favoriteButton.Name = "favoriteButton";
this.favoriteButton.Size = new System.Drawing.Size(148, 22);
this.favoriteButton.Text = "★ Add to Favorites";
this.favoriteButton.Click += new System.EventHandler(this.favoriteButton_Click);
//
// gamesGalleryView
//
this.gamesGalleryView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.gamesGalleryView.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
this.gamesGalleryView.Location = new System.Drawing.Point(258, 44);
this.gamesGalleryView.Name = "gamesGalleryView";
this.gamesGalleryView.Size = new System.Drawing.Size(984, 409);
this.gamesGalleryView.TabIndex = 102;
//
// webViewPlaceholderPanel
//
this.webViewPlaceholderPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.webViewPlaceholderPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
this.webViewPlaceholderPanel.Location = new System.Drawing.Point(259, 496);
this.webViewPlaceholderPanel.Name = "webViewPlaceholderPanel";
this.webViewPlaceholderPanel.Size = new System.Drawing.Size(384, 217);
this.webViewPlaceholderPanel.TabIndex = 103;
this.webViewPlaceholderPanel.Paint += new System.Windows.Forms.PaintEventHandler(this.webViewPlaceholderPanel_Paint);
//
// searchPanel
//
this.searchPanel.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(56)))), ((int)(((byte)(70)))));
@@ -1531,57 +1497,59 @@ namespace AndroidSideloader
this.btnNewerThanList.Transparency = false;
this.btnNewerThanList.Click += new System.EventHandler(this.btnNewerThanList_Click);
//
// webView21
// progressBar
//
this.webView21.AllowExternalDrop = true;
this.webView21.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.webView21.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
this.webView21.CreationProperties = null;
this.webView21.DefaultBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
this.webView21.Location = new System.Drawing.Point(259, 496);
this.webView21.Name = "webView21";
this.webView21.Size = new System.Drawing.Size(384, 216);
this.webView21.TabIndex = 98;
this.webView21.ZoomFactor = 1D;
//
// favoriteGame
//
this.favoriteGame.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48)))));
this.favoriteGame.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.favoriteButton});
this.favoriteGame.Name = "favoriteGame";
this.favoriteGame.ShowImageMargin = false;
this.favoriteGame.Size = new System.Drawing.Size(149, 26);
//
// favoriteButton
//
this.favoriteButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48)))));
this.favoriteButton.ForeColor = System.Drawing.Color.White;
this.favoriteButton.Name = "favoriteButton";
this.favoriteButton.Size = new System.Drawing.Size(148, 22);
this.favoriteButton.Text = "★ Add to Favorites";
this.favoriteButton.Click += new System.EventHandler(this.favoriteButton_Click);
//
// gamesGalleryView
//
this.gamesGalleryView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.gamesGalleryView.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
this.gamesGalleryView.Location = new System.Drawing.Point(258, 44);
this.gamesGalleryView.Name = "gamesGalleryView";
this.gamesGalleryView.Size = new System.Drawing.Size(984, 409);
this.gamesGalleryView.TabIndex = 102;
this.progressBar.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(35)))), ((int)(((byte)(45)))));
this.progressBar.BackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(28)))), ((int)(((byte)(32)))), ((int)(((byte)(38)))));
this.progressBar.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
this.progressBar.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
this.progressBar.IndeterminateColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
this.progressBar.IsIndeterminate = false;
this.progressBar.Location = new System.Drawing.Point(1, 23);
this.progressBar.Maximum = 100F;
this.progressBar.Minimum = 0F;
this.progressBar.MinimumSize = new System.Drawing.Size(200, 13);
this.progressBar.Name = "progressBar";
this.progressBar.OperationType = "";
this.progressBar.ProgressEndColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(160)))), ((int)(((byte)(130)))));
this.progressBar.ProgressStartColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(220)))), ((int)(((byte)(190)))));
this.progressBar.Radius = 6;
this.progressBar.Size = new System.Drawing.Size(983, 13);
this.progressBar.StatusText = "";
this.progressBar.TabIndex = 7;
this.progressBar.TextColor = System.Drawing.Color.FromArgb(((int)(((byte)(230)))), ((int)(((byte)(230)))), ((int)(((byte)(230)))));
this.progressBar.Value = 0F;
//
// webViewPlaceholderPanel
// downloadInstallGameButton
//
this.webViewPlaceholderPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.webViewPlaceholderPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
this.webViewPlaceholderPanel.Location = new System.Drawing.Point(259, 496);
this.webViewPlaceholderPanel.Name = "webViewPlaceholderPanel";
this.webViewPlaceholderPanel.Size = new System.Drawing.Size(384, 217);
this.webViewPlaceholderPanel.TabIndex = 103;
this.webViewPlaceholderPanel.Paint += new System.Windows.Forms.PaintEventHandler(this.webViewPlaceholderPanel_Paint);
this.downloadInstallGameButton.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190)))));
this.downloadInstallGameButton.Active2 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190)))));
this.downloadInstallGameButton.BackColor = System.Drawing.Color.Transparent;
this.downloadInstallGameButton.Cursor = System.Windows.Forms.Cursors.Hand;
this.downloadInstallGameButton.DialogResult = System.Windows.Forms.DialogResult.OK;
this.downloadInstallGameButton.Disabled1 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22)))));
this.downloadInstallGameButton.Disabled2 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22)))));
this.downloadInstallGameButton.DisabledStrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(55)))), ((int)(((byte)(65)))));
this.downloadInstallGameButton.Enabled = false;
this.downloadInstallGameButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
this.downloadInstallGameButton.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(59)))), ((int)(((byte)(67)))), ((int)(((byte)(82)))));
this.downloadInstallGameButton.Inactive1 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
this.downloadInstallGameButton.Inactive2 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
this.downloadInstallGameButton.Location = new System.Drawing.Point(6, 177);
this.downloadInstallGameButton.Margin = new System.Windows.Forms.Padding(0);
this.downloadInstallGameButton.Name = "downloadInstallGameButton";
this.downloadInstallGameButton.Radius = 4;
this.downloadInstallGameButton.Size = new System.Drawing.Size(238, 30);
this.downloadInstallGameButton.Stroke = true;
this.downloadInstallGameButton.StrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
this.downloadInstallGameButton.TabIndex = 94;
this.downloadInstallGameButton.Text = "DOWNLOAD";
this.downloadInstallGameButton.Transparency = false;
this.downloadInstallGameButton.Click += new System.EventHandler(this.downloadInstallGameButton_Click);
this.downloadInstallGameButton.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop);
this.downloadInstallGameButton.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter);
//
// MainForm
//
@@ -1594,7 +1562,6 @@ namespace AndroidSideloader
this.Controls.Add(this.ULLabel);
this.Controls.Add(this.tableLayoutPanel1);
this.Controls.Add(this.progressDLbtnContainer);
this.Controls.Add(this.DragDropLbl);
this.Controls.Add(this.lblNotes);
this.Controls.Add(this.gamesQueueLabel);
this.Controls.Add(this.gamesQueListBox);
@@ -1633,11 +1600,11 @@ namespace AndroidSideloader
this.statusInfoPanel.ResumeLayout(false);
this.sidebarMediaPanel.ResumeLayout(false);
this.tableLayoutPanel1.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.webView21)).EndInit();
this.favoriteGame.ResumeLayout(false);
this.searchPanel.ResumeLayout(false);
this.searchPanel.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.webView21)).EndInit();
this.favoriteGame.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
@@ -1646,7 +1613,6 @@ namespace AndroidSideloader
#endregion
private SergeUtils.EasyCompletionComboBox m_combo;
private ModernProgressBar progressBar;
private System.Windows.Forms.Label etaLabel;
private System.Windows.Forms.Label speedLabel;
private System.Windows.Forms.Label freeDisclaimer;
private System.Windows.Forms.ComboBox devicesComboBox;
@@ -1657,7 +1623,6 @@ namespace AndroidSideloader
private System.Windows.Forms.PictureBox gamesPictureBox;
private System.Windows.Forms.Label gamesQueueLabel;
private System.Windows.Forms.RichTextBox notesRichTextBox;
private System.Windows.Forms.Label DragDropLbl;
private System.Windows.Forms.Label lblNotes;
public System.Windows.Forms.ComboBox remotesList;
public System.Windows.Forms.ColumnHeader GameNameIndex;
@@ -1751,5 +1716,6 @@ namespace AndroidSideloader
private Label activeMirrorLabel;
private Label sideloadingStatusLabel;
private Label rookieStatusLabel;
private ModernListView _listViewRenderer;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -120,9 +120,6 @@
<metadata name="speedLabel_Tooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>1165, 17</value>
</metadata>
<metadata name="etaLabel_Tooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>428, 54</value>
</metadata>
<metadata name="startsideloadbutton_Tooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>966, 17</value>
</metadata>
@@ -177,6 +174,9 @@
<metadata name="listApkButton_Tooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>1320, 17</value>
</metadata>
<metadata name="etaLabel_Tooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>428, 54</value>
</metadata>
<metadata name="favoriteGame.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>1021, 91</value>
</metadata>

1308
ModernListView.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,9 @@ namespace AndroidSideloader
{
#region Fields
private int _value;
private int _minimum;
private int _maximum = 100;
private float _value;
private float _minimum;
private float _maximum = 100f;
private int _radius = 8;
private bool _isIndeterminate;
private string _statusText = string.Empty;
@@ -66,7 +66,7 @@ namespace AndroidSideloader
[Category("Progress")]
[Description("The current value of the progress bar.")]
public int Value
public float Value
{
get => _value;
set
@@ -78,7 +78,7 @@ namespace AndroidSideloader
[Category("Progress")]
[Description("The minimum value of the progress bar.")]
public int Minimum
public float Minimum
{
get => _minimum;
set
@@ -91,7 +91,7 @@ namespace AndroidSideloader
[Category("Progress")]
[Description("The maximum value of the progress bar.")]
public int Maximum
public float Maximum
{
get => _maximum;
set
@@ -122,7 +122,7 @@ namespace AndroidSideloader
set
{
// If there is no change, do nothing
if (_isIndeterminate == value)
if (_isIndeterminate == value)
return;
_isIndeterminate = value;
@@ -205,7 +205,7 @@ namespace AndroidSideloader
// Gets the progress as a percentage (0-100)
public float ProgressPercent =>
_maximum > _minimum ? (float)(_value - _minimum) / (_maximum - _minimum) * 100f : 0f;
_maximum > _minimum ? (_value - _minimum) / (_maximum - _minimum) * 100f : 0f;
#endregion
@@ -250,7 +250,7 @@ namespace AndroidSideloader
private void DrawProgress(Graphics g, Rectangle outerRect)
{
float percent = (_maximum > _minimum)
? (float)(_value - _minimum) / (_maximum - _minimum)
? (_value - _minimum) / (_maximum - _minimum)
: 0f;
if (percent <= 0f) return;
@@ -363,10 +363,11 @@ namespace AndroidSideloader
if (!_isIndeterminate && _value > _minimum)
{
string percentText = $"{(int)ProgressPercent}%";
// Show one decimal place for sub-percent precision
string percentText = $"{ProgressPercent:0.0}%";
if (!string.IsNullOrEmpty(_operationType))
{
// E.g. "Downloading · 73%"
// E.g. "Downloading · 73.5%"
return $"{_operationType} · {percentText}";
}
return percentText;
@@ -435,4 +436,4 @@ namespace AndroidSideloader
#endregion
}
}
}

511
ModernQueuePanel.cs Normal file
View File

@@ -0,0 +1,511 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace AndroidSideloader
{
// Modern download queue panel with drag-reorder, cancel buttons
// and custom scrollbar with auto-scrolling during drag
public sealed class ModernQueuePanel : Control
{
// Layout constants
private const int ItemHeight = 28, ItemMargin = 4, ItemRadius = 5;
private const int XButtonSize = 18, DragHandleWidth = 20, TextPadding = 6;
private const int ScrollbarWidth = 6, ScrollbarWidthHover = 8, ScrollbarMargin = 2;
private const int ScrollbarRadius = 3, MinThumbHeight = 20;
private const int AutoScrollZoneHeight = 30, AutoScrollSpeed = 3;
// Color palette
private static readonly Color BgColor = Color.FromArgb(24, 26, 30);
private static readonly Color ItemBg = Color.FromArgb(32, 36, 44);
private static readonly Color ItemHoverBg = Color.FromArgb(42, 46, 54);
private static readonly Color ItemDragBg = Color.FromArgb(45, 55, 70);
private static readonly Color TextColor = Color.FromArgb(210, 210, 210);
private static readonly Color TextDimColor = Color.FromArgb(140, 140, 140);
private static readonly Color AccentColor = Color.FromArgb(93, 203, 173);
private static readonly Color XButtonBg = Color.FromArgb(55, 60, 70);
private static readonly Color XButtonHoverBg = Color.FromArgb(200, 60, 60);
private static readonly Color GripColor = Color.FromArgb(70, 75, 85);
private static readonly Color ItemDragBorder = Color.FromArgb(55, 65, 80);
private static readonly Color ScrollTrackColor = Color.FromArgb(35, 38, 45);
private static readonly Color ScrollThumbColor = Color.FromArgb(70, 75, 85);
private static readonly Color ScrollThumbHoverColor = Color.FromArgb(90, 95, 105);
private static readonly Color ScrollThumbDragColor = Color.FromArgb(110, 115, 125);
private readonly List<string> _items = new List<string>();
private readonly Timer _autoScrollTimer;
// State tracking
private int _hoveredIndex = -1, _dragIndex = -1, _dropIndex = -1, _scrollOffset;
private bool _hoveringX, _scrollbarHovered, _scrollbarDragging;
private int _scrollDragStartY, _scrollDragStartOffset, _autoScrollDirection;
private Rectangle _scrollThumbRect, _scrollTrackRect;
public event EventHandler<int> ItemRemoved;
public event EventHandler<ReorderEventArgs> ItemReordered;
public ModernQueuePanel()
{
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
BackColor = BgColor;
_autoScrollTimer = new Timer { Interval = 16 }; // ~60 FPS
_autoScrollTimer.Tick += (s, e) => HandleAutoScroll();
}
public bool IsDownloading { get; set; }
public int Count => _items.Count;
private int ContentHeight => _items.Count * (ItemHeight + ItemMargin) + ItemMargin;
private int MaxScroll => Math.Max(0, ContentHeight - Height);
private bool ScrollbarVisible => ContentHeight > Height;
public void SetItems(IEnumerable<string> items)
{
_items.Clear();
_items.AddRange(items);
ResetState();
}
private void ResetState()
{
_hoveredIndex = _dragIndex = -1;
ClampScroll();
Invalidate();
}
private void ClampScroll() =>
_scrollOffset = Math.Max(0, Math.Min(MaxScroll, _scrollOffset));
// Auto-scroll when dragging near edges
private void HandleAutoScroll()
{
if (_dragIndex < 0 || _autoScrollDirection == 0)
{
_autoScrollTimer.Stop();
return;
}
int oldOffset = _scrollOffset;
_scrollOffset += _autoScrollDirection * AutoScrollSpeed;
ClampScroll();
if (_scrollOffset != oldOffset)
{
UpdateDropIndex(PointToClient(MousePosition).Y);
Invalidate();
}
}
private void UpdateAutoScroll(int mouseY)
{
if (_dragIndex < 0 || MaxScroll <= 0)
{
StopAutoScroll();
return;
}
_autoScrollDirection = mouseY < AutoScrollZoneHeight && _scrollOffset > 0 ? -1 :
mouseY > Height - AutoScrollZoneHeight && _scrollOffset < MaxScroll ? 1 : 0;
if (_autoScrollDirection != 0 && !_autoScrollTimer.Enabled)
_autoScrollTimer.Start();
else if (_autoScrollDirection == 0)
_autoScrollTimer.Stop();
}
private void StopAutoScroll()
{
_autoScrollDirection = 0;
_autoScrollTimer.Stop();
}
private void UpdateDropIndex(int mouseY) =>
_dropIndex = Math.Max(1, Math.Min(_items.Count, (mouseY + _scrollOffset + ItemHeight / 2) / (ItemHeight + ItemMargin)));
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(BgColor);
if (_items.Count == 0)
{
DrawEmptyState(g);
return;
}
// Draw visible items
for (int i = 0; i < _items.Count; i++)
{
var rect = GetItemRect(i);
if (rect.Bottom >= 0 && rect.Top <= Height)
DrawItem(g, i, rect);
}
// Draw drop indicator and scrollbar
if (_dragIndex >= 0 && _dropIndex >= 0 && _dropIndex != _dragIndex)
DrawDropIndicator(g);
if (ScrollbarVisible)
DrawScrollbar(g);
}
private void DrawEmptyState(Graphics g)
{
using (var brush = new SolidBrush(TextDimColor))
using (var font = new Font("Segoe UI", 8.5f, FontStyle.Italic))
{
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
g.DrawString("Queue is empty", font, brush, ClientRectangle, sf);
}
}
private void DrawItem(Graphics g, int index, Rectangle rect)
{
bool isFirst = index == 0;
bool isDragging = index == _dragIndex;
bool isHovered = !isDragging && index == _hoveredIndex;
Color bg = isDragging ? ItemDragBg : isHovered ? ItemHoverBg : ItemBg;
// Draw item background
using (var path = CreateRoundedRect(rect, ItemRadius))
using (var brush = new SolidBrush(bg))
g.FillPath(brush, path);
// Active download (first item) gets gradient accent and border
if (isFirst)
DrawFirstItemAccent(g, rect);
// Dragged items get subtle highlight border
else if (isDragging)
DrawBorder(g, rect, ItemDragBorder, 0.5f);
// Draw drag handle, text, and close button
if (!isFirst)
DrawDragHandle(g, rect);
DrawItemText(g, index, rect, isFirst);
DrawXButton(g, rect, isHovered && _hoveringX);
}
// Draw gradient accent and border for active download (first item)
private void DrawFirstItemAccent(Graphics g, Rectangle rect)
{
using (var path = CreateRoundedRect(rect, ItemRadius))
using (var gradBrush = new LinearGradientBrush(rect,
Color.FromArgb(60, AccentColor), Color.FromArgb(0, AccentColor), LinearGradientMode.Horizontal))
{
var oldClip = g.Clip;
g.SetClip(path);
g.FillRectangle(gradBrush, rect);
g.Clip = oldClip;
}
DrawBorder(g, rect, AccentColor, 1.5f);
}
private void DrawBorder(Graphics g, Rectangle rect, Color color, float width)
{
using (var path = CreateRoundedRect(rect, ItemRadius))
using (var pen = new Pen(color, width))
g.DrawPath(pen, path);
}
private void DrawDragHandle(Graphics g, Rectangle rect)
{
int cx = rect.X + 8, cy = rect.Y + rect.Height / 2;
using (var brush = new SolidBrush(GripColor))
{
for (int row = -1; row <= 1; row++)
for (int col = 0; col < 2; col++)
g.FillEllipse(brush, cx + col * 4, cy + row * 4 - 1, 2, 2);
}
}
private void DrawItemText(Graphics g, int index, Rectangle rect, bool isFirst)
{
int textLeft = isFirst ? rect.X + TextPadding : rect.X + DragHandleWidth;
int rightPad = ScrollbarVisible ? ScrollbarWidthHover + ScrollbarMargin * 2 : 0;
var textRect = new Rectangle(textLeft, rect.Y, rect.Right - XButtonSize - 6 - textLeft - rightPad, rect.Height);
using (var brush = new SolidBrush(TextColor))
using (var font = new Font("Segoe UI", isFirst ? 8.5f : 8f, isFirst ? FontStyle.Bold : FontStyle.Regular))
{
var sf = new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter,
FormatFlags = StringFormatFlags.NoWrap
};
g.DrawString(_items[index], font, brush, textRect, sf);
}
}
private void DrawXButton(Graphics g, Rectangle itemRect, bool hovered)
{
var xRect = GetXButtonRect(itemRect);
using (var path = CreateRoundedRect(xRect, 3))
using (var brush = new SolidBrush(hovered ? XButtonHoverBg : XButtonBg))
g.FillPath(brush, path);
using (var pen = new Pen(Color.White, 1.4f) { StartCap = LineCap.Round, EndCap = LineCap.Round })
{
int p = 4;
g.DrawLine(pen, xRect.X + p, xRect.Y + p, xRect.Right - p, xRect.Bottom - p);
g.DrawLine(pen, xRect.Right - p, xRect.Y + p, xRect.X + p, xRect.Bottom - p);
}
}
private void DrawDropIndicator(Graphics g)
{
int y = (_dropIndex >= _items.Count ? _items.Count : _dropIndex) * (ItemHeight + ItemMargin) + ItemMargin / 2 - _scrollOffset;
int left = ItemMargin + 2;
int right = Width - ItemMargin - 2 - (ScrollbarVisible ? ScrollbarWidthHover + ScrollbarMargin : 0);
using (var pen = new Pen(AccentColor, 2.5f) { StartCap = LineCap.Round, EndCap = LineCap.Round })
g.DrawLine(pen, left, y, right, y);
}
// Draw custom scrollbar with hover expansion
private void DrawScrollbar(Graphics g)
{
if (MaxScroll <= 0) return;
bool expanded = _scrollbarHovered || _scrollbarDragging;
int sbWidth = expanded ? ScrollbarWidthHover : ScrollbarWidth;
int trackX = Width - ScrollbarWidth - ScrollbarMargin - (expanded ? (ScrollbarWidthHover - ScrollbarWidth) / 2 : 0);
_scrollTrackRect = new Rectangle(trackX, ScrollbarMargin, sbWidth, Height - ScrollbarMargin * 2);
using (var trackBrush = new SolidBrush(Color.FromArgb(40, ScrollTrackColor)))
using (var trackPath = CreateRoundedRect(_scrollTrackRect, ScrollbarRadius))
g.FillPath(trackBrush, trackPath);
// Calculate thumb position and size
int trackHeight = _scrollTrackRect.Height;
int thumbHeight = Math.Max(MinThumbHeight, (int)(trackHeight * ((float)Height / ContentHeight)));
float scrollRatio = MaxScroll > 0 ? (float)_scrollOffset / MaxScroll : 0;
int thumbY = ScrollbarMargin + (int)((trackHeight - thumbHeight) * scrollRatio);
_scrollThumbRect = new Rectangle(trackX, thumbY, sbWidth, thumbHeight);
Color thumbColor = _scrollbarDragging ? ScrollThumbDragColor : _scrollbarHovered ? ScrollThumbHoverColor : ScrollThumbColor;
using (var thumbBrush = new SolidBrush(thumbColor))
using (var thumbPath = CreateRoundedRect(_scrollThumbRect, ScrollbarRadius))
g.FillPath(thumbBrush, thumbPath);
}
private Rectangle GetItemRect(int index)
{
int y = index * (ItemHeight + ItemMargin) + ItemMargin - _scrollOffset;
int w = Width - ItemMargin * 2 - (ScrollbarVisible ? ScrollbarWidthHover + ScrollbarMargin + 2 : 0);
return new Rectangle(ItemMargin, y, w, ItemHeight);
}
private Rectangle GetXButtonRect(Rectangle itemRect) =>
new Rectangle(itemRect.Right - XButtonSize - 3, itemRect.Y + (itemRect.Height - XButtonSize) / 2, XButtonSize, XButtonSize);
private int HitTest(Point pt)
{
for (int i = 0; i < _items.Count; i++)
if (GetItemRect(i).Contains(pt)) return i;
return -1;
}
private bool HitTestScrollbar(Point pt) =>
ScrollbarVisible && new Rectangle(_scrollTrackRect.X - 4, _scrollTrackRect.Y, _scrollTrackRect.Width + 8, _scrollTrackRect.Height).Contains(pt);
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (_scrollbarDragging)
{
HandleScrollbarDrag(e.Y);
return;
}
// Update scrollbar hover state
bool wasHovered = _scrollbarHovered;
_scrollbarHovered = HitTestScrollbar(e.Location);
if (_scrollbarHovered != wasHovered) Invalidate();
if (_scrollbarHovered)
{
Cursor = Cursors.Default;
_hoveredIndex = -1;
_hoveringX = false;
return;
}
// Handle drag operation
if (_dragIndex >= 0)
{
UpdateAutoScroll(e.Y);
int newDrop = Math.Max(1, Math.Min(_items.Count, (e.Y + _scrollOffset + ItemHeight / 2) / (ItemHeight + ItemMargin)));
if (newDrop != _dropIndex) { _dropIndex = newDrop; Invalidate(); }
return;
}
// Update hover state
int hit = HitTest(e.Location);
bool overX = hit >= 0 && GetXButtonRect(GetItemRect(hit)).Contains(e.Location);
if (hit != _hoveredIndex || overX != _hoveringX)
{
_hoveredIndex = hit;
_hoveringX = overX;
Cursor = overX ? Cursors.Hand : hit > 0 ? Cursors.SizeNS : Cursors.Default;
Invalidate();
}
}
private void HandleScrollbarDrag(int mouseY)
{
int trackHeight = Height - ScrollbarMargin * 2;
int thumbHeight = Math.Max(MinThumbHeight, (int)(trackHeight * ((float)Height / ContentHeight)));
int scrollableHeight = trackHeight - thumbHeight;
if (scrollableHeight > 0)
{
float scrollRatio = (float)(mouseY - _scrollDragStartY) / scrollableHeight;
_scrollOffset = _scrollDragStartOffset + (int)(scrollRatio * MaxScroll);
ClampScroll();
Invalidate();
}
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (e.Button != MouseButtons.Left) return;
// Handle scrollbar thumb drag
if (ScrollbarVisible && _scrollThumbRect.Contains(e.Location))
{
_scrollbarDragging = true;
_scrollDragStartY = e.Y;
_scrollDragStartOffset = _scrollOffset;
Capture = true;
return;
}
// Handle scrollbar track click
if (ScrollbarVisible && HitTestScrollbar(e.Location))
{
_scrollOffset += e.Y < _scrollThumbRect.Top ? -Height : Height;
ClampScroll();
Invalidate();
return;
}
int hit = HitTest(e.Location);
if (hit < 0) return;
// Handle close button click
if (GetXButtonRect(GetItemRect(hit)).Contains(e.Location))
{
ItemRemoved?.Invoke(this, hit);
return;
}
// Start drag operation (only for non-first items)
if (hit > 0)
{
_dragIndex = _dropIndex = hit;
Capture = true;
Invalidate();
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
StopAutoScroll();
if (_scrollbarDragging)
{
_scrollbarDragging = false;
Capture = false;
Invalidate();
return;
}
// Complete drag reorder operation
if (_dragIndex > 0 && _dropIndex > 0 && _dropIndex != _dragIndex)
{
int from = _dragIndex;
int to = Math.Max(1, _dropIndex > _dragIndex ? _dropIndex - 1 : _dropIndex);
var item = _items[from];
_items.RemoveAt(from);
_items.Insert(to, item);
ItemReordered?.Invoke(this, new ReorderEventArgs(from, to));
}
_dragIndex = _dropIndex = -1;
Capture = false;
Cursor = Cursors.Default;
Invalidate();
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
if (_dragIndex < 0 && !_scrollbarDragging)
{
_hoveredIndex = -1;
_hoveringX = _scrollbarHovered = false;
Invalidate();
}
}
protected override void OnMouseWheel(MouseEventArgs e)
{
base.OnMouseWheel(e);
if (MaxScroll <= 0) return;
_scrollOffset -= e.Delta / 4;
ClampScroll();
Invalidate();
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
ClampScroll();
Invalidate();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_autoScrollTimer?.Stop();
_autoScrollTimer?.Dispose();
}
base.Dispose(disposing);
}
private static GraphicsPath CreateRoundedRect(Rectangle rect, int radius)
{
var path = new GraphicsPath();
if (radius <= 0 || rect.Width <= 0 || rect.Height <= 0)
{
path.AddRectangle(rect);
return path;
}
int d = Math.Min(radius * 2, Math.Min(rect.Width, rect.Height));
path.AddArc(rect.X, rect.Y, d, d, 180, 90);
path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90);
path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
path.CloseFigure();
return path;
}
}
public class ReorderEventArgs : EventArgs
{
public int FromIndex { get; }
public int ToIndex { get; }
public ReorderEventArgs(int from, int to) { FromIndex = from; ToIndex = to; }
}
}

2
NewApps.Designer.cs generated
View File

@@ -29,7 +29,6 @@ namespace AndroidSideloader
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(NewApps));
this.NewAppsListView = new System.Windows.Forms.ListView();
this.GameNameIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.PackageNameIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
@@ -173,7 +172,6 @@ namespace AndroidSideloader
this.Controls.Add(this.panel1);
this.ForeColor = System.Drawing.Color.White;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "NewApps";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Load += new System.EventHandler(this.NewApps_Load);

View File

@@ -39,6 +39,10 @@ namespace AndroidSideloader
public NewApps()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
ApplyModernTheme();
CenterToScreen();
}

File diff suppressed because it is too large Load Diff

View File

@@ -191,5 +191,8 @@
<Setting Name="proxyPort" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="selectedMirror" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
</Settings>
</SettingsFile>

3
QuestForm.Designer.cs generated
View File

@@ -30,7 +30,6 @@ namespace AndroidSideloader
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(QuestForm));
this.lblUsernameSection = new System.Windows.Forms.Label();
this.lblMediaSection = new System.Windows.Forms.Label();
this.lblPerformanceSection = new System.Windows.Forms.Label();
@@ -444,11 +443,9 @@ namespace AndroidSideloader
this.Controls.Add(this.btnApplyTempSettings);
this.Controls.Add(this.btnClose);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "QuestForm";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Quest Settings";
this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.QuestForm_FormClosed);

View File

@@ -16,6 +16,9 @@ namespace AndroidSideloader
public QuestForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
private void btnApplyTempSettings_Click(object sender, EventArgs e)

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,6 @@
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SettingsForm));
this.downloadDirectorySetter = new System.Windows.Forms.FolderBrowserDialog();
this.backupDirectorySetter = new System.Windows.Forms.FolderBrowserDialog();
this.crashlogID = new System.Windows.Forms.Label();
@@ -866,11 +865,9 @@
this.Controls.Add(this.resetSettingsButton);
this.ForeColor = System.Drawing.Color.White;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "SettingsForm";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Settings";
this.Load += new System.EventHandler(this.SettingsForm_Load);

View File

@@ -17,6 +17,9 @@ namespace AndroidSideloader
public SettingsForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
private void SettingsForm_Load(object sender, EventArgs e)
@@ -324,20 +327,33 @@ namespace AndroidSideloader
private void setDownloadDirectory_Click(object sender, EventArgs e)
{
if (downloadDirectorySetter.ShowDialog() == DialogResult.OK)
var dialog = new FolderSelectDialog
{
Title = "Select Download Folder",
InitialDirectory = _settings.CustomDownloadDir && Directory.Exists(_settings.DownloadDir)
? _settings.DownloadDir
: Environment.CurrentDirectory
};
if (dialog.Show(this.Handle))
{
_settings.CustomDownloadDir = true;
_settings.DownloadDir = downloadDirectorySetter.SelectedPath;
_settings.DownloadDir = dialog.FileName;
}
}
private void setBackupDirectory_Click(object sender, EventArgs e)
{
if (backupDirectorySetter.ShowDialog() == DialogResult.OK)
var dialog = new FolderSelectDialog
{
Title = "Select Backup Folder",
InitialDirectory = _settings.GetEffectiveBackupDir()
};
if (dialog.Show(this.Handle))
{
_settings.CustomBackupDir = true;
_settings.BackupDir = backupDirectorySetter.SelectedPath;
MainForm.backupFolder = _settings.BackupDir;
_settings.BackupDir = dialog.FileName;
}
}
@@ -359,9 +375,7 @@ namespace AndroidSideloader
private void openBackupDirectory_Click(object sender, EventArgs e)
{
string pathToOpen = _settings.CustomBackupDir
? Path.Combine(_settings.BackupDir, "Rookie Backups")
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Rookie Backups");
string pathToOpen = _settings.GetEffectiveBackupDir();
MainForm.OpenDirectory(pathToOpen);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,14 @@
using JR.Utils.GUI.Forms;
using AndroidSideloader.Utilities;
using JR.Utils.GUI.Forms;
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Management;
using System.Net;
using System.Text.RegularExpressions;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
using AndroidSideloader.Utilities;
namespace AndroidSideloader
{
@@ -84,9 +85,12 @@ namespace AndroidSideloader
return output;
}
//Recursive sideload any apk fileD
//Recursive sideload any apk file
public static ProcessOutput RecursiveOutput = new ProcessOutput();
public static void RecursiveSideload(string FolderPath)
public static async Task RecursiveSideloadAsync(
string FolderPath,
Action<float, TimeSpan?> progressCallback = null,
Action<string> statusCallback = null)
{
try
{
@@ -94,31 +98,40 @@ namespace AndroidSideloader
{
if (Path.GetExtension(f) == ".apk")
{
RecursiveOutput += ADB.Sideload(f);
string gameName = Path.GetFileNameWithoutExtension(f);
statusCallback?.Invoke(gameName);
RecursiveOutput += await ADB.SideloadWithProgressAsync(f, progressCallback, statusCallback, "", gameName);
}
}
foreach (string d in Directory.GetDirectories(FolderPath))
{
RecursiveSideload(d);
await RecursiveSideloadAsync(d, progressCallback, statusCallback);
}
}
catch (Exception ex) { _ = Logger.Log(ex.Message, LogLevel.ERROR); }
}
//Recursive copy any obb folder
public static void RecursiveCopyOBB(string FolderPath)
public static async Task RecursiveCopyOBBAsync(
string FolderPath,
Action<float, TimeSpan?> progressCallback = null,
Action<string> statusCallback = null)
{
try
{
foreach (string f in Directory.GetFiles(FolderPath))
{
RecursiveOutput += ADB.CopyOBB(f);
}
foreach (string d in Directory.GetDirectories(FolderPath))
{
RecursiveCopyOBB(d);
string folderName = Path.GetFileName(d);
if (folderName.Contains("."))
{
statusCallback?.Invoke(folderName);
RecursiveOutput += await ADB.CopyOBBWithProgressAsync(d, progressCallback, statusCallback, folderName);
}
else
{
await RecursiveCopyOBBAsync(d, progressCallback, statusCallback);
}
}
}
catch (Exception ex) { _ = Logger.Log(ex.Message, LogLevel.ERROR); }
@@ -136,14 +149,7 @@ namespace AndroidSideloader
public static void BackupGame(string packagename)
{
if (!settings.CustomBackupDir)
{
MainForm.backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
}
else
{
MainForm.backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
}
MainForm.backupFolder = settings.GetEffectiveBackupDir();
if (!Directory.Exists(MainForm.backupFolder))
{
_ = Directory.CreateDirectory(MainForm.backupFolder);
@@ -204,7 +210,7 @@ namespace AndroidSideloader
if (Directory.Exists($"{settings.MainDir}\\{packageName}"))
{
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{packageName}");
}
_ = Directory.CreateDirectory($"{settings.MainDir}\\{packageName}");

View File

@@ -137,7 +137,7 @@ namespace AndroidSideloader
Application.Exit();
}
string wantedRcloneVersion = "1.68.2";
string wantedRcloneVersion = "1.72.1";
bool rcloneSuccess = false;
rcloneSuccess = downloadRclone(wantedRcloneVersion, false);
@@ -309,7 +309,7 @@ namespace AndroidSideloader
}
File.Move(file, destFile);
}
Directory.Delete(dirExtractedRclone, true);
FileSystemUtilities.TryDeleteDirectory(dirExtractedRclone);
// Restore vrp.download.config if it was backed up
if (hasConfig && File.Exists(tempConfigPath))

View File

@@ -14,8 +14,6 @@ namespace AndroidSideloader
public static string RcloneGamesFolder = "Quest Games";
//This shit sucks but i'll switch to programatically adding indexes from the gamelist txt sometimes maybe
public static int GameNameIndex = 0;
public static int ReleaseNameIndex = 1;
public static int PackageNameIndex = 2;
@@ -23,6 +21,7 @@ namespace AndroidSideloader
public static int ReleaseAPKPathIndex = 4;
public static int VersionNameIndex = 5;
public static int DownloadsIndex = 6;
public static int InstalledVersion = 7;
public static List<string> gameProperties = new List<string>();
/* Game Name
@@ -291,7 +290,7 @@ namespace AndroidSideloader
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
FileSystemUtilities.TryDeleteDirectory(path);
}
}
catch (Exception ex)

View File

@@ -28,7 +28,6 @@
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(UpdateForm));
this.panel1 = new System.Windows.Forms.Panel();
this.YesUpdate = new AndroidSideloader.RoundButton();
this.panel3 = new System.Windows.Forms.Panel();
@@ -166,7 +165,6 @@
this.ControlBox = false;
this.Controls.Add(this.panel1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "UpdateForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.UpdateForm_MouseDown);

View File

@@ -20,6 +20,10 @@ namespace AndroidSideloader
public UpdateForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
ApplyModernTheme();
CenterToScreen();
CurVerLabel.Text = $"Current Version: {Updater.LocalVersion}";

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ namespace AndroidSideloader
private static readonly string RawGitHubUrl = "https://raw.githubusercontent.com/VRPirates/rookie";
public static readonly string GitHubUrl = "https://github.com/VRPirates/rookie";
public static readonly string LocalVersion = "3.0";
public static readonly string LocalVersion = "3.0.1";
public static string currentVersion = string.Empty;
public static string changelog = string.Empty;

View File

@@ -28,28 +28,30 @@
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(UsernameForm));
this.textBox1 = new System.Windows.Forms.TextBox();
this.button1 = new AndroidSideloader.RoundButton();
this.SuspendLayout();
//
//
// textBox1
//
//
this.textBox1.BackColor = global::AndroidSideloader.Properties.Settings.Default.TextBoxColor;
this.textBox1.Font = global::AndroidSideloader.Properties.Settings.Default.FontStyle;
this.textBox1.ForeColor = System.Drawing.Color.White;
this.textBox1.Location = new System.Drawing.Point(13, 13);
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System.Drawing.Size(418, 24);
this.textBox1.Size = new System.Drawing.Size(418, 23);
this.textBox1.TabIndex = 0;
this.textBox1.Text = "Enter your username here";
//
//
// button1
//
//
this.button1.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(45)))));
this.button1.Active2 = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(45)))));
this.button1.BackColor = System.Drawing.Color.Transparent;
this.button1.DialogResult = System.Windows.Forms.DialogResult.OK;
this.button1.Disabled1 = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(35)))), ((int)(((byte)(45)))));
this.button1.Disabled2 = System.Drawing.Color.FromArgb(((int)(((byte)(25)))), ((int)(((byte)(28)))), ((int)(((byte)(35)))));
this.button1.DisabledStrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(55)))), ((int)(((byte)(65)))));
this.button1.Font = new System.Drawing.Font("Microsoft Sans Serif", 11.25F);
this.button1.ForeColor = System.Drawing.Color.White;
this.button1.Inactive1 = System.Drawing.Color.FromArgb(((int)(((byte)(25)))), ((int)(((byte)(25)))), ((int)(((byte)(25)))));
@@ -64,9 +66,9 @@
this.button1.Text = "Create User.Json";
this.button1.Transparency = false;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
//
// UsernameForm
//
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = global::AndroidSideloader.Properties.Settings.Default.BackColor;
@@ -74,11 +76,9 @@
this.Controls.Add(this.button1);
this.Controls.Add(this.textBox1);
this.ForeColor = System.Drawing.Color.White;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximumSize = new System.Drawing.Size(459, 139);
this.MinimumSize = new System.Drawing.Size(459, 139);
this.Name = "UsernameForm";
this.ShowIcon = false;
this.Text = "USER.JSON";
this.Load += new System.EventHandler(this.usernameForm_Load);
this.ResumeLayout(false);

View File

@@ -12,6 +12,9 @@ namespace AndroidSideloader
public UsernameForm()
{
InitializeComponent();
// Use same icon as the executable
this.Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
private string defaultText;

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,13 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace AndroidSideloader.Utilities
{
@@ -20,49 +22,161 @@ namespace AndroidSideloader.Utilities
"raw.githubusercontent.com",
"downloads.rclone.org",
"vrpirates.wiki",
"go.vrpyourself.online",
"github.com"
};
private static readonly ConcurrentDictionary<string, IPAddress> _dnsCache =
new ConcurrentDictionary<string, IPAddress>(StringComparer.OrdinalIgnoreCase);
private static bool _initialized;
private static bool _useFallbackDns;
private static readonly object _lock = new object();
// Local proxy for rclone
private static TcpListener _proxyListener;
private static CancellationTokenSource _proxyCts;
private static int _proxyPort;
private static bool _initialized;
private static bool _proxyRunning;
public static bool UseFallbackDns
{
get { if (!_initialized) Initialize(); return _useFallbackDns; }
}
public static bool UseFallbackDns { get; private set; }
// Gets the proxy URL for rclone to use, or empty string if not needed
public static string ProxyUrl => _proxyRunning ? $"http://127.0.0.1:{_proxyPort}" : string.Empty;
// Called after vrp-public.json is created/updated to test the hostname
// Enable fallback DNS if the hostname fails on system DNS but works with fallback DNS
public static void TestPublicConfigDns()
{
string hostname = GetPublicConfigHostname();
if (string.IsNullOrEmpty(hostname))
return;
lock (_lock)
{
// If already using fallback, just pre-resolve the new hostname
if (UseFallbackDns)
{
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
_dnsCache[hostname] = ip;
Logger.Log($"Pre-resolved public config hostname {hostname} -> {ip}");
}
return;
}
// Test if system DNS can resolve the public config hostname
bool systemDnsWorks = TestHostnameWithSystemDns(hostname);
if (!systemDnsWorks)
{
Logger.Log($"System DNS failed for {hostname}. Testing fallback...", LogLevel.WARNING);
// Test if fallback DNS works for this hostname
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
UseFallbackDns = true;
_dnsCache[hostname] = ip;
Logger.Log($"Enabled fallback DNS for {hostname} -> {ip}", LogLevel.INFO);
ServicePointManager.DnsRefreshTimeout = 0;
// Pre-resolve base hostnames too
PreResolveHostnames(CriticalHostnames);
// Start proxy if not already running
if (!_proxyRunning)
{
StartProxy();
}
}
else
{
Logger.Log($"Both system and fallback DNS failed for {hostname}", LogLevel.ERROR);
}
}
else
{
Logger.Log($"System DNS works for public config hostname: {hostname}");
}
}
}
private static string GetPublicConfigHostname()
{
try
{
string configPath = Path.Combine(Environment.CurrentDirectory, "vrp-public.json");
if (!File.Exists(configPath))
return null;
var config = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(configPath));
if (config != null && config.TryGetValue("baseUri", out string baseUri))
{
return ExtractHostname(baseUri);
}
}
catch (Exception ex)
{
Logger.Log($"Failed to get hostname from vrp-public.json: {ex.Message}", LogLevel.WARNING);
}
return null;
}
private static string[] GetCriticalHostnames()
{
var hostnames = new List<string>(CriticalHostnames);
string host = GetPublicConfigHostname();
if (!string.IsNullOrWhiteSpace(host) && !hostnames.Contains(host))
{
hostnames.Add(host);
Logger.Log($"Added {host} from vrp-public.json to critical hostnames");
}
return hostnames.ToArray();
}
private static string ExtractHostname(string uriString)
{
if (string.IsNullOrWhiteSpace(uriString)) return null;
if (!uriString.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!uriString.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
uriString = "https://" + uriString;
}
if (Uri.TryCreate(uriString, UriKind.Absolute, out Uri uri))
return uri.Host;
// Fallback: manual extraction
string hostname = uriString.Replace("https://", "").Replace("http://", "");
int idx = hostname.IndexOfAny(new[] { '/', ':' });
return idx > 0 ? hostname.Substring(0, idx) : hostname;
}
public static void Initialize()
{
lock (_lock)
{
if (_initialized) return;
Logger.Log("Testing DNS resolution for critical hostnames...");
if (!TestSystemDns())
Logger.Log("Testing DNS resolution for critical hostnames...");
var hostnames = GetCriticalHostnames();
if (TestDns(hostnames, useSystem: true))
{
Logger.Log("System DNS is working correctly.");
}
else
{
Logger.Log("System DNS failed. Testing Cloudflare DNS fallback...", LogLevel.WARNING);
if (TestFallbackDns())
if (TestDns(hostnames, useSystem: false))
{
_useFallbackDns = true;
UseFallbackDns = true;
Logger.Log("Using Cloudflare DNS fallback.", LogLevel.INFO);
PreResolveHostnames();
PreResolveHostnames(hostnames);
ServicePointManager.DnsRefreshTimeout = 0;
// Start local proxy for rclone
StartProxy();
}
else
@@ -70,77 +184,65 @@ namespace AndroidSideloader.Utilities
Logger.Log("Both system and fallback DNS failed.", LogLevel.ERROR);
}
}
else
{
Logger.Log("System DNS is working correctly.");
}
_initialized = true;
}
}
// Cleans up resources. Called on application exit
public static void Cleanup()
{
StopProxy();
}
public static void Cleanup() => StopProxy();
private static void PreResolveHostnames()
private static bool TestHostnameWithSystemDns(string hostname)
{
foreach (string hostname in CriticalHostnames)
try
{
try
{
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
_dnsCache[hostname] = ip;
Logger.Log($"Pre-resolved {hostname} -> {ip}");
}
}
catch (Exception ex)
{
Logger.Log($"Failed to pre-resolve {hostname}: {ex.Message}", LogLevel.WARNING);
}
var addresses = Dns.GetHostAddresses(hostname);
return addresses?.Length > 0;
}
catch
{
return false;
}
}
private static bool TestSystemDns()
private static bool TestDns(string[] hostnames, bool useSystem)
{
foreach (string hostname in CriticalHostnames)
if (useSystem)
{
try
return hostnames.All(h =>
{
var addresses = Dns.GetHostAddresses(hostname);
if (addresses == null || addresses.Length == 0) return false;
}
try { return Dns.GetHostAddresses(h)?.Length > 0; }
catch { return false; }
});
}
return FallbackDnsServers.Any(server =>
{
try { return ResolveWithDns(hostnames[0], server)?.Count > 0; }
catch { return false; }
}
return true;
});
}
private static bool TestFallbackDns()
private static void PreResolveHostnames(string[] hostnames)
{
foreach (string dnsServer in FallbackDnsServers)
foreach (string hostname in hostnames)
{
try
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
var addresses = ResolveWithDns(CriticalHostnames[0], dnsServer);
if (addresses != null && addresses.Count > 0) return true;
_dnsCache[hostname] = ip;
Logger.Log($"Pre-resolved {hostname} -> {ip}");
}
catch { }
}
return false;
}
private static IPAddress ResolveWithFallbackDns(string hostname)
{
foreach (string dnsServer in FallbackDnsServers)
foreach (string server in FallbackDnsServers)
{
try
{
var addresses = ResolveWithDns(hostname, dnsServer);
if (addresses != null && addresses.Count > 0)
return addresses[0];
var addresses = ResolveWithDns(hostname, server);
if (addresses?.Count > 0) return addresses[0];
}
catch { }
}
@@ -149,56 +251,59 @@ namespace AndroidSideloader.Utilities
private static List<IPAddress> ResolveWithDns(string hostname, string dnsServer, int timeoutMs = 5000)
{
byte[] query = BuildDnsQuery(hostname);
using (var udp = new UdpClient())
using (var udp = new UdpClient { Client = { ReceiveTimeout = timeoutMs, SendTimeout = timeoutMs } })
{
udp.Client.ReceiveTimeout = timeoutMs;
udp.Client.SendTimeout = timeoutMs;
byte[] query = BuildDnsQuery(hostname);
udp.Send(query, query.Length, new IPEndPoint(IPAddress.Parse(dnsServer), 53));
IPEndPoint remoteEp = null;
byte[] response = udp.Receive(ref remoteEp);
return ParseDnsResponse(response);
return ParseDnsResponse(udp.Receive(ref remoteEp));
}
}
private static byte[] BuildDnsQuery(string hostname)
{
var ms = new MemoryStream();
var writer = new BinaryWriter(ms);
writer.Write(IPAddress.HostToNetworkOrder((short)new Random().Next(0, ushort.MaxValue)));
writer.Write(IPAddress.HostToNetworkOrder((short)0x0100));
writer.Write(IPAddress.HostToNetworkOrder((short)1));
writer.Write(IPAddress.HostToNetworkOrder((short)0));
writer.Write(IPAddress.HostToNetworkOrder((short)0));
writer.Write(IPAddress.HostToNetworkOrder((short)0));
foreach (string label in hostname.Split('.'))
using (var ms = new MemoryStream())
using (var writer = new BinaryWriter(ms))
{
writer.Write((byte)label.Length);
writer.Write(Encoding.ASCII.GetBytes(label));
writer.Write(IPAddress.HostToNetworkOrder((short)new Random().Next(0, ushort.MaxValue)));
writer.Write(IPAddress.HostToNetworkOrder((short)0x0100)); // Flags
writer.Write(IPAddress.HostToNetworkOrder((short)1)); // Questions
writer.Write(IPAddress.HostToNetworkOrder((short)0)); // Answer RRs
writer.Write(IPAddress.HostToNetworkOrder((short)0)); // Authority RRs
writer.Write(IPAddress.HostToNetworkOrder((short)0)); // Additional RRs
foreach (string label in hostname.Split('.'))
{
writer.Write((byte)label.Length);
writer.Write(Encoding.ASCII.GetBytes(label));
}
writer.Write((byte)0);
writer.Write(IPAddress.HostToNetworkOrder((short)1)); // Type A
writer.Write(IPAddress.HostToNetworkOrder((short)1)); // Class IN
return ms.ToArray();
}
writer.Write((byte)0);
writer.Write(IPAddress.HostToNetworkOrder((short)1));
writer.Write(IPAddress.HostToNetworkOrder((short)1));
return ms.ToArray();
}
private static List<IPAddress> ParseDnsResponse(byte[] response)
{
var addresses = new List<IPAddress>();
if (response.Length < 12) return addresses;
int pos = 12;
while (pos < response.Length && response[pos] != 0) pos += response[pos] + 1;
pos += 5;
int answerCount = (response[6] << 8) | response[7];
for (int i = 0; i < answerCount && pos + 12 <= response.Length; i++)
{
if ((response[pos] & 0xC0) == 0xC0) pos += 2;
else { while (pos < response.Length && response[pos] != 0) pos += response[pos] + 1; pos++; }
pos += (response[pos] & 0xC0) == 0xC0 ? 2 : SkipName(response, pos);
if (pos + 10 > response.Length) break;
ushort type = (ushort)((response[pos] << 8) | response[pos + 1]);
pos += 8;
ushort rdLength = (ushort)((response[pos] << 8) | response[pos + 1]);
pos += 2;
ushort rdLength = (ushort)((response[pos + 8] << 8) | response[pos + 9]);
pos += 10;
if (pos + rdLength > response.Length) break;
if (type == 1 && rdLength == 4)
addresses.Add(new IPAddress(new[] { response[pos], response[pos + 1], response[pos + 2], response[pos + 3] }));
@@ -207,7 +312,73 @@ namespace AndroidSideloader.Utilities
return addresses;
}
#region Local HTTP CONNECT Proxy for rclone
private static int SkipName(byte[] data, int pos)
{
int start = pos;
while (pos < data.Length && data[pos] != 0) pos += data[pos] + 1;
return pos - start + 1;
}
public static IPAddress ResolveHostname(string hostname, bool alwaysTryFallback = false)
{
if (_dnsCache.TryGetValue(hostname, out IPAddress cached))
return cached;
try
{
var addresses = Dns.GetHostAddresses(hostname);
if (addresses?.Length > 0)
{
_dnsCache[hostname] = addresses[0];
return addresses[0];
}
}
catch { }
if (alwaysTryFallback || UseFallbackDns || !_initialized)
{
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
_dnsCache[hostname] = ip;
return ip;
}
}
return null;
}
public static HttpWebRequest CreateWebRequest(string url)
{
var uri = new Uri(url);
if (!UseFallbackDns)
{
try
{
Dns.GetHostAddresses(uri.Host);
return (HttpWebRequest)WebRequest.Create(url);
}
catch
{
if (!_initialized) Initialize();
}
}
if (UseFallbackDns)
{
var ip = ResolveHostname(uri.Host, alwaysTryFallback: true);
if (ip != null)
{
var builder = new UriBuilder(uri) { Host = ip.ToString() };
var request = (HttpWebRequest)WebRequest.Create(builder.Uri);
request.Host = uri.Host;
return request;
}
}
return (HttpWebRequest)WebRequest.Create(url);
}
private static void StartProxy()
{
@@ -246,7 +417,7 @@ namespace AndroidSideloader.Utilities
try
{
var client = await _proxyListener.AcceptTcpClientAsync();
_ = Task.Run(() => HandleProxyClient(client, ct));
_ = HandleProxyClient(client, ct);
}
catch (ObjectDisposedException) { break; }
catch (Exception ex)
@@ -264,10 +435,8 @@ namespace AndroidSideloader.Utilities
using (client)
using (var stream = client.GetStream())
{
client.ReceiveTimeout = 30000;
client.SendTimeout = 30000;
client.ReceiveTimeout = client.SendTimeout = 30000;
// Read the HTTP request
var buffer = new byte[8192];
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, ct);
if (bytesRead == 0) return;
@@ -279,19 +448,12 @@ namespace AndroidSideloader.Utilities
string[] requestLine = lines[0].Split(' ');
if (requestLine.Length < 2) return;
string method = requestLine[0];
string target = requestLine[1];
if (method == "CONNECT")
{
if (requestLine[0] == "CONNECT")
// HTTPS proxy - tunnel mode
await HandleConnectRequest(stream, target, ct);
}
await HandleConnectRequest(stream, requestLine[1], ct);
else
{
// HTTP proxy - forward mode
await HandleHttpRequest(stream, request, target, ct);
}
await HandleHttpRequest(stream, request, requestLine[1], ct);
}
}
catch (Exception ex)
@@ -306,14 +468,13 @@ namespace AndroidSideloader.Utilities
// Parse host:port
string[] parts = target.Split(':');
string host = parts[0];
int port = parts.Length > 1 ? int.Parse(parts[1]) : 443;
int port = parts.Length > 1 && int.TryParse(parts[1], out int p) ? p : 443;
// Resolve hostname using our DNS
IPAddress ip = ResolveAnyHostname(host);
// Resolve hostname
IPAddress ip = ResolveHostname(host, alwaysTryFallback: true);
if (ip == null)
{
byte[] errorResponse = Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n");
await clientStream.WriteAsync(errorResponse, 0, errorResponse.Length, ct);
await SendResponse(clientStream, "HTTP/1.1 502 Bad Gateway\r\n\r\n", ct);
return;
}
@@ -326,21 +487,16 @@ namespace AndroidSideloader.Utilities
using (var targetStream = targetClient.GetStream())
{
// Send 200 OK to client
byte[] okResponse = Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n");
await clientStream.WriteAsync(okResponse, 0, okResponse.Length, ct);
await SendResponse(clientStream, "HTTP/1.1 200 Connection Established\r\n\r\n", ct);
// Tunnel data bidirectionally
var clientToTarget = RelayData(clientStream, targetStream, ct);
var targetToClient = RelayData(targetStream, clientStream, ct);
await Task.WhenAny(clientToTarget, targetToClient);
await Task.WhenAny(RelayData(clientStream, targetStream, ct), RelayData(targetStream, clientStream, ct));
}
}
}
catch (Exception ex)
{
Logger.Log($"CONNECT tunnel error to {host}: {ex.Message}", LogLevel.WARNING);
byte[] errorResponse = Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n");
try { await clientStream.WriteAsync(errorResponse, 0, errorResponse.Length, ct); } catch { }
await SendResponse(clientStream, "HTTP/1.1 502 Bad Gateway\r\n\r\n", ct);
}
}
@@ -349,19 +505,16 @@ namespace AndroidSideloader.Utilities
try
{
var uri = new Uri(url);
IPAddress ip = ResolveAnyHostname(uri.Host);
IPAddress ip = ResolveHostname(uri.Host, alwaysTryFallback: true);
if (ip == null)
{
byte[] errorResponse = Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n");
await clientStream.WriteAsync(errorResponse, 0, errorResponse.Length, ct);
await SendResponse(clientStream, "HTTP/1.1 502 Bad Gateway\r\n\r\n", ct);
return;
}
int port = uri.Port > 0 ? uri.Port : 80;
using (var targetClient = new TcpClient())
{
await targetClient.ConnectAsync(ip, port);
await targetClient.ConnectAsync(ip, uri.Port > 0 ? uri.Port : 80);
using (var targetStream = targetClient.GetStream())
{
// Modify request to use relative path
@@ -380,6 +533,12 @@ namespace AndroidSideloader.Utilities
}
}
private static async Task SendResponse(NetworkStream stream, string response, CancellationToken ct)
{
byte[] bytes = Encoding.ASCII.GetBytes(response);
try { await stream.WriteAsync(bytes, 0, bytes.Length, ct); } catch { }
}
private static async Task RelayData(NetworkStream from, NetworkStream to, CancellationToken ct)
{
byte[] buffer = new byte[8192];
@@ -387,105 +546,9 @@ namespace AndroidSideloader.Utilities
{
int bytesRead;
while ((bytesRead = await from.ReadAsync(buffer, 0, buffer.Length, ct)) > 0)
{
await to.WriteAsync(buffer, 0, bytesRead, ct);
}
}
catch { }
}
#endregion
public static IPAddress ResolveHostname(string hostname)
{
if (_dnsCache.TryGetValue(hostname, out IPAddress cached))
return cached;
try
{
var addresses = Dns.GetHostAddresses(hostname);
if (addresses != null && addresses.Length > 0)
{
_dnsCache[hostname] = addresses[0];
return addresses[0];
}
}
catch { }
if (_useFallbackDns || !_initialized)
{
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
_dnsCache[hostname] = ip;
return ip;
}
}
return null;
}
public static IPAddress ResolveAnyHostname(string hostname)
{
if (_dnsCache.TryGetValue(hostname, out IPAddress cached))
return cached;
try
{
var addresses = Dns.GetHostAddresses(hostname);
if (addresses != null && addresses.Length > 0)
{
_dnsCache[hostname] = addresses[0];
return addresses[0];
}
}
catch { }
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
_dnsCache[hostname] = ip;
return ip;
}
return null;
}
public static HttpWebRequest CreateWebRequest(string url)
{
var uri = new Uri(url);
if (!_useFallbackDns)
{
try
{
Dns.GetHostAddresses(uri.Host);
return (HttpWebRequest)WebRequest.Create(url);
}
catch
{
if (!_initialized) Initialize();
}
}
if (_useFallbackDns)
{
var ip = ResolveHostname(uri.Host);
if (ip == null)
{
ip = ResolveAnyHostname(uri.Host);
}
if (ip != null)
{
var builder = new UriBuilder(uri) { Host = ip.ToString() };
var request = (HttpWebRequest)WebRequest.Create(builder.Uri);
request.Host = uri.Host;
return request;
}
}
return (HttpWebRequest)WebRequest.Create(url);
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.IO;
using System.Threading;
namespace AndroidSideloader.Utilities
{
internal static class FileSystemUtilities
{
public static bool TryDeleteDirectory(string directoryPath, int maxRetries = 3, int delayMs = 150) // 3x 150ms = 450ms total
{
if (string.IsNullOrWhiteSpace(directoryPath))
return true;
if (!Directory.Exists(directoryPath))
return true;
Exception lastError = null;
// Retry deletion several times in case of lock ups
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
StripReadOnlyAttributes(directoryPath);
Directory.Delete(directoryPath, true);
return true;
}
catch (DirectoryNotFoundException)
{
return true;
}
catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException)
{
lastError = ex;
if (attempt < maxRetries)
{
Thread.Sleep(delayMs);
continue;
}
break;
}
catch (Exception ex)
{
// Non-retryable error
lastError = ex;
break;
}
}
// Last resort: rename then delete
try
{
string renamedPath = directoryPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ ".deleting." + DateTime.UtcNow.Ticks;
Directory.Move(directoryPath, renamedPath);
StripReadOnlyAttributes(renamedPath);
Directory.Delete(renamedPath, true);
return true;
}
catch (Exception ex)
{
lastError = ex;
}
Logger.Log($"Failed to delete directory: {directoryPath}. Error: {lastError}", LogLevel.WARNING);
return false;
}
private static void StripReadOnlyAttributes(string directoryPath)
{
var root = new DirectoryInfo(directoryPath);
if (!root.Exists) return;
root.Attributes &= ~FileAttributes.ReadOnly;
foreach (var dir in root.EnumerateDirectories("*", SearchOption.AllDirectories))
{
dir.Attributes &= ~FileAttributes.ReadOnly;
}
foreach (var file in root.EnumerateFiles("*", SearchOption.AllDirectories))
{
file.Attributes &= ~FileAttributes.ReadOnly;
}
}
}
}

View File

@@ -135,8 +135,24 @@ namespace AndroidSideloader.Utilities
public bool useProxy { get; set; } = false;
public string ProxyAddress { get; set; } = string.Empty;
public string ProxyPort { get; set; } = string.Empty;
public string selectedMirror { get; set; } = string.Empty;
public bool TrailersEnabled { get; set; } = true;
public bool UseGalleryView { get; set; } = true;
// Window state persistence
public int WindowX { get; set; } = -1;
public int WindowY { get; set; } = -1;
public int WindowWidth { get; set; } = -1;
public int WindowHeight { get; set; } = -1;
public bool WindowMaximized { get; set; } = false;
// Sort state persistence
public int SortColumn { get; set; } = 0;
public bool SortAscending { get; set; } = true;
// Download queue persistence
public string[] QueuedGames { get; set; } = new string[0];
private SettingsManager()
{
Load();
@@ -259,8 +275,17 @@ namespace AndroidSideloader.Utilities
useProxy = false;
ProxyAddress = string.Empty;
ProxyPort = string.Empty;
selectedMirror = string.Empty;
TrailersEnabled = true;
UseGalleryView = true;
WindowX = -1;
WindowY = -1;
WindowWidth = -1;
WindowHeight = -1;
WindowMaximized = false;
SortColumn = 0;
SortAscending = true;
QueuedGames = new string[0];
Save();
Debug.WriteLine("Default settings created.");
@@ -288,6 +313,16 @@ namespace AndroidSideloader.Utilities
}
}
public string GetEffectiveBackupDir()
{
if (CustomBackupDir && Directory.Exists(BackupDir))
{
return BackupDir;
}
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Rookie Backups");
}
public void Dispose()
{
FontStyle?.Dispose();

View File

@@ -61,5 +61,13 @@ namespace AndroidSideloader.Utilities
return s;
}
public static bool TryParseDouble(string value, out double result)
{
return double.TryParse(value,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out result);
}
}
}

View File

@@ -3,10 +3,7 @@ using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
namespace AndroidSideloader.Utilities
@@ -19,6 +16,11 @@ namespace AndroidSideloader.Utilities
internal class Zip
{
private static readonly SettingsManager settings = SettingsManager.Instance;
// Progress callback: (percent, eta)
public static Action<float, TimeSpan?> ExtractionProgressCallback { get; set; }
public static Action<string> ExtractionStatusCallback { get; set; }
public static void ExtractFile(string sourceArchive, string destination)
{
string args = $"x \"{sourceArchive}\" -y -o\"{destination}\" -bsp1";
@@ -33,6 +35,7 @@ namespace AndroidSideloader.Utilities
private static string extractionError = null;
private static bool errorMessageShown = false;
private static void DoExtract(string args)
{
if (!File.Exists(Path.Combine(Environment.CurrentDirectory, "7z.exe")) || !File.Exists(Path.Combine(Environment.CurrentDirectory, "7z.dll")))
@@ -68,24 +71,98 @@ namespace AndroidSideloader.Utilities
_ = Logger.Log($"Extract: 7z {string.Join(" ", args.Split(' ').Where(a => !a.StartsWith("-p")))}");
// Throttle percent reports
float lastReportedPercent = -1;
// ETA engine (percent units)
var etaEstimator = new EtaEstimator(alpha: 0.10, reanchorThreshold: 0.20, minSampleSeconds: 0.10);
// Smooth progress (sub-percent) interpolation (because 7z -bsp1 is integer-only)
System.Threading.Timer smoothTimer = null;
int extractingFlag = 1; // 1 = extracting, 0 = stop
float smoothLastTickPercent = 0f;
DateTime smoothLastTickTime = DateTime.UtcNow;
float smoothLastReported = -1f;
const int SmoothIntervalMs = 80; // ~12.5 updates/sec
const float SmoothReportDelta = 0.10f; // report only if change >= 0.10%
using (Process x = new Process())
{
x.StartInfo = pro;
if (MainForm.isInDownloadExtract && x != null)
{
// Smooth sub-percent UI, while keeping ETA ticking
smoothTimer = new System.Threading.Timer(_ =>
{
if (System.Threading.Volatile.Read(ref extractingFlag) == 0) return;
if (smoothLastTickPercent <= 0) return; // need at least one 7z tick
// Use current ETA to approximate seconds-per-percent
TimeSpan? displayEta = etaEstimator.GetDisplayEta();
if (!displayEta.HasValue) return; // Skip until ETA exists
var now = DateTime.UtcNow;
var elapsed = (now - smoothLastTickTime).TotalSeconds;
// Approx seconds-per-percent from remaining ETA / remaining percent
double remainingPercent = Math.Max(1.0, 100.0 - smoothLastTickPercent);
double spp = Math.Max(0.05, displayEta.Value.TotalSeconds / remainingPercent);
float candidate = smoothLastTickPercent + (float)(elapsed / spp);
// Clamp
float floorTick = (float)Math.Floor(smoothLastTickPercent);
float ceiling = Math.Min(99.99f, floorTick + 0.999f);
if (candidate > ceiling) candidate = ceiling;
if (candidate < smoothLastTickPercent) candidate = smoothLastTickPercent;
if (smoothLastReported >= 0 && Math.Abs(candidate - smoothLastReported) < SmoothReportDelta) return;
smoothLastReported = candidate;
try
{
MainForm mainForm = (MainForm)Application.OpenForms[0];
if (mainForm != null && !mainForm.IsDisposed)
{
mainForm.BeginInvoke((Action)(() => mainForm.SetProgress(candidate)));
}
}
catch { }
// ETA countdown ticks even if 7z percent is unchanged
ExtractionProgressCallback?.Invoke(candidate, etaEstimator.GetDisplayEta());
}, null, SmoothIntervalMs, SmoothIntervalMs);
x.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
{
var match = Regex.Match(e.Data, @"(\d+)%");
if (match.Success)
var match = Regex.Match(e.Data, @"^\s*(\d+)%");
if (match.Success && float.TryParse(match.Groups[1].Value, out float percent))
{
int progress = int.Parse(match.Groups[1].Value);
MainForm mainForm = (MainForm)Application.OpenForms[0];
if (mainForm != null)
// Update ETA from integer percent
if (percent <= 0.0f) etaEstimator.Reset();
else if (percent < 100.0f) etaEstimator.Update(totalUnits: 100, doneUnits: (long)Math.Round(percent));
// Reset smoothing baseline on each integer tick
smoothLastTickPercent = percent;
smoothLastTickTime = DateTime.UtcNow;
smoothLastReported = percent;
if (Math.Abs(percent - lastReportedPercent) >= 0.1f)
{
mainForm.Invoke((Action)(() => mainForm.SetProgress(progress)));
lastReportedPercent = percent;
MainForm mainForm = (MainForm)Application.OpenForms[0];
if (mainForm != null)
{
mainForm.Invoke((Action)(() => mainForm.SetProgress(percent)));
}
ExtractionProgressCallback?.Invoke(percent, etaEstimator.GetDisplayEta());
}
}
}
@@ -119,6 +196,16 @@ namespace AndroidSideloader.Utilities
x.BeginOutputReadLine();
x.BeginErrorReadLine();
x.WaitForExit();
// Stop smoother
System.Threading.Interlocked.Exchange(ref extractingFlag, 0);
smoothTimer?.Dispose();
smoothTimer = null;
// Clear callbacks
ExtractionProgressCallback?.Invoke(100, null);
ExtractionStatusCallback?.Invoke("");
errorMessageShown = false;
if (!string.IsNullOrEmpty(extractionError))

View File

@@ -1,16 +1,12 @@
RSL 3.0
RSL 3.0.1
Major Rookie overhaul with modernized UI, significant performance improvements and upgraded UX.
- Added high-performance Gallery View with search, filters, sorting, favorites, hover animations, smooth scrolling and uninstall buttons
- Toggle seamlessly between List and Gallery views with your preference remembered across launches
- Complete UI redesign with new dark theme, modernized components and subtle animations throughout
- Refined navigation, layouts, sizing and color consistency across the entire application
- Added uninstall buttons directly in List and Gallery views for quicker app management
- Improved startup performance through overhaul of initialization logic, removal of splash screen, parallelized async loading, batched version retrieval, optimized metadata extraction and game list initialization
- Instant list filtering through caching and streamlined filter logic (INSTALLED / UPDATE AVAILABLE / NEWER THAN LIST)
- Improved search speed and responsiveness
- Fixed and improved trailer handling with faster trailer loading
- Fixed multiple startup issues including connection errors and zombie ADB instances
- Added local blacklist support allowing users to permanently suppress donation prompts for specific apps
- Reduced application size by removal of now unused assets
- Fixed popularity ranking not working on some systems
- Fixed favorites not updating immediately when removing items
- Improved YouTube trailer matching accuracy
- Implemented real-time progress updates for drag and drop operations
- Refined backup button labels and dialogs
- Gallery View: Added grouped tiles for games with multiple versions (e.g. Beat Saber)
- ListView: Uninstall button now shows on hover instead of click
- Public config file is now created automatically without prompt
- Sideloading status label now shows device connection state
- Download button text now reflects sideloading status

View File

@@ -1 +1 @@
3.0
3.0.1