commit a6199c8d51ecf394b1182ca59dc99c7a3f75daf1
Author: Manos
Date: Sun Oct 4 02:23:01 2026 +0300
v7.1.5: PS5 Suite โ Avalonia client for Windows/Linux/macOS, Android (WIP), FPKG engine, PS5 payload
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..840baf0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,46 @@
+# Build outputs
+bin/
+obj/
+dist/
+publish/
+publish_*/
+out/
+*.user
+*.suo
+msbuild.binlog
+
+# Release binaries (uploaded as GitHub Release assets, not tracked in git)
+release/
+*.apk
+*.aab
+PS5UploadSuite-*.exe
+PS5Upload_Standalone_*/
+
+# Runtime settings/logs (machine-local)
+ps5suite_settings.json
+ps5upload_settings.json
+ps5_favorites.json
+ps5_profiles.json
+ps5suite_*.log
+*.tmp.png
+
+# AI/tooling junk
+.aider*
+.freebuff/
+.aider.tags.cache.v4/
+
+# OS files
+Thumbs.db
+desktop.ini
+.DS_Store
+
+# internal test/debug artifacts
+publish-*/
+payload/appdb_*.db
+payload/port9113_killer.*
+lib-prospero-pkg/test-stream/realbuild-out/
+
+# legacy clients (WPF v5/v6, old MAUI app, empty stub template)
+client/
+mobile/
+client-android/
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..19b4f99
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,640 @@
+# Changelog - PS5 Upload Suite
+
+## Version 4.2.1 - Bugfix Release (February 8, 2026)
+
+### ๐ Bug Fixes
+
+#### UI Scrolling Fix
+- **Fixed connection panel eating screen space** - Added `MaxHeight` constraint (250px) and `ScrollViewer` to the connection/payload settings panel
+- **Fixed header overflow** - Added `MaxHeight="120"` to header row
+- **Fixed content area too small** - Added `MinHeight="200"` to main content area
+- When the Auto-send Payload Expander was open, it consumed half the screen leaving no room for file lists
+
+#### File Counting Fix
+- **Fixed chunked files never counting as completed** - `fileChunkCounts` was set to the actual number of chunks (e.g., 4 for a 2GB file), but `UploadFileParallelAsync` handles all chunks internally as a single Task. The main loop only saw 1 task completion per file, so chunked files never reached their completion threshold
+- Now correctly sets `fileChunkCounts = 1` per file since each file = 1 task
+
+#### Progress Display Fix
+- **Fixed progress appearing frozen during heavy uploads** - All UI updates used `DispatcherPriority.Background` (lowest priority), which got starved when 24 parallel connections flooded the dispatcher queue
+- Upgraded `UpdateUploadStats` to `DispatcherPriority.Normal`
+- Upgraded per-file progress bars to `DispatcherPriority.Render`
+- Progress now updates smoothly even at 95%+ network saturation
+
+---
+
+## Version 4.2.0 - Game Mounter Integration (February 7, 2026)
+
+### ๐ฎ Game Mounter Integration
+- **Mount Games Button** - New green "๐ฎ Mount Games" button in the client UI
+- One-click mounting of all uploaded games directly from the PS5 Upload Suite
+- Button disabled when disconnected, shows "โณ Mounting..." during operation
+
+### ๐ Multi-Path Game Scanning
+- Scans **6 locations** for games:
+ - `/data/etaHEN/games` (internal storage)
+ - `/mnt/usb0/games` through `/mnt/usb3/games` (USB drives)
+ - `/mnt/ext0/games` (M.2 SSD)
+- First pass counts total games, second pass processes them
+- PS5 notifications show real-time progress: "Mounting 3/10 (30%) - Game Name"
+
+### ๐ Smart Mount Features
+- **Duplicate Detection** - Same title ID across multiple drives mounted only once
+- **Auto-Cleanup** - `auto_unmount_deleted_games()` removes stale mount entries
+- **Already Mounted Detection** - Skips games that are already mounted via nullfs
+- **sce_sys Validation** - Verifies game directory has valid `sce_sys/` before mounting
+
+### ๐ง Payload Changes
+- **New Command** - `CMD_MOUNT_GAMES` (0x30) added to protocol
+- **nullfs Mount** - Games mounted to `/system_ex/app/`
+- **DRM Patching** - Automatic `applicationDrmType` fix in `param.json`
+- **Metadata Copy** - `sce_sys` copied to `/user/app/` and `/user/appmeta/`
+- **mount.lnk** - Tracks mount source path for cleanup and remount
+- **Safe Delete** - Replaced `system("rm -rf ...")` with `rmdir_recursive()` to prevent hangs
+- **remount_system_ex()** - Remounts `/system_ex` as writable for game installation
+
+### ๐ฅ๏ธ Client Changes
+- **Protocol.cs** - Added `MountGames = 0x30` enum and `MountGamesAsync()` method
+- **MainWindow.xaml** - Green "๐ฎ Mount Games" button between Upload and Cancel
+- **MainWindow.xaml.cs** - Click handler with progress display and auto-registration
+- **Auto Registration** - If new games detected, automatically runs `game_mounter.elf` via shell for home screen registration
+
+### โก Client Improvements
+- **Real-Time Speed Display** - New sliding window algorithm (10 samples over 5 seconds) for accurate real-time upload speed
+- **Smoother ETA** - Blended speed calculation (60% real-time + 40% average) with better time formatting (hours/minutes/seconds)
+- **Speed Display** - Shows both real-time and average speed: `Speed: 95 MB/s (avg 88 MB/s)`
+- **Duplicate File Dialog** - New `DuplicateFileDialog` for handling file conflicts during upload
+- **Move Dialog Refactor** - Replaced inline move dialog code with reusable `ShowPathPickerDialogAsync()`
+- **FormatFileSize Refactor** - Moved to static `FileUtils.FormatFileSize()` class for code reuse
+- **ETA Smoothing** - Reduced `ETASmoothingFactor` from 0.3 to 0.15 for less jumpy ETA display
+
+### ๐ Bug Fixes
+
+#### Payload Fixes
+- **Fixed IOVEC_ENTRY(NULL) crash** - `strlen(NULL)` with `-O3` optimization caused segfault in `remount_system_ex()`
+- **Fixed libSceAppInstUtil startup crash** - Library cannot be loaded in etaHEN payload context (crashes dynamic linker). Switched to standalone `game_mounter.elf` for registration
+- **Fixed dlopen crash** - `dlopen("libSceAppInstUtil.sprx")` crashes payload thread. Removed dynamic loading approach
+- **Removed system() calls** - `system("rm -rf ...")` replaced with safe recursive `rmdir_recursive()` to prevent payload hangs
+- **Fixed compile warnings** - Removed unused `#include `, re-enabled `auto_unmount_deleted_games()`
+
+#### Client Fixes
+- **Fixed Shell Terminal hardcoded IP** - Shell was connecting to hardcoded `192.168.0.160` instead of the configured PS5 IP address
+- **Fixed ETA jumping** - ETA no longer jumps erratically during uploads due to improved smoothing
+- **Fixed speed dropping to 0** - Sliding window prevents speed display from showing 0 during brief pauses
+- **Fixed files remaining counter** - Simplified calculation with `Math.Max(0, ...)` to prevent negative values
+
+### ๐ Technical Notes
+- `libSceAppInstUtil.sprx` is **not available** in etaHEN payload context (both static linking and dlopen crash)
+- Game registration (`sceAppInstUtilAppInstallTitleDir`) is handled by the standalone `game_mounter.elf` which runs as a separate ELF with its own library dependencies
+- The upload suite payload handles nullfs mounting, metadata copy, and DRM patching โ everything except the final system registration
+
+---
+
+## Version 4.1.0 - Memory Optimization & Large File Handling (February 5, 2026)
+
+### ๐ง Memory & Performance Improvements
+
+#### 1. Large File Count Support (156K+ Files)
+- **Fixed crash** with games containing 156,253+ files (e.g., Astrobot)
+- **Removed massive dictionary** - No longer creates 156K FileInfo objects at once
+- **Incremental calculation** - Total bytes calculated progressively during scan
+- **Memory efficient** - Handles unlimited file counts without OutOfMemoryException
+
+#### 2. Lock-Free Progress Tracking
+- **ConcurrentDictionary** - Replaced Dictionary with thread-safe collections
+- **Zero lock contention** - Eliminated `lock (_progressLock)` blocks
+- **Atomic operations** - Uses `Interlocked.Add()` for progress updates
+- **Smooth UI** - No stuttering from lock contention
+
+#### 3. Auto Memory Cleanup
+- **Completed files removed** - Dictionaries cleaned after file completion
+- **Reduced memory footprint** - Only active uploads tracked
+- **Prevents memory overflow** - Long-running uploads stay stable
+
+#### 4. Reduced UI Stuttering
+- **Batch logging** - Small files logged every 50 completions (up from 25)
+- **Less UI pressure** - Fewer `Dispatcher.Invoke()` calls
+- **Smoother experience** - Especially noticeable with 100K+ files
+
+### ๐ง Technical Changes
+- Changed `_fileProgressBytes` from Dictionary to ConcurrentDictionary
+- Changed `_fileChunkProgressBytes` from Dictionary to ConcurrentDictionary
+- Changed `_chunkLogLastBytes` from Dictionary to ConcurrentDictionary
+- Removed `fileSizeLookup` dictionary creation (caused OutOfMemoryException)
+- Increased `SmallFileLogBatchSize` from 25 to 50
+- Added dictionary cleanup in file completion handlers
+
+### ๐ Bug Fixes
+- Fixed application crash when uploading games with 156K+ files
+- Fixed UI stuttering during small file uploads
+- Fixed memory overflow from massive dictionary allocations
+- Fixed lock contention causing UI freezes
+
+### ๐ Tested With
+- **Astrobot**: 156,253 files, 1,724 directories - **53 minutes total**
+- **Result**: Zero crashes, smooth upload, stable memory usage
+
+---
+
+## Version 4.0.0 - Retry Feature & Stability Improvements (January 27, 2026)
+
+### โจ New Features
+
+#### ๐ Retry Failed Transfers
+- **Right-click context menu** on failed transfers
+- **Retry Upload** - Re-queue failed files for upload
+- **Remove from List** - Clear individual failed items
+- **Persistent paths** - Failed transfers store local and remote paths
+- **Smart retry** - Validates file existence before retry
+
+#### ๐๏ธ Clear All Button Fix
+- **Transfer History Clear All** now works correctly
+- **Confirmation dialog** before clearing history
+- **Clears both** completed and failed transfers
+
+### ๐ง Performance & Stability
+
+#### Connection Stability
+- **Reverted 16MB buffer** - Caused connection drops with 16 parallel connections
+- **Stable 8MB buffer** - Proven reliable for high-speed transfers
+- **TCP NoDelay** - Already enabled for optimal performance
+- **16MB socket buffers** - Maintained for maximum throughput
+
+#### Upload Performance
+- **Large files (>100MB)**: 30-36 MB/s per file
+- **Small files (<100MB)**: 8-12 MB/s per file
+- **4 parallel large files** - Prevents PS5 memory exhaustion
+- **16 total connections** - Optimal for mixed workloads
+
+### ๐ Bug Fixes
+- Fixed Clear All button not clearing transfer history
+- Fixed connection drops when using oversized buffers
+- Added proper event handlers for failed transfer context menu
+- Improved error handling for retry operations
+
+### ๐ Code Quality
+- Added comprehensive inline documentation
+- Improved code organization and readability
+- Better error messages for failed operations
+- Enhanced UI feedback for user actions
+
+---
+
+## Version 3.3.0 - Smart Search & Enhanced Mobile (January 26, 2026)
+
+### ๐ Smart Search Feature
+- **Full Filesystem Indexing** - Index entire PS5 filesystem (200,000+ files)
+- **Instant Search** - Lightning-fast search with wildcards (`*.pkg`, `*loader*`)
+- **Size Filters** - Search by file size (`size:>1GB`, `size:<100MB`)
+- **Case-Insensitive** - Matches both filename and full path
+- **Navigate from Search** - Double-click results to jump to folder (Desktop) / Tap to navigate (Mobile)
+- **Index Status** - Real-time indexing progress with file/directory counts
+- **Smart Indexing** - Skips problematic directories (`/dev`, `/proc`, `/sys`)
+
+### ๐ฑ Mobile App v1.2
+- **๐ Search Tab** - Full Smart Search functionality on Android
+- **Navigate to Location** - Tap search results to navigate to folder
+- **Copy Path** - Copy file paths to clipboard from search results
+- **Connection Manager** - Singleton pattern for reliable IP/port management
+- **Improved Navigation** - Fixed MainPage reference for seamless navigation
+
+### ๐ฅ๏ธ Desktop Client Updates
+- **Search Tab** - New dedicated tab for Smart Search
+- **๐ป Shell Terminal** - Execute commands directly on PS5
+ - Run system commands remotely
+ - Real-time output display
+ - Command history
+ - Working directory support
+- **Equal Panel Layout** - Local Files, PS5 Files, and Right panel now equal width
+- **Improved UI** - Index Status and buttons positioned above search box
+- **Debounced Search** - Smooth typing experience with 300ms debounce
+
+### ๐ง Payload Updates
+- **Indexing Commands** - `CMD_INDEX_START` (0x40), `CMD_INDEX_STATUS` (0x41), `CMD_SEARCH_INDEX` (0x42)
+- **In-Memory Index** - Linked list structure for fast search
+- **Wildcard Matching** - Support for `*` and `?` wildcards
+- **Thread-Safe** - Mutex-protected index operations
+- **Root Path Support** - Can index from `/` (entire filesystem)
+
+### ๐ Bug Fixes
+- Fixed indexing hangs when scanning root directory
+- Fixed search protocol compatibility between mobile and desktop
+- Fixed navigation stack issues in mobile app
+- Fixed nullable reference warnings in mobile code
+- Added proper error handling for index operations
+
+---
+
+## Version 3.2.1 - Privacy & Bug Fixes (January 24, 2026)
+
+### ๐ Privacy Improvements
+- **Removed disk space display** - No longer shows PS5 storage capacity (Free/Used/Total)
+- **Fixed storage query bug** - Resolved syntax errors in statvfs code that could cause crashes
+- **Privacy-focused** - Application no longer queries or displays sensitive storage information
+
+### ๐ Bug Fixes
+- Fixed potential crash from malformed storage query code
+- Removed unused protocol busy flag that caused compiler warnings
+- Cleaned up all storage-related UI elements and backend code
+
+---
+
+## Version 3.2.0 - Mobile Improvements & Bug Fixes (January 24, 2026)
+
+### ๐ฏ Improvements
+
+#### ๐พ Accurate Storage Display
+- **Storage now matches PS5 UI** - Shows ~848 GB instead of raw 872 GB
+- **Accounts for reserved space** - Same calculation as PS5 system
+- Uses proper calculation: `(total_blocks - reserved) * block_size`
+
+#### ๐ฑ Mobile App Improvements
+- **Multi-select mode** - Toggle button (โ๏ธ/โ
) to select multiple files/folders
+- **Folder browser** - Visual folder picker for Copy/Move destinations with recursive navigation
+- **Sorted file list** - Folders always displayed at top, then files (alphabetically)
+- **Exit confirmation** - Prompt dialog when pressing back button
+- **Delete folders** - Now supports folder deletion (not just files)
+- **Batch operations** - Delete/download multiple items at once
+
+#### 3. ๐ Bug Fixes
+- **Fixed empty folder deletion** - No more "Unexpected response" error when deleting empty directories
+- **Fixed Copy/Move crash** - No longer crashes when selecting destination folder (saved file info before browsing)
+- **Fixed multi-select count** - Correct item count in delete confirmation dialog
+- **Fixed folder navigation** - Can enter folders after actions complete (auto-exit multi-select mode)
+
+---
+
+## Version 3.1.0 - Parallel Chunked Uploads (January 2026)
+
+### ๐ Performance Improvements
+
+#### 1. โก Parallel Chunked Uploads (104 MB/s!)
+- **Large files (>100MB)** are now split into 500MB chunks
+- **4 parallel connections** upload chunks simultaneously
+- **Result:** ~104 MB/s upload speed (2x faster than before!)
+- **Fixed race condition:** First chunk creates file before others start
+- **Fixed closure bug:** Correct offset/size capture for parallel tasks
+
+#### 2. ๐ Real-time Progress Display
+- **Speed display** now updates in real-time during chunked uploads
+- **ETA display** shows accurate time remaining
+- **Progress bar** correctly shows 0-100% for chunked files
+- No more speed dropping to 0 during large file uploads
+
+#### 3. ๐พ Storage Display Improvements
+- **Changed label** to "Data Storage (/data)" for clarity
+- **Uses statfs with f_bavail** for more accurate available space
+- **Real-time updates** every 5 seconds when connected
+
+---
+
+## Version 3.0.0 - Mobile Client & Path Fixes (January 2026)
+
+### ๐ Critical Bug Fixes
+
+#### 1. ๐ง Path Normalization Fix
+- **Fixed:** Double-slash paths (`//mnt/ext1/...`) that caused directory creation to fail
+- **Affected:** All file operations (upload, download, delete, rename, copy, move)
+- **Solution:** Added `NormalizePath()` function in both client and server
+- **Result:** 100% reliable directory creation and file uploads
+
+#### 2. ๐ฑ Android Mobile Client
+- **NEW:** Full-featured Android app for PS5 file management
+- **Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
+- Upload files from phone to PS5
+- Download files from PS5 with Share option
+- Browse PS5 filesystem
+- Create new folders
+- Rename files/folders
+- Copy files/folders
+- Move files/folders
+- Delete files
+- Favorites for quick navigation
+- Debug Log with Copy to clipboard
+- Transfer History tracking
+
+#### 3. ๐ ๏ธ Server Improvements
+- Path normalization in all handler functions
+- More robust error handling
+- Improved stability for parallel uploads
+
+---
+
+## Version 2.1.0 - Performance & History (January 2026)
+
+### ๐ Performance Optimizations
+
+#### 1. โก Massive Upload Speed Boost (88-110 MB/s)
+- **Server-side:** Replaced `fwrite()` with direct `write()` syscalls
+- **Client-side:** 8 parallel large file uploads (optimal for PS5 disk)
+- **Result:** 80-110 MB/s aggregate upload speed on Gigabit Ethernet
+- **Peak bursts:** Up to 2.05 GB/s when hitting disk cache
+- **Per-file:** 11-14 MB/s sustained per connection
+
+#### 2. ๐ Transfer History
+- Complete history of all uploads and downloads
+- Success/Failed status tracking with error messages
+- Speed statistics (average, min, max)
+- Export to CSV/JSON for analysis
+- Persistent storage across sessions
+
+#### 3. ๐ Auto-Clear History on Startup
+- Optional checkbox to clear history automatically
+- Useful for keeping UI clean between sessions
+- Setting saved in `ps5_upload_settings.json`
+
+#### 4. ๐ฅ๏ธ Maximized Window UI
+- Application opens in full-screen mode by default
+- Better visibility for large file transfers
+- Can be resized/restored as needed
+
+### Technical Improvements
+- โ
**16MB socket buffers** (up from 4MB) for maximum throughput
+- โ
**Per-file mutex locking** - Parallel writes without race conditions
+- โ
**File pre-allocation** - Reduces disk fragmentation for large files
+- โ
**Direct syscalls** - Bypasses stdio buffering overhead
+
+---
+
+## Version 2.0.0 - Download & Multi-PS5 (January 2026)
+
+### ๐ 4 Major New Features
+
+#### 1. ๐ฅ Download Files (PS5 โ PC)
+- Right-click any file โ "โฌ๏ธ Download to PC"
+- Save file dialog for destination selection
+- Real-time progress tracking with speed display
+- Optimized with sendfile for maximum speed
+
+#### 2. ๐ File Search
+- Search box in PS5 Files panel
+- Real-time filtering as you type
+- Case-insensitive search
+- Quick "Clear" button to reset
+
+#### 3. โญ Favorites/Bookmarks
+- Save frequently used PS5 paths
+- Quick dropdown navigation
+- Add/Remove favorite paths
+- Persistent storage in JSON
+
+#### 4. ๐ฎ Multi-PS5 Support
+- Save multiple PS5 profiles (IP + name)
+- Quick switch between different PS5 consoles
+- Dropdown profile selector
+- Persistent profile storage
+
+---
+
+## Version 1.3.0 - Stable Release (January 13, 2026)
+
+### ๐ฏ Major Achievement
+**Complete stability overhaul** - Fixed all critical bugs causing connection drops, UI freezes, and upload failures. System now handles **42,801+ files** with **zero errors** and **full gigabit speeds**.
+
+---
+
+## ๐ง Bug Fixes
+
+### **PAYLOAD (Server-Side) - 7 Critical Fixes**
+
+#### 1. **Per-File Mutex Implementation**
+- **Issue:** Multiple threads could write to the same file simultaneously causing data corruption
+- **Fix:** Implemented per-file mutex system using hash map for file path locking
+- **Impact:** Prevents race conditions during chunked uploads
+
+#### 2. **Memory/Mutex Cleanup on Disconnect**
+- **Issue:** Mutexes and memory not properly released when client disconnected
+- **Fix:** Added proper cleanup in client disconnect handler
+- **Impact:** Prevents memory leaks and mutex deadlocks
+
+#### 3. **fflush() Race Condition**
+- **Issue:** `fflush()` called outside file mutex, causing potential data corruption
+- **Fix:** Moved `fflush()` inside mutex lock in `handle_upload_chunk`
+- **Location:** `main.c:869-890`
+- **Impact:** Ensures data integrity during concurrent writes
+
+#### 4. **File Creation Race Condition**
+- **Issue:** Multiple threads could attempt to create/open same file simultaneously
+- **Fix:** Added `pthread_mutex_lock` around file creation and `fseeko` in `handle_start_upload`
+- **Location:** `main.c:814-844`
+- **Impact:** Prevents file corruption during parallel uploads
+
+#### 5. **Global Scan Counter Issues**
+- **Issue:** Static variables in recursive `count_files_recursive` caused stale values
+- **Fix:** Moved `scan_count` and `last_scan_notify` to global scope
+- **Location:** `main.c:324-366`
+- **Impact:** Accurate progress reporting during folder deletion
+
+#### 6. **Directory Counting Bug**
+- **Issue:** `count_files_recursive` only counted files, not directories
+- **Fix:** Added directory counting in recursive function
+- **Location:** `main.c:350-351`
+- **Impact:** Correct file count for folders with only subdirectories
+
+#### 7. **Socket Timeout for Large Files** โญ **CRITICAL FIX**
+- **Issue:** PS5 closed connections during large file uploads (100MB+) due to default timeout
+- **Fix:** Added 5-minute socket timeout (`SO_RCVTIMEO`/`SO_SNDTIMEO`)
+- **Location:** `main.c:1142-1148`
+- **Impact:** **Eliminates all connection drops** - enables stable upload of 15GB+ files
+- **Additional:** Aggressive TCP keepalive (10s idle, 5s interval, 3 probes)
+
+---
+
+### **CLIENT (Windows App) - 8 Critical Fixes**
+
+#### 1. **ConnectAsync Timeout Memory Leak**
+- **Issue:** `TcpClient` not disposed on connection timeout
+- **Fix:** Added proper `Dispose()` call in timeout exception handler
+- **Location:** `Protocol.cs:62-67`
+- **Impact:** Prevents memory leaks during connection failures
+
+#### 2. **ConnectAsync Exception Memory Leak**
+- **Issue:** `TcpClient` not disposed on general connection exceptions
+- **Fix:** Added proper `Dispose()` call in exception handler
+- **Location:** `Protocol.cs:81-84`
+- **Impact:** Prevents memory leaks during network errors
+
+#### 3. **Disconnect Memory Leak**
+- **Issue:** `NetworkStream` and `TcpClient` not properly disposed
+- **Fix:** Added explicit `Dispose()` calls for both objects
+- **Location:** `Protocol.cs:91-92`
+- **Impact:** Prevents memory leaks during normal disconnection
+
+#### 4. **Upload Deadlock During Chunked Uploads**
+- **Issue:** Tasks never removed from `activeTasks` when connection index mismatched
+- **Fix:** Always remove completed task regardless of connection index validity
+- **Location:** `MainWindow.xaml.cs:692-734`
+- **Impact:** Prevents infinite wait during parallel chunked uploads
+
+#### 5. **Chunking Disabled for Stability** โญ **CRITICAL FIX**
+- **Issue:** PS5 cannot handle concurrent writes to same file, even with per-file mutex
+- **Fix:** Disabled chunking completely - use single connection per file
+- **Location:** `MainWindow.xaml.cs:611-614`
+- **Impact:** **Eliminates all "connection aborted" errors**
+- **Rationale:** Even with 2 chunks, PS5 actively closes connections during concurrent writes
+
+#### 6. **UI Freeze During Uploads**
+- **Issue:** `Dispatcher.Invoke()` blocked UI thread when uploading thousands of files
+- **Fix:** Replaced with `Dispatcher.InvokeAsync()` in `UpdateUploadStats`
+- **Location:** `MainWindow.xaml.cs:115-158`
+- **Impact:** **Fully responsive UI** during uploads
+
+#### 7. **Log Flooding Causing UI Freeze**
+- **Issue:** LogTextBox filled with hundreds of thousands of entries, causing UI slowdown
+- **Fix:**
+ - Throttle verbose messages (log every 50th message)
+ - Limit log size to 1000 lines (remove oldest 200 when limit reached)
+ - Use `InvokeAsync` with `Background` priority
+- **Location:** `MainWindow.xaml.cs:165-206`
+- **Impact:** **Prevents UI freeze** and memory issues
+
+#### 8. **Optimal Parallel Connection Strategy**
+- **Issue:** Need to balance speed vs stability
+- **Fix:** Use **6 parallel single-connection uploads** (no chunking)
+- **Impact:**
+ - **Full gigabit speeds** (60-150 MB/s total throughput)
+ - **Zero connection drops**
+ - **Maximum stability**
+
+---
+
+## ๐ Performance Improvements
+
+### Before Fixes:
+- โ Connection drops every 100-200 files
+- โ UI completely frozen during uploads
+- โ Chunked uploads failed with "connection aborted" errors
+- โ Memory leaks causing application slowdown
+- โ Upload deadlocks requiring application restart
+
+### After Fixes:
+- โ
**42,801+ files uploaded with ZERO errors**
+- โ
**Fully responsive UI** throughout entire upload
+- โ
**Consistent speeds:** 11-190 MB/s per connection
+- โ
**Total throughput:** 60-150 MB/s (full gigabit)
+- โ
**No memory leaks** - stable memory usage
+- โ
**No deadlocks** - perfect task management
+
+---
+
+## ๐ฏ Technical Architecture
+
+### Payload (PS5 Server):
+```c
+// 5 minute socket timeout for large files
+struct timeval tv;
+tv.tv_sec = 300;
+setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+setsockopt(client_sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
+
+// Aggressive TCP keepalive
+int keepidle = 10; // Start after 10s idle
+int keepintvl = 5; // Send probe every 5s
+int keepcnt = 3; // Drop after 3 failures
+setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
+setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
+setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));
+
+// Per-file mutex for concurrent uploads
+pthread_mutex_t* file_mutex = get_file_mutex(file_path);
+pthread_mutex_lock(file_mutex);
+fwrite(buffer, 1, bytes_received, fp);
+fflush(fp);
+pthread_mutex_unlock(file_mutex);
+```
+
+### Client (Windows):
+```csharp
+// 6 parallel single-connection uploads
+const int MaxParallelUploads = 6;
+
+// Chunking disabled for stability
+if (false) // Chunking permanently disabled
+{
+ // PS5 cannot handle concurrent writes to same file
+}
+
+// Throttled logging for responsive UI
+if (message.Contains("๐") || message.Contains("โฌ๏ธ Uploading:"))
+{
+ _logCounter++;
+ if (_logCounter % 50 != 0)
+ return; // Skip verbose messages
+}
+
+// Async UI updates
+Dispatcher.InvokeAsync(() => {
+ // Update UI without blocking
+}, DispatcherPriority.Background);
+```
+
+---
+
+## ๐ Tested Configuration
+
+### Test Case:
+- **Files:** 42,801 files
+- **Total Size:** ~7.52 GB
+- **File Types:** .xpps, .pak, .mp4, .bk2, .bank, .json, .prx, .sprx
+- **Largest File:** 15.96 GB (ac2-ps5.pak)
+
+### Results:
+- **Success Rate:** 100% (zero errors)
+- **Average Speed:** 60-150 MB/s
+- **Peak Speed:** 190 MB/s per connection
+- **UI Responsiveness:** Fully responsive throughout
+- **Memory Usage:** Stable (no leaks)
+- **Completion Time:** ~1.7 hours for 42,801 files
+
+---
+
+## ๐ Known Limitations
+
+1. **Chunking Disabled:** While this reduces speed for individual large files, it's necessary for PS5 stability
+2. **6 Connection Limit:** More connections cause overhead without speed benefit
+3. **PS5 Hardware Constraint:** PS5 cannot handle concurrent writes to same file, even with proper locking
+
+---
+
+## ๐ Migration Guide
+
+### From v1.2 to v1.3:
+1. **Upload new payload** (`ps5_upload_server.elf`) to PS5
+2. **Restart payload** with elfldr
+3. **Use new client** (`PS5Upload.exe`)
+4. **No configuration changes needed** - works out of the box
+
+### Breaking Changes:
+- None - fully backward compatible
+
+---
+
+## ๐จโ๐ป Developer Notes
+
+### Why Chunking Was Disabled:
+Multiple attempts were made to enable chunking with various configurations:
+- 4 chunks: Connection drops
+- 3 chunks: Connection drops
+- 2 chunks: Connection drops
+
+**Root Cause:** PS5 FreeBSD kernel actively closes TCP connections when multiple threads perform concurrent `fwrite()` operations on the same file, even with proper per-file mutex locking. The 5-minute socket timeout helps, but doesn't solve the fundamental issue of concurrent writes.
+
+**Solution:** Single connection per file ensures only one thread writes to each file, eliminating the issue entirely.
+
+### Why 6 Parallel Connections:
+- **Less than 6:** Underutilizes network bandwidth
+- **More than 6:** Increases CPU/memory overhead without speed benefit
+- **6 connections:** Perfect balance for PS5 hardware
+
+---
+
+## ๐ Credits
+
+**Developed by:** Manos
+**Testing:** Extensive real-world testing with 42,801+ files
+**Platform:** PS5 (FreeBSD) + Windows 10/11
+
+---
+
+## ๐ License
+
+This software is provided as-is for educational and personal use.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3be8ee2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,217 @@
+# ๐ฎ PS5 Suite v7.1.5 โ Complete PS5 Management Platform
+
+**By Manos**
+
+All-in-one management suite for jailbroken PS5 consoles. One app โ on Windows, Linux or macOS (Android build in progress) โ that does everything: high-speed file transfers, game mount/launch, decrypted save backup/restore, screenshots, live hardware monitoring, PKG streaming installs, full FPKG building on the PC, memory tools, fan control, kernel log and a remote shell.
+
+
+
+## ๐ธ Screenshots
+
+| | |
+|---|---|
+|  |  |
+| **File Transfer** โ dual PC/PS5 browser, storage bar | **Games** โ list, mount/launch context menu |
+|  |  |
+| **Saves & Media** โ decrypted mount/backup/restore | **System Info** โ live sensors, per-core CPU, RAM |
+|  |  |
+| **Shell** โ remote terminal on the console | **Debug Log** โ every operation, timestamped |
+
+---
+
+## ๐ฅ What's in this release
+
+| Asset | Platform |
+|---|---|
+| `PS5Suite-Windows-x64.exe` | Windows 10/11 64-bit โ self-contained, no .NET install needed |
+| `PS5Suite-Linux-x64` | Linux 64-bit โ `chmod +x` and run |
+| `PS5Suite-Linux-ARM64` | ARM64 Linux (Raspberry Pi, etc.) |
+| `PS5Suite-macOS-x64` | Intel Macs โ `chmod +x`, Gatekeeper: right-click โ Open |
+| `PS5Suite-macOS-ARM64` | Apple Silicon (M1/M2/M3/M4) |
+| `ps5_suite_server.elf` | **Required** on-console server component |
+
+---
+
+## ๐ Setup (2 minutes)
+
+1. Load a jailbreak / payload loader on the PS5 (e.g. GoldHEN, or etaHEN itemzflow).
+2. Open the app โ expand **โ๏ธ Payload Settings** โ **๐ Browse** โ pick `payload/ps5_suite_server.elf` โ **๐ค Send Payload** (default port 9020).
+3. The payload installs itself and starts the suite server (ports 9113โ9116).
+4. Press **๐** to auto-discover the console on your LAN, or type its IP โ **๐ Connect**.
+5. Done. Future payload updates go through **๐ Self-Update Payload** (pushes the new ELF through the running server) or **โฌ๏ธ Update from GitHub**.
+
+๐พ Save your console(s) as named **connection profiles** for one-click reconnect.
+
+---
+
+## ๐ EVERYTHING the program does โ feature by feature
+
+### ๐ Top bar / Connection
+
+- **Auto-discover (๐)** โ broadcasts on the LAN and finds the PS5 automatically.
+- **Profiles** โ save multiple consoles by name, delete them, reconnect instantly.
+- **Storage Info panel** โ free/total space on the console's drive, **๐ Refresh Storage** button.
+- **Payload Settings** โ pick the ELF, set the injection port (default 9020), **Send Payload**, **Self-Update Payload** (updates the running server in place), **Update from GitHub** (checks for new suite releases).
+- **Buy Me a Coffee** link in the header. โค๏ธ
+
+---
+
+### ๐ FILE TRANSFER (main screen โ dual browser)
+
+Two side-by-side panes: your PC on the left, the PS5 on the right.
+
+**Local PC pane**
+- Browse all local drives/folders like a file manager.
+- **๐ Files** โ multi-select files to queue. **๐ Folder** โ queue an entire folder (recursive).
+- **๐ NAS** โ add a network/SMB path (`\\server\share\folder`) directly โ uploads straight from your NAS to the PS5.
+- Right-click a local `.pkg` โ **๐ฆ Install on PS5** โ *streams the package from the PC to the console's installer over HTTP; no 20 GB copy to the PS5 drive needed.*
+- **๐๏ธ Clear** empties the upload queue.
+
+**PS5 remote pane**
+- Full filesystem browser: path bar + **Go**, โญ **favorites** (saved per console), live **search/filter** box.
+- Right-click menu: **โฌ๏ธ Download**, **๐ Rename**, **๐ Copy**, **โ๏ธ Move**, **๐๏ธ Delete**, **๐๏ธ Delete Selected** (multi-select).
+- Downloads ask where to save on the PC; folders download recursively.
+
+**Transfer engine**
+- **๐ค Upload** โ parallel chunked transfers; ~**280 MB/s** on fast LANs (2.5GbE), saturates gigabit easily.
+- Duplicate files on the PS5 trigger a dialog: skip / overwrite / rename.
+- Live progress bar with per-file %, overall speed (MB/s) and ETA. **โน STOP** cancels cleanly.
+- **๐ฎ Mount Games** โ mounts uploaded game dumps so they appear in the PS5's game list.
+- **Transfer History** โ every completed/failed transfer logged; **๐ Retry** failed items, **๐๏ธ Remove**/**Clear All**.
+
+---
+
+### ๐ฎ GAMES (sidebar)
+
+- Lists every installed + mounted title: name, title ID, region, version, size, path.
+- **โถ Launch Game** โ boots the title directly.
+- **โน๏ธ View Details** โ full info window (param.sfo metadata, content ID, category, versions).
+- **๐ฅ Mount Game** / **๐๏ธ Unmount Game** โ mount/unmount game images on the fly.
+- **๐ Open Path** โ jumps the PS5 browser straight into that game's folder.
+- **๐ฅ Mount All** batch-mounts everything in the list.
+
+---
+
+### ๐พ SAVES & MEDIA โ Saves
+
+- Enumerates all save data on the console (per user, per title) with size/type badges.
+- **๐ Mount (decrypt)** โ mounts the save decrypted on the PS5 so its files are readable.
+- **๐ Browse** โ opens the Save Browser window into the mounted save's files.
+- **๐ Unmount & save** โ unmounts and re-seals the save.
+- **๐ฅ Backup (decrypted)** โ downloads the decrypted save contents to PC.
+- **๐ฅ Backup (raw image)** โ downloads the raw encrypted save image 1:1.
+- **๐ค Restore** โ pushes a backup back onto the console.
+- **๐๏ธ Delete save** / **๐ Copy path**.
+
+---
+
+### ๐ท SAVES & MEDIA โ Screenshots
+
+- Grid of all captured screenshots & video clips on the PS5 with **thumbnails**, filename, size, date.
+- **โฌ Download** selected (or batch to a chosen folder), **๐๏ธ Delete**, **๐ Copy Path**.
+- Double-click/preview to view the image full-size.
+
+---
+
+### ๐ฅ๏ธ SYSTEM INFO (live hardware monitor)
+
+Everything is read **live from the console** โ nothing hardcoded; values the console can't report show as unavailable.
+
+- **System Information** โ model (CFI-xxxx), serial number, architecture, OS version, CPU cores, physical RAM.
+- **Extended Info** โ firmware version, product code, uptime this boot, power-cycle count, BD drive presence, thermal alert status.
+- **Live Sensors** (โ LIVE, auto-refresh) โ ๐ฅ CPU temp, ๐ก๏ธ SoC temp, โก CPU frequency, ๐ SoC power draw (watts).
+- **CPU Usage** โ per-core usage bars (all 8 cores) + total.
+- **RAM** โ used/free memory.
+- **Storage** โ free/total per mount point.
+- **Modules** โ **๐ Load** lists all loaded kernel/user modules.
+
+---
+
+### ๐ง TOOLS โ Shell
+
+- Run shell commands directly on the PS5 โ type, Enter, output streams back.
+- Scrollable session history, **๐๏ธ Clear**, **๐พ Save Log** to a PC file.
+
+---
+
+### ๐ง TOOLS โ Search
+
+- **๐ Start Index** โ the payload crawls the PS5 filesystem and builds a searchable index.
+- Instant search across **every** file/folder on the console โ results open straight in the browser.
+- **๐ Refresh** rebuilds, **๐๏ธ Clear** wipes the index.
+
+---
+
+### ๐ง TOOLS โ Fan Control
+
+- Reads the current fan threshold + live temperature.
+- **๐ก๏ธ Set Threshold** โ custom temperature target.
+- Presets: **โ๏ธ Cool (50 ยฐC)**, **โ๏ธ Balanced (60 ยฐC)**, **๐ Quiet (70 ยฐC)**.
+
+---
+
+### ๐ง TOOLS โ Memory
+
+- **Process list** of running PS5 processes (**โป refresh**).
+- **๐ First Scan / ๐ Next Scan** โ cheat-style value scanning in a process's memory.
+- **๐ Read** (hex dump, 256 B) and **โ๏ธ Write** at arbitrary addresses โ full debugging/patching toolkit.
+
+---
+
+### ๐ง TOOLS โ Kernel Log
+
+- Live kernel log viewer โ choose **Last 32 KB / Last 128 KB / Full buffer**.
+- **Auto (3s)** checkbox for continuous refresh, **๐ Refresh** manual.
+- **๐ Copy** to clipboard, **๐พ Saveโฆ** to file.
+
+---
+
+### ๐ง TOOLS โ PKG
+
+- **Install PKG** โ three ways:
+ - **๐ Browse PS5** โ pick a `.pkg` already on the console.
+ - **๐ป Browse PC** โ pick a local `.pkg`; it's **streamed over HTTP** (ranged requests, persistent keep-alive connections, 512 KB relay) straight into the PS5 installer. Verified with a 22.7 GB package.
+ - Via the local-pane right-click "Install on PS5".
+- **๐ Check Status** โ queries the console's installer for progress/result.
+- Staged `.pkg` files on the PS5 are **auto-deleted** after successful install.
+- PS5 notifications show the real package name (not a generic label).
+
+- **Convert to FPKG** โ turn an extracted game folder into an installable fake package **entirely on the PC**:
+ - Point at the source folder (with `sce_sys`), choose output, done.
+ - Auto-fills title ID / content ID / version from `param.json`.
+ - Optional **fake-signing** of `eboot.bin`, `*.elf`, `*.prx`, `*.sprx` (originals restored automatically after the build).
+ - Builds the full PS5 container: inner PFS image, encrypted+signed outer PFS, CNT entry table, all SHA3-256 digests, RSA-3072 signatures, FIH finalization โ powered by the bundled **LibProsperoPkg** engine.
+ - Output verified against real package structure (field-accurate, validator-tested).
+
+---
+
+### ๐งพ DEBUG LOG (sidebar)
+
+- Timestamped log of **every** operation the app performs โ connections, transfers, errors, PS5 responses.
+- **๐ Copy Log**, **๐๏ธ Clear Log**; a `ps5suite_*.log` file is also written automatically next to the executable for post-mortem debugging.
+- **Failed transfers** section with per-item **๐ Retry** and **๐๏ธ Remove**.
+
+---
+
+## ๐ง Technical details
+
+| Component | Technology |
+|---|---|
+| Client (Windows/Linux/macOS/Android) | C# / .NET 10, **Avalonia UI** โ one shared codebase everywhere |
+| On-console payload | C, built with the **PS5 Payload SDK** (`prospero-clang`) |
+| FPKG engine | **LibProsperoPkg** (bundled) โ CNT/PFS/PFSC/PFSv3/FIH build, read & verify |
+| Transfer protocol | Raw TCP, chunked + parallel streams |
+| Suite server ports | 9113โ9116 |
+| Local PKG streamer ports | 18990โ19009 |
+| Payload proxy port | 13801+ |
+
+- All heavy work (transfers, FPKG builds) runs on background threads โ the UI never freezes.
+- The Android app shares **the same UI + protocol code as the desktop EXE** (source included in the repo โ APK build in progress).
+-
+---
+
+## โ ๏ธ Disclaimer
+
+Requires a jailbroken PS5 with a payload loader. Save backup/restore and memory write features modify console state โ **use at your own risk**. For personal/educational use; no copyrighted content included.
+
+**Enjoy. โ Manos**
diff --git a/client-avalonia-android/MainActivity.cs b/client-avalonia-android/MainActivity.cs
new file mode 100644
index 0000000..8cc48b0
--- /dev/null
+++ b/client-avalonia-android/MainActivity.cs
@@ -0,0 +1,33 @@
+using Android.App;
+using Android.Content.PM;
+using Avalonia;
+using Avalonia.Android;
+
+namespace PS5Upload.Android;
+
+[Application]
+public class Application : AvaloniaAndroidApplication
+{
+ public Application(nint javaReference, global::Android.Runtime.JniHandleOwnership transfer)
+ : base(javaReference, transfer)
+ {
+ }
+
+ protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
+ {
+ return base.CustomizeAppBuilder(builder)
+ .WithInterFont()
+ .LogToTrace();
+ }
+}
+
+[Activity(
+ Label = "PS5 Suite",
+ Theme = "@style/MyTheme.NoActionBar",
+ Icon = "@mipmap/ic_launcher",
+ MainLauncher = true,
+ LaunchMode = LaunchMode.SingleTop,
+ ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)]
+public class MainActivity : AvaloniaMainActivity
+{
+}
diff --git a/client-avalonia-android/PS5SuiteAndroid.csproj b/client-avalonia-android/PS5SuiteAndroid.csproj
new file mode 100644
index 0000000..d0804ff
--- /dev/null
+++ b/client-avalonia-android/PS5SuiteAndroid.csproj
@@ -0,0 +1,51 @@
+
+
+ Exe
+ net10.0-android
+ 23
+ enable
+ 14
+ disable
+ false
+ com.manos.ps5suite
+ 1
+ 7.1.5
+ PS5 Suite
+ PS5Suite
+ PS5Upload
+ apk
+ false
+
+ false
+ false
+ Manos
+ PS5 Suite - Complete PS5 Management Platform
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\client-avalonia\libs\LibProsperoPkg.dll
+
+
+
+
+
+
+
+
diff --git a/client-avalonia-android/Properties/AndroidManifest.xml b/client-avalonia-android/Properties/AndroidManifest.xml
new file mode 100644
index 0000000..d3d82b0
--- /dev/null
+++ b/client-avalonia-android/Properties/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/client-avalonia-android/Resources/mipmap-anydpi-v26/ic_launcher.xml b/client-avalonia-android/Resources/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..4bc0db4
--- /dev/null
+++ b/client-avalonia-android/Resources/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/client-avalonia-android/Resources/mipmap-anydpi-v26/ic_launcher_round.xml b/client-avalonia-android/Resources/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..4bc0db4
--- /dev/null
+++ b/client-avalonia-android/Resources/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/client-avalonia-android/Resources/mipmap-hdpi/ic_launcher.png b/client-avalonia-android/Resources/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..918fea8
Binary files /dev/null and b/client-avalonia-android/Resources/mipmap-hdpi/ic_launcher.png differ
diff --git a/client-avalonia-android/Resources/mipmap-hdpi/ic_launcher_foreground.png b/client-avalonia-android/Resources/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..3824d66
Binary files /dev/null and b/client-avalonia-android/Resources/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/client-avalonia-android/Resources/mipmap-mdpi/ic_launcher.png b/client-avalonia-android/Resources/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..6051f41
Binary files /dev/null and b/client-avalonia-android/Resources/mipmap-mdpi/ic_launcher.png differ
diff --git a/client-avalonia-android/Resources/mipmap-mdpi/ic_launcher_foreground.png b/client-avalonia-android/Resources/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..47aa5ca
Binary files /dev/null and b/client-avalonia-android/Resources/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/client-avalonia-android/Resources/mipmap-xhdpi/ic_launcher.png b/client-avalonia-android/Resources/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..3ecfb44
Binary files /dev/null and b/client-avalonia-android/Resources/mipmap-xhdpi/ic_launcher.png differ
diff --git a/client-avalonia-android/Resources/mipmap-xhdpi/ic_launcher_foreground.png b/client-avalonia-android/Resources/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..cc73412
Binary files /dev/null and b/client-avalonia-android/Resources/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/client-avalonia-android/Resources/mipmap-xxhdpi/ic_launcher.png b/client-avalonia-android/Resources/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..9ebbc2d
Binary files /dev/null and b/client-avalonia-android/Resources/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/client-avalonia-android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png b/client-avalonia-android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..4c63654
Binary files /dev/null and b/client-avalonia-android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/client-avalonia-android/Resources/mipmap-xxxhdpi/ic_launcher.png b/client-avalonia-android/Resources/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b5e5d56
Binary files /dev/null and b/client-avalonia-android/Resources/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/client-avalonia-android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png b/client-avalonia-android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..f99702e
Binary files /dev/null and b/client-avalonia-android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/client-avalonia-android/Resources/values/ic_launcher_background.xml b/client-avalonia-android/Resources/values/ic_launcher_background.xml
new file mode 100644
index 0000000..471cb52
--- /dev/null
+++ b/client-avalonia-android/Resources/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #1E1E1E
+
diff --git a/client-avalonia-android/Resources/values/styles.xml b/client-avalonia-android/Resources/values/styles.xml
new file mode 100644
index 0000000..d99626a
--- /dev/null
+++ b/client-avalonia-android/Resources/values/styles.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/client-avalonia/App.axaml b/client-avalonia/App.axaml
new file mode 100644
index 0000000..bdd677a
--- /dev/null
+++ b/client-avalonia/App.axaml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/client-avalonia/App.axaml.cs b/client-avalonia/App.axaml.cs
new file mode 100644
index 0000000..b266aa7
--- /dev/null
+++ b/client-avalonia/App.axaml.cs
@@ -0,0 +1,63 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace PS5Upload
+{
+ public partial class App : Application
+ {
+ private static readonly string LogFilePath = Path.Combine(
+ AppDomain.CurrentDomain.BaseDirectory,
+ $"ps5suite_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log");
+
+ private static readonly object _logLock = new object();
+
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ LogToFile("=== Application Started ===");
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow();
+ }
+ else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
+ {
+#if ANDROID
+ // On Android there is no real Window host โ the single view embeds controls.
+ // Build the MainWindow (for its code-behind + dialog ownership) but host its
+ // content directly in the single view; a TopLevel can't be a child control.
+ var window = new MainWindow();
+ if (window.Content is Avalonia.Controls.Control content)
+ {
+ window.Content = null;
+ singleView.MainView = content;
+ }
+#else
+ singleView.MainView = new MainWindow();
+#endif
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ public static void LogToFile(string message)
+ {
+ try
+ {
+ lock (_logLock)
+ {
+ string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
+ File.AppendAllText(LogFilePath, $"[{timestamp}] {message}\n");
+ }
+ }
+ catch { /* Ignore logging errors */ }
+ }
+ }
+}
diff --git a/client-avalonia/DuplicateFileDialog.axaml b/client-avalonia/DuplicateFileDialog.axaml
new file mode 100644
index 0000000..a07a059
--- /dev/null
+++ b/client-avalonia/DuplicateFileDialog.axaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client-avalonia/DuplicateFileDialog.axaml.cs b/client-avalonia/DuplicateFileDialog.axaml.cs
new file mode 100644
index 0000000..65f7cbf
--- /dev/null
+++ b/client-avalonia/DuplicateFileDialog.axaml.cs
@@ -0,0 +1,47 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace PS5Upload
+{
+ public partial class DuplicateFileDialog : Window
+ {
+ public enum FileAction { Replace, Skip, ReplaceAll, SkipAll }
+ public FileAction UserAction { get; private set; } = FileAction.Skip;
+
+ public DuplicateFileDialog()
+ {
+ InitializeComponent();
+ }
+
+ public DuplicateFileDialog(string fileName, long localSize, long remoteSize) : this()
+ {
+ FileNameText.Text = fileName;
+ LocalSizeText.Text = $"Local file size: {FileUtils.FormatFileSize(localSize)}";
+ RemoteSizeText.Text = $"Remote file size: {FileUtils.FormatFileSize(remoteSize)}";
+ }
+
+ private void ReplaceButton_Click(object? sender, RoutedEventArgs e)
+ {
+ UserAction = FileAction.Replace;
+ Close();
+ }
+
+ private void SkipButton_Click(object? sender, RoutedEventArgs e)
+ {
+ UserAction = FileAction.Skip;
+ Close();
+ }
+
+ private void ReplaceAllButton_Click(object? sender, RoutedEventArgs e)
+ {
+ UserAction = FileAction.ReplaceAll;
+ Close();
+ }
+
+ private void SkipAllButton_Click(object? sender, RoutedEventArgs e)
+ {
+ UserAction = FileAction.SkipAll;
+ Close();
+ }
+ }
+}
diff --git a/client-avalonia/GameDetailsWindow.axaml b/client-avalonia/GameDetailsWindow.axaml
new file mode 100644
index 0000000..82115bd
--- /dev/null
+++ b/client-avalonia/GameDetailsWindow.axaml
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client-avalonia/GameDetailsWindow.axaml.cs b/client-avalonia/GameDetailsWindow.axaml.cs
new file mode 100644
index 0000000..d637ba1
--- /dev/null
+++ b/client-avalonia/GameDetailsWindow.axaml.cs
@@ -0,0 +1,318 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+
+namespace PS5Upload
+{
+ public partial class GameDetailsWindow : Window
+ {
+ private readonly PS5MountedGame _game;
+ private readonly PS5Protocol? _protocol;
+ private readonly string _paramJsonRaw;
+ private static readonly HttpClient _http = CreateHttpClient();
+
+ private static HttpClient CreateHttpClient()
+ {
+ var client = new HttpClient();
+ client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
+ client.Timeout = TimeSpan.FromSeconds(8);
+ return client;
+ }
+
+ public GameDetailsWindow()
+ {
+ InitializeComponent();
+ _game = new PS5MountedGame();
+ _paramJsonRaw = "";
+ }
+
+ public GameDetailsWindow(PS5MountedGame game, Dictionary details, PS5Protocol? protocol = null) : this()
+ {
+ _game = game;
+ _protocol = protocol;
+ _paramJsonRaw = details.TryGetValue("param_json", out var pj) ? pj : "";
+
+ if (game.Icon != null) GameIconImage.Source = game.Icon;
+ GameNameText.Text = details.TryGetValue("name", out var n) ? n : game.Name;
+ TitleIdText.Text = game.TitleId;
+ RegionText.Text = details.TryGetValue("region", out var r) ? r : game.Region;
+ bool isActive = details.TryGetValue("is_active", out var act) && act == "1";
+ StatusText.Text = isActive ? "โ Mounted" : "โ Not Mounted";
+ StatusText.Foreground = isActive
+ ? new SolidColorBrush(Color.FromRgb(0x28, 0xA7, 0x45))
+ : new SolidColorBrush(Color.FromRgb(0xDC, 0x35, 0x45));
+
+ if (details.TryGetValue("total_size", out var ts) && ulong.TryParse(ts, out ulong totalBytes))
+ TotalSizeText.Text = FormatSize(totalBytes);
+ if (details.TryGetValue("eboot_size", out var es) && ulong.TryParse(es, out ulong ebootBytes))
+ EbootSizeText.Text = FormatSize(ebootBytes);
+
+ SourcePathText.Text = details.TryGetValue("path", out var p) ? p : game.Path;
+ InstallDateText.Text = details.TryGetValue("install_date", out var d) ? d : "Unknown";
+
+ ParamJsonText.Text = details.TryGetValue("param_json", out var param) ? FormatJson(param) : "(not available)";
+
+ _ = LoadCoverArtAsync();
+ }
+
+ private async Task LoadCoverArtAsync()
+ {
+ if (_protocol == null || !_protocol.IsConnected) return;
+ try
+ {
+ var picBytes = await _protocol.GetGamePicAsync(_game.TitleId, 0);
+ if (picBytes == null || picBytes.Length == 0) return;
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ try
+ {
+ using var ms = new MemoryStream(picBytes);
+ var bitmap = new Bitmap(ms);
+ BackgroundCoverImage.Source = bitmap;
+ }
+ catch { }
+ });
+ }
+ catch { }
+ }
+
+ private void OpenInBrowserButton_Click(object? sender, RoutedEventArgs e)
+ {
+ try
+ {
+ string query = !string.IsNullOrWhiteSpace(_game.Name) && _game.Name != "Unknown" ? _game.Name : _game.TitleId;
+ string url = $"https://store.playstation.com/en-gb/search/{Uri.EscapeDataString(query)}";
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = url, UseShellExecute = true });
+ }
+ catch { }
+ }
+
+ private async void FetchPsnInfoButton_Click(object? sender, RoutedEventArgs e)
+ {
+ FetchPsnInfoButton.IsEnabled = false;
+ PsnStatusText.Text = "Fetching from PSN Store...";
+ PsnInfoPanel.IsVisible = false;
+
+ try
+ {
+ var info = await FetchPsnInfoAsync(_game.TitleId);
+ if (info == null)
+ {
+ PsnStatusText.Text = "โ Not found on PSN Store. Try the browser search button below.";
+ return;
+ }
+ PsnTitleText.Text = info.Title;
+ PsnPublisherText.Text = string.IsNullOrEmpty(info.Publisher) ? "" : $"๐ข {info.Publisher}";
+ PsnReleaseDateText.Text = string.IsNullOrEmpty(info.ReleaseDate) ? "" : $"๐
Released: {info.ReleaseDate}";
+ PsnDescriptionText.Text = info.Description ?? "";
+ PsnInfoPanel.IsVisible = true;
+ PsnStatusText.Text = $"โ Found on PSN Store ({info.Source})";
+ }
+ catch (Exception ex) { PsnStatusText.Text = $"โ Error: {ex.Message}"; }
+ finally { FetchPsnInfoButton.IsEnabled = true; }
+ }
+
+ private class PsnGameInfo
+ {
+ public string Title { get; set; } = "";
+ public string Publisher { get; set; } = "";
+ public string ReleaseDate { get; set; } = "";
+ public string Description { get; set; } = "";
+ public string Source { get; set; } = "";
+ }
+
+ private async Task FetchPsnInfoAsync(string titleId)
+ {
+ var idsToTry = new List { titleId + "_00" };
+ var alternateIds = ExtractAlternateTitleIds(_paramJsonRaw);
+ foreach (var altId in alternateIds)
+ if (!idsToTry.Contains(altId)) idsToTry.Add(altId);
+
+ string[] chihiroRegions = { "US/en", "GB/en", "DE/de", "FR/fr", "JP/ja", "IT/it", "ES/es" };
+ foreach (var tryId in idsToTry)
+ {
+ foreach (var region in chihiroRegions)
+ {
+ try
+ {
+ var parts = region.Split('/');
+ string url = $"https://store.playstation.com/store/api/chihiro/00_09_000/titlecontainer/{parts[0]}/{parts[1]}/999/{tryId}";
+ var response = await _http.GetAsync(url);
+ if (!response.IsSuccessStatusCode) continue;
+ string json = await response.Content.ReadAsStringAsync();
+ if (json.Length < 50) continue;
+ var info = ParsePsnJson(json);
+ if (info != null) { string tag = tryId == titleId + "_00" ? "" : $" via alt ID {tryId}"; info.Source = $"Chihiro API ({region}){tag}"; return info; }
+ }
+ catch { }
+ }
+ }
+
+ string[] valkyrieRegions = { "gb/GB", "us/US", "de/DE", "fr/FR", "ja/JP", "it/IT", "es/ES" };
+ foreach (var region in valkyrieRegions)
+ {
+ try
+ {
+ string url = $"https://store.playstation.com/valkyrie-api/{region.Split('/')[0]}/{region.Split('/')[1]}/19/resolve/{titleId}_00";
+ var response = await _http.GetAsync(url);
+ if (!response.IsSuccessStatusCode) continue;
+ string json = await response.Content.ReadAsStringAsync();
+ if (json.Length < 50) continue;
+ var info = ParseValkyrieJson(json);
+ if (info != null) { info.Source = $"Valkyrie API ({region})"; return info; }
+ }
+ catch { }
+ }
+
+ string gameName = _game.Name;
+ if (!string.IsNullOrWhiteSpace(gameName) && gameName != "Unknown")
+ {
+ try
+ {
+ string encoded = Uri.EscapeDataString(gameName);
+ string url = $"https://store.playstation.com/valkyrie-api/en/US/19/tumbler-search/{encoded}?suggested_size=5&mode=game";
+ var response = await _http.GetAsync(url);
+ if (response.IsSuccessStatusCode)
+ {
+ string json = await response.Content.ReadAsStringAsync();
+ var info = ParseValkyrieSearchJson(json, gameName);
+ if (info != null) { info.Source = "Valkyrie Search (by name)"; return info; }
+ }
+ }
+ catch { }
+ }
+ return null;
+ }
+
+ private static PsnGameInfo? ParseValkyrieJson(string json)
+ {
+ try
+ {
+ using var doc = System.Text.Json.JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ if (!root.TryGetProperty("included", out var included) || included.ValueKind != System.Text.Json.JsonValueKind.Array) return null;
+ foreach (var item in included.EnumerateArray())
+ {
+ if (!item.TryGetProperty("attributes", out var attrs)) continue;
+ var info = new PsnGameInfo();
+ if (attrs.TryGetProperty("name", out var name)) info.Title = name.GetString() ?? "";
+ if (attrs.TryGetProperty("long-description", out var ld)) info.Description = StripHtml(ld.GetString() ?? "");
+ else if (attrs.TryGetProperty("description", out var sd)) info.Description = StripHtml(sd.GetString() ?? "");
+ if (attrs.TryGetProperty("release-date", out var rd)) { string raw = rd.GetString() ?? ""; info.ReleaseDate = DateTime.TryParse(raw, out DateTime dt) ? dt.ToString("yyyy-MM-dd") : raw; }
+ if (attrs.TryGetProperty("provider-name", out var pub)) info.Publisher = pub.GetString() ?? "";
+ else if (attrs.TryGetProperty("publisher-name", out var pub2)) info.Publisher = pub2.GetString() ?? "";
+ if (!string.IsNullOrWhiteSpace(info.Title)) return info;
+ }
+ }
+ catch { }
+ return null;
+ }
+
+ private static PsnGameInfo? ParseValkyrieSearchJson(string json, string searchName)
+ {
+ try
+ {
+ using var doc = System.Text.Json.JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ if (!root.TryGetProperty("included", out var included) || included.ValueKind != System.Text.Json.JsonValueKind.Array) return null;
+ PsnGameInfo? bestMatch = null;
+ foreach (var item in included.EnumerateArray())
+ {
+ if (!item.TryGetProperty("type", out var type) || type.GetString() != "game") continue;
+ if (!item.TryGetProperty("attributes", out var attrs) || !attrs.TryGetProperty("name", out var nameProp)) continue;
+ string name = nameProp.GetString() ?? "";
+ if (string.IsNullOrWhiteSpace(name)) continue;
+ if (string.Equals(name, searchName, StringComparison.OrdinalIgnoreCase) || name.Contains(searchName, StringComparison.OrdinalIgnoreCase) || searchName.Contains(name, StringComparison.OrdinalIgnoreCase))
+ {
+ var info = new PsnGameInfo { Title = name };
+ if (attrs.TryGetProperty("long-description", out var ld)) info.Description = StripHtml(ld.GetString() ?? "");
+ if (attrs.TryGetProperty("release-date", out var rd)) { string raw = rd.GetString() ?? ""; if (DateTime.TryParse(raw, out DateTime dt)) info.ReleaseDate = dt.ToString("yyyy-MM-dd"); }
+ if (attrs.TryGetProperty("provider-name", out var pub)) info.Publisher = pub.GetString() ?? "";
+ if (string.Equals(name, searchName, StringComparison.OrdinalIgnoreCase)) return info;
+ bestMatch ??= info;
+ }
+ }
+ return bestMatch;
+ }
+ catch { }
+ return null;
+ }
+
+ private static PsnGameInfo? ParsePsnJson(string json)
+ {
+ try
+ {
+ using var doc = System.Text.Json.JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ var info = new PsnGameInfo();
+ if (root.TryGetProperty("name", out var name)) info.Title = name.GetString() ?? "";
+ else if (root.TryGetProperty("title_name", out var tn)) info.Title = tn.GetString() ?? "";
+ if (root.TryGetProperty("provider_name", out var pub)) info.Publisher = pub.GetString() ?? "";
+ else if (root.TryGetProperty("publisher_name", out var pub2)) info.Publisher = pub2.GetString() ?? "";
+ if (root.TryGetProperty("release_date", out var rd)) { string rawDate = rd.GetString() ?? ""; info.ReleaseDate = DateTime.TryParse(rawDate, out DateTime dt) ? dt.ToString("yyyy-MM-dd") : rawDate; }
+ if (root.TryGetProperty("long_desc", out var ld)) info.Description = StripHtml(ld.GetString() ?? "");
+ else if (root.TryGetProperty("short_desc", out var sd)) info.Description = StripHtml(sd.GetString() ?? "");
+ if (!string.IsNullOrWhiteSpace(info.Title)) return info;
+ }
+ catch { }
+ return null;
+ }
+
+ private static List ExtractAlternateTitleIds(string paramJson)
+ {
+ var ids = new List();
+ if (string.IsNullOrWhiteSpace(paramJson)) return ids;
+ var regex = new System.Text.RegularExpressions.Regex(@"(?:[A-Z]{2}\d{4}-)?(PPSA\d{5}_\d{2}|CUSA\d{5}_\d{2})");
+ var matches = regex.Matches(paramJson);
+ foreach (System.Text.RegularExpressions.Match m in matches)
+ if (m.Groups.Count >= 2) { string id = m.Groups[1].Value; if (!ids.Contains(id)) ids.Add(id); }
+ return ids;
+ }
+
+ private static string StripHtml(string html)
+ {
+ if (string.IsNullOrWhiteSpace(html)) return "";
+ html = System.Text.RegularExpressions.Regex.Replace(html, @"
", "\n", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+ html = System.Text.RegularExpressions.Regex.Replace(html, @"
", "\n\n", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+ html = System.Text.RegularExpressions.Regex.Replace(html, @"<[^>]+>", "");
+ html = System.Net.WebUtility.HtmlDecode(html);
+ html = System.Text.RegularExpressions.Regex.Replace(html, @"[ \t]+", " ");
+ html = System.Text.RegularExpressions.Regex.Replace(html, @"\n{3,}", "\n\n");
+ return html.Trim();
+ }
+
+ private static string FormatSize(ulong bytes)
+ {
+ double mb = bytes / (1024.0 * 1024.0);
+ return mb < 1024 ? $"{mb:F1} MB ({bytes:N0} bytes)" : $"{mb / 1024.0:F2} GB ({bytes:N0} bytes)";
+ }
+
+ private static string FormatJson(string raw)
+ {
+ if (string.IsNullOrWhiteSpace(raw)) return "(empty)";
+ var sb = new System.Text.StringBuilder();
+ int depth = 0; bool inString = false;
+ foreach (char c in raw)
+ {
+ if (c == '"') inString = !inString;
+ if (!inString)
+ {
+ if (c == '{' || c == '[') { sb.Append(c); sb.Append('\n'); depth++; sb.Append(new string(' ', depth * 2)); continue; }
+ if (c == '}' || c == ']') { sb.Append('\n'); depth = Math.Max(0, depth - 1); sb.Append(new string(' ', depth * 2)); sb.Append(c); continue; }
+ if (c == ',') { sb.Append(c); sb.Append('\n'); sb.Append(new string(' ', depth * 2)); continue; }
+ }
+ sb.Append(c);
+ }
+ return sb.ToString();
+ }
+
+ private void CloseButton_Click(object? sender, RoutedEventArgs e) => Close();
+ }
+}
diff --git a/client-avalonia/MainWindow.Connection.cs b/client-avalonia/MainWindow.Connection.cs
new file mode 100644
index 0000000..5b3c2a0
--- /dev/null
+++ b/client-avalonia/MainWindow.Connection.cs
@@ -0,0 +1,788 @@
+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>(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
+ {
+ ["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(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();
+ 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