Compare commits

...

36 Commits

Author SHA1 Message Date
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
Fenopy
c48043a178 Merge pull request #271 from jp64k/RSL-3.0-2
Added efficient automatic Cloudflare DNS fallback with local proxy for RCLONE, fixed several error messages being opened behind mainwindow, refactored ShowError_QuotaExceeded() logic, fixed proxy settings parsing, fixed broken config preventing startup, fixed ampersands (&) not being rendered in selectedGameLabel and rookieStatusLabel
2025-12-15 06:15:31 -06:00
Fenopy
15f0c1ee72 chore: update proxy handling order
changed setRcloneProxy to default to user's proxy first.
2025-12-15 06:14:03 -06:00
jp64k
44df1666f4 Fixed ampersands (&) not being rendered in selectedGameLabel and rookieStatusLabel 2025-12-15 02:42:43 +01:00
jp64k
6cbfdbe52c Refactored file download logic to use DNS fallback for 7-zip and WebView2 downloads, fixed crash craused by corrupted user.config preventing startup
Refactored all file download logic to use DNS fallback and applied it to 7-zip and WebView2 runtime downloads. Moved WebView2 runtime download logic to GetDependencies and ensures it is downloaded at startup if missing. Added robust handling for corrupted user.config files in Program.cs, including auto-repair and fallback guidance.
2025-12-15 00:07:54 +01:00
jp64k
75d22ab504 Parse proxy settings only when proxy is enabled
Updated the applyButton_Click handler to parse and validate proxy address and port only if the proxy toggle is checked. This prevents unnecessary validation and error messages when the proxy is not enabled
2025-12-14 20:45:13 +01:00
jp64k
0c20841db3 Added efficient Cloudflare DNS fallback with local proxy for RCLONE
Introduced DnsHelper to detect system DNS failures and fall back to Cloudflare (1.1.1.1 / 1.0.0.1) DNS resolving, with caching and helpers for downloads. When fallback is required a lightweight local proxy is started and HTTP_PROXY/HTTPS_PROXY are set for spawned rclone processes so rclone uses the proxy’s DNS resolution; the proxy is cleaned up on exit. This finally resolves the very common ISP DNS blockage issues of users.
2025-12-14 20:25:48 +01:00
jp64k
b33251d98b Moved ShowError_QuotaExceeded() outside of try-catch, updated message, close application after showing the error
Moved quota exceeded check outside of try-catch block so it always runs the check. Updated the error message in ShowError_QuotaExceeded(). The application now exits after showing the error.
2025-12-14 18:45:21 +01:00
jp64k
3ef0652a85 Fixed several error messages being opened behind mainwindow
Updated all FlexibleMessageBox.Show invocations to include Program.form as the parent form. This ensures message boxes are properly parented to the main application window, putting them infront of the main window, instead of behind it.
2025-12-14 16:54:35 +01:00
35 changed files with 3198 additions and 41575 deletions

230
ADB.cs
View File

@@ -154,13 +154,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 +175,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,7 +264,6 @@ 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"))
{
@@ -241,7 +284,6 @@ namespace AndroidSideloader
if (cancelClicked)
return new ProcessOutput("", "Installation cancelled by user");
// Perform reinstall
statusCallback?.Invoke("Performing reinstall...");
try
@@ -250,26 +292,22 @@ 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}\"");
// 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/");
@@ -279,7 +317,7 @@ namespace AndroidSideloader
Directory.Delete(directoryToDelete, true);
}
progressCallback?.Invoke(100);
progressCallback?.Invoke(100, null);
return new ProcessOutput($"{gameName}: Reinstall: Success\n", "");
}
catch (Exception reinstallEx)
@@ -295,7 +333,7 @@ namespace AndroidSideloader
// 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 +356,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 +367,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 +387,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 +395,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 +440,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 +452,6 @@ namespace AndroidSideloader
catch (Exception ex)
{
Logger.Log($"CopyOBBWithProgressAsync error: {ex.Message}", LogLevel.ERROR);
return new ProcessOutput("", $"{gameName}: OBB transfer: Failed: {ex.Message}\n");
}
}
@@ -656,4 +710,102 @@ namespace AndroidSideloader
: new ProcessOutput("No OBB Folder found");
}
}
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)
{
_alpha = alpha;
_reanchorThreshold = reanchorThreshold;
_minSampleSeconds = minSampleSeconds;
Reset();
}
public void Reset()
{
_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,6 +190,7 @@
<Compile Include="GalleryView.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="ModernListView.cs" />
<Compile Include="ModernProgessBar.cs">
<SubType>Component</SubType>
</Compile>
@@ -239,6 +240,7 @@
<Compile Include="Sideloader\ProcessOutput.cs" />
<Compile Include="Sideloader\RCLONE.cs" />
<Compile Include="Sideloader\Utilities.cs" />
<Compile Include="Utilities\DnsHelper.cs" />
<Compile Include="Utilities\Logger.cs" />
<Compile Include="QuestForm.cs">
<SubType>Form</SubType>

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

@@ -170,7 +170,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 +198,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 +222,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 +259,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 +277,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;
@@ -843,6 +843,11 @@ 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;
SetDialogStartPosition(flexibleMessageBoxForm, owner);
return flexibleMessageBoxForm.ShowDialog(owner);

View File

@@ -6,6 +6,7 @@ using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public enum SortField { Name, LastUpdated, Size, Popularity }
@@ -51,7 +52,7 @@ public class FastGalleryPanel : Control
// Interaction
private int _hoveredIndex = -1;
private int _selectedIndex = -1;
public int _selectedIndex = -1;
private bool _isHoveringDeleteButton = false;
// Context Menu & Favorites
@@ -64,7 +65,7 @@ public class FastGalleryPanel : Control
// Visual constants
private const int CORNER_RADIUS = 10;
private const int THUMB_CORNER_RADIUS = 6;
private const int THUMB_CORNER_RADIUS = 8;
private const float HOVER_SCALE = 1.07f;
private const float ANIMATION_SPEED = 0.25f;
private const float SCROLL_SMOOTHING = 0.3f;
@@ -90,6 +91,24 @@ public class FastGalleryPanel : Control
public event EventHandler<int> TileRightClicked;
public event EventHandler<SortField> SortChanged;
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
[DllImport("uxtheme.dll", CharSet = CharSet.Unicode)]
private static extern int SetWindowTheme(IntPtr hwnd, string pszSubAppName, string pszSubIdList);
private void ApplyModernScrollbars()
{
if (_scrollBar == null || !_scrollBar.IsHandleCreated) return;
int dark = 1;
int hr = DwmSetWindowAttribute(_scrollBar.Handle, 20, ref dark, sizeof(int));
if (hr != 0)
DwmSetWindowAttribute(_scrollBar.Handle, 19, ref dark, sizeof(int));
if (SetWindowTheme(_scrollBar.Handle, "DarkMode_Explorer", null) != 0)
SetWindowTheme(_scrollBar.Handle, "Explorer", null);
}
private class TileAnimationState
{
public float Scale = 1.0f;
@@ -155,6 +174,8 @@ public class FastGalleryPanel : Control
_isScrolling = false;
Invalidate();
};
_scrollBar.HandleCreated += (s, e) => ApplyModernScrollbars();
Controls.Add(_scrollBar);
// Animation timer (~120fps)
@@ -353,10 +374,10 @@ public class FastGalleryPanel : Control
case SortField.Popularity:
if (_currentSortDirection == SortDirection.Ascending)
_items = _items.OrderBy(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "0"))
_items = _items.OrderByDescending(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "-"))
.ThenBy(i => i.Text, new GameNameComparer()).ToList();
else
_items = _items.OrderByDescending(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "0"))
_items = _items.OrderBy(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "-"))
.ThenBy(i => i.Text, new GameNameComparer()).ToList();
break;
}
@@ -378,12 +399,35 @@ public class FastGalleryPanel : Control
Invalidate();
}
private double ParsePopularity(string popStr)
private int ParsePopularity(string popStr)
{
if (double.TryParse(popStr?.Trim(), System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double pop))
return pop;
return 0;
if (string.IsNullOrEmpty(popStr))
return int.MaxValue; // Unranked goes to end
popStr = popStr.Trim();
// Handle new format: "#123" or "-"
if (popStr == "-")
{
return int.MaxValue; // Unranked items sort to the end
}
if (popStr.StartsWith("#"))
{
string numPart = popStr.Substring(1);
if (int.TryParse(numPart, out int rank))
{
return rank;
}
}
// Fallback: try parsing as raw number
if (int.TryParse(popStr, out int rawNum))
{
return rawNum;
}
return int.MaxValue; // Unparseable goes to end
}
// Custom sort to match list sort behaviour: '_' before digits, digits before letters (case-insensitive)
@@ -430,15 +474,54 @@ public class FastGalleryPanel : Control
private DateTime ParseDate(string dateStr)
{
if (string.IsNullOrEmpty(dateStr)) return DateTime.MinValue;
string datePart = dateStr.Split(' ')[0];
return DateTime.TryParse(datePart, out DateTime date) ? date : DateTime.MinValue;
string[] formats = { "yyyy-MM-dd HH:mm 'UTC'", "yyyy-MM-dd HH:mm" };
return DateTime.TryParseExact(
dateStr,
formats,
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal,
out DateTime date)
? date
: DateTime.MinValue;
}
private double ParseSize(string sizeStr)
{
if (double.TryParse(sizeStr?.Trim(), System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out double mb))
return mb;
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;
}
@@ -500,12 +583,12 @@ public class FastGalleryPanel : Control
int y = baseY - (scaledH - _tileHeight) / 2;
// Calculate thumbnail area
int thumbPadding = 4;
int thumbHeight = scaledH - 26; // Same as in DrawTile
int thumbPadding = 2;
int thumbHeight = scaledH - (thumbPadding * 2);
// Position delete button in bottom-right corner of thumbnail
int btnX = x + scaledW - DELETE_BUTTON_SIZE - thumbPadding - DELETE_BUTTON_MARGIN;
int btnY = y + thumbPadding + thumbHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN;
int btnY = y + thumbPadding + thumbHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN - 20;
return new Rectangle(btnX, btnY, DELETE_BUTTON_SIZE, DELETE_BUTTON_SIZE);
}
@@ -792,9 +875,26 @@ public class FastGalleryPanel : Control
}
// Thumbnail
int thumbPadding = 4;
int thumbHeight = scaledH - 26;
var thumbRect = new Rectangle(x + thumbPadding, y + thumbPadding, scaledW - thumbPadding * 2, thumbHeight);
int thumbPadding = 2;
int thumbHeight = scaledH - (thumbPadding * 2);
var thumbRect = new Rectangle(
x + thumbPadding,
y + thumbPadding,
scaledW - (thumbPadding * 2),
thumbHeight
);
// Base (non-scaled) thumbnail size for stable placeholder text layout
int baseThumbW = _tileWidth - (thumbPadding * 2);
int baseThumbH = _tileHeight - (thumbPadding * 2);
var baseThumbRect = new Rectangle(
thumbRect.X + (thumbRect.Width - baseThumbW) / 2,
thumbRect.Y + (thumbRect.Height - baseThumbH) / 2,
baseThumbW,
baseThumbH
);
string packageName = item.SubItems.Count > 2 ? item.SubItems[2].Text : "";
var thumbnail = GetCachedImage(packageName);
@@ -817,12 +917,24 @@ public class FastGalleryPanel : Control
{
using (var brush = new SolidBrush(Color.FromArgb(35, 35, 40)))
g.FillPath(brush, thumbPath);
using (var textBrush = new SolidBrush(Color.FromArgb(70, 70, 80)))
// Show game name when thumbnail is missing, centered
var nameRect = new Rectangle(baseThumbRect.X + 10, baseThumbRect.Y, baseThumbRect.Width - 20, baseThumbRect.Height);
using (var font = new Font("Segoe UI", 10f, FontStyle.Bold))
{
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
g.DrawString("🎮", new Font("Segoe UI Emoji", 18f), textBrush, thumbRect, sf);
var sfName = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter
};
using (var text = new SolidBrush(Color.FromArgb(110, 110, 120)))
g.DrawString(item.Text, font, text, nameRect, sfName);
}
}
g.Clip = oldClip;
}
@@ -861,7 +973,7 @@ public class FastGalleryPanel : Control
// Size badge (top right) - always visible
if (item.SubItems.Count > 5)
{
string sizeText = FormatSize(item.SubItems[5].Text);
string sizeText = item.SubItems[5].Text;
if (!string.IsNullOrEmpty(sizeText))
{
DrawRightAlignedBadge(g, sizeText, x + scaledW - thumbPadding - 4, rightBadgeY, 1.0f);
@@ -887,12 +999,40 @@ public class FastGalleryPanel : Control
}
// Game name
var nameRect = new Rectangle(x + 6, y + thumbHeight + thumbPadding, scaledW - 12, 20);
using (var font = new Font("Segoe UI Semibold", 8f))
using (var brush = new SolidBrush(TextColor))
if (state.TooltipOpacity > 0.01f)
{
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap };
g.DrawString(item.Text, font, brush, nameRect, sf);
int overlayH = 20;
var overlayRect = new Rectangle(thumbRect.X, thumbRect.Bottom - overlayH, thumbRect.Width, overlayH);
// Clip to the exact rounded thumbnail so the overlay corners match perfectly
Region oldClip = g.Clip;
using (var clipPath = CreateRoundedRectangle(thumbRect, THUMB_CORNER_RADIUS))
{
g.SetClip(clipPath, CombineMode.Intersect);
// Slightly overdraw to avoid 1px seams from AA / integer rounding
var fillRect = new Rectangle(overlayRect.X - 1, overlayRect.Y, overlayRect.Width + 2, overlayRect.Height + 1);
using (var overlayBrush = new SolidBrush(Color.FromArgb((int)(180 * state.TooltipOpacity), 0, 0, 0)))
g.FillRectangle(overlayBrush, fillRect);
g.Clip = oldClip;
}
using (var font = new Font("Segoe UI", 8f, FontStyle.Bold))
using (var brush = new SolidBrush(Color.FromArgb((int)(TextColor.A * state.TooltipOpacity), TextColor.R, TextColor.G, TextColor.B)))
{
var sf = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter,
FormatFlags = StringFormatFlags.NoWrap
};
var textRect = new Rectangle(overlayRect.X, overlayRect.Y + 1, overlayRect.Width, overlayRect.Height);
g.DrawString(item.Text, font, brush, textRect, sf);
}
}
}
@@ -900,7 +1040,7 @@ public class FastGalleryPanel : Control
{
// Position in bottom-right corner of thumbnail
int btnX = tileX + tileWidth - DELETE_BUTTON_SIZE - thumbPadding - DELETE_BUTTON_MARGIN;
int btnY = tileY + thumbPadding + thumbHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN;
int btnY = tileY + thumbPadding + thumbHeight - DELETE_BUTTON_SIZE - DELETE_BUTTON_MARGIN - 20;
var btnRect = new Rectangle(btnX, btnY, DELETE_BUTTON_SIZE, DELETE_BUTTON_SIZE);
int bgAlpha = (int)(opacity * 255);

106
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();
@@ -93,6 +91,7 @@ namespace AndroidSideloader
this.speedLabel_Tooltip = new System.Windows.Forms.ToolTip(this.components);
this.etaLabel_Tooltip = new System.Windows.Forms.ToolTip(this.components);
this.progressDLbtnContainer = new System.Windows.Forms.Panel();
this.progressBar = new AndroidSideloader.ModernProgressBar();
this.diskLabel = new System.Windows.Forms.Label();
this.questStorageProgressBar = new System.Windows.Forms.Panel();
this.batteryLevImg = new System.Windows.Forms.PictureBox();
@@ -172,60 +171,19 @@ namespace AndroidSideloader
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
//
@@ -310,10 +268,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 +289,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
//
@@ -808,14 +770,38 @@ 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;
//
// 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, 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;
//
// diskLabel
//
this.diskLabel.BackColor = System.Drawing.Color.Transparent;
@@ -1266,6 +1252,7 @@ namespace AndroidSideloader
this.rookieStatusLabel.Size = new System.Drawing.Size(225, 17);
this.rookieStatusLabel.TabIndex = 0;
this.rookieStatusLabel.Text = "Status";
this.rookieStatusLabel.UseMnemonic = false;
//
// sidebarMediaPanel
//
@@ -1320,6 +1307,7 @@ namespace AndroidSideloader
this.selectedGameLabel.Size = new System.Drawing.Size(217, 20);
this.selectedGameLabel.TabIndex = 99;
this.selectedGameLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
this.selectedGameLabel.UseMnemonic = false;
//
// tableLayoutPanel1
//
@@ -1644,7 +1632,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;
@@ -1749,5 +1736,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>

1205
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
}
}
}

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

@@ -8,7 +8,7 @@ namespace AndroidSideloader
{
internal static class Program
{
private static readonly SettingsManager settings = SettingsManager.Instance;
private static SettingsManager settings;
/// <summary>
/// The main entry point for the application.
/// </summary>
@@ -16,7 +16,54 @@ namespace AndroidSideloader
[SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)]
private static void Main()
{
// Handle corrupted user.config files
bool configFixed = false;
Exception configException = null;
try
{
// Force settings initialization to trigger any config errors early
var test = AndroidSideloader.Properties.Settings.Default.FontStyle;
}
catch (Exception ex)
{
configException = ex;
// Delete the corrupted config file and retry
try
{
string configPath = GetUserConfigPath();
if (!string.IsNullOrEmpty(configPath) && File.Exists(configPath))
{
File.Delete(configPath);
configFixed = true;
}
}
catch
{
// If we can't delete it, try to continue anyway
}
}
if (configFixed)
{
// Restart the application after fixing config
Application.Restart();
return;
}
if (configException != null)
{
MessageBox.Show(
"Settings file is corrupted and could not be repaired automatically.\n\n" +
"Please delete this folder and restart the application:\n" +
Path.GetDirectoryName(GetUserConfigPath()),
"Configuration Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return;
}
settings = SettingsManager.Instance;
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.UnhandledException += new UnhandledExceptionEventHandler(CrashHandler);
Application.EnableVisualStyles();
@@ -25,6 +72,23 @@ namespace AndroidSideloader
Application.Run(form);
//form.Show();
}
private static string GetUserConfigPath()
{
try
{
string appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string companyName = "Rookie.AndroidSideloader";
string exeName = "AndroidSideloader.exe_Url_dkp0unsd4fjaabhwwafgfxvvbrerf10b";
string version = "2.0.0.0";
return Path.Combine(appData, companyName, exeName, version, "user.config");
}
catch
{
return null;
}
}
public static MainForm form;
private static void CrashHandler(object sender, UnhandledExceptionEventArgs args)
@@ -37,10 +101,10 @@ namespace AndroidSideloader
string date_time = DateTime.Now.ToString("dddd, MMMM dd @ hh:mmtt (UTC)");
File.WriteAllText(Sideloader.CrashLogPath, $"Date/Time of crash: {date_time}\nMessage: {e.Message}\nInner Message: {innerExceptionMessage}\nData: {e.Data}\nSource: {e.Source}\nTargetSite: {e.TargetSite}\nStack Trace: \n{e.StackTrace}\n\n\nDebuglog: \n\n\n");
// If a debuglog exists we append it to the crashlog.
if (File.Exists(settings.CurrentLogPath))
if (settings != null && File.Exists(settings.CurrentLogPath))
{
File.AppendAllText(Sideloader.CrashLogPath, File.ReadAllText($"{settings.CurrentLogPath}"));
}
}
}
}
}

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

@@ -357,27 +357,35 @@ namespace AndroidSideloader
private static void setRcloneProxy()
{
// Use the user's proxy settings if set, otherwise fallback to DNS fallback proxy if active
string proxyUrl = DnsHelper.ProxyUrl;
if (settings.useProxy)
{
if (!rclone.StartInfo.EnvironmentVariables.ContainsKey("HTTP_PROXY")) {
rclone.StartInfo.EnvironmentVariables.Add("HTTP_PROXY", $"http://{settings.ProxyAddress}:{settings.ProxyPort}");
}
if (!rclone.StartInfo.EnvironmentVariables.ContainsKey("HTTPS_PROXY"))
{
rclone.StartInfo.EnvironmentVariables.Add("HTTPS_PROXY", $"http://{settings.ProxyAddress}:{settings.ProxyPort}");
}
// Use user's configured proxy
var url = $"http://{settings.ProxyAddress}:{settings.ProxyPort}";
rclone.StartInfo.EnvironmentVariables["HTTP_PROXY"] = url;
rclone.StartInfo.EnvironmentVariables["HTTPS_PROXY"] = url;
rclone.StartInfo.EnvironmentVariables["http_proxy"] = url;
rclone.StartInfo.EnvironmentVariables["https_proxy"] = url;
}
else if (!string.IsNullOrEmpty(proxyUrl))
{
// Use our DNS-resolving proxy
rclone.StartInfo.EnvironmentVariables["HTTP_PROXY"] = proxyUrl;
rclone.StartInfo.EnvironmentVariables["HTTPS_PROXY"] = proxyUrl;
rclone.StartInfo.EnvironmentVariables["http_proxy"] = proxyUrl;
rclone.StartInfo.EnvironmentVariables["https_proxy"] = proxyUrl;
}
else
{
if (rclone.StartInfo.EnvironmentVariables.ContainsKey("HTTP_PROXY"))
{
rclone.StartInfo.EnvironmentVariables.Remove("HTTP_PROXY");
}
if (rclone.StartInfo.EnvironmentVariables.ContainsKey("HTTPS_PROXY"))
{
rclone.StartInfo.EnvironmentVariables.Remove("HTTPS_PROXY");
}
// No proxy
rclone.StartInfo.EnvironmentVariables.Remove("HTTP_PROXY");
rclone.StartInfo.EnvironmentVariables.Remove("HTTPS_PROXY");
rclone.StartInfo.EnvironmentVariables.Remove("http_proxy");
rclone.StartInfo.EnvironmentVariables.Remove("https_proxy");
}
}
}
}

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)
@@ -139,7 +142,7 @@ namespace AndroidSideloader
private void applyButton_Click(object sender, EventArgs e)
{
//parsing bandwidth value
// Parse bandwidth value
var bandwidthInput = bandwidthLimitTextBox.Text;
Regex regex = new Regex(@"^\d+(\.\d+)?$");
@@ -153,38 +156,42 @@ namespace AndroidSideloader
return;
}
//parsing proxy address
var proxyAddressInput = proxyAddressTextBox.Text;
// Parse proxy values if proxy is enabled
if (toggleProxy.Checked)
{
// Parse proxy address
var proxyAddressInput = proxyAddressTextBox.Text;
if (proxyAddressInput.StartsWith("http://"))
{
proxyAddressInput = proxyAddressInput.Substring("http://".Length);
}
else if (proxyAddressInput.StartsWith("https://"))
{
proxyAddressInput = proxyAddressInput.Substring("https://".Length);
}
if (proxyAddressInput.StartsWith("http://"))
{
proxyAddressInput = proxyAddressInput.Substring("http://".Length);
}
else if (proxyAddressInput.StartsWith("https://"))
{
proxyAddressInput = proxyAddressInput.Substring("https://".Length);
}
if (proxyAddressInput.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
IPAddress.TryParse(proxyAddressInput, out _))
{
_settings.ProxyAddress = proxyAddressInput;
}
else
{
MessageBox.Show("Please enter a valid address for the proxy.");
}
if (proxyAddressInput.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
IPAddress.TryParse(proxyAddressInput, out _))
{
_settings.ProxyAddress = proxyAddressInput;
}
else
{
MessageBox.Show("Please enter a valid address for the proxy.");
}
//parsing proxy port
var proxyPortInput = proxyPortTextBox.Text;
// Parse proxy port
var proxyPortInput = proxyPortTextBox.Text;
if (ushort.TryParse(proxyPortInput, out _))
{
_settings.ProxyPort = proxyPortInput;
}
else
{
MessageBox.Show("Please enter a valid port for the proxy.");
if (ushort.TryParse(proxyPortInput, out _))
{
_settings.ProxyPort = proxyPortInput;
}
else
{
MessageBox.Show("Please enter a valid port for the proxy.");
}
}
SaveAllSettings();

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
using JR.Utils.GUI.Forms;
using AndroidSideloader.Utilities;
using JR.Utils.GUI.Forms;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -30,7 +31,7 @@ namespace AndroidSideloader
string resultString;
// Try fetching raw JSON data from the provided link
HttpWebRequest getUrl = (HttpWebRequest)WebRequest.Create(configUrl);
HttpWebRequest getUrl = DnsHelper.CreateWebRequest(configUrl);
using (StreamReader responseReader = new StreamReader(getUrl.GetResponse().GetResponseStream()))
{
resultString = responseReader.ReadToEnd();
@@ -44,7 +45,7 @@ namespace AndroidSideloader
_ = Logger.Log($"Failed to update public config from main: {mainException.Message}, trying fallback.", LogLevel.ERROR);
try
{
HttpWebRequest getUrl = (HttpWebRequest)WebRequest.Create(fallbackUrl);
HttpWebRequest getUrl = DnsHelper.CreateWebRequest(fallbackUrl);
using (StreamReader responseReader = new StreamReader(getUrl.GetResponse().GetResponseStream()))
{
string resultString = responseReader.ReadToEnd();
@@ -60,9 +61,12 @@ namespace AndroidSideloader
}
}
// Download required dependencies.
// Download required dependencies
public static void downloadFiles()
{
// Initialize DNS helper early to detect and configure fallback if needed
DnsHelper.Initialize();
WebClient client = new WebClient();
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
@@ -73,7 +77,7 @@ namespace AndroidSideloader
{
currentAccessedWebsite = "github";
_ = Logger.Log($"Missing 'Sideloader Launcher.exe'. Attempting to download from {currentAccessedWebsite}");
client.DownloadFile("https://github.com/VRPirates/rookie/raw/master/Sideloader%20Launcher.exe", "Sideloader Launcher.exe");
DownloadFileWithDnsFallback(client, "https://github.com/VRPirates/rookie/raw/master/Sideloader%20Launcher.exe", "Sideloader Launcher.exe");
_ = Logger.Log($"'Sideloader Launcher.exe' download successful");
}
@@ -81,7 +85,7 @@ namespace AndroidSideloader
{
currentAccessedWebsite = "github";
_ = Logger.Log($"Missing 'Rookie Offline.cmd'. Attempting to download from {currentAccessedWebsite}");
client.DownloadFile("https://github.com/VRPirates/rookie/raw/master/Rookie%20Offline.cmd", "Rookie Offline.cmd");
DownloadFileWithDnsFallback(client, "https://github.com/VRPirates/rookie/raw/master/Rookie%20Offline.cmd", "Rookie Offline.cmd");
_ = Logger.Log($"'Rookie Offline.cmd' download successful");
}
@@ -89,7 +93,7 @@ namespace AndroidSideloader
{
currentAccessedWebsite = "github";
_ = Logger.Log($"Missing 'CleanupInstall.cmd'. Attempting to download from {currentAccessedWebsite}");
client.DownloadFile("https://github.com/VRPirates/rookie/raw/master/CleanupInstall.cmd", "CleanupInstall.cmd");
DownloadFileWithDnsFallback(client, "https://github.com/VRPirates/rookie/raw/master/CleanupInstall.cmd", "CleanupInstall.cmd");
_ = Logger.Log($"'CleanupInstall.cmd' download successful");
}
@@ -97,13 +101,13 @@ namespace AndroidSideloader
{
currentAccessedWebsite = "github";
_ = Logger.Log($"Missing 'AddDefenderExceptions.ps1'. Attempting to download from {currentAccessedWebsite}");
client.DownloadFile("https://github.com/VRPirates/rookie/raw/master/AddDefenderExceptions.ps1", "AddDefenderExceptions.ps1");
DownloadFileWithDnsFallback(client, "https://github.com/VRPirates/rookie/raw/master/AddDefenderExceptions.ps1", "AddDefenderExceptions.ps1");
_ = Logger.Log($"'AddDefenderExceptions.ps1' download successful");
}
}
catch (Exception ex)
{
_ = FlexibleMessageBox.Show($"You are unable to access raw.githubusercontent.com with the Exception:\n{ex.Message}\n\nSome files may be missing (Offline/Cleanup Script, Launcher)");
_ = FlexibleMessageBox.Show(Program.form, $"You are unable to access raw.githubusercontent.com with the Exception:\n{ex.Message}\n\nSome files may be missing (Offline/Cleanup Script, Launcher)");
}
string adbPath = Path.Combine(Environment.CurrentDirectory, "platform-tools", "adb.exe");
@@ -120,7 +124,7 @@ namespace AndroidSideloader
currentAccessedWebsite = "github";
_ = Logger.Log($"Missing adb within {platformToolsDir}. Attempting to download from {currentAccessedWebsite}");
client.DownloadFile("https://github.com/VRPirates/rookie/raw/master/dependencies.7z", "dependencies.7z");
DownloadFileWithDnsFallback(client, "https://github.com/VRPirates/rookie/raw/master/dependencies.7z", "dependencies.7z");
Utilities.Zip.ExtractFile(Path.Combine(Environment.CurrentDirectory, "dependencies.7z"), platformToolsDir);
File.Delete("dependencies.7z");
_ = Logger.Log($"adb download successful");
@@ -128,25 +132,97 @@ namespace AndroidSideloader
}
catch (Exception ex)
{
_ = FlexibleMessageBox.Show($"You are unable to access raw.githubusercontent.com page with the Exception:\n{ex.Message}\n\nSome files may be missing (ADB)");
_ = FlexibleMessageBox.Show("ADB was unable to be downloaded\nRookie will now close.");
_ = FlexibleMessageBox.Show(Program.form, $"You are unable to access raw.githubusercontent.com page with the Exception:\n{ex.Message}\n\nSome files may be missing (ADB)");
_ = FlexibleMessageBox.Show(Program.form, "ADB was unable to be downloaded\nRookie will now close.");
Application.Exit();
}
string wantedRcloneVersion = "1.68.2";
string wantedRcloneVersion = "1.72.1";
bool rcloneSuccess = false;
rcloneSuccess = downloadRclone(wantedRcloneVersion, false);
if (!rcloneSuccess) {
if (!rcloneSuccess)
{
rcloneSuccess = downloadRclone(wantedRcloneVersion, true);
}
if (!rcloneSuccess) {
if (!rcloneSuccess)
{
_ = Logger.Log($"Unable to download rclone", LogLevel.ERROR);
_ = FlexibleMessageBox.Show("Rclone was unable to be downloaded\nRookie will now close, please use Offline Mode for manual sideloading if needed");
_ = FlexibleMessageBox.Show(Program.form, "Rclone was unable to be downloaded\nRookie will now close, please use Offline Mode for manual sideloading if needed");
Application.Exit();
}
// Download WebView2 runtime if needed
downloadWebView2Runtime();
}
// Downloads a file using the DNS fallback proxy if active
public static void DownloadFileWithDnsFallback(WebClient client, string url, string localPath)
{
try
{
// Use DNS fallback proxy if active
if (DnsHelper.UseFallbackDns && !string.IsNullOrEmpty(DnsHelper.ProxyUrl))
{
client.Proxy = new WebProxy(DnsHelper.ProxyUrl);
}
client.DownloadFile(url, localPath);
}
catch (Exception ex)
{
_ = Logger.Log($"Download failed for {url}: {ex.Message}", LogLevel.ERROR);
throw;
}
finally
{
// Reset proxy to avoid affecting other operations
client.Proxy = null;
}
}
// Overload that creates its own WebClient for convenience
public static void DownloadFileWithDnsFallback(string url, string localPath)
{
using (var client = new WebClient())
{
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
DownloadFileWithDnsFallback(client, url, localPath);
}
}
// Downloads WebView2 runtime if not present
private static void downloadWebView2Runtime()
{
string runtimesPath = Path.Combine(Environment.CurrentDirectory, "runtimes");
string webView2LoaderArm64 = Path.Combine(runtimesPath, "win-arm64", "native", "WebView2Loader.dll");
string webView2LoaderX86 = Path.Combine(runtimesPath, "win-x86", "native", "WebView2Loader.dll");
string webView2LoaderX64 = Path.Combine(runtimesPath, "win-x64", "native", "WebView2Loader.dll");
bool runtimeExists = File.Exists(webView2LoaderX86) || File.Exists(webView2LoaderX64) || File.Exists(webView2LoaderArm64);
if (!runtimeExists)
{
try
{
_ = Logger.Log("Missing WebView2 runtime. Attempting to download...");
string archivePath = Path.Combine(Environment.CurrentDirectory, "runtimes.7z");
DownloadFileWithDnsFallback("https://vrpirates.wiki/downloads/runtimes.7z", archivePath);
_ = Logger.Log("Extracting WebView2 runtime...");
Utilities.Zip.ExtractFile(archivePath, Environment.CurrentDirectory);
File.Delete(archivePath);
_ = Logger.Log("WebView2 runtime download successful");
}
catch (Exception ex)
{
_ = Logger.Log($"Failed to download WebView2 runtime: {ex.Message}", LogLevel.ERROR);
// Don't show message box here - let CreateEnvironment handle the UI feedback
}
}
}
public static bool downloadRclone(string wantedRcloneVersion, bool useFallback = false)
{
@@ -175,12 +251,15 @@ namespace AndroidSideloader
_ = Logger.Log($"RCLONE Version does not match ({currentRcloneVersion})! Downloading required version ({wantedRcloneVersion})");
}
}
} else {
}
else
{
updateRclone = true;
_ = Logger.Log($"RCLONE exe does not exist, attempting to download");
}
if (!Directory.Exists(dirRclone)) {
if (!Directory.Exists(dirRclone))
{
updateRclone = true;
_ = Logger.Log($"Missing RCLONE Folder, attempting to download");
@@ -203,14 +282,15 @@ namespace AndroidSideloader
string architecture = Environment.Is64BitOperatingSystem ? "amd64" : "386";
string url = $"https://downloads.rclone.org/v{wantedRcloneVersion}/rclone-v{wantedRcloneVersion}-windows-{architecture}.zip";
if (useFallback == true) {
if (useFallback == true)
{
_ = Logger.Log($"Using git fallback for rclone download");
url = $"https://raw.githubusercontent.com/VRPirates/rookie/master/dep/rclone-v{wantedRcloneVersion}-windows-{architecture}.zip";
}
_ = Logger.Log($"Downloading rclone from {url}");
_ = Logger.Log("Begin download rclone");
client.DownloadFile(url, "rclone.zip");
DownloadFileWithDnsFallback(client, url, "rclone.zip");
_ = Logger.Log("Complete download rclone");
_ = Logger.Log($"Extract {Environment.CurrentDirectory}\\rclone.zip");
@@ -250,4 +330,4 @@ namespace AndroidSideloader
}
}
}
}
}

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
@@ -215,7 +214,8 @@ namespace AndroidSideloader
{
string configUrl = "https://vrpirates.wiki/downloads/vrp.upload.config";
var getUrl = (HttpWebRequest)WebRequest.Create(configUrl);
// Use DnsHelper for fallback DNS support
var getUrl = DnsHelper.CreateWebRequest(configUrl);
using (var response = getUrl.GetResponse())
using (var stream = response.GetResponseStream())
using (var responseReader = new StreamReader(stream))

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

@@ -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

491
Utilities/DnsHelper.cs Normal file
View File

@@ -0,0 +1,491 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace AndroidSideloader.Utilities
{
// Provides DNS fallback functionality using Cloudflare DNS (1.1.1.1, 1.0.0.1) if system DNS fails to resolve critical hostnames
// Also provides a proxy for rclone that handles DNS resolution
public static class DnsHelper
{
private static readonly string[] FallbackDnsServers = { "1.1.1.1", "1.0.0.1" };
private static readonly string[] CriticalHostnames =
{
"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 _proxyRunning;
public static bool UseFallbackDns
{
get { if (!_initialized) Initialize(); return _useFallbackDns; }
}
// 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;
public static void Initialize()
{
lock (_lock)
{
if (_initialized) return;
Logger.Log("Testing DNS resolution for critical hostnames...");
if (!TestSystemDns())
{
Logger.Log("System DNS failed. Testing Cloudflare DNS fallback...", LogLevel.WARNING);
if (TestFallbackDns())
{
_useFallbackDns = true;
Logger.Log("Using Cloudflare DNS fallback.", LogLevel.INFO);
PreResolveHostnames();
ServicePointManager.DnsRefreshTimeout = 0;
// Start local proxy for rclone
StartProxy();
}
else
{
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();
}
private static void PreResolveHostnames()
{
foreach (string hostname in CriticalHostnames)
{
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);
}
}
}
private static bool TestSystemDns()
{
foreach (string hostname in CriticalHostnames)
{
try
{
var addresses = Dns.GetHostAddresses(hostname);
if (addresses == null || addresses.Length == 0) return false;
}
catch { return false; }
}
return true;
}
private static bool TestFallbackDns()
{
foreach (string dnsServer in FallbackDnsServers)
{
try
{
var addresses = ResolveWithDns(CriticalHostnames[0], dnsServer);
if (addresses != null && addresses.Count > 0) return true;
}
catch { }
}
return false;
}
private static IPAddress ResolveWithFallbackDns(string hostname)
{
foreach (string dnsServer in FallbackDnsServers)
{
try
{
var addresses = ResolveWithDns(hostname, dnsServer);
if (addresses != null && addresses.Count > 0)
return addresses[0];
}
catch { }
}
return null;
}
private static List<IPAddress> ResolveWithDns(string hostname, string dnsServer, int timeoutMs = 5000)
{
byte[] query = BuildDnsQuery(hostname);
using (var udp = new UdpClient())
{
udp.Client.ReceiveTimeout = timeoutMs;
udp.Client.SendTimeout = timeoutMs;
udp.Send(query, query.Length, new IPEndPoint(IPAddress.Parse(dnsServer), 53));
IPEndPoint remoteEp = null;
byte[] response = udp.Receive(ref remoteEp);
return ParseDnsResponse(response);
}
}
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('.'))
{
writer.Write((byte)label.Length);
writer.Write(Encoding.ASCII.GetBytes(label));
}
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++; }
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;
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] }));
pos += rdLength;
}
return addresses;
}
#region Local HTTP CONNECT Proxy for rclone
private static void StartProxy()
{
try
{
// Find an available port
_proxyListener = new TcpListener(IPAddress.Loopback, 0);
_proxyListener.Start();
_proxyPort = ((IPEndPoint)_proxyListener.LocalEndpoint).Port;
_proxyCts = new CancellationTokenSource();
_proxyRunning = true;
Logger.Log($"Started DNS proxy on port {_proxyPort}");
// Accept connections in background
Task.Run(() => ProxyAcceptLoop(_proxyCts.Token));
}
catch (Exception ex)
{
Logger.Log($"Failed to start DNS proxy: {ex.Message}", LogLevel.WARNING);
_proxyRunning = false;
}
}
private static void StopProxy()
{
_proxyRunning = false;
_proxyCts?.Cancel();
try { _proxyListener?.Stop(); } catch { }
}
private static async Task ProxyAcceptLoop(CancellationToken ct)
{
while (!ct.IsCancellationRequested && _proxyRunning)
{
try
{
var client = await _proxyListener.AcceptTcpClientAsync();
_ = Task.Run(() => HandleProxyClient(client, ct));
}
catch (ObjectDisposedException) { break; }
catch (Exception ex)
{
if (!ct.IsCancellationRequested)
Logger.Log($"Proxy accept error: {ex.Message}", LogLevel.WARNING);
}
}
}
private static async Task HandleProxyClient(TcpClient client, CancellationToken ct)
{
try
{
using (client)
using (var stream = client.GetStream())
{
client.ReceiveTimeout = 30000;
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;
string request = Encoding.ASCII.GetString(buffer, 0, bytesRead);
string[] lines = request.Split(new[] { "\r\n" }, StringSplitOptions.None);
if (lines.Length == 0) return;
string[] requestLine = lines[0].Split(' ');
if (requestLine.Length < 2) return;
string method = requestLine[0];
string target = requestLine[1];
if (method == "CONNECT")
{
// HTTPS proxy - tunnel mode
await HandleConnectRequest(stream, target, ct);
}
else
{
// HTTP proxy - forward mode
await HandleHttpRequest(stream, request, target, ct);
}
}
}
catch (Exception ex)
{
if (!ct.IsCancellationRequested)
Logger.Log($"Proxy client error: {ex.Message}", LogLevel.WARNING);
}
}
private static async Task HandleConnectRequest(NetworkStream clientStream, string target, CancellationToken ct)
{
// Parse host:port
string[] parts = target.Split(':');
string host = parts[0];
int port = parts.Length > 1 ? int.Parse(parts[1]) : 443;
// Resolve hostname using our DNS
IPAddress ip = ResolveAnyHostname(host);
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);
return;
}
try
{
// Connect to target
using (var targetClient = new TcpClient())
{
await targetClient.ConnectAsync(ip, port);
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);
// Tunnel data bidirectionally
var clientToTarget = RelayData(clientStream, targetStream, ct);
var targetToClient = RelayData(targetStream, clientStream, ct);
await Task.WhenAny(clientToTarget, targetToClient);
}
}
}
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 { }
}
}
private static async Task HandleHttpRequest(NetworkStream clientStream, string request, string url, CancellationToken ct)
{
try
{
var uri = new Uri(url);
IPAddress ip = ResolveAnyHostname(uri.Host);
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);
return;
}
int port = uri.Port > 0 ? uri.Port : 80;
using (var targetClient = new TcpClient())
{
await targetClient.ConnectAsync(ip, port);
using (var targetStream = targetClient.GetStream())
{
// Modify request to use relative path
string modifiedRequest = request.Replace(url, uri.PathAndQuery);
byte[] requestBytes = Encoding.ASCII.GetBytes(modifiedRequest);
await targetStream.WriteAsync(requestBytes, 0, requestBytes.Length, ct);
// Relay response
await RelayData(targetStream, clientStream, ct);
}
}
}
catch (Exception ex)
{
Logger.Log($"HTTP proxy error: {ex.Message}", LogLevel.WARNING);
}
}
private static async Task RelayData(NetworkStream from, NetworkStream to, CancellationToken ct)
{
byte[] buffer = new byte[8192];
try
{
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

@@ -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,22 +35,23 @@ 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")))
{
_ = Logger.Log("Begin download 7-zip");
WebClient client = new WebClient();
string architecture = Environment.Is64BitOperatingSystem ? "64" : "";
try
{
client.DownloadFile($"https://github.com/VRPirates/rookie/raw/master/7z{architecture}.exe", $"7z.exe");
client.DownloadFile($"https://github.com/VRPirates/rookie/raw/master/7z{architecture}.dll", $"7z.dll");
// Use DNS fallback download method from GetDependencies
GetDependencies.DownloadFileWithDnsFallback($"https://github.com/VRPirates/rookie/raw/master/7z{architecture}.exe", "7z.exe");
GetDependencies.DownloadFileWithDnsFallback($"https://github.com/VRPirates/rookie/raw/master/7z{architecture}.dll", "7z.dll");
}
catch (Exception ex)
{
_ = FlexibleMessageBox.Show($"You are unable to access the GitHub page with the Exception: {ex.Message}\nSome files may be missing (7z)");
_ = FlexibleMessageBox.Show("7z was unable to be downloaded\nRookie will now close");
_ = FlexibleMessageBox.Show(Program.form, $"You are unable to access the GitHub page with the Exception: {ex.Message}\nSome files may be missing (7z)");
_ = FlexibleMessageBox.Show(Program.form, "7z was unable to be downloaded\nRookie will now close");
Application.Exit();
}
_ = Logger.Log("Complete download 7-zip");
@@ -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))
@@ -127,7 +214,6 @@ namespace AndroidSideloader.Utilities
extractionError = null; // Reset the error message
throw new ExtractionException(errorMessage);
}
}
}
}