mirror of
https://github.com/manos555555/PS5-Suite.git
synced 2026-10-06 10:00:26 +02:00
789 lines
37 KiB
C#
789 lines
37 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Media;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.Threading;
|
|
|
|
namespace PS5Upload
|
|
{
|
|
public partial class MainWindow
|
|
{
|
|
// ============================================================
|
|
// SETTINGS
|
|
// ============================================================
|
|
private void LoadSettings()
|
|
{
|
|
try
|
|
{
|
|
// Renamed with the app (PS5 Suite); fall back to the legacy file.
|
|
const string settingsFile = "ps5suite_settings.json";
|
|
string effective = File.Exists(settingsFile) ? settingsFile
|
|
: File.Exists("ps5upload_settings.json") ? "ps5upload_settings.json" : settingsFile;
|
|
if (File.Exists(effective))
|
|
{
|
|
string json = File.ReadAllText(effective);
|
|
var settings = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(json);
|
|
if (settings != null)
|
|
{
|
|
if (settings.ContainsKey("AutoSendPayload"))
|
|
_autoSendPayload = settings["AutoSendPayload"].ToString() == "True";
|
|
if (settings.ContainsKey("PayloadPath"))
|
|
_payloadPath = settings["PayloadPath"].ToString() ?? "";
|
|
if (settings.ContainsKey("PayloadPort"))
|
|
int.TryParse(settings["PayloadPort"].ToString(), out _payloadPort);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex) { Log($"⚠️ Failed to load settings: {ex.Message}"); }
|
|
}
|
|
|
|
private void SaveSettings()
|
|
{
|
|
try
|
|
{
|
|
const string settingsFile = "ps5suite_settings.json";
|
|
var settings = new Dictionary<string, object>
|
|
{
|
|
["AutoSendPayload"] = _autoSendPayload,
|
|
["PayloadPath"] = _payloadPath,
|
|
["PayloadPort"] = _payloadPort
|
|
};
|
|
string json = System.Text.Json.JsonSerializer.Serialize(settings, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
|
|
File.WriteAllText(settingsFile, json);
|
|
}
|
|
catch (Exception ex) { Log($"⚠️ Failed to save settings: {ex.Message}"); }
|
|
}
|
|
|
|
// ============================================================
|
|
// CONNECTION
|
|
// ============================================================
|
|
private async Task AutoSendPayloadOnStartup()
|
|
{
|
|
await Task.Delay(1000);
|
|
if (string.IsNullOrEmpty(_ps5IpAddress)) { Log("⚠️ Auto-send payload: No PS5 IP address configured"); return; }
|
|
if (string.IsNullOrEmpty(_payloadPath) || !File.Exists(_payloadPath)) { Log("⚠️ Auto-send payload: Payload file not found"); return; }
|
|
|
|
Log($"📤 Auto-sending payload to {_ps5IpAddress}:{_payloadPort}...");
|
|
var progress = new InlineProgress<long>(bytes => { });
|
|
bool success = await PS5Protocol.SendPayloadAsync(_ps5IpAddress, _payloadPath, _payloadPort, progress);
|
|
if (success)
|
|
{
|
|
Log($"✅ Payload sent successfully ({new FileInfo(_payloadPath).Length} bytes)");
|
|
await Task.Delay(2000);
|
|
await ConnectToPS5Async();
|
|
}
|
|
else
|
|
Log("❌ Failed to send payload");
|
|
}
|
|
|
|
private async Task ConnectToPS5Async()
|
|
{
|
|
if (await _protocol.ConnectAsync(_ps5IpAddress))
|
|
{
|
|
Log("✅ Connected to PS5 successfully");
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
ConnectButton.Content = "🟢 Disconnect";
|
|
ConnectButton.Background = new SolidColorBrush(Colors.Green);
|
|
UploadButton.IsEnabled = true;
|
|
MountGamesButton.IsEnabled = true;
|
|
});
|
|
await LoadPS5DirectoryAsync(_currentPS5Path);
|
|
}
|
|
else
|
|
Log("❌ Failed to connect to PS5");
|
|
}
|
|
|
|
private CancellationTokenSource? _connectCts;
|
|
private bool _connecting;
|
|
|
|
private async void AutoDetectButton_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
Log("🔍 Auto-discovering PS5 on the network...");
|
|
AutoDetectButton.IsEnabled = false;
|
|
try
|
|
{
|
|
var found = await PS5Protocol.DiscoverPS5Async(3000);
|
|
if (found == null)
|
|
{
|
|
Log("❌ No PS5 answered — is the payload running?");
|
|
await ShowMessageAsync("No PS5 found on the network.\nMake sure the payload is running.", "Auto-Discovery");
|
|
return;
|
|
}
|
|
IpAddressTextBox.Text = found.Value.ip;
|
|
Log($"🔍 Found PS5 at {found.Value.ip}:{found.Value.port}");
|
|
}
|
|
catch (Exception ex) { Log($"❌ Discovery failed: {ex.Message}"); }
|
|
finally { AutoDetectButton.IsEnabled = true; }
|
|
}
|
|
|
|
private async void ConnectButton_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
// While a connect attempt is running the button becomes Cancel
|
|
if (_connecting)
|
|
{
|
|
Log("⏹ Connect cancelled by user");
|
|
_connectCts?.Cancel();
|
|
return;
|
|
}
|
|
|
|
string ipAddress = IpAddressTextBox.Text?.Trim() ?? "";
|
|
|
|
// Empty IP => auto-discover: broadcast a UDP probe; the running
|
|
// payload answers with its IP + actual bound port.
|
|
if (string.IsNullOrEmpty(ipAddress))
|
|
{
|
|
Log("🔍 Auto-discovering PS5 on the network...");
|
|
try
|
|
{
|
|
var found = await PS5Protocol.DiscoverPS5Async(2500);
|
|
if (found == null)
|
|
{
|
|
Log("❌ No PS5 answered the discovery probe");
|
|
await ShowMessageAsync("No PS5 found on the network.\nMake sure the payload is running, or enter the IP manually.", "Auto-Discovery");
|
|
return;
|
|
}
|
|
ipAddress = found.Value.ip;
|
|
Log($"🔍 Found PS5 at {ipAddress}:{found.Value.port}");
|
|
await Dispatcher.UIThread.InvokeAsync(() => IpAddressTextBox.Text = ipAddress);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"❌ Discovery failed: {ex.Message}");
|
|
await ShowMessageAsync($"Discovery failed:\n{ex.Message}", "Auto-Discovery");
|
|
return;
|
|
}
|
|
}
|
|
|
|
_ps5IpAddress = ipAddress;
|
|
|
|
if (_protocol.IsConnected)
|
|
{
|
|
ConnectButton.IsEnabled = false;
|
|
Log("🔌 Disconnecting from PS5...");
|
|
Log("🔌 Disconnecting from PS5...");
|
|
StopHwAutoRefresh();
|
|
_protocol.Disconnect();
|
|
_shellActive = false;
|
|
_shellCurrentDir = "/data";
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
ConnectButton.Content = "🔴 Disconnected";
|
|
ConnectButton.Background = new SolidColorBrush(Colors.DarkRed);
|
|
UploadButton.IsEnabled = false;
|
|
MountGamesButton.IsEnabled = false;
|
|
_ps5Files.Clear();
|
|
});
|
|
Log("✅ Disconnected from PS5");
|
|
|
|
await Task.Delay(1000);
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
ConnectButton.Content = "🔵 Connect";
|
|
ConnectButton.Background = new SolidColorBrush(Color.FromRgb(0, 122, 204));
|
|
});
|
|
ConnectButton.IsEnabled = true;
|
|
}
|
|
else
|
|
{
|
|
Log($"Connecting to PS5 at {ipAddress}...");
|
|
_connecting = true;
|
|
_connectCts = new CancellationTokenSource();
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
ConnectButton.Content = "⏹ Cancel";
|
|
ConnectButton.Background = new SolidColorBrush(Colors.DarkOrange);
|
|
});
|
|
try
|
|
{
|
|
if (await _protocol.ConnectAsync(ipAddress, 9113, _connectCts.Token))
|
|
{
|
|
Log("✅ Connected to PS5 successfully");
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
ConnectButton.Content = "🟢 Disconnect";
|
|
ConnectButton.Background = new SolidColorBrush(Colors.DarkGreen);
|
|
UploadButton.IsEnabled = true;
|
|
MountGamesButton.IsEnabled = true;
|
|
});
|
|
await OpenShellAsync();
|
|
await LoadPS5DirectoryAsync(_currentPS5Path);
|
|
await RefreshStorageInfoAsync();
|
|
}
|
|
else
|
|
{
|
|
Log("❌ Failed to connect to PS5");
|
|
await ShowMessageAsync("Failed to connect to PS5. Make sure the payload is running.", "Connection Error");
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Log("⏹ Connect cancelled");
|
|
}
|
|
finally
|
|
{
|
|
_connecting = false;
|
|
_connectCts?.Dispose();
|
|
_connectCts = null;
|
|
bool stillConnected = _protocol.IsConnected;
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
if (!stillConnected)
|
|
{
|
|
ConnectButton.Content = "🔵 Connect";
|
|
ConnectButton.Background = new SolidColorBrush(Color.FromRgb(0, 122, 204));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// PS5 DIRECTORY BROWSING
|
|
// ============================================================
|
|
private async Task LoadPS5DirectoryAsync(string path)
|
|
{
|
|
try
|
|
{
|
|
if (!_protocol.IsConnected)
|
|
{
|
|
Log($"⚠️ Connection lost (LastError: {_protocol.LastError}), reconnecting...");
|
|
if (!await _protocol.ConnectAsync(_ps5IpAddress))
|
|
{
|
|
await ShowMessageAsync("Failed to reconnect to PS5", "Connection Error");
|
|
return;
|
|
}
|
|
Log("✅ Reconnected successfully");
|
|
}
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
_currentPS5Path = path;
|
|
CurrentPathTextBox.Text = path;
|
|
|
|
var entries = await _protocol.ListDirAsync(path);
|
|
Log($"🔍 DEBUG: ListDir returned {entries.Count()} entries for path: {path}");
|
|
foreach (var entry in entries.Take(5))
|
|
Log($" - {(entry.IsDirectory ? "📁" : "📄")} {entry.Name} ({entry.Size} bytes)");
|
|
if (entries.Count() > 5)
|
|
Log($" ... and {entries.Count() - 5} more");
|
|
|
|
var items = new List<PS5FileItem>();
|
|
if (path != "/")
|
|
{
|
|
string? parentPath = Path.GetDirectoryName(path);
|
|
if (string.IsNullOrEmpty(parentPath)) parentPath = "/";
|
|
else parentPath = parentPath.Replace("\\", "/");
|
|
items.Add(new PS5FileItem { Name = "..", FullPath = parentPath, Icon = "📁", IsDirectory = true, Size = 0 });
|
|
}
|
|
|
|
foreach (var entry in entries)
|
|
{
|
|
items.Add(new PS5FileItem
|
|
{
|
|
Name = entry.Name,
|
|
IsDirectory = entry.IsDirectory,
|
|
Size = entry.Size,
|
|
FullPath = $"{path}/{entry.Name}".Replace("//", "/"),
|
|
Icon = entry.IsDirectory ? "📁" : "📄"
|
|
});
|
|
}
|
|
|
|
var sortedItems = items.OrderBy(i => i.IsDirectory ? 0 : 1).ThenBy(i => i.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
|
|
|
_ps5Files.Clear();
|
|
foreach (var item in sortedItems) _ps5Files.Add(item);
|
|
|
|
var t4 = sw.ElapsedMilliseconds;
|
|
Log("📂 Loaded " + entries.Count().ToString() + " items (Total: " + t4.ToString() + "ms)");
|
|
ApplySearchFilter();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_protocol.IsConnected)
|
|
{
|
|
Log($"❌ Failed to load directory: {ex.Message}");
|
|
await ShowMessageAsync($"Failed to load directory: {ex.Message}", "Error");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplySearchFilter()
|
|
{
|
|
_ps5FilesFiltered.Clear();
|
|
if (string.IsNullOrWhiteSpace(_searchQuery))
|
|
{
|
|
foreach (var file in _ps5Files) _ps5FilesFiltered.Add(file);
|
|
}
|
|
else
|
|
{
|
|
string query = _searchQuery.ToLower();
|
|
foreach (var file in _ps5Files)
|
|
if (file.Name.ToLower().Contains(query))
|
|
_ps5FilesFiltered.Add(file);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// LOCAL FILES
|
|
// ============================================================
|
|
private async void BrowseFilesButton_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
var topLevel = TopLevel.GetTopLevel(this);
|
|
if (topLevel == null) return;
|
|
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = "Select files to upload", AllowMultiple = true });
|
|
foreach (var file in files)
|
|
{
|
|
var path = file.TryGetLocalPath();
|
|
if (path != null)
|
|
{
|
|
FileInfo info = new FileInfo(path);
|
|
|
|
// Check if it's a ZIP file - extract and add contents
|
|
if (info.Extension.Equals(".zip", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
await ExtractAndAddZipContents(path, info.Name);
|
|
}
|
|
else
|
|
{
|
|
_localFiles.Add(new LocalFileItem { Name = info.Name, FullPath = path, Icon = "📄", IsDirectory = false, Size = info.Length });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void BrowseFolderButton_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
var topLevel = TopLevel.GetTopLevel(this);
|
|
if (topLevel == null) return;
|
|
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Select folder to upload", AllowMultiple = false });
|
|
if (folders.Count > 0)
|
|
{
|
|
var path = folders[0].TryGetLocalPath();
|
|
if (path != null)
|
|
{
|
|
DirectoryInfo dirInfo = new DirectoryInfo(path);
|
|
_localFiles.Add(new LocalFileItem { Name = dirInfo.Name, FullPath = path, Icon = "📁", IsDirectory = true, Size = 0 });
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ClearLocalFiles_Click(object? sender, RoutedEventArgs e) => _localFiles.Clear();
|
|
|
|
private void LocalFilesListBox_DragOver(object? sender, DragEventArgs e) => e.DragEffects = DragDropEffects.Copy;
|
|
|
|
private void LocalFilesListBox_Drop(object? sender, DragEventArgs e)
|
|
{
|
|
// Avalonia 12: Use TryGetFiles() on DataTransfer
|
|
if (e.DataTransfer.TryGetFiles() is { } files)
|
|
{
|
|
foreach (var item in files)
|
|
{
|
|
var path = item.Path.LocalPath;
|
|
if (path == null) continue;
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
FileInfo info = new FileInfo(path);
|
|
|
|
// Check if it's a ZIP file - extract and add contents
|
|
if (info.Extension.Equals(".zip", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_ = ExtractAndAddZipContents(path, info.Name);
|
|
}
|
|
else
|
|
{
|
|
_localFiles.Add(new LocalFileItem { Name = info.Name, FullPath = path, Icon = "📄", IsDirectory = false, Size = info.Length });
|
|
}
|
|
}
|
|
else if (Directory.Exists(path))
|
|
{
|
|
DirectoryInfo dirInfo = new DirectoryInfo(path);
|
|
_localFiles.Add(new LocalFileItem { Name = dirInfo.Name, FullPath = path, Icon = "📁", IsDirectory = true, Size = 0 });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// ZIP ARCHIVE SUPPORT (ps5upload-style)
|
|
// Extracts .zip files and adds contents to upload queue
|
|
// ============================================================
|
|
private async Task ExtractAndAddZipContents(string zipPath, string zipName)
|
|
{
|
|
try
|
|
{
|
|
Log($"📦 Extracting ZIP: {zipName}");
|
|
|
|
// Create temp extraction directory
|
|
string tempDir = Path.Combine(Path.GetTempPath(), "ps5suite_" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempDir);
|
|
|
|
// Extract ZIP
|
|
await Task.Run(() => System.IO.Compression.ZipFile.ExtractToDirectory(zipPath, tempDir));
|
|
|
|
// Add all extracted files
|
|
var files = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories);
|
|
var dirs = Directory.GetDirectories(tempDir, "*", SearchOption.AllDirectories);
|
|
|
|
Log($"📦 Found {files.Length} files, {dirs.Length} folders in ZIP");
|
|
|
|
// Add root directory as a folder item
|
|
string zipBaseName = Path.GetFileNameWithoutExtension(zipName);
|
|
_localFiles.Add(new LocalFileItem {
|
|
Name = zipBaseName,
|
|
FullPath = tempDir,
|
|
Icon = "📁",
|
|
IsDirectory = true,
|
|
Size = 0
|
|
});
|
|
|
|
Log($"✅ ZIP extracted to temp folder - added '{zipBaseName}' to upload queue");
|
|
Log($"⚠️ Will upload extracted contents, not the .zip file");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"❌ Failed to extract ZIP: {ex.Message}");
|
|
// Fall back to adding the zip file itself
|
|
FileInfo info = new FileInfo(zipPath);
|
|
_localFiles.Add(new LocalFileItem { Name = info.Name, FullPath = zipPath, Icon = "📄", IsDirectory = false, Size = info.Length });
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// PS5 FILE OPERATIONS
|
|
// ============================================================
|
|
private void PS5FilesListBox_DoubleTapped(object? sender, TappedEventArgs e)
|
|
{
|
|
if (PS5FilesListBox.SelectedItem is PS5FileItem item && item.IsDirectory)
|
|
_ = LoadPS5DirectoryAsync(item.FullPath);
|
|
}
|
|
|
|
private void ClearSearchButton_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
SearchTextBox.Text = "";
|
|
_searchQuery = "";
|
|
ApplySearchFilter();
|
|
}
|
|
|
|
private void GoButton_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
string path = CurrentPathTextBox.Text?.Trim() ?? "";
|
|
if (!string.IsNullOrEmpty(path)) _ = LoadPS5DirectoryAsync(path);
|
|
}
|
|
|
|
// ============================================================
|
|
// NAS/SMB SUPPORT (ps5upload-style)
|
|
// Adds network paths (\\server\share\folder) to upload queue
|
|
// ============================================================
|
|
private async void AddNasPath_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
string? nasPath = await ShowInputDialogAsync(
|
|
"Add NAS/Network Path",
|
|
"Enter UNC path (\\\\server\\share\\folder):",
|
|
"\\\\NAS\\games");
|
|
|
|
if (string.IsNullOrWhiteSpace(nasPath)) return;
|
|
|
|
nasPath = nasPath.Trim();
|
|
|
|
// Validate UNC path format
|
|
if (!nasPath.StartsWith("\\\\") && !nasPath.StartsWith("//"))
|
|
{
|
|
await ShowMessageAsync(
|
|
"Invalid path format.\n\n" +
|
|
"Use UNC format: \\\\server\\share\\folder\n" +
|
|
"Example: \\\\NAS\\games\\PS4\\MyGame",
|
|
"Invalid Path");
|
|
return;
|
|
}
|
|
|
|
// Normalize path (convert // to \\ for Windows)
|
|
if (nasPath.StartsWith("//"))
|
|
{
|
|
nasPath = "\\\\" + nasPath.Substring(2).Replace("/", "\\");
|
|
}
|
|
|
|
Log($"🌐 Checking NAS path: {nasPath}");
|
|
|
|
// Check if path exists
|
|
if (Directory.Exists(nasPath))
|
|
{
|
|
// It's a directory - add it
|
|
DirectoryInfo dirInfo = new DirectoryInfo(nasPath);
|
|
_localFiles.Add(new LocalFileItem {
|
|
Name = dirInfo.Name,
|
|
FullPath = nasPath,
|
|
Icon = "🌐",
|
|
IsDirectory = true,
|
|
Size = 0
|
|
});
|
|
Log($"✅ Added NAS folder: {dirInfo.Name} ({nasPath})");
|
|
}
|
|
else if (File.Exists(nasPath))
|
|
{
|
|
// It's a file - add it
|
|
FileInfo fileInfo = new FileInfo(nasPath);
|
|
|
|
// Check if it's a ZIP file
|
|
if (fileInfo.Extension.Equals(".zip", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
await ExtractAndAddZipContents(nasPath, fileInfo.Name);
|
|
}
|
|
else
|
|
{
|
|
_localFiles.Add(new LocalFileItem {
|
|
Name = fileInfo.Name,
|
|
FullPath = nasPath,
|
|
Icon = "🌐",
|
|
IsDirectory = false,
|
|
Size = fileInfo.Length
|
|
});
|
|
Log($"✅ Added NAS file: {fileInfo.Name} ({FormatFileSize(fileInfo.Length)})");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log($"❌ NAS path not found: {nasPath}");
|
|
await ShowMessageAsync(
|
|
$"Cannot access: {nasPath}\n\n" +
|
|
"Make sure:\n" +
|
|
"• The network share is accessible\n" +
|
|
"• You have permission to read it\n" +
|
|
"• The path is correct",
|
|
"NAS Path Not Found");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"❌ NAS error: {ex.Message}");
|
|
await ShowMessageAsync($"Error accessing NAS path: {ex.Message}", "Error");
|
|
}
|
|
}
|
|
|
|
private async void RenameMenuItem_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (PS5FilesListBox.SelectedItem is PS5FileItem item)
|
|
{
|
|
string? newName = await ShowInputDialogAsync("Rename", "New name:", item.Name);
|
|
if (!string.IsNullOrWhiteSpace(newName))
|
|
{
|
|
try
|
|
{
|
|
string newPath = Path.Combine(Path.GetDirectoryName(item.FullPath)?.Replace("\\", "/") ?? "", newName).Replace("\\", "/");
|
|
await _protocol.RenameAsync(item.FullPath, newPath);
|
|
Log($"✅ Renamed {item.Name} to {newName}");
|
|
await LoadPS5DirectoryAsync(_currentPS5Path);
|
|
}
|
|
catch (Exception ex) { await ShowMessageAsync($"Rename failed: {ex.Message}", "Error"); Log($"❌ Rename failed: {ex.Message}"); }
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void CopyMenuItem_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (PS5FilesListBox.SelectedItem is PS5FileItem item)
|
|
{
|
|
string? destPath = await ShowInputDialogAsync("Copy To", "Destination path:", _currentPS5Path + "/" + item.Name);
|
|
if (destPath != null)
|
|
{
|
|
try { await _protocol.CopyFileAsync(item.FullPath, destPath); Log($"✅ Copied {item.Name} to {destPath}"); await LoadPS5DirectoryAsync(_currentPS5Path); }
|
|
catch (Exception ex) { await ShowMessageAsync($"Copy failed: {ex.Message}", "Error"); Log($"❌ Copy failed: {ex.Message}"); }
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void MoveMenuItem_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (PS5FilesListBox.SelectedItem is PS5FileItem item)
|
|
{
|
|
string? destPath = await ShowInputDialogAsync("Move To", "Destination path:", _currentPS5Path + "/" + item.Name);
|
|
if (destPath != null)
|
|
{
|
|
try { await _protocol.RenameAsync(item.FullPath, destPath); Log($"✅ Moved {item.Name} to {destPath}"); await LoadPS5DirectoryAsync(_currentPS5Path); }
|
|
catch (Exception ex) { await ShowMessageAsync($"Move failed: {ex.Message}", "Error"); Log($"❌ Move failed: {ex.Message}"); }
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void DeleteMenuItem_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (PS5FilesListBox.SelectedItem is PS5FileItem item)
|
|
{
|
|
if (await ShowConfirmAsync($"Delete {item.Name}?"))
|
|
{
|
|
try
|
|
{
|
|
if (item.IsDirectory) { Log($"🗑️ Deleting folder: {item.Name}"); await _protocol.DeleteDirAsync(item.FullPath); Log($"✅ Folder deletion complete: {item.Name}"); }
|
|
else { Log($"🗑️ Deleting file: {item.Name}"); await _protocol.DeleteFileAsync(item.FullPath); Log($"✅ File deletion complete: {item.Name}"); }
|
|
await Task.Delay(1500);
|
|
await LoadPS5DirectoryAsync(_currentPS5Path);
|
|
}
|
|
catch (Exception ex) { await ShowMessageAsync($"Delete failed: {ex.Message}", "Error"); Log($"❌ Delete failed: {ex.Message}"); }
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void DeleteSelectedMenuItem_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
var selectedItems = (PS5FilesListBox.SelectedItems ?? Array.Empty<object>()).Cast<PS5FileItem>().ToList();
|
|
if (selectedItems.Count == 0) { await ShowMessageAsync("No items selected."); return; }
|
|
|
|
int folderCount = selectedItems.Count(i => i.IsDirectory);
|
|
int fileCount = selectedItems.Count(i => !i.IsDirectory);
|
|
string message = $"Delete {selectedItems.Count} items?\n\n";
|
|
if (folderCount > 0) message += $"📁 {folderCount} folders\n";
|
|
if (fileCount > 0) message += $"📄 {fileCount} files";
|
|
|
|
if (!await ShowConfirmAsync(message, "Confirm Bulk Delete")) return;
|
|
|
|
Log($"🗑️ Starting bulk delete of {selectedItems.Count} items...");
|
|
int successCount = 0, failCount = 0;
|
|
foreach (var item in selectedItems)
|
|
{
|
|
try
|
|
{
|
|
if (item.IsDirectory) await _protocol.DeleteDirAsync(item.FullPath);
|
|
else await _protocol.DeleteFileAsync(item.FullPath);
|
|
successCount++;
|
|
Log($"✅ Deleted: {item.Name}");
|
|
await Task.Delay(300);
|
|
}
|
|
catch (Exception ex) { Log($"❌ Failed to delete {item.Name}: {ex.Message}"); failCount++; }
|
|
}
|
|
Log($"🗑️ Bulk delete complete: {successCount} succeeded, {failCount} failed");
|
|
await Task.Delay(500);
|
|
await LoadPS5DirectoryAsync(_currentPS5Path);
|
|
await ShowMessageAsync($"Bulk delete complete!\n\n✅ {successCount} deleted\n❌ {failCount} failed");
|
|
}
|
|
|
|
private async void DownloadMenuItem_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (PS5FilesListBox.SelectedItem is PS5FileItem item)
|
|
{
|
|
var topLevel = TopLevel.GetTopLevel(this);
|
|
if (topLevel == null) return;
|
|
|
|
if (item.IsDirectory)
|
|
{
|
|
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = $"Select destination for '{item.Name}' folder" });
|
|
if (folders.Count == 0) return;
|
|
var folderPath = folders[0].TryGetLocalPath();
|
|
if (folderPath == null) return;
|
|
string localBasePath = Path.Combine(folderPath, item.Name);
|
|
|
|
try
|
|
{
|
|
Log($"⬇️ Downloading folder: {item.FullPath} → {localBasePath}");
|
|
_uploadCancellation = new CancellationTokenSource();
|
|
var ct = _uploadCancellation.Token;
|
|
int dlCallCount = 0;
|
|
var progress = new InlineProgress<DownloadFolderProgress>(p =>
|
|
{
|
|
if (++dlCallCount % 8 != 0 && p.Phase != "Complete") return;
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
if (p.Phase == "Scanning")
|
|
TotalProgressText.Text = $"Scanning: {p.FilesCompleted} files found... ({p.CurrentFile})";
|
|
else if (p.Phase == "Downloading")
|
|
{
|
|
double percent = p.TotalBytes > 0 ? (double)p.BytesDownloaded / p.TotalBytes * 100 : 0;
|
|
TotalProgressBar.Value = percent;
|
|
TotalProgressText.Text = $"Downloading: {p.FilesCompleted}/{p.TotalFiles} files ({FormatFileSize(p.BytesDownloaded)}/{FormatFileSize(p.TotalBytes)}) [{percent:F1}%]";
|
|
if (!string.IsNullOrEmpty(p.CurrentFile)) UploadFileNameText.Text = $"⬇️ {p.CurrentFile}";
|
|
}
|
|
else if (p.Phase == "Complete")
|
|
{
|
|
TotalProgressBar.Value = 100;
|
|
TotalProgressText.Text = $"Complete: {p.FilesCompleted}/{p.TotalFiles} files ({FormatFileSize(p.BytesDownloaded)})";
|
|
}
|
|
});
|
|
});
|
|
var (downloaded, failed, totalBytes) = await _protocol.DownloadFolderAsync(item.FullPath, localBasePath, _ps5IpAddress, progress, ct);
|
|
Log($"✅ Folder download complete: {downloaded} files ({FormatFileSize(totalBytes)}), {failed} failed");
|
|
await ShowMessageAsync($"Folder downloaded!\n\nFiles: {downloaded} downloaded, {failed} failed\nSize: {FormatFileSize(totalBytes)}\nSaved to: {localBasePath}", "Download Complete");
|
|
}
|
|
catch (Exception ex) { Log($"❌ Folder download error: {ex.Message}"); await ShowMessageAsync($"Folder download error: {ex.Message}", "Error"); }
|
|
finally { _uploadCancellation = null; TotalProgressBar.Value = 0; TotalProgressText.Text = ""; UploadFileNameText.Text = ""; }
|
|
return;
|
|
}
|
|
|
|
// Single file download
|
|
var saveFile = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = "Save Downloaded File", SuggestedFileName = item.Name });
|
|
if (saveFile != null)
|
|
{
|
|
var savePath = saveFile.TryGetLocalPath();
|
|
if (savePath == null) return;
|
|
try
|
|
{
|
|
Log($"⬇️ Downloading: {item.Name} ({FormatFileSize(item.Size)})");
|
|
int sfCallCount = 0;
|
|
var progress = new InlineProgress<UploadProgress>(p =>
|
|
{
|
|
if (++sfCallCount % 8 != 0 && p.BytesSent != p.TotalBytes) return;
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
double percent = p.TotalBytes > 0 ? (double)p.BytesSent / p.TotalBytes * 100 : 0;
|
|
TotalProgressBar.Value = percent;
|
|
TotalProgressText.Text = $"Downloading: {FormatFileSize(p.BytesSent)}/{FormatFileSize(p.TotalBytes)} ({percent:F1}%) @ {FormatFileSize((long)p.SpeedBytesPerSecond)}/s";
|
|
});
|
|
});
|
|
bool success = await _protocol.DownloadFileAsync(item.FullPath, savePath, progress);
|
|
if (success) { Log($"✅ Downloaded: {item.Name} → {savePath}"); await ShowMessageAsync($"File downloaded successfully!\n\nSaved to: {savePath}", "Success"); }
|
|
else { Log($"❌ Download failed: {item.Name}"); await ShowMessageAsync("Download failed", "Error"); }
|
|
}
|
|
catch (Exception ex) { Log($"❌ Download error: {ex.Message}"); await ShowMessageAsync($"Download error: {ex.Message}", "Error"); }
|
|
finally { TotalProgressBar.Value = 0; TotalProgressText.Text = ""; }
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// LOG BUTTONS
|
|
// ============================================================
|
|
private void ClearLogButton_Click(object? sender, RoutedEventArgs e) { LogTextBox.Text = ""; Log("Log cleared"); }
|
|
|
|
private async void CopyLogButton_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
|
|
if (clipboard != null)
|
|
{
|
|
var item = new Avalonia.Input.DataTransferItem();
|
|
item.Set(Avalonia.Input.DataFormat.Text, LogTextBox.Text ?? "");
|
|
var data = new Avalonia.Input.DataTransfer();
|
|
data.Add(item);
|
|
await clipboard.SetDataAsync(data);
|
|
Log("📋 Log copied to clipboard!");
|
|
}
|
|
}
|
|
catch (Exception ex) { Log($"⚠️ Clipboard error: {ex.Message}"); }
|
|
}
|
|
|
|
private async Task<PS5StorageInfo?> FetchStorageInfoWithFallbackAsync()
|
|
{
|
|
bool uploadsIdle = (_uploadCancellation == null || _uploadCancellation.IsCancellationRequested) && _activeTaskCount == 0;
|
|
if (_protocol.IsConnected && uploadsIdle)
|
|
return await _protocol.ListStorageAsync();
|
|
|
|
var tempProtocol = new PS5Protocol();
|
|
try
|
|
{
|
|
if (!await tempProtocol.ConnectAsync(_ps5IpAddress)) { Log("❌ Storage refresh: failed to open temporary connection"); return null; }
|
|
return await tempProtocol.ListStorageAsync();
|
|
}
|
|
finally { tempProtocol.Dispose(); }
|
|
}
|
|
}
|
|
}
|