Merge pull request #283 from jp64k/RSL-3.0.4
Improved YouTube trailer search accuracy, added TryDeleteDirectory utility for all directory deletions, moved WebView2 cache from hardcoded path to app directory, replaced dated folder dialogs with modern FolderSelectDialog, improved backup/restore logic
This commit is contained in:
5
ADB.cs
5
ADB.cs
@@ -357,7 +357,7 @@ namespace AndroidSideloader
|
||||
string directoryToDelete = Path.Combine(Environment.CurrentDirectory, packagename);
|
||||
if (Directory.Exists(directoryToDelete) && directoryToDelete != Environment.CurrentDirectory)
|
||||
{
|
||||
Directory.Delete(directoryToDelete, true);
|
||||
FileSystemUtilities.TryDeleteDirectory(directoryToDelete);
|
||||
}
|
||||
|
||||
progressCallback?.Invoke(100, null);
|
||||
@@ -689,7 +689,6 @@ namespace AndroidSideloader
|
||||
|
||||
if (out2.Contains("failed"))
|
||||
{
|
||||
_ = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||||
_ = Logger.Log(out2);
|
||||
|
||||
if (out2.Contains("offline") && !settings.NodeviceMode)
|
||||
@@ -732,7 +731,7 @@ namespace AndroidSideloader
|
||||
{
|
||||
if (directoryToDelete != Environment.CurrentDirectory)
|
||||
{
|
||||
Directory.Delete(directoryToDelete, true);
|
||||
FileSystemUtilities.TryDeleteDirectory(directoryToDelete);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,7 @@
|
||||
<Compile Include="Sideloader\RCLONE.cs" />
|
||||
<Compile Include="Sideloader\Utilities.cs" />
|
||||
<Compile Include="Utilities\DnsHelper.cs" />
|
||||
<Compile Include="Utilities\FileSystemUtilities.cs" />
|
||||
<Compile Include="Utilities\Logger.cs" />
|
||||
<Compile Include="QuestForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
|
||||
8
MainForm.Designer.cs
generated
8
MainForm.Designer.cs
generated
@@ -478,9 +478,9 @@ namespace AndroidSideloader
|
||||
this.backupbutton.Padding = new System.Windows.Forms.Padding(30, 0, 0, 0);
|
||||
this.backupbutton.Size = new System.Drawing.Size(233, 28);
|
||||
this.backupbutton.TabIndex = 1;
|
||||
this.backupbutton.Text = "BACKUP GAMEDATA";
|
||||
this.backupbutton.Text = "BACKUP GAMESAVES";
|
||||
this.backupbutton.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
this.backupbutton_Tooltip.SetToolTip(this.backupbutton, "Save game and apps data to the sideloader folder (Does not save APKs or OBBs)");
|
||||
this.backupbutton_Tooltip.SetToolTip(this.backupbutton, "Save game and apps data to the backup folder (Does not save APKs or OBBs)");
|
||||
this.backupbutton.UseVisualStyleBackColor = false;
|
||||
this.backupbutton.Click += new System.EventHandler(this.backupbutton_Click);
|
||||
//
|
||||
@@ -498,9 +498,9 @@ namespace AndroidSideloader
|
||||
this.restorebutton.Padding = new System.Windows.Forms.Padding(30, 0, 0, 0);
|
||||
this.restorebutton.Size = new System.Drawing.Size(233, 28);
|
||||
this.restorebutton.TabIndex = 0;
|
||||
this.restorebutton.Text = "RESTORE GAMEDATA";
|
||||
this.restorebutton.Text = "RESTORE GAMESAVES";
|
||||
this.restorebutton.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
this.restorebutton_Tooltip.SetToolTip(this.restorebutton, "Restore game and apps data to the device (Use BACKUP GAMEDATA first)");
|
||||
this.restorebutton_Tooltip.SetToolTip(this.restorebutton, "Restore game and apps data to the device (Use BACKUP GAMESAVES first)");
|
||||
this.restorebutton.UseVisualStyleBackColor = false;
|
||||
this.restorebutton.Click += new System.EventHandler(this.restorebutton_Click);
|
||||
//
|
||||
|
||||
681
MainForm.cs
681
MainForm.cs
@@ -434,6 +434,20 @@ namespace AndroidSideloader
|
||||
|
||||
changeTitle(isOffline ? "Starting in Offline Mode..." : "Initializing...");
|
||||
|
||||
// Non-blocking WebView cleanup
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string webViewDirectoryPath = Path.Combine(Environment.CurrentDirectory, "WebView2Cache");
|
||||
if (Directory.Exists(webViewDirectoryPath))
|
||||
{
|
||||
FileSystemUtilities.TryDeleteDirectory(webViewDirectoryPath);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
|
||||
// Non-blocking background cleanup
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
@@ -441,7 +455,7 @@ namespace AndroidSideloader
|
||||
{
|
||||
if (Directory.Exists(Sideloader.TempFolder))
|
||||
{
|
||||
Directory.Delete(Sideloader.TempFolder, true);
|
||||
FileSystemUtilities.TryDeleteDirectory(Sideloader.TempFolder);
|
||||
_ = Directory.CreateDirectory(Sideloader.TempFolder);
|
||||
}
|
||||
}
|
||||
@@ -588,20 +602,6 @@ namespace AndroidSideloader
|
||||
}
|
||||
}
|
||||
|
||||
// WebView cleanup in background
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string webViewDirectoryPath = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "RSL", "EBWebView");
|
||||
if (Directory.Exists(webViewDirectoryPath))
|
||||
{
|
||||
Directory.Delete(webViewDirectoryPath, true);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
|
||||
// Pre-initialize trailer player in background
|
||||
try
|
||||
{
|
||||
@@ -831,7 +831,8 @@ namespace AndroidSideloader
|
||||
try
|
||||
{
|
||||
string titleSuffix = string.IsNullOrWhiteSpace(txt) ? "" : " | " + txt;
|
||||
this.Invoke(() => {
|
||||
this.Invoke(() =>
|
||||
{
|
||||
Text = "Rookie Sideloader " + Updater.LocalVersion + titleSuffix;
|
||||
rookieStatusLabel.Text = txt;
|
||||
});
|
||||
@@ -843,8 +844,9 @@ namespace AndroidSideloader
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
// Reset to base title without any status message
|
||||
this.Invoke(() => {
|
||||
Text = "Rookie Sideloader " + Updater.LocalVersion;
|
||||
this.Invoke(() =>
|
||||
{
|
||||
Text = "Rookie Sideloader " + Updater.LocalVersion;
|
||||
rookieStatusLabel.Text = "";
|
||||
});
|
||||
}
|
||||
@@ -985,14 +987,16 @@ namespace AndroidSideloader
|
||||
|
||||
output = await ADB.CopyOBBWithProgressAsync(
|
||||
path,
|
||||
(progress, eta) => this.Invoke(() => {
|
||||
(progress, eta) => this.Invoke(() =>
|
||||
{
|
||||
progressBar.Value = progress;
|
||||
string etaStr = eta.HasValue && eta.Value.TotalSeconds > 0
|
||||
? $" · ETA: {eta.Value:mm\\:ss}"
|
||||
: "";
|
||||
speedLabel.Text = $"Progress: {progress}%{etaStr}";
|
||||
}),
|
||||
status => this.Invoke(() => {
|
||||
status => this.Invoke(() =>
|
||||
{
|
||||
progressBar.StatusText = status;
|
||||
}),
|
||||
folderName);
|
||||
@@ -1120,181 +1124,325 @@ namespace AndroidSideloader
|
||||
return deviceId ?? string.Empty;
|
||||
}
|
||||
|
||||
public static string taa = String.Empty;
|
||||
|
||||
private async void backupadbbutton_Click(object sender, EventArgs e)
|
||||
{
|
||||
string selectedApp = ShowInstalledAppSelector("Select an app to backup with ADB");
|
||||
if (selectedApp == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (selectedApp == null) return;
|
||||
|
||||
if (!settings.CustomBackupDir)
|
||||
backupFolder = settings.GetEffectiveBackupDir();
|
||||
string date_str = "ab." + DateTime.Today.ToString("yyyy.MM.dd");
|
||||
string CurrBackups = Path.Combine(backupFolder, date_str);
|
||||
|
||||
Directory.CreateDirectory(CurrBackups);
|
||||
|
||||
string packageName = Sideloader.gameNameToPackageName(selectedApp);
|
||||
string backupFile = Path.Combine(CurrBackups, $"{packageName}.ab");
|
||||
|
||||
FlexibleMessageBox.Show(Program.form,
|
||||
$"Backing up {selectedApp} to:\n{backupFile}\n\nClick OK, then on your Quest:\n1. Unlock device\n2. Click 'Back Up My Data'");
|
||||
|
||||
changeTitle($"Backing up {selectedApp}...");
|
||||
progressBar.IsIndeterminate = true;
|
||||
|
||||
var output = await Task.Run(() =>
|
||||
ADB.RunAdbCommandToString($"backup -f \"{backupFile}\" {packageName}")
|
||||
);
|
||||
|
||||
progressBar.IsIndeterminate = false;
|
||||
changeTitle("");
|
||||
|
||||
// Success = file exists, has content, no errors
|
||||
bool fileExists = File.Exists(backupFile);
|
||||
bool hasContent = fileExists && new FileInfo(backupFile).Length > 0;
|
||||
bool hasErrors = !string.IsNullOrEmpty(output.Error);
|
||||
|
||||
if (hasContent && !hasErrors)
|
||||
{
|
||||
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||||
Logger.Log($"Successfully backed up {selectedApp} to {backupFile}", LogLevel.INFO);
|
||||
FlexibleMessageBox.Show(Program.form,
|
||||
$"Backup successful!\n\nApp: {selectedApp}\nFile: {backupFile}\nSize: {new FileInfo(backupFile).Length / 1024} KB",
|
||||
"Backup Complete");
|
||||
}
|
||||
else
|
||||
{
|
||||
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||||
// Cleanup failed backup file
|
||||
if (File.Exists(backupFile))
|
||||
File.Delete(backupFile);
|
||||
|
||||
string errorMsg = hasErrors ? output.Error : "No backup data created";
|
||||
Logger.Log($"Failed to backup {selectedApp}: {errorMsg}", LogLevel.ERROR);
|
||||
FlexibleMessageBox.Show(Program.form,
|
||||
$"Backup failed!\n\nApp: {selectedApp}\nError: {errorMsg}",
|
||||
"Backup Failed");
|
||||
}
|
||||
if (!Directory.Exists(backupFolder))
|
||||
{
|
||||
_ = Directory.CreateDirectory(backupFolder);
|
||||
}
|
||||
string output = String.Empty;
|
||||
|
||||
string date_str = "ab." + DateTime.Today.ToString("yyyy.MM.dd");
|
||||
string CurrBackups = Path.Combine(backupFolder, date_str);
|
||||
Program.form.Invoke(new Action(() =>
|
||||
{
|
||||
FlexibleMessageBox.Show(Program.form, $"Backing up Game Data to {backupFolder}\\{date_str}");
|
||||
}));
|
||||
_ = Directory.CreateDirectory(CurrBackups);
|
||||
|
||||
string GameName = selectedApp;
|
||||
string packageName = Sideloader.gameNameToPackageName(GameName);
|
||||
string InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {packageName} | grep versionCode -F\"").Output;
|
||||
|
||||
changeTitle("Running ADB Backup...");
|
||||
_ = FlexibleMessageBox.Show(Program.form, "Click OK on this Message...\r\nThen on your Quest, Unlock your device and confirm the backup operation by clicking on 'Back Up My Data'");
|
||||
output = ADB.RunAdbCommandToString($"adb backup -f \"{CurrBackups}\\{packageName}.ab\" {packageName}").Output;
|
||||
|
||||
changeTitle("");
|
||||
}
|
||||
|
||||
private async void backupbutton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!settings.CustomBackupDir)
|
||||
{
|
||||
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||||
}
|
||||
else
|
||||
{
|
||||
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||||
}
|
||||
if (!Directory.Exists(backupFolder))
|
||||
{
|
||||
_ = Directory.CreateDirectory(backupFolder);
|
||||
}
|
||||
DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, $"Do you want to backup to {backupFolder}?", "Backup?", MessageBoxButtons.YesNo);
|
||||
if (dialogResult1 == DialogResult.No)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ProcessOutput output = new ProcessOutput(String.Empty, String.Empty);
|
||||
Thread t1 = new Thread(() =>
|
||||
{
|
||||
string date_str = DateTime.Today.ToString("yyyy.MM.dd");
|
||||
string CurrBackups = Path.Combine(backupFolder, date_str);
|
||||
Program.form.Invoke(new Action(() =>
|
||||
{
|
||||
FlexibleMessageBox.Show(Program.form, $"This may take up to a minute. Backing up gamesaves to {backupFolder}\\{date_str} (year.month.date)");
|
||||
}));
|
||||
_ = Directory.CreateDirectory(CurrBackups);
|
||||
output = ADB.RunAdbCommandToString($"pull \"/sdcard/Android/data\" \"{CurrBackups}\"");
|
||||
changeTitle("Backing up Game Data in SD/Android/data...");
|
||||
try
|
||||
{
|
||||
Directory.Move(ADB.adbFolderPath + "\\data", CurrBackups + "\\data");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ = Logger.Log($"Exception on backup: {ex}", LogLevel.ERROR);
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
t1.Start();
|
||||
backupFolder = settings.GetEffectiveBackupDir();
|
||||
string date_str = DateTime.Today.ToString("yyyy.MM.dd");
|
||||
string CurrBackups = Path.Combine(backupFolder, date_str);
|
||||
|
||||
while (t1.IsAlive)
|
||||
DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form,
|
||||
$"Do you want to backup all gamesaves to:\n{CurrBackups}\\",
|
||||
"Backup Gamesaves",
|
||||
MessageBoxButtons.YesNo);
|
||||
|
||||
if (dialogResult1 == DialogResult.No || dialogResult1 == DialogResult.Cancel) return;
|
||||
|
||||
Directory.CreateDirectory(CurrBackups); // Create parent dir if needed
|
||||
|
||||
changeTitle("Backing up gamesaves...");
|
||||
progressBar.IsIndeterminate = true;
|
||||
progressBar.OperationType = "Backing Up";
|
||||
|
||||
var successList = new List<string>();
|
||||
var failedList = new List<string>();
|
||||
int totalGames = 0;
|
||||
int processedGames = 0;
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
changeTitle("Backing up Game Data in SD/Android/data...");
|
||||
}
|
||||
ShowPrcOutput(output);
|
||||
// Get all game folders in /sdcard/Android/data
|
||||
var listOutput = ADB.RunAdbCommandToString("shell ls -1 /sdcard/Android/data", suppressLogging: true);
|
||||
|
||||
if (string.IsNullOrEmpty(listOutput.Output) || !string.IsNullOrEmpty(listOutput.Error))
|
||||
{
|
||||
Logger.Log($"Failed to list game folders: {listOutput.Error}", LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
var gameFolders = listOutput.Output
|
||||
.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(f => !string.IsNullOrWhiteSpace(f) && f.Contains("."))
|
||||
.Select(f => f.Trim())
|
||||
.ToList();
|
||||
|
||||
totalGames = gameFolders.Count;
|
||||
|
||||
foreach (var gameFolder in gameFolders)
|
||||
{
|
||||
processedGames++;
|
||||
this.Invoke(() => changeTitle($"Backing up {gameFolder} ({processedGames}/{totalGames})..."));
|
||||
|
||||
string gamePath = $"/sdcard/Android/data/{gameFolder}";
|
||||
string backupPath = Path.Combine(CurrBackups, gameFolder);
|
||||
|
||||
var pullOutput = ADB.RunAdbCommandToString($"pull \"{gamePath}\" \"{backupPath}\"", suppressLogging: true);
|
||||
|
||||
// Success = no errors and has content
|
||||
bool hasContent = Directory.Exists(backupPath) && Directory.GetFileSystemEntries(backupPath).Length > 0;
|
||||
bool hasErrors = !string.IsNullOrEmpty(pullOutput.Error);
|
||||
|
||||
if (hasContent && !hasErrors)
|
||||
{
|
||||
successList.Add(gameFolder);
|
||||
Logger.Log($"Successfully backed up: {gameFolder}", LogLevel.INFO);
|
||||
}
|
||||
else if (hasErrors)
|
||||
{
|
||||
// Cleanup empty/failed directory
|
||||
if (Directory.Exists(backupPath))
|
||||
Directory.Delete(backupPath, true);
|
||||
|
||||
failedList.Add($"{gameFolder}: {pullOutput.Error.Split('\n')[0].Trim()}");
|
||||
Logger.Log($"Failed to backup {gameFolder}: {pullOutput.Error}", LogLevel.WARNING);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No content but no errors = app has no save data (not a failure)
|
||||
if (Directory.Exists(backupPath))
|
||||
Directory.Delete(backupPath, true);
|
||||
|
||||
Logger.Log($"No save data for: {gameFolder}", LogLevel.INFO);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
progressBar.IsIndeterminate = false;
|
||||
changeTitle("");
|
||||
|
||||
// Build summary
|
||||
var summary = new StringBuilder();
|
||||
summary.AppendLine($"Backup completed to:\n{CurrBackups}\\\n");
|
||||
summary.AppendLine($"Successfully backed up: {successList.Count} games");
|
||||
|
||||
if (failedList.Count > 0)
|
||||
{
|
||||
summary.AppendLine($"Failed to backup: {failedList.Count} games\n");
|
||||
summary.AppendLine("Failed games:");
|
||||
foreach (var failed in failedList)
|
||||
summary.AppendLine($" • {failed}");
|
||||
}
|
||||
|
||||
FlexibleMessageBox.Show(Program.form, summary.ToString(), "Backup Complete");
|
||||
}
|
||||
|
||||
private async void restorebutton_Click(object sender, EventArgs e)
|
||||
{
|
||||
ProcessOutput output = new ProcessOutput("", "");
|
||||
string output_abRestore = string.Empty;
|
||||
backupFolder = settings.GetEffectiveBackupDir();
|
||||
|
||||
if (!settings.CustomBackupDir)
|
||||
// Create restore method dialog
|
||||
string restoreMethod = null;
|
||||
using (Form dialog = new Form())
|
||||
{
|
||||
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||||
}
|
||||
else
|
||||
{
|
||||
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||||
}
|
||||
dialog.Text = "Restore Gamesaves";
|
||||
dialog.Size = new Size(340, 130);
|
||||
dialog.StartPosition = FormStartPosition.CenterParent;
|
||||
dialog.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
dialog.MaximizeBox = false;
|
||||
dialog.MinimizeBox = false;
|
||||
dialog.BackColor = Color.FromArgb(20, 24, 29);
|
||||
dialog.ForeColor = Color.White;
|
||||
|
||||
|
||||
FileDialog fileDialog = new OpenFileDialog();
|
||||
fileDialog.Title = "Select a .ab Backup file or press Cancel to select a Folder";
|
||||
fileDialog.CheckFileExists = true;
|
||||
fileDialog.CheckPathExists = true;
|
||||
fileDialog.ValidateNames = false;
|
||||
fileDialog.InitialDirectory = backupFolder;
|
||||
fileDialog.Filter = "Android Backup Files (*.ab)|*.ab|All Files (*.*)|*.*";
|
||||
|
||||
FolderBrowserDialog folderDialog = new FolderBrowserDialog();
|
||||
folderDialog.Description = "Select Game Backup folder";
|
||||
folderDialog.SelectedPath = backupFolder;
|
||||
folderDialog.ShowNewFolderButton = false; // To prevent creating new folders
|
||||
|
||||
DialogResult fileDialogResult = fileDialog.ShowDialog();
|
||||
DialogResult folderDialogResult = DialogResult.Cancel;
|
||||
|
||||
if (fileDialogResult == DialogResult.OK)
|
||||
{
|
||||
string selectedPath = fileDialog.FileName;
|
||||
Logger.Log("Selected .ab file: " + selectedPath);
|
||||
|
||||
_ = FlexibleMessageBox.Show(Program.form, "Click OK on this Message...\r\nThen on your Quest, Unlock your device and confirm the backup operation by clicking on 'Restore My Data'\r\nRookie will remain frozen until the process is completed.");
|
||||
output_abRestore = ADB.RunAdbCommandToString($"adb restore \"{selectedPath}\"").Output;
|
||||
}
|
||||
if (fileDialogResult != DialogResult.OK)
|
||||
{
|
||||
folderDialogResult = folderDialog.ShowDialog();
|
||||
}
|
||||
|
||||
if (folderDialogResult == DialogResult.OK)
|
||||
{
|
||||
string selectedFolder = folderDialog.SelectedPath;
|
||||
Logger.Log("Selected folder: " + selectedFolder);
|
||||
|
||||
Thread t1 = new Thread(() =>
|
||||
var label = new Label
|
||||
{
|
||||
if (selectedFolder.Contains("data"))
|
||||
{
|
||||
output += ADB.RunAdbCommandToString($"push \"{selectedFolder}\" /sdcard/Android/");
|
||||
}
|
||||
else
|
||||
{
|
||||
output += ADB.RunAdbCommandToString($"push \"{selectedFolder}\" /sdcard/Android/data/");
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true
|
||||
Text = "Choose restore source:",
|
||||
ForeColor = Color.White,
|
||||
AutoSize = true,
|
||||
Location = new Point(15, 15)
|
||||
};
|
||||
t1.Start();
|
||||
|
||||
while (t1.IsAlive)
|
||||
var btnFolder = CreateStyledButton("From Folder", DialogResult.None, new Point(15, 45));
|
||||
btnFolder.Size = new Size(145, 32);
|
||||
btnFolder.Click += (s, ev) => { restoreMethod = "folder"; dialog.DialogResult = DialogResult.OK; };
|
||||
|
||||
var btnAbFile = CreateStyledButton("From .ab File", DialogResult.None, new Point(170, 45));
|
||||
btnAbFile.Size = new Size(145, 32);
|
||||
btnAbFile.Click += (s, ev) => { restoreMethod = "ab"; dialog.DialogResult = DialogResult.OK; };
|
||||
|
||||
dialog.Controls.AddRange(new Control[] { label, btnFolder, btnAbFile });
|
||||
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK || restoreMethod == null) return;
|
||||
}
|
||||
|
||||
// .ab file restore
|
||||
if (restoreMethod == "ab")
|
||||
{
|
||||
using (var fileDialog = new OpenFileDialog())
|
||||
{
|
||||
await Task.Delay(100);
|
||||
fileDialog.Title = "Select Android Backup (.ab) file";
|
||||
fileDialog.InitialDirectory = backupFolder;
|
||||
fileDialog.Filter = "Android Backup Files (*.ab)|*.ab|All Files (*.*)|*.*";
|
||||
|
||||
if (fileDialog.ShowDialog() != DialogResult.OK) return;
|
||||
|
||||
Logger.Log($"Selected .ab file: {fileDialog.FileName}");
|
||||
FlexibleMessageBox.Show(Program.form,
|
||||
"Click OK, then on your Quest:\n1. Unlock device\n2. Confirm 'Restore My Data'");
|
||||
|
||||
var output = ADB.RunAdbCommandToString($"restore \"{fileDialog.FileName}\"");
|
||||
FlexibleMessageBox.Show(Program.form,
|
||||
string.IsNullOrEmpty(output.Error) ? "Restore completed" : $"Restore result:\n{output.Output}\n{output.Error}",
|
||||
"Restore Complete");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (folderDialogResult == DialogResult.OK)
|
||||
// Folder restore: find newest date folder to preselect
|
||||
string initialPath = backupFolder;
|
||||
if (Directory.Exists(backupFolder))
|
||||
{
|
||||
ShowPrcOutput(output);
|
||||
var newestDateFolder = Directory.GetDirectories(backupFolder)
|
||||
.Select(d => new DirectoryInfo(d))
|
||||
.Where(d => Regex.IsMatch(d.Name, @"^\d{4}\.\d{2}\.\d{2}$"))
|
||||
.OrderByDescending(d => d.Name)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (newestDateFolder != null)
|
||||
initialPath = newestDateFolder.FullName;
|
||||
}
|
||||
else if (fileDialogResult == DialogResult.OK)
|
||||
|
||||
using (var folderDialog = new FolderBrowserDialog())
|
||||
{
|
||||
_ = FlexibleMessageBox.Show(Program.form, $"{output_abRestore}");
|
||||
folderDialog.Description = "Select a date folder (e.g., 2026.01.01) to restore ALL gamesaves,\nor a specific game folder (e.g., com.game.name) to restore just that game.";
|
||||
folderDialog.SelectedPath = initialPath;
|
||||
folderDialog.ShowNewFolderButton = false;
|
||||
|
||||
if (folderDialog.ShowDialog() != DialogResult.OK) return;
|
||||
|
||||
string selectedFolder = folderDialog.SelectedPath;
|
||||
string folderName = Path.GetFileName(selectedFolder);
|
||||
Logger.Log($"Selected folder: {selectedFolder}");
|
||||
|
||||
// Determine if this is a date folder or a single game folder
|
||||
bool isDateFolder = Regex.IsMatch(folderName, @"^\d{4}\.\d{2}\.\d{2}$");
|
||||
bool isGameFolder = folderName.Contains(".") && !isDateFolder;
|
||||
|
||||
List<string> gameFoldersToRestore;
|
||||
|
||||
if (isGameFolder)
|
||||
{
|
||||
// Single game folder selected: restore just this one
|
||||
gameFoldersToRestore = new List<string> { folderName };
|
||||
// Parent folder becomes the source
|
||||
selectedFolder = Path.GetDirectoryName(selectedFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Date folder or other: get all game subfolders
|
||||
gameFoldersToRestore = Directory.GetDirectories(selectedFolder)
|
||||
.Select(Path.GetFileName)
|
||||
.Where(f => !string.IsNullOrWhiteSpace(f) && f.Contains("."))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (gameFoldersToRestore.Count == 0)
|
||||
{
|
||||
FlexibleMessageBox.Show(Program.form, "No game folders found in the selected directory.", "Nothing to Restore");
|
||||
return;
|
||||
}
|
||||
|
||||
changeTitle("Restoring gamesaves...");
|
||||
progressBar.IsIndeterminate = true;
|
||||
progressBar.OperationType = "Restoring";
|
||||
|
||||
var successList = new List<string>();
|
||||
var failedList = new List<string>();
|
||||
int totalGames = gameFoldersToRestore.Count;
|
||||
int processedGames = 0;
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach (var gameFolder in gameFoldersToRestore)
|
||||
{
|
||||
processedGames++;
|
||||
this.Invoke(() => changeTitle($"Restoring {gameFolder} ({processedGames}/{totalGames})..."));
|
||||
|
||||
string sourcePath = Path.Combine(selectedFolder, gameFolder);
|
||||
string targetPath = $"/sdcard/Android/data/{gameFolder}";
|
||||
|
||||
var pushOutput = ADB.RunAdbCommandToString($"push \"{sourcePath}\" \"{targetPath}\"", suppressLogging: true);
|
||||
|
||||
if (string.IsNullOrEmpty(pushOutput.Error))
|
||||
{
|
||||
successList.Add(gameFolder);
|
||||
Logger.Log($"Successfully restored: {gameFolder}", LogLevel.INFO);
|
||||
}
|
||||
else
|
||||
{
|
||||
failedList.Add($"{gameFolder}: {pushOutput.Error.Split('\n')[0].Trim()}");
|
||||
Logger.Log($"Failed to restore {gameFolder}: {pushOutput.Error}", LogLevel.WARNING);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
progressBar.IsIndeterminate = false;
|
||||
changeTitle("");
|
||||
|
||||
var summary = new StringBuilder();
|
||||
summary.AppendLine($"Restore completed from:\n{selectedFolder}\\\n");
|
||||
summary.AppendLine($"Successfully restored: {successList.Count} game(s)");
|
||||
|
||||
if (failedList.Count > 0)
|
||||
{
|
||||
summary.AppendLine($"Failed to restore: {failedList.Count} game(s)\n");
|
||||
summary.AppendLine("Failed games:");
|
||||
foreach (var failed in failedList)
|
||||
summary.AppendLine($" • {failed}");
|
||||
}
|
||||
|
||||
FlexibleMessageBox.Show(Program.form, summary.ToString(), "Restore Complete");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1489,7 +1637,7 @@ namespace AndroidSideloader
|
||||
File.Delete($"{settings.MainDir}\\{gameZipName}");
|
||||
|
||||
this.Invoke(() => FlexibleMessageBox.Show(Program.form, $"Upload of {currentlyUploading} is complete! Thank you for your contribution!"));
|
||||
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{packageName}");
|
||||
})
|
||||
{
|
||||
IsBackground = true
|
||||
@@ -1525,14 +1673,7 @@ namespace AndroidSideloader
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings.CustomBackupDir)
|
||||
{
|
||||
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||||
}
|
||||
else
|
||||
{
|
||||
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||||
}
|
||||
backupFolder = settings.GetEffectiveBackupDir();
|
||||
|
||||
string packagename;
|
||||
string GameName = selectedApp;
|
||||
@@ -1750,7 +1891,7 @@ namespace AndroidSideloader
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
Directory.Delete($"{zippath}\\{datazip}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{zippath}\\{datazip}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1907,7 +2048,7 @@ namespace AndroidSideloader
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
Directory.Delete(foldername, true);
|
||||
FileSystemUtilities.TryDeleteDirectory(foldername);
|
||||
changeTitle("");
|
||||
}
|
||||
// BMBF Zip extraction then push to BMBF song folder on Quest.
|
||||
@@ -1937,7 +2078,7 @@ namespace AndroidSideloader
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
Directory.Delete($"{zippath}\\{datazip}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{zippath}\\{datazip}");
|
||||
}
|
||||
else if (extension == ".txt")
|
||||
{
|
||||
@@ -2635,7 +2776,7 @@ namespace AndroidSideloader
|
||||
_ = ADB.RunCommandToString(cmd, path);
|
||||
if (Directory.Exists($"{settings.MainDir}\\{game.Pckgcommand}"))
|
||||
{
|
||||
Directory.Delete($"{settings.MainDir}\\{game.Pckgcommand}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{game.Pckgcommand}");
|
||||
}
|
||||
Program.form.changeTitle("Uploading to server, you can continue to use Rookie while it uploads.");
|
||||
|
||||
@@ -3740,7 +3881,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
// only delete after extraction; allows for resume if the fetch fails midway.
|
||||
if (Directory.Exists($"{settings.DownloadDir}\\{gameName}"))
|
||||
{
|
||||
Directory.Delete($"{settings.DownloadDir}\\{gameName}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{settings.DownloadDir}\\{gameName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3909,14 +4050,14 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
if (UsingPublicConfig)
|
||||
{
|
||||
if (Directory.Exists($"{settings.DownloadDir}\\{cancelledHash}"))
|
||||
Directory.Delete($"{settings.DownloadDir}\\{cancelledHash}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{settings.DownloadDir}\\{cancelledHash}");
|
||||
if (Directory.Exists($"{settings.DownloadDir}\\{cancelledGame}"))
|
||||
Directory.Delete($"{settings.DownloadDir}\\{cancelledGame}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{settings.DownloadDir}\\{cancelledGame}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Directory.Exists($"{settings.DownloadDir}\\{cancelledGame}"))
|
||||
Directory.Delete($"{settings.DownloadDir}\\{cancelledGame}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{settings.DownloadDir}\\{cancelledGame}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4024,7 +4165,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
|
||||
if (Directory.Exists($"{settings.DownloadDir}\\{gameNameHash}"))
|
||||
{
|
||||
Directory.Delete($"{settings.DownloadDir}\\{gameNameHash}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{settings.DownloadDir}\\{gameNameHash}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4099,7 +4240,8 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
// Use async method with progress
|
||||
output += await ADB.SideloadWithProgressAsync(
|
||||
apkFile,
|
||||
(progress, eta) => this.Invoke(() => {
|
||||
(progress, eta) => this.Invoke(() =>
|
||||
{
|
||||
if (progress == 0)
|
||||
{
|
||||
progressBar.IsIndeterminate = true;
|
||||
@@ -4113,7 +4255,8 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
UpdateProgressStatus("Installing", percent: (int)Math.Round(progress), eta: eta);
|
||||
progressBar.StatusText = $"Installing · {progress:0.0}%";
|
||||
}),
|
||||
status => this.Invoke(() => {
|
||||
status => this.Invoke(() =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
{
|
||||
if (status.Contains("Completing Installation"))
|
||||
@@ -4192,7 +4335,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
if (settings.DeleteAllAfterInstall && !nodeviceonstart && DeviceConnected)
|
||||
{
|
||||
changeTitle("Deleting game files");
|
||||
try { Directory.Delete(settings.DownloadDir + "\\" + gameName, true); } catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}"); }
|
||||
try { FileSystemUtilities.TryDeleteDirectory(settings.DownloadDir + "\\" + gameName); } catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}"); }
|
||||
}
|
||||
|
||||
// Update device space after successful installation
|
||||
@@ -4405,7 +4548,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
|
||||
timerticked = false;
|
||||
if (Directory.Exists(Path.Combine(Environment.CurrentDirectory, CurrPCKG)))
|
||||
{
|
||||
Directory.Delete(Path.Combine(Environment.CurrentDirectory, CurrPCKG), true);
|
||||
FileSystemUtilities.TryDeleteDirectory(Path.Combine(Environment.CurrentDirectory, CurrPCKG));
|
||||
}
|
||||
|
||||
changeTitle("");
|
||||
@@ -5454,7 +5597,8 @@ CTRL + F4 - Instantly relaunch Rookie Sideloader");
|
||||
|
||||
try
|
||||
{
|
||||
var appDataFolder = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "RSL");
|
||||
var appDataFolder = Path.Combine(Environment.CurrentDirectory, "WebView2Cache");
|
||||
Directory.CreateDirectory(appDataFolder); // Ensure it exists
|
||||
var env = await CoreWebView2Environment.CreateAsync(userDataFolder: appDataFolder);
|
||||
|
||||
await webView21.EnsureCoreWebView2Async(env);
|
||||
@@ -5622,30 +5766,153 @@ function onYouTubeIframeAPIReady() {
|
||||
if (_videoIdCache.TryGetValue(gameName, out var cached))
|
||||
return cached;
|
||||
|
||||
// Lightweight search
|
||||
string cleanedName = CleanGameNameForSearch(gameName);
|
||||
string searchTerm = $"{cleanedName} VR trailer";
|
||||
|
||||
try
|
||||
{
|
||||
string query = WebUtility.UrlEncode($"\"{gameName}\" VR trailer");
|
||||
string searchUrl = $"https://www.youtube.com/results?search_query={query}";
|
||||
using (var http = new HttpClient())
|
||||
{
|
||||
http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/119.0");
|
||||
http.Timeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
string query = WebUtility.UrlEncode(searchTerm);
|
||||
string searchUrl = $"https://www.youtube.com/results?search_query={query}";
|
||||
|
||||
var html = await http.GetStringAsync(searchUrl);
|
||||
var vid = ExtractVideoId(html);
|
||||
if (!string.IsNullOrEmpty(vid))
|
||||
var videoId = ExtractBestVideoId(html, cleanedName);
|
||||
|
||||
if (!string.IsNullOrEmpty(videoId))
|
||||
{
|
||||
_videoIdCache[gameName] = vid;
|
||||
return vid;
|
||||
_videoIdCache[gameName] = videoId;
|
||||
return videoId;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// swallow – return empty
|
||||
// swallow
|
||||
}
|
||||
|
||||
// Cache empty result to prevent repeated lookups
|
||||
_videoIdCache[gameName] = string.Empty;
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string CleanGameNameForSearch(string gameName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(gameName))
|
||||
return gameName;
|
||||
|
||||
// Clean up game name, remove:
|
||||
string[] patternsToRemove = new[]
|
||||
{
|
||||
@"\s*\([^)]+\)", // (anything in parentheses)
|
||||
@"\s*\[[^\]]*\]", // [anything in brackets]
|
||||
@"\s+v?\d+\.\d+[\d.]*\b", // version numbers. v1.0, 1.35.0, 1.37.0 etc.
|
||||
};
|
||||
|
||||
string cleaned = gameName;
|
||||
foreach (string pattern in patternsToRemove)
|
||||
cleaned = Regex.Replace(cleaned, pattern, "", RegexOptions.IgnoreCase);
|
||||
|
||||
// Clean up trailing punctuation and whitespace
|
||||
cleaned = Regex.Replace(cleaned, @"[-:,]+$", "");
|
||||
cleaned = Regex.Replace(cleaned, @"\s+", " ").Trim();
|
||||
|
||||
// If cleaning removed everything, return original
|
||||
return string.IsNullOrWhiteSpace(cleaned) ? gameName.Trim() : cleaned;
|
||||
}
|
||||
|
||||
private static readonly Regex VideoDataRegex = new Regex(
|
||||
@"""videoRenderer""\s*:\s*\{\s*[^}]*?""videoId""\s*:\s*""([A-Za-z0-9_\-]{11})""[\s\S]*?""title""\s*:\s*\{\s*""runs""\s*:\s*\[\s*\{\s*""text""\s*:\s*""([^""]+)""",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex UnicodeEscapeRegex = new Regex(
|
||||
@"\\u([0-9A-Fa-f]{4})",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static string ExtractBestVideoId(string html, string cleanedGameName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return string.Empty;
|
||||
|
||||
var videoMatches = VideoDataRegex.Matches(html);
|
||||
|
||||
// Fallback: no matches found, do simple extraction
|
||||
if (videoMatches.Count == 0)
|
||||
{
|
||||
var simpleMatch = Regex.Match(html, @"\/watch\?v=([A-Za-z0-9_\-]{11})");
|
||||
return simpleMatch.Success ? simpleMatch.Groups[1].Value : string.Empty;
|
||||
}
|
||||
|
||||
// Prepare game name words for matching
|
||||
string lowerGameName = cleanedGameName.ToLowerInvariant();
|
||||
var gameWords = lowerGameName
|
||||
.Split(new[] { ' ', '-', ':', '&' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
|
||||
int requiredMatches = Math.Max(1, gameWords.Count / 2);
|
||||
string bestVideoId = null;
|
||||
int bestScore = 0;
|
||||
int position = 0;
|
||||
|
||||
// Score each match
|
||||
foreach (Match match in videoMatches)
|
||||
{
|
||||
string videoId = match.Groups[1].Value;
|
||||
string title = match.Groups[2].Value.ToLowerInvariant();
|
||||
|
||||
title = UnicodeEscapeRegex.Replace(title, m =>
|
||||
((char)Convert.ToInt32(m.Groups[1].Value, 16)).ToString());
|
||||
|
||||
// Entry must match at least half the game name
|
||||
int matchedWords = gameWords.Count(w => title.Contains(w));
|
||||
if (matchedWords < requiredMatches)
|
||||
continue;
|
||||
|
||||
position++;
|
||||
|
||||
// Only process first 5 matches
|
||||
if (position > 5)
|
||||
break;
|
||||
|
||||
int score = matchedWords * 10;
|
||||
|
||||
// Position bonus
|
||||
if (position == 1) score += 30;
|
||||
else if (position == 2) score += 20;
|
||||
else if (position == 3) score += 10;
|
||||
|
||||
// Word bonus
|
||||
if (title.Contains("trailer")) score += 20;
|
||||
if (title.Contains("official") || title.Contains("launch") || title.Contains("release")) score += 15;
|
||||
if (title.Contains("announce")) score += 12; // also includes "announcement"
|
||||
if (title.Contains("gameplay") || title.Contains("vr")) score += 5;
|
||||
|
||||
// Noise penalty for extra words
|
||||
int totalWords = title.Split(new[] { ' ', '-', '|', ':', '–' },
|
||||
StringSplitOptions.RemoveEmptyEntries).Length;
|
||||
int extraWords = totalWords - gameWords.Count;
|
||||
score += extraWords * -3; // -3 per extra word
|
||||
|
||||
// Hard penalties for junk
|
||||
if (title.Contains("review") ||
|
||||
title.Contains("tutorial") ||
|
||||
title.Contains("how to") ||
|
||||
title.Contains("reaction"))
|
||||
score -= 30;
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestVideoId = videoId;
|
||||
}
|
||||
}
|
||||
|
||||
return bestVideoId ?? string.Empty;
|
||||
}
|
||||
|
||||
public async void gamesListView_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
// Hide the uninstall button initially
|
||||
@@ -5930,7 +6197,7 @@ function onYouTubeIframeAPIReady() {
|
||||
ulong VersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode));
|
||||
if (Directory.Exists($"{settings.MainDir}\\{packageName}"))
|
||||
{
|
||||
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{packageName}");
|
||||
}
|
||||
|
||||
ProcessOutput output = new ProcessOutput("", "");
|
||||
@@ -5996,7 +6263,7 @@ function onYouTubeIframeAPIReady() {
|
||||
|
||||
File.Copy($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip", $"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip");
|
||||
File.Delete($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip");
|
||||
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{packageName}");
|
||||
isworking = false;
|
||||
changeTitle("");
|
||||
progressBar.IsIndeterminate = false;
|
||||
@@ -6263,7 +6530,7 @@ function onYouTubeIframeAPIReady() {
|
||||
|
||||
gamesListView.SelectedItems.Clear();
|
||||
_rightClickedItem.Selected = true;
|
||||
|
||||
|
||||
UpdateFavoriteMenuItemText();
|
||||
favoriteGame.Show(gamesListView, e.Location);
|
||||
}
|
||||
@@ -6371,7 +6638,9 @@ function onYouTubeIframeAPIReady() {
|
||||
if (string.IsNullOrEmpty(firmware))
|
||||
{
|
||||
firmware = string.Empty;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
firmware = Utilities.StringUtilities.RemoveEverythingBeforeFirst(firmware, "-v");
|
||||
firmware = Utilities.StringUtilities.KeepOnlyNumbers(firmware);
|
||||
}
|
||||
@@ -6383,7 +6652,7 @@ function onYouTubeIframeAPIReady() {
|
||||
long totalSpace = 0;
|
||||
long usedSpace = 0;
|
||||
long freeSpace = 0;
|
||||
|
||||
|
||||
if (lines.Length > 1)
|
||||
{
|
||||
string[] parts = lines[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
@@ -8000,24 +8269,17 @@ function onYouTubeIframeAPIReady() {
|
||||
|
||||
// Confirm uninstall
|
||||
DialogResult dialogresult = FlexibleMessageBox.Show(
|
||||
$"Are you sure you want to uninstall {gameName}?",
|
||||
$"Are you sure you want to uninstall {gameName}?",
|
||||
"Proceed with uninstall?", MessageBoxButtons.YesNo);
|
||||
|
||||
if (dialogresult == DialogResult.No)
|
||||
return;
|
||||
|
||||
// Ask about backup
|
||||
if (!settings.CustomBackupDir)
|
||||
{
|
||||
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||||
}
|
||||
else
|
||||
{
|
||||
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||||
}
|
||||
backupFolder = settings.GetEffectiveBackupDir();
|
||||
|
||||
DialogResult dialogresult2 = FlexibleMessageBox.Show(
|
||||
$"Do you want to attempt to automatically backup any saves to {backupFolder}\\{DateTime.Today.ToString("yyyy.MM.dd")}\\",
|
||||
$"Do you want to attempt to automatically backup any saves to {backupFolder}\\{DateTime.Today.ToString("yyyy.MM.dd")}\\",
|
||||
"Attempt Game Backup?", MessageBoxButtons.YesNo);
|
||||
|
||||
if (dialogresult2 == DialogResult.Yes)
|
||||
@@ -8030,7 +8292,8 @@ function onYouTubeIframeAPIReady() {
|
||||
progressBar.IsIndeterminate = true;
|
||||
progressBar.OperationType = "";
|
||||
|
||||
await Task.Run(() => {
|
||||
await Task.Run(() =>
|
||||
{
|
||||
output += Sideloader.UninstallGame(packageName);
|
||||
});
|
||||
|
||||
|
||||
@@ -327,20 +327,33 @@ namespace AndroidSideloader
|
||||
|
||||
private void setDownloadDirectory_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (downloadDirectorySetter.ShowDialog() == DialogResult.OK)
|
||||
var dialog = new FolderSelectDialog
|
||||
{
|
||||
Title = "Select Download Folder",
|
||||
InitialDirectory = _settings.CustomDownloadDir && Directory.Exists(_settings.DownloadDir)
|
||||
? _settings.DownloadDir
|
||||
: Environment.CurrentDirectory
|
||||
};
|
||||
|
||||
if (dialog.Show(this.Handle))
|
||||
{
|
||||
_settings.CustomDownloadDir = true;
|
||||
_settings.DownloadDir = downloadDirectorySetter.SelectedPath;
|
||||
_settings.DownloadDir = dialog.FileName;
|
||||
}
|
||||
}
|
||||
|
||||
private void setBackupDirectory_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (backupDirectorySetter.ShowDialog() == DialogResult.OK)
|
||||
var dialog = new FolderSelectDialog
|
||||
{
|
||||
Title = "Select Backup Folder",
|
||||
InitialDirectory = _settings.GetEffectiveBackupDir()
|
||||
};
|
||||
|
||||
if (dialog.Show(this.Handle))
|
||||
{
|
||||
_settings.CustomBackupDir = true;
|
||||
_settings.BackupDir = backupDirectorySetter.SelectedPath;
|
||||
MainForm.backupFolder = _settings.BackupDir;
|
||||
_settings.BackupDir = dialog.FileName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,9 +375,7 @@ namespace AndroidSideloader
|
||||
|
||||
private void openBackupDirectory_Click(object sender, EventArgs e)
|
||||
{
|
||||
string pathToOpen = _settings.CustomBackupDir
|
||||
? Path.Combine(_settings.BackupDir, "Rookie Backups")
|
||||
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Rookie Backups");
|
||||
string pathToOpen = _settings.GetEffectiveBackupDir();
|
||||
MainForm.OpenDirectory(pathToOpen);
|
||||
}
|
||||
|
||||
|
||||
@@ -136,14 +136,7 @@ namespace AndroidSideloader
|
||||
|
||||
public static void BackupGame(string packagename)
|
||||
{
|
||||
if (!settings.CustomBackupDir)
|
||||
{
|
||||
MainForm.backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
|
||||
}
|
||||
else
|
||||
{
|
||||
MainForm.backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
|
||||
}
|
||||
MainForm.backupFolder = settings.GetEffectiveBackupDir();
|
||||
if (!Directory.Exists(MainForm.backupFolder))
|
||||
{
|
||||
_ = Directory.CreateDirectory(MainForm.backupFolder);
|
||||
@@ -204,7 +197,7 @@ namespace AndroidSideloader
|
||||
|
||||
if (Directory.Exists($"{settings.MainDir}\\{packageName}"))
|
||||
{
|
||||
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
|
||||
FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{packageName}");
|
||||
}
|
||||
|
||||
_ = Directory.CreateDirectory($"{settings.MainDir}\\{packageName}");
|
||||
|
||||
@@ -309,7 +309,7 @@ namespace AndroidSideloader
|
||||
}
|
||||
File.Move(file, destFile);
|
||||
}
|
||||
Directory.Delete(dirExtractedRclone, true);
|
||||
FileSystemUtilities.TryDeleteDirectory(dirExtractedRclone);
|
||||
|
||||
// Restore vrp.download.config if it was backed up
|
||||
if (hasConfig && File.Exists(tempConfigPath))
|
||||
|
||||
@@ -290,7 +290,7 @@ namespace AndroidSideloader
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
FileSystemUtilities.TryDeleteDirectory(path);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
91
Utilities/FileSystemUtilities.cs
Normal file
91
Utilities/FileSystemUtilities.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace AndroidSideloader.Utilities
|
||||
{
|
||||
internal static class FileSystemUtilities
|
||||
{
|
||||
public static bool TryDeleteDirectory(string directoryPath, int maxRetries = 3, int delayMs = 150) // 3x 150ms = 450ms total
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||
return true;
|
||||
|
||||
if (!Directory.Exists(directoryPath))
|
||||
return true;
|
||||
|
||||
Exception lastError = null;
|
||||
|
||||
// Retry deletion several times in case of lock ups
|
||||
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
StripReadOnlyAttributes(directoryPath);
|
||||
Directory.Delete(directoryPath, true);
|
||||
return true;
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException)
|
||||
{
|
||||
lastError = ex;
|
||||
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
Thread.Sleep(delayMs);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Non-retryable error
|
||||
lastError = ex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: rename then delete
|
||||
try
|
||||
{
|
||||
string renamedPath = directoryPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||
+ ".deleting." + DateTime.UtcNow.Ticks;
|
||||
|
||||
Directory.Move(directoryPath, renamedPath);
|
||||
|
||||
StripReadOnlyAttributes(renamedPath);
|
||||
Directory.Delete(renamedPath, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastError = ex;
|
||||
}
|
||||
|
||||
Logger.Log($"Failed to delete directory: {directoryPath}. Error: {lastError}", LogLevel.WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void StripReadOnlyAttributes(string directoryPath)
|
||||
{
|
||||
var root = new DirectoryInfo(directoryPath);
|
||||
if (!root.Exists) return;
|
||||
|
||||
root.Attributes &= ~FileAttributes.ReadOnly;
|
||||
|
||||
foreach (var dir in root.EnumerateDirectories("*", SearchOption.AllDirectories))
|
||||
{
|
||||
dir.Attributes &= ~FileAttributes.ReadOnly;
|
||||
}
|
||||
|
||||
foreach (var file in root.EnumerateFiles("*", SearchOption.AllDirectories))
|
||||
{
|
||||
file.Attributes &= ~FileAttributes.ReadOnly;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,6 +313,16 @@ namespace AndroidSideloader.Utilities
|
||||
}
|
||||
}
|
||||
|
||||
public string GetEffectiveBackupDir()
|
||||
{
|
||||
if (CustomBackupDir && Directory.Exists(BackupDir))
|
||||
{
|
||||
return BackupDir;
|
||||
}
|
||||
|
||||
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Rookie Backups");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
FontStyle?.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user