mirror of
https://github.com/manos555555/PS5-Suite.git
synced 2026-10-06 11:00:32 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0be4325bc5 | ||
|
|
343a5a531e |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
|
||||
# Release executables (too large for git, uploaded as release assets)
|
||||
release/**/PS5Upload.exe
|
||||
@@ -1,12 +1,83 @@
|
||||
# 🚀 PS5 Upload Suite - High-Speed File Transfer
|
||||
# 🚀 PS5 Upload Suite - Complete PS5 Management Platform
|
||||
|
||||
**By Manos**
|
||||
|
||||
**Version 4.2.1 - Bugfix Release**
|
||||
**Version 5.0.0 - Complete Management Suite**
|
||||
|
||||
Custom high-speed file transfer system for PS5 with etaHEN. Achieves **104+ MB/s** upload speeds for large files using parallel chunked uploads!
|
||||
Complete all-in-one PS5 management platform with **104+ MB/s** high-speed file transfers, live hardware monitoring, full game/save/screenshot management, and deep PS5 integration — all through a modern desktop client and a robust PS5 payload.
|
||||
|
||||
⭐ **NEW in v4.2.1 (Bugfix):**
|
||||
---
|
||||
|
||||
⭐ **NEW in v5.0.0 (Complete Management Suite):**
|
||||
|
||||
### 🖥️ **NEW: Hardware Monitor Tab**
|
||||
- 📋 **System Info** — Model (CFI-xxxx), Serial Number, Architecture, OS version
|
||||
- 💾 **Physical RAM** — With 5-step detection fallback (PS5-specific API + sysctl chain + 16 GB default)
|
||||
- 🔥 **Live Sensors** — CPU temperature, SoC temperature, CPU frequency, SoC power consumption
|
||||
- 🔄 **Auto-refresh** — Configurable 5-second polling with automatic disconnect detection
|
||||
- 🛡️ **Payload Hardened** — Mutex + 1s cache for sensors, 5s throttle for power API, rejection of abnormal readings
|
||||
|
||||
### 📷 **NEW: Screenshots Tab**
|
||||
- 🖼️ **Visual Gallery** — Live thumbnail previews (200px downsampled for fast loading)
|
||||
- 💾 **Smart Cache** — Local cache in `%TEMP%` keyed by remote path hash — zero re-downloads on refresh
|
||||
- ⬇️ **Batch Download** — Multi-select and download multiple screenshots
|
||||
- 🗑️ **Clean Delete** — Removes ALL 3 files per screenshot (`.dat`, `.meta`, `.jpg.jpeg`) — no more ghost entries in PS5 gallery
|
||||
- 👁️ **Double-click Preview** — Opens in default image viewer
|
||||
- 🔍 **Full Path Scanning** — 5-level recursive scan of `/user/av_contents/thumbnails/photo/…`
|
||||
|
||||
### ▶️ **NEW: Launch Game**
|
||||
- Right-click any mounted game → **Launch Game**
|
||||
- Uses `sceLncUtilLaunchApp` (low-level) with fallback to `sceSystemServiceLaunchApp`
|
||||
- Works on mounted/sideloaded games (bypasses `0x80940005` permission error)
|
||||
- Triple-strategy launcher with detailed diagnostic error messages
|
||||
|
||||
### 💾 **NEW: Save Manager Tab**
|
||||
- **Full Save Browsing** — Per-game save data with metadata
|
||||
- **Game Icons** — Fetched from PSN/local param.sfo (cached)
|
||||
- **Download Saves** — Batch-download multiple saves to local folder
|
||||
- **Upload Saves** — Drag & drop save files back to PS5
|
||||
- **Save Details** — Size, modification time, title ID, path
|
||||
|
||||
### 🎮 **NEW: Game Details Window**
|
||||
- **PSN Cover Art** — Official game covers fetched from PlayStation Store
|
||||
- **Full Metadata** — param.json contents formatted nicely
|
||||
- **Online PSN Info** — Description, genres, age rating, publisher
|
||||
- **Alternate Title ID detection** — Auto-fallback for regional variants
|
||||
- **Browser Search Fallback** — Opens PSN store search if API fails
|
||||
|
||||
### 🔧 **Improved Storage Reporting**
|
||||
- ✅ Matches **PS5 Settings "Console Storage"** formula exactly
|
||||
- ✅ Total: `/user` effective + `/system_data` + `/system_ex`
|
||||
- ✅ Free: proper `f_bavail` across all partitions
|
||||
- ✅ Accounts for reserved space & per-partition blocksize
|
||||
- Example real-world PS5: **848 GB total / 354 GB free** ✓
|
||||
|
||||
### 🎮 **Improved Mount Games**
|
||||
- ✅ Correct `sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0)` signature
|
||||
- ✅ Registration BEFORE `mount.lnk` (correct order)
|
||||
- ✅ Direct in-payload registration (no external `game_mounter.elf` dependency)
|
||||
- ✅ Proper DRM patching & metadata copy
|
||||
|
||||
### 🛡️ **Stability Overhaul**
|
||||
- ✅ **Thread-safe sensor reads** — `pthread_mutex_t` prevents concurrent API crashes
|
||||
- ✅ **API call throttling** — SoC power read at most every 5s, sensor cache every 1s
|
||||
- ✅ **Cached static HW info** — Read once, serve forever (Model, Serial, OS)
|
||||
- ✅ **Unsafe APIs removed** — `sceKernelIccGet*`, `HwHasWlanBt`, `HwHasOpticalOut` (caused crashes)
|
||||
- ✅ **Client busy-flag guard** — `Interlocked` prevents overlapping refreshes
|
||||
- ✅ **Auto-stop timers** — Hardware auto-refresh stops on disconnect
|
||||
- ✅ **Graceful reconnect** — Connection lost → automatic reconnection attempt
|
||||
|
||||
---
|
||||
|
||||
⭐ **v4.2.2 (Stability & Mount Fix):**
|
||||
- 🎮 **Fixed Mount Games** - Games now properly appear on PS5 home screen after mounting
|
||||
- 🗑️ **Fixed Unmount Games** - Games now properly removed from PS5 home screen when unmounted
|
||||
- 🖥️ **Fixed Hardware Tab** - Added ScrollViewer for proper scrolling
|
||||
- 🔧 **Fixed sceAppInstUtil calls** - Correct API usage with proper arguments
|
||||
- ⚠️ **Disabled auto hardware refresh** - Prevents payload crashes (manual refresh still available)
|
||||
- 🛡️ **Improved stability** - Fixed delayed payload crashes during folder navigation
|
||||
|
||||
⭐ **v4.2.1 (Bugfix):**
|
||||
- 🖥️ **Fixed UI scrolling** - Connection panel no longer eats half the screen
|
||||
- 🔢 **Fixed file counting** - Chunked files now count correctly as completed
|
||||
- 📊 **Fixed frozen progress** - Progress updates no longer freeze during heavy uploads
|
||||
@@ -38,13 +109,18 @@ Download the latest release from the [Releases](https://github.com/manos555555/P
|
||||
|
||||
### 2. Windows GUI Client
|
||||
- **File:** `PS5Upload.exe`
|
||||
- Self-contained executable (no .NET installation required)
|
||||
- Modern dark theme UI
|
||||
- Self-contained executable (no .NET installation required, **~147 MB**)
|
||||
- Modern dark theme UI with multi-tab layout
|
||||
- **Tabs:** Debug Log, Transfer History, Shell, Search, Saves, Games, Screenshots, **Hardware 🆕**
|
||||
- Drag & drop file/folder upload
|
||||
- Real-time upload progress with sliding window speed tracking
|
||||
- Transfer History with success/failed tracking and retry
|
||||
- PS5 storage display (free/used/total)
|
||||
- PS5 storage display (free/used/total — matches PS5 Settings)
|
||||
- Game mounting with one-click Mount Games button
|
||||
- **NEW:** Launch any mounted game with one click
|
||||
- **NEW:** Live hardware monitor (CPU/SoC temps, frequency, power)
|
||||
- **NEW:** Screenshot gallery with thumbnail previews and batch download/delete
|
||||
- **NEW:** Save Manager with game icons and PSN metadata
|
||||
|
||||
### 3. 📱 Android Mobile Client
|
||||
- **File:** `PS5UploadMobile.apk`
|
||||
@@ -90,13 +166,19 @@ Download the latest release from the [Releases](https://github.com/manos555555/P
|
||||
✅ **Direct Disk I/O** - No temp files, direct write syscalls
|
||||
✅ **Multi-threaded** - Handle multiple clients simultaneously
|
||||
✅ **Per-File Mutex** - Safe parallel uploads without corruption
|
||||
✅ **Thread-safe Sensor Reads** - `pthread_mutex_t` around all kernel API calls
|
||||
✅ **Smart Caching** - 1s sensor cache, 5s power-API throttle, permanent HW info cache
|
||||
✅ **5-Minute Socket Timeout** - No connection drops on large files
|
||||
✅ **Game Mounting** - nullfs mount, DRM patching, metadata copy
|
||||
✅ **Game Mounting** - nullfs mount, DRM patching, metadata copy, `sceAppInstUtil` registration
|
||||
✅ **Game Launching** - `sceLncUtilLaunchApp` + fallback chain for pirated/mounted games
|
||||
✅ **Shell Commands** - Built-in shell with ls, cd, cat, mkdir, rm, etc.
|
||||
✅ **Filesystem Indexing** - In-memory index for instant search
|
||||
✅ **Screenshot Management** - Full scan + safe delete of PS5 media files (.dat + .meta + .jpg.jpeg)
|
||||
✅ **Hardware APIs** - Model, serial, temps, CPU freq, power via verified Sony kernel APIs
|
||||
✅ **Storage Matching PS5 Settings** - Aggregates /user + /system_data + /system_ex
|
||||
|
||||
### Windows Client
|
||||
✅ **Modern UI** - Dark theme, maximized window
|
||||
✅ **Modern UI** - Dark theme, maximized window, 8+ functional tabs
|
||||
✅ **Drag & Drop** - Files and folders
|
||||
✅ **Browse PS5** - Navigate filesystem with favorites
|
||||
✅ **Real-time Speed** - Sliding window algorithm for accurate speed display
|
||||
@@ -106,26 +188,65 @@ Download the latest release from the [Releases](https://github.com/manos555555/P
|
||||
✅ **Duplicate File Dialog** - Handle file conflicts during upload
|
||||
✅ **Folder Upload** - Recursive directory upload
|
||||
✅ **Download Files** - Download from PS5 to PC
|
||||
✅ **💾 Storage Display** - Real-time PS5 storage info (free/used/total)
|
||||
✅ **💾 Storage Display** - Real-time PS5 storage info (matches PS5 Settings exactly)
|
||||
✅ **🔍 Smart Search** - Full filesystem indexing with instant search
|
||||
- Wildcard support: `*.pkg`, `*loader*`, `game*.bin`
|
||||
- Size filters: `size:>1GB`, `size:<100MB`
|
||||
- Case-insensitive matching
|
||||
- Search both filename and full path
|
||||
- Double-click to navigate to folder
|
||||
|
||||
✅ **💻 Shell Terminal** - Execute commands directly on PS5
|
||||
- Run system commands remotely
|
||||
- Real-time output display
|
||||
- Command history
|
||||
- Working directory support
|
||||
|
||||
✅ **⭐ Favorites/Bookmarks** - Quick navigation to saved paths
|
||||
✅ **👥 Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
|
||||
|
||||
✅ **🎮 Mount Games** - Mount uploaded games directly from the client
|
||||
- Scans all game paths: internal, USB drives (0-3), M.2 SSD
|
||||
- Duplicate detection across storage locations
|
||||
- Auto-cleanup of deleted game mounts
|
||||
- PS5 notifications with mount progress
|
||||
- Automatic game registration via game_mounter.elf
|
||||
- In-payload registration (no external binary required)
|
||||
|
||||
✅ **▶️ Launch Games** - One-click launch for mounted games
|
||||
- `sceLncUtilLaunchApp` low-level launcher
|
||||
- Works on pirated/sideloaded games
|
||||
- Fallback to SystemService API
|
||||
- Right-click context menu on any game
|
||||
|
||||
✅ **💾 Save Manager Tab** - Full PS5 save file management
|
||||
- Per-game save browsing with thumbnails
|
||||
- Game icons fetched and cached from PSN/param.sfo
|
||||
- Batch download saves to local backup folder
|
||||
- Upload saves back to PS5
|
||||
- Size, date, and metadata display
|
||||
|
||||
✅ **🎮 Game Details Window** - Rich game information
|
||||
- PSN cover art (fetched from PlayStation Store)
|
||||
- Full param.json metadata formatted
|
||||
- Online PSN description, genres, age rating, publisher
|
||||
- Alternate title ID detection (regional variants)
|
||||
- Browser search fallback
|
||||
|
||||
✅ **📷 Screenshots Tab** - Complete media gallery
|
||||
- Visual thumbnail previews (downsampled 200px)
|
||||
- Local cache in %TEMP% (reuses across refreshes)
|
||||
- Batch download/delete multi-select
|
||||
- Double-click preview in default image viewer
|
||||
- Clean delete removes ALL 3 PS5 files (.dat + .meta + .jpg.jpeg)
|
||||
- No more ghost entries in PS5 Media Gallery
|
||||
|
||||
✅ **🖥️ Hardware Monitor Tab** - Live system stats
|
||||
- **System Info:** Model (CFI-xxxx), Serial, Architecture, OS, CPU Cores, RAM, Wi-Fi/BT
|
||||
- **Live Sensors:** CPU temp, SoC temp, CPU frequency, SoC power consumption
|
||||
- Auto-refresh with configurable 5s interval
|
||||
- Busy-flag guard prevents overlapping refreshes
|
||||
- Auto-stops on disconnect
|
||||
- Progress bars for visual feedback
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<Window x:Class="PS5Upload.GameDetailsWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Game Details"
|
||||
Height="720" Width="860"
|
||||
Background="#1E1E1E"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="CanResize">
|
||||
<Grid>
|
||||
<!-- Cover Art Background (pic0.png) -->
|
||||
<Image x:Name="BackgroundCoverImage"
|
||||
Stretch="UniformToFill"
|
||||
Opacity="0.25"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
|
||||
<!-- Gradient overlay for readability -->
|
||||
<Rectangle>
|
||||
<Rectangle.Fill>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#CC1E1E1E" Offset="0"/>
|
||||
<GradientStop Color="#E61E1E1E" Offset="0.5"/>
|
||||
<GradientStop Color="#FF1E1E1E" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header: icon + title -->
|
||||
<Border Grid.Row="0" Background="#CC2D2D30" CornerRadius="6" Padding="15">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" Width="128" Height="128" CornerRadius="8"
|
||||
Background="#3E3E42" ClipToBounds="True" Margin="0,0,20,0">
|
||||
<Grid>
|
||||
<TextBlock Text="🎮" FontSize="64"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Image x:Name="GameIconImage"
|
||||
Stretch="UniformToFill"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="GameNameText"
|
||||
Text="Game Name"
|
||||
FontSize="22" FontWeight="Bold"
|
||||
Foreground="White"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock x:Name="TitleIdText"
|
||||
Text="TITLE_ID"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="#007ACC"/>
|
||||
<TextBlock Text=" • " Foreground="#666666" FontSize="14"/>
|
||||
<TextBlock x:Name="RegionText"
|
||||
Text="Region"
|
||||
FontSize="14" Foreground="#FFC107"/>
|
||||
<TextBlock Text=" • " Foreground="#666666" FontSize="14"/>
|
||||
<TextBlock x:Name="StatusText"
|
||||
Text="Status"
|
||||
FontSize="14" Foreground="#28A745"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Details panel -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="0,15,0,15">
|
||||
<StackPanel>
|
||||
<Border Background="#CC2D2D30" CornerRadius="6" Padding="15" Margin="0,0,0,10">
|
||||
<StackPanel>
|
||||
<TextBlock Text="📊 Size & Storage" FontSize="14" FontWeight="Bold"
|
||||
Foreground="#9CDCFE" Margin="0,0,0,8"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="140"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Total size:"
|
||||
Foreground="#999999" Margin="0,0,0,4"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" x:Name="TotalSizeText"
|
||||
Text="-" Foreground="White" FontWeight="SemiBold"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="eboot.bin:"
|
||||
Foreground="#999999" Margin="0,0,0,4"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="EbootSizeText"
|
||||
Text="-" Foreground="White"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Background="#CC2D2D30" CornerRadius="6" Padding="15" Margin="0,0,0,10">
|
||||
<StackPanel>
|
||||
<TextBlock Text="📁 Paths" FontSize="14" FontWeight="Bold"
|
||||
Foreground="#9CDCFE" Margin="0,0,0,8"/>
|
||||
<TextBlock Text="Source path:" Foreground="#999999"/>
|
||||
<TextBlock x:Name="SourcePathText" Text="-"
|
||||
Foreground="White" TextWrapping="Wrap" Margin="0,2,0,8"/>
|
||||
<TextBlock Text="Install date:" Foreground="#999999"/>
|
||||
<TextBlock x:Name="InstallDateText" Text="-"
|
||||
Foreground="White" Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Background="#CC2D2D30" CornerRadius="6" Padding="15" Margin="0,0,0,10">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="🌐 Online Info (PSN Store)" FontSize="14" FontWeight="Bold"
|
||||
Foreground="#9CDCFE" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" x:Name="FetchPsnInfoButton"
|
||||
Content="🔍 Fetch from Internet"
|
||||
Click="FetchPsnInfoButton_Click"
|
||||
Background="#007ACC" Foreground="White"
|
||||
Padding="10,4" BorderThickness="0"/>
|
||||
</Grid>
|
||||
<TextBlock x:Name="PsnStatusText" Text="Click the button above to fetch online info"
|
||||
Foreground="#888888" FontStyle="Italic" Margin="0,6,0,0"
|
||||
TextWrapping="Wrap"/>
|
||||
<StackPanel x:Name="PsnInfoPanel" Visibility="Collapsed" Margin="0,8,0,0">
|
||||
<TextBlock x:Name="PsnTitleText" FontSize="15" FontWeight="Bold"
|
||||
Foreground="White" Margin="0,0,0,4" TextWrapping="Wrap"/>
|
||||
<TextBlock x:Name="PsnPublisherText" Foreground="#9CDCFE" Margin="0,0,0,4"/>
|
||||
<TextBlock x:Name="PsnReleaseDateText" Foreground="#FFC107" Margin="0,0,0,6"/>
|
||||
<ScrollViewer MaxHeight="180" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<TextBlock x:Name="PsnDescriptionText" Foreground="#CCCCCC"
|
||||
TextWrapping="Wrap"/>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
<Button x:Name="OpenInBrowserButton"
|
||||
Content="🌍 Search on PSN Store (browser)"
|
||||
Click="OpenInBrowserButton_Click"
|
||||
Background="#3E3E42" Foreground="White"
|
||||
Padding="10,4" BorderThickness="0"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,10,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Background="#CC2D2D30" CornerRadius="6" Padding="15">
|
||||
<StackPanel>
|
||||
<TextBlock Text="📜 param.json" FontSize="14" FontWeight="Bold"
|
||||
Foreground="#9CDCFE" Margin="0,0,0,8"/>
|
||||
<TextBox x:Name="ParamJsonText"
|
||||
IsReadOnly="True"
|
||||
TextWrapping="Wrap"
|
||||
Background="#1E1E1E"
|
||||
Foreground="#CCCCCC"
|
||||
BorderThickness="1"
|
||||
BorderBrush="#3E3E42"
|
||||
Padding="8"
|
||||
FontFamily="Consolas"
|
||||
FontSize="11"
|
||||
MinHeight="120"
|
||||
MaxHeight="240"
|
||||
VerticalScrollBarVisibility="Auto"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Footer buttons -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="Close" Click="CloseButton_Click"
|
||||
Background="#007ACC" Foreground="White"
|
||||
Padding="20,8" BorderThickness="0"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,496 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace PS5Upload
|
||||
{
|
||||
public partial class GameDetailsWindow : Window
|
||||
{
|
||||
private readonly PS5MountedGame _game;
|
||||
private readonly PS5Protocol? _protocol;
|
||||
private readonly string _paramJsonRaw;
|
||||
// Pre-configured HttpClient (properties locked after first use, set once here)
|
||||
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(PS5MountedGame game, Dictionary<string, string> details, PS5Protocol? protocol = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_game = game;
|
||||
_protocol = protocol;
|
||||
_paramJsonRaw = details.TryGetValue("param_json", out var pj) ? pj : "";
|
||||
|
||||
// Header
|
||||
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 System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x28, 0xA7, 0x45))
|
||||
: new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xDC, 0x35, 0x45));
|
||||
|
||||
// Sizes
|
||||
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);
|
||||
}
|
||||
|
||||
// Paths
|
||||
SourcePathText.Text = details.TryGetValue("path", out var p) ? p : game.Path;
|
||||
InstallDateText.Text = details.TryGetValue("install_date", out var d) ? d : "Unknown";
|
||||
|
||||
// param.json
|
||||
if (details.TryGetValue("param_json", out var param))
|
||||
{
|
||||
ParamJsonText.Text = FormatJson(param);
|
||||
}
|
||||
else
|
||||
{
|
||||
ParamJsonText.Text = "(not available)";
|
||||
}
|
||||
|
||||
// Load cover art async (pic0.png)
|
||||
_ = 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;
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var bitmap = new System.Windows.Media.Imaging.BitmapImage();
|
||||
bitmap.BeginInit();
|
||||
bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
|
||||
bitmap.StreamSource = new MemoryStream(picBytes);
|
||||
bitmap.EndInit();
|
||||
bitmap.Freeze();
|
||||
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 (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Could not open browser: {ex.Message}", "Error",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private async void FetchPsnInfoButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FetchPsnInfoButton.IsEnabled = false;
|
||||
PsnStatusText.Text = "Fetching from PSN Store...";
|
||||
PsnInfoPanel.Visibility = Visibility.Collapsed;
|
||||
|
||||
try
|
||||
{
|
||||
var info = await FetchPsnInfoAsync(_game.TitleId);
|
||||
if (info == null)
|
||||
{
|
||||
PsnStatusText.Text = "❌ Not found on PSN Store (game may be pirated/unreleased/region-locked). 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.Visibility = Visibility.Visible;
|
||||
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<PsnGameInfo?> FetchPsnInfoAsync(string titleId)
|
||||
{
|
||||
// HttpClient is already configured in the static initializer (UserAgent + Timeout)
|
||||
|
||||
// Build list of IDs to try: primary title_id first, then alternates from param.json
|
||||
var idsToTry = new List<string> { titleId + "_00" };
|
||||
|
||||
// Extract alternate IDs from param.json serviceIdForSharing field
|
||||
// Format examples: "UP4572-PPSA01768_00", "UP4572-CUSA12650_00", "UP4572-PPSA16388_00-00000000000HWOAD"
|
||||
var alternateIds = ExtractAlternateTitleIds(_paramJsonRaw);
|
||||
foreach (var altId in alternateIds)
|
||||
{
|
||||
if (!idsToTry.Contains(altId)) idsToTry.Add(altId);
|
||||
}
|
||||
|
||||
// Strategy 1: Chihiro API (titlecontainer) - VERIFIED working (returns full game data)
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Valkyrie API resolve endpoint (newer, for newer games)
|
||||
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 { }
|
||||
}
|
||||
|
||||
// Strategy 3: Search by game name on Valkyrie search API (useful for pirated games with real title)
|
||||
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;
|
||||
|
||||
// Valkyrie format: { "included": [ { "attributes": { "name", "long-description", "release-date", ... } } ] }
|
||||
if (!root.TryGetProperty("included", out var included)) return null;
|
||||
if (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() ?? "";
|
||||
if (DateTime.TryParse(raw, out DateTime dt))
|
||||
info.ReleaseDate = dt.ToString("yyyy-MM-dd");
|
||||
else
|
||||
info.ReleaseDate = 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)) return null;
|
||||
if (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)) continue;
|
||||
if (type.GetString() != "game") continue;
|
||||
|
||||
if (!item.TryGetProperty("attributes", out var attrs)) continue;
|
||||
if (!attrs.TryGetProperty("name", out var nameProp)) continue;
|
||||
|
||||
string name = nameProp.GetString() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||
|
||||
// Find best match by name similarity
|
||||
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() ?? "";
|
||||
|
||||
// Exact match wins immediately
|
||||
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() ?? "";
|
||||
if (DateTime.TryParse(rawDate, out DateTime dt))
|
||||
info.ReleaseDate = dt.ToString("yyyy-MM-dd");
|
||||
else
|
||||
info.ReleaseDate = 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;
|
||||
}
|
||||
|
||||
// Extract alternate title IDs from param.json serviceIdForSharing field.
|
||||
// The payload strips newlines from param.json but keeps structure intact.
|
||||
// Examples of entries to match:
|
||||
// "UP4572-PPSA01768_00"
|
||||
// "UP4572-CUSA12650_00"
|
||||
// "UP4572-PPSA16388_00-00000000000HWOAD" (full contentId with suffix)
|
||||
// Returns list of IDs in "PPSAxxxxx_00" or "CUSAxxxxx_00" format ready for API use.
|
||||
private static List<string> ExtractAlternateTitleIds(string paramJson)
|
||||
{
|
||||
var ids = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(paramJson)) return ids;
|
||||
|
||||
// Match publisher-prefix-free IDs: "PPSA12345_00" or "CUSA12345_00"
|
||||
// Handles both "UP4572-PPSA01768_00" (short) and "UP4572-PPSA16388_00-SUFFIX" (full contentId)
|
||||
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;
|
||||
}
|
||||
|
||||
// Strip HTML tags and decode common entities
|
||||
private static string StripHtml(string html)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(html)) return "";
|
||||
// Replace <br>, <br/>, </p> with newlines
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, @"<br\s*/?>", "\n", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, @"</p>", "\n\n", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
// Strip all remaining tags
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, @"<[^>]+>", "");
|
||||
// Decode common HTML entities
|
||||
html = System.Net.WebUtility.HtmlDecode(html);
|
||||
// Collapse excessive whitespace but keep newlines
|
||||
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);
|
||||
if (mb < 1024) return $"{mb:F1} MB ({bytes:N0} bytes)";
|
||||
return $"{mb / 1024.0:F2} GB ({bytes:N0} bytes)";
|
||||
}
|
||||
|
||||
private static string FormatJson(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return "(empty)";
|
||||
// Basic pretty-print: add newlines after { [ , and before } ]
|
||||
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 = System.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1030,6 +1030,498 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- Save Manager Tab -->
|
||||
<TabItem Header="💾 Saves">
|
||||
<Grid Background="#1E1E1E">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header with actions -->
|
||||
<Border Grid.Row="0"
|
||||
Background="#2D2D30"
|
||||
Padding="10"
|
||||
Margin="10,10,10,5"
|
||||
CornerRadius="4">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="💾 PS5 Save Games"
|
||||
FontSize="14"
|
||||
FontWeight="Bold"
|
||||
Foreground="#007ACC"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,15,0"/>
|
||||
<TextBlock x:Name="SaveCountText"
|
||||
Text=" (0 saves)"
|
||||
FontSize="12"
|
||||
Foreground="#999999"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,15,0"/>
|
||||
<Button x:Name="RefreshSavesButton"
|
||||
Content="🔄 Refresh"
|
||||
Click="RefreshSavesButton_Click"
|
||||
Background="#007ACC"
|
||||
Foreground="White"
|
||||
BorderThickness="0"
|
||||
Padding="10,5"/>
|
||||
<Button Content="📥 Backup Selected"
|
||||
Click="BackupSaveButton_Click"
|
||||
Background="#28A745"
|
||||
Foreground="White"
|
||||
BorderThickness="0"
|
||||
Padding="10,5"
|
||||
Margin="8,0,0,0"/>
|
||||
<Button Content="📤 Restore to PS5"
|
||||
Click="RestoreSaveButton_Click"
|
||||
Background="#FFC107"
|
||||
Foreground="#1E1E1E"
|
||||
BorderThickness="0"
|
||||
Padding="10,5"
|
||||
Margin="8,0,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Saves List -->
|
||||
<ListBox Grid.Row="1"
|
||||
x:Name="SavesListBox"
|
||||
Background="#1E1E1E"
|
||||
Foreground="White"
|
||||
BorderThickness="0"
|
||||
Margin="10,5,10,10"
|
||||
SelectionMode="Single">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="📥 Backup this save" Click="BackupSaveMenuItem_Click"/>
|
||||
<MenuItem Header="📤 Restore from backup" Click="RestoreSaveMenuItem_Click"/>
|
||||
<MenuItem Header="📂 Copy path to clipboard" Click="CopySavePathMenuItem_Click"/>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="#2D2D30"
|
||||
CornerRadius="4"
|
||||
Padding="10"
|
||||
Margin="2">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Save Icon -->
|
||||
<Grid Grid.Column="0" Width="48" Height="48" Margin="0,0,12,0">
|
||||
<Border Background="#3E3E42" CornerRadius="6">
|
||||
<TextBlock Text="💾"
|
||||
FontSize="24"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border CornerRadius="6" ClipToBounds="True">
|
||||
<Image Source="{Binding Icon}"
|
||||
Stretch="UniformToFill"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Save Info -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding GameName}"
|
||||
FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Foreground="White"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,3,0,0">
|
||||
<TextBlock Text="{Binding TitleId}"
|
||||
FontSize="11"
|
||||
Foreground="#007ACC"
|
||||
FontWeight="SemiBold"/>
|
||||
<TextBlock Text=" • User " Foreground="#666666" FontSize="11"/>
|
||||
<TextBlock Text="{Binding ShortUserId}"
|
||||
FontSize="11"
|
||||
Foreground="#FFC107"/>
|
||||
<TextBlock Text=" • " Foreground="#666666" FontSize="11"/>
|
||||
<TextBlock Text="{Binding SizeDisplay}"
|
||||
FontSize="11"
|
||||
Foreground="#9CDCFE"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding ModifiedDisplay}"
|
||||
FontSize="10"
|
||||
Foreground="#888888"
|
||||
Margin="0,3,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Status pill -->
|
||||
<Border Grid.Column="2"
|
||||
Background="#007ACC"
|
||||
CornerRadius="10"
|
||||
Padding="8,4"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="SAVE"
|
||||
FontSize="10"
|
||||
FontWeight="Bold"
|
||||
Foreground="White"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- Mounted Games Tab -->
|
||||
<TabItem Header="🎮 Games">
|
||||
<Grid Background="#1E1E1E">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header with Refresh button -->
|
||||
<Border Grid.Row="0"
|
||||
Background="#2D2D30"
|
||||
Padding="10"
|
||||
Margin="10,10,10,5"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<TextBlock Text="🎮 Mounted Games"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="#007ACC"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="GameCountText"
|
||||
Text=" (0 games)"
|
||||
FontSize="14"
|
||||
Foreground="#999999"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
x:Name="RefreshGameListButton"
|
||||
Content="🔄 Refresh"
|
||||
Click="RefreshGameListButton_Click"
|
||||
Background="#007ACC"
|
||||
Padding="15,8"
|
||||
Margin="0,0,10,0"/>
|
||||
<Button Grid.Column="2"
|
||||
x:Name="MountGamesFromListButton"
|
||||
Content="📥 Mount All"
|
||||
Click="MountGamesButton_Click"
|
||||
Background="#28A745"
|
||||
Padding="15,8"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Games List -->
|
||||
<ListBox Grid.Row="1"
|
||||
x:Name="MountedGamesListBox"
|
||||
Background="#1E1E1E"
|
||||
Foreground="White"
|
||||
BorderThickness="0"
|
||||
Margin="10,5,10,10"
|
||||
MouseDoubleClick="MountedGamesListBox_MouseDoubleClick">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="▶ Launch Game" Click="LaunchGameMenuItem_Click"/>
|
||||
<MenuItem Header="ℹ️ View Details" Click="ViewGameDetailsMenuItem_Click"/>
|
||||
<MenuItem Header="🗑️ Unmount Game" Click="UnmountGameMenuItem_Click"/>
|
||||
<MenuItem Header="📂 Open Path in PS5 Browser" Click="OpenGamePathMenuItem_Click"/>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="#2D2D30"
|
||||
CornerRadius="4"
|
||||
Padding="10"
|
||||
Margin="2">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Game Icon -->
|
||||
<Grid Grid.Column="0" Width="64" Height="64" Margin="0,0,15,0">
|
||||
<Border Background="#3E3E42" CornerRadius="6">
|
||||
<TextBlock Text="🎮"
|
||||
FontSize="32"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border CornerRadius="6" ClipToBounds="True">
|
||||
<Image Source="{Binding Icon}"
|
||||
Stretch="UniformToFill"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Game Info -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontSize="14"
|
||||
FontWeight="Bold"
|
||||
Foreground="White"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,3,0,0">
|
||||
<TextBlock Text="{Binding TitleId}"
|
||||
FontSize="12"
|
||||
Foreground="#007ACC"
|
||||
FontWeight="SemiBold"/>
|
||||
<TextBlock Text=" • " Foreground="#666666"/>
|
||||
<TextBlock Text="{Binding Region}"
|
||||
FontSize="12"
|
||||
Foreground="#FFC107"/>
|
||||
<TextBlock Text=" • " Foreground="#666666"/>
|
||||
<TextBlock Text="{Binding SizeDisplay}"
|
||||
FontSize="12"
|
||||
Foreground="#9CDCFE"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Path}"
|
||||
FontSize="10"
|
||||
Foreground="#666666"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Margin="0,3,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Status -->
|
||||
<StackPanel Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right">
|
||||
<Border Background="#28A745"
|
||||
CornerRadius="10"
|
||||
Padding="8,4">
|
||||
<TextBlock Text="Mounted"
|
||||
FontSize="11"
|
||||
Foreground="White"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- Screenshots Tab -->
|
||||
<TabItem Header="📷 Screenshots">
|
||||
<Grid Background="#1E1E1E">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="10,10,10,5">
|
||||
<TextBlock Text="📷 PS5 Screenshots" FontWeight="Bold"
|
||||
Foreground="#0E9CFF" FontSize="14"
|
||||
VerticalAlignment="Center" Margin="0,0,15,0"/>
|
||||
<TextBlock x:Name="ScreenshotsCountText" Text="(0 items)"
|
||||
Foreground="#A0A0A0" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="0,0,15,0"/>
|
||||
<Button Content="🔄 Refresh" Padding="10,4"
|
||||
Background="#0E639C" Foreground="White" BorderThickness="0"
|
||||
Margin="0,0,8,0" Click="RefreshScreenshotsButton_Click"/>
|
||||
<Button Content="⬇ Download Selected" Padding="10,4"
|
||||
Background="#2D7D2D" Foreground="White" BorderThickness="0"
|
||||
Margin="0,0,8,0" Click="DownloadScreenshotButton_Click"/>
|
||||
<Button Content="🗑️ Delete Selected" Padding="10,4"
|
||||
Background="#C44" Foreground="White" BorderThickness="0"
|
||||
Click="DeleteScreenshotButton_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- List -->
|
||||
<ListBox Grid.Row="1"
|
||||
x:Name="ScreenshotsListBox"
|
||||
Background="#1E1E1E" Foreground="White"
|
||||
BorderThickness="0" Margin="10,5,10,10"
|
||||
SelectionMode="Extended"
|
||||
MouseDoubleClick="ScreenshotsListBox_MouseDoubleClick">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="⬇ Download" Click="DownloadScreenshotButton_Click"/>
|
||||
<MenuItem Header="📋 Copy Path" Click="CopyScreenshotPathMenuItem_Click"/>
|
||||
<MenuItem Header="🗑️ Delete" Click="DeleteScreenshotButton_Click"/>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="#2D2D30" CornerRadius="4" Padding="10" Margin="2">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid Grid.Column="0" Width="72" Height="48" Margin="0,0,12,0">
|
||||
<Border Background="#3E3E42" CornerRadius="6">
|
||||
<TextBlock Text="🖼️" FontSize="24"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border CornerRadius="6" ClipToBounds="True">
|
||||
<Image Source="{Binding Thumbnail}" Stretch="UniformToFill"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding FileName}"
|
||||
FontWeight="Bold" FontSize="13" Foreground="White"/>
|
||||
<TextBlock Text="{Binding FullPath}"
|
||||
FontSize="10" Foreground="#808080"
|
||||
TextTrimming="CharacterEllipsis" Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right">
|
||||
<TextBlock Text="{Binding SizeDisplay}" FontSize="11"
|
||||
Foreground="#0E9CFF" HorizontalAlignment="Right"/>
|
||||
<TextBlock Text="{Binding DateDisplay}" FontSize="10"
|
||||
Foreground="#A0A0A0" HorizontalAlignment="Right"
|
||||
Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- Hardware Tab -->
|
||||
<TabItem Header="🖥️ Hardware">
|
||||
<ScrollViewer Background="#1E1E1E" VerticalScrollBarVisibility="Auto">
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<TextBlock Text="🖥️ PS5 Hardware Monitor" FontWeight="Bold"
|
||||
Foreground="#0E9CFF" FontSize="16"
|
||||
VerticalAlignment="Center" Margin="0,0,15,0"/>
|
||||
<Button Content="🔄 Refresh" Padding="10,4"
|
||||
Background="#0E639C" Foreground="White" BorderThickness="0"
|
||||
Margin="0,0,8,0" Click="RefreshHardwareButton_Click"/>
|
||||
<CheckBox x:Name="HwAutoRefreshCheckBox" Content="Auto-refresh (3s)"
|
||||
Foreground="#DCDCDC" VerticalAlignment="Center"
|
||||
Checked="HwAutoRefresh_Changed" Unchecked="HwAutoRefresh_Changed"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="1" Background="#252526" BorderBrush="#3E3E42"
|
||||
BorderThickness="1" CornerRadius="4" Padding="15" Margin="0,0,0,10">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Text="📋 System Info" FontWeight="Bold"
|
||||
FontSize="14" Foreground="#0E9CFF" Margin="0,0,0,10"/>
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="160"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="160"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Model:" Foreground="#A0A0A0" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" x:Name="HwModelText" Text="—" Foreground="White" FontWeight="Bold" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="2" Text="Serial:" Foreground="#A0A0A0" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="3" x:Name="HwSerialText" Text="—" Foreground="White" FontWeight="Bold" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Architecture:" Foreground="#A0A0A0" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="HwMachineText" Text="—" Foreground="White" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="2" Text="OS:" Foreground="#A0A0A0" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="3" x:Name="HwOsText" Text="—" Foreground="White" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="CPU Cores:" Foreground="#A0A0A0" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" x:Name="HwCpuCoresText" Text="—" Foreground="White" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="2" Text="Physical RAM:" Foreground="#A0A0A0" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="3" x:Name="HwPhysMemText" Text="—" Foreground="White" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Wi-Fi / Bluetooth:" Foreground="#A0A0A0" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" x:Name="HwWlanBtText" Text="—" Foreground="White" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="2" Text="Optical Out:" Foreground="#A0A0A0" Margin="0,3"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="3" x:Name="HwOpticalOutText" Text="—" Foreground="White" Margin="0,3"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="2" Background="#252526" BorderBrush="#3E3E42"
|
||||
BorderThickness="1" CornerRadius="4" Padding="15" Margin="0,0,0,10">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Text="🌡️ Live Sensors" FontWeight="Bold"
|
||||
FontSize="14" Foreground="#0E9CFF" Margin="0,0,0,10"/>
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Row="0" Grid.Column="0" Margin="0,0,10,10">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock Text="🔥 CPU Temperature" Foreground="#A0A0A0" FontSize="12"/>
|
||||
<TextBlock x:Name="HwCpuTempText" Text="— °C" Foreground="#FFA500" FontWeight="Bold"
|
||||
FontSize="14" Margin="8,0,0,0"/>
|
||||
</StackPanel>
|
||||
<ProgressBar x:Name="HwCpuTempBar" Height="18" Minimum="0" Maximum="100" Value="0"
|
||||
Background="#1E1E1E" Foreground="#FF6B35" BorderBrush="#3E3E42"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" Margin="10,0,0,10">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock Text="🌡️ SoC Temperature" Foreground="#A0A0A0" FontSize="12"/>
|
||||
<TextBlock x:Name="HwSocTempText" Text="— °C" Foreground="#FFA500" FontWeight="Bold"
|
||||
FontSize="14" Margin="8,0,0,0"/>
|
||||
</StackPanel>
|
||||
<ProgressBar x:Name="HwSocTempBar" Height="18" Minimum="0" Maximum="100" Value="0"
|
||||
Background="#1E1E1E" Foreground="#FF6B35" BorderBrush="#3E3E42"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Margin="0,10,10,0">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock Text="⚡ CPU Frequency" Foreground="#A0A0A0" FontSize="12"/>
|
||||
<TextBlock x:Name="HwCpuFreqText" Text="— MHz" Foreground="#0E9CFF" FontWeight="Bold"
|
||||
FontSize="14" Margin="8,0,0,0"/>
|
||||
</StackPanel>
|
||||
<ProgressBar x:Name="HwCpuFreqBar" Height="18" Minimum="0" Maximum="3500" Value="0"
|
||||
Background="#1E1E1E" Foreground="#0E9CFF" BorderBrush="#3E3E42"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" Margin="10,10,0,0">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock Text="🔋 SoC Power" Foreground="#A0A0A0" FontSize="12"/>
|
||||
<TextBlock x:Name="HwSocPowerText" Text="— W" Foreground="#2D7D2D" FontWeight="Bold"
|
||||
FontSize="14" Margin="8,0,0,0"/>
|
||||
</StackPanel>
|
||||
<ProgressBar x:Name="HwSocPowerBar" Height="18" Minimum="0" Maximum="250" Value="0"
|
||||
Background="#1E1E1E" Foreground="#2D7D2D" BorderBrush="#3E3E42"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Row="3" x:Name="HwStatusText" Foreground="#707070" FontSize="11"
|
||||
HorizontalAlignment="Right" Margin="0,0,0,10"
|
||||
Text="Not loaded yet — click Refresh"/>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
+870
-33
@@ -108,6 +108,7 @@ namespace PS5Upload
|
||||
private enum DuplicateAction { Ask, Replace, Skip, ReplaceAll, SkipAll }
|
||||
private DuplicateAction _duplicateAction = DuplicateAction.Ask;
|
||||
|
||||
|
||||
// Shell Terminal
|
||||
private bool _shellActive = false;
|
||||
private ObservableCollection<string> _shellOutput = new ObservableCollection<string>();
|
||||
@@ -133,6 +134,7 @@ namespace PS5Upload
|
||||
_uiUpdateTimer.Tick += UiUpdateTimer_Tick;
|
||||
|
||||
|
||||
|
||||
// Subscribe to progress messages from protocol
|
||||
_protocol.OnProgressMessage += (message) =>
|
||||
{
|
||||
@@ -638,6 +640,9 @@ namespace PS5Upload
|
||||
|
||||
// Load storage info
|
||||
await RefreshStorageInfoAsync();
|
||||
|
||||
// NOTE: Hardware refresh disabled for stability
|
||||
// User can manually refresh with the Refresh button in Hardware tab
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -656,7 +661,7 @@ namespace PS5Upload
|
||||
// Ensure connection is active before loading directory
|
||||
if (!_protocol.IsConnected)
|
||||
{
|
||||
Log("⚠️ Connection lost, reconnecting...");
|
||||
Log($"⚠️ Connection lost (LastError: {_protocol.LastError}), reconnecting...");
|
||||
if (!await _protocol.ConnectAsync(_ps5IpAddress))
|
||||
{
|
||||
MessageBox.Show("Failed to reconnect to PS5", "Connection Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
@@ -2258,11 +2263,80 @@ namespace PS5Upload
|
||||
{
|
||||
if (item.IsDirectory)
|
||||
{
|
||||
MessageBox.Show("Folder download not yet implemented. Please select a file.", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
// Folder download - use FolderBrowserDialog
|
||||
using var folderDialog = new System.Windows.Forms.FolderBrowserDialog
|
||||
{
|
||||
Description = $"Select destination for '{item.Name}' folder",
|
||||
UseDescriptionForTitle = true
|
||||
};
|
||||
|
||||
if (folderDialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
|
||||
return;
|
||||
|
||||
string localBasePath = Path.Combine(folderDialog.SelectedPath, item.Name);
|
||||
|
||||
try
|
||||
{
|
||||
Log($"⬇️ Downloading folder: {item.FullPath} → {localBasePath}");
|
||||
|
||||
_uploadCancellation = new CancellationTokenSource();
|
||||
var ct = _uploadCancellation.Token;
|
||||
|
||||
var progress = new Progress<DownloadFolderProgress>(p =>
|
||||
{
|
||||
Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
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)})";
|
||||
}
|
||||
}, System.Windows.Threading.DispatcherPriority.Normal);
|
||||
});
|
||||
|
||||
var (downloaded, failed, totalBytes) = await _protocol.DownloadFolderAsync(
|
||||
item.FullPath, localBasePath, _ps5IpAddress, progress, ct);
|
||||
|
||||
Log($"✅ Folder download complete: {downloaded} files ({FormatFileSize(totalBytes)}), {failed} failed");
|
||||
|
||||
string msg = $"Folder downloaded!\n\n" +
|
||||
$"Files: {downloaded} downloaded, {failed} failed\n" +
|
||||
$"Size: {FormatFileSize(totalBytes)}\n" +
|
||||
$"Saved to: {localBasePath}";
|
||||
MessageBox.Show(msg, "Download Complete", MessageBoxButton.OK,
|
||||
failed > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"❌ Folder download error: {ex.Message}");
|
||||
MessageBox.Show($"Folder download error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_uploadCancellation = null;
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
TotalProgressBar.Value = 0;
|
||||
TotalProgressText.Text = "";
|
||||
UploadFileNameText.Text = "";
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show save file dialog
|
||||
// Single file download - use SaveFileDialog
|
||||
var dialog = new Microsoft.Win32.SaveFileDialog
|
||||
{
|
||||
FileName = item.Name,
|
||||
@@ -2277,11 +2351,12 @@ namespace PS5Upload
|
||||
|
||||
var progress = new Progress<UploadProgress>(p =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
double percent = p.TotalBytes > 0 ? (double)p.BytesSent / p.TotalBytes * 100 : 0;
|
||||
Log($"📊 Download progress: {FormatFileSize(p.BytesSent)}/{FormatFileSize(p.TotalBytes)} ({percent:F1}%) @ {FormatFileSize((long)p.SpeedBytesPerSecond)}/s");
|
||||
});
|
||||
TotalProgressBar.Value = percent;
|
||||
TotalProgressText.Text = $"Downloading: {FormatFileSize(p.BytesSent)}/{FormatFileSize(p.TotalBytes)} ({percent:F1}%) @ {FormatFileSize((long)p.SpeedBytesPerSecond)}/s";
|
||||
}, System.Windows.Threading.DispatcherPriority.Normal);
|
||||
});
|
||||
|
||||
bool success = await _protocol.DownloadFileAsync(item.FullPath, dialog.FileName, progress);
|
||||
@@ -2302,6 +2377,14 @@ namespace PS5Upload
|
||||
Log($"❌ Download error: {ex.Message}");
|
||||
MessageBox.Show($"Download error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
TotalProgressBar.Value = 0;
|
||||
TotalProgressText.Text = "";
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3279,33 +3362,6 @@ namespace PS5Upload
|
||||
Log($" {line.Trim()}");
|
||||
}
|
||||
|
||||
// Check if there were new mounts that need registration
|
||||
bool hasNewMounts = result.Contains("New mounts:") && !result.Contains("New mounts: 0");
|
||||
|
||||
if (hasNewMounts)
|
||||
{
|
||||
Log("🎮 New games detected - running game_mounter.elf for registration...");
|
||||
MountGamesButton.Content = "⏳ Registering...";
|
||||
|
||||
// Execute standalone game_mounter.elf for sceAppInstUtil registration
|
||||
try
|
||||
{
|
||||
var shellResult = await _protocol.ExecuteShellCommandAsync("exec /data/etaHEN/payloads/game_mounter.elf");
|
||||
if (shellResult != null)
|
||||
{
|
||||
Log("✅ Game registration completed via game_mounter.elf");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("⚠️ game_mounter.elf may not be available - games mounted but may need manual registration");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Log("⚠️ Could not run game_mounter.elf - games are mounted via nullfs but may not appear on home screen until game_mounter.elf is run");
|
||||
}
|
||||
}
|
||||
|
||||
MessageBox.Show(result, "Mount Games - Result", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
else
|
||||
@@ -3504,5 +3560,786 @@ namespace PS5Upload
|
||||
Log($"❌ Failed to open link: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SAVES TAB - Event Handlers
|
||||
// ============================================================================
|
||||
|
||||
private List<PS5SaveGame> _currentSaves = new();
|
||||
|
||||
private async void RefreshSavesButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RefreshSavesAsync();
|
||||
}
|
||||
|
||||
private async Task RefreshSavesAsync()
|
||||
{
|
||||
if (!_protocol.IsConnected)
|
||||
{
|
||||
Log("❌ Not connected to PS5");
|
||||
return;
|
||||
}
|
||||
|
||||
Log("💾 Fetching save games list...");
|
||||
try
|
||||
{
|
||||
var saves = await _protocol.ListSavesAsync();
|
||||
_currentSaves = saves;
|
||||
|
||||
// Enrich with game names + icons from mounted games list (if available)
|
||||
var mounted = await _protocol.GetGameListAsync();
|
||||
var nameByTitle = mounted.ToDictionary(g => g.TitleId, g => g.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var save in saves)
|
||||
{
|
||||
if (nameByTitle.TryGetValue(save.TitleId, out var gameName))
|
||||
save.GameName = gameName;
|
||||
else
|
||||
save.GameName = save.TitleId;
|
||||
|
||||
if (_iconCache.TryGetValue(save.TitleId, out var cachedIcon))
|
||||
save.Icon = cachedIcon;
|
||||
}
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
SavesListBox.ItemsSource = saves;
|
||||
SaveCountText.Text = $" ({saves.Count} saves)";
|
||||
});
|
||||
|
||||
Log($"💾 Found {saves.Count} save games");
|
||||
|
||||
// Fetch missing icons in background
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
foreach (var save in saves)
|
||||
{
|
||||
if (save.Icon != null) continue;
|
||||
try
|
||||
{
|
||||
var bytes = await _protocol.GetGameIconAsync(save.TitleId);
|
||||
if (bytes != null && bytes.Length > 0)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var bmp = new System.Windows.Media.Imaging.BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
|
||||
bmp.StreamSource = new MemoryStream(bytes);
|
||||
bmp.EndInit();
|
||||
bmp.Freeze();
|
||||
_iconCache[save.TitleId] = bmp;
|
||||
save.Icon = bmp;
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"❌ Error fetching saves: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void BackupSaveButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SavesListBox.SelectedItem is PS5SaveGame save)
|
||||
await BackupSaveAsync(save);
|
||||
else
|
||||
MessageBox.Show("Select a save first.", "No Selection",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private async void BackupSaveMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SavesListBox.SelectedItem is PS5SaveGame save)
|
||||
await BackupSaveAsync(save);
|
||||
}
|
||||
|
||||
private async Task BackupSaveAsync(PS5SaveGame save)
|
||||
{
|
||||
if (!_protocol.IsConnected)
|
||||
{
|
||||
MessageBox.Show("Not connected to PS5.", "Error",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var dlg = new Microsoft.Win32.SaveFileDialog
|
||||
{
|
||||
Title = "Choose backup location",
|
||||
FileName = $"{save.TitleId}_{save.UserId}_{DateTime.Now:yyyyMMdd_HHmmss}",
|
||||
Filter = "Folder (choose any file name to pick a folder)|*",
|
||||
OverwritePrompt = false,
|
||||
CheckPathExists = true
|
||||
};
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
|
||||
string parentDir = Path.GetDirectoryName(dlg.FileName) ?? "";
|
||||
string backupDir = Path.Combine(parentDir, Path.GetFileNameWithoutExtension(dlg.FileName));
|
||||
Directory.CreateDirectory(backupDir);
|
||||
|
||||
Log($"📥 Backing up save {save.TitleId} to {backupDir}...");
|
||||
try
|
||||
{
|
||||
var result = await _protocol.DownloadFolderAsync(
|
||||
save.SavePath,
|
||||
backupDir,
|
||||
_ps5IpAddress,
|
||||
null,
|
||||
System.Threading.CancellationToken.None);
|
||||
|
||||
Log($"✅ Backup complete: {result.filesDownloaded} files, " +
|
||||
$"{result.filesFailed} failed, {FormatFileSize(result.totalBytes)} total");
|
||||
|
||||
MessageBox.Show($"Backup complete!\n\n" +
|
||||
$"Location: {backupDir}\n" +
|
||||
$"Files: {result.filesDownloaded}\n" +
|
||||
$"Failed: {result.filesFailed}\n" +
|
||||
$"Size: {FormatFileSize(result.totalBytes)}",
|
||||
"Backup Complete",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"❌ Backup failed: {ex.Message}");
|
||||
MessageBox.Show($"Backup failed:\n{ex.Message}", "Error",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void RestoreSaveButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SavesListBox.SelectedItem is PS5SaveGame save)
|
||||
await RestoreSaveAsync(save);
|
||||
else
|
||||
MessageBox.Show("Select a save first.", "No Selection",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private async void RestoreSaveMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SavesListBox.SelectedItem is PS5SaveGame save)
|
||||
await RestoreSaveAsync(save);
|
||||
}
|
||||
|
||||
private async Task RestoreSaveAsync(PS5SaveGame save)
|
||||
{
|
||||
if (!_protocol.IsConnected)
|
||||
{
|
||||
MessageBox.Show("Not connected to PS5.", "Error",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = "Select any file from the backup folder to restore",
|
||||
CheckFileExists = true
|
||||
};
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
|
||||
string backupFolder = Path.GetDirectoryName(dlg.FileName) ?? "";
|
||||
if (string.IsNullOrWhiteSpace(backupFolder) || !Directory.Exists(backupFolder))
|
||||
{
|
||||
MessageBox.Show("Invalid folder.", "Error",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var confirm = MessageBox.Show(
|
||||
$"Restore save {save.TitleId} to PS5?\n\n" +
|
||||
$"From: {backupFolder}\n" +
|
||||
$"To: {save.SavePath}\n\n" +
|
||||
$"Files in the PS5 save will be overwritten.",
|
||||
"Confirm Restore",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||
if (confirm != MessageBoxResult.Yes) return;
|
||||
|
||||
Log($"📤 Restoring save {save.TitleId} from {backupFolder}...");
|
||||
int uploaded = 0, failed = 0;
|
||||
long totalBytes = 0;
|
||||
try
|
||||
{
|
||||
foreach (var localFile in Directory.EnumerateFiles(backupFolder, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
string rel = Path.GetRelativePath(backupFolder, localFile).Replace('\\', '/');
|
||||
string remote = save.SavePath.TrimEnd('/') + "/" + rel;
|
||||
try
|
||||
{
|
||||
bool ok = await _protocol.UploadFileAsync(localFile, remote);
|
||||
if (ok)
|
||||
{
|
||||
uploaded++;
|
||||
totalBytes += new FileInfo(localFile).Length;
|
||||
}
|
||||
else failed++;
|
||||
}
|
||||
catch { failed++; }
|
||||
}
|
||||
|
||||
Log($"✅ Restore complete: {uploaded} uploaded, {failed} failed, {FormatFileSize(totalBytes)}");
|
||||
MessageBox.Show($"Restore complete!\n\n" +
|
||||
$"Uploaded: {uploaded}\n" +
|
||||
$"Failed: {failed}\n" +
|
||||
$"Size: {FormatFileSize(totalBytes)}",
|
||||
"Restore Complete",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"❌ Restore failed: {ex.Message}");
|
||||
MessageBox.Show($"Restore failed:\n{ex.Message}", "Error",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopySavePathMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SavesListBox.SelectedItem is PS5SaveGame save)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Windows.Clipboard.SetText(save.SavePath);
|
||||
Log($"📋 Copied: {save.SavePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"❌ Clipboard: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GAMES TAB - Event Handlers
|
||||
// ============================================================================
|
||||
|
||||
private async void RefreshGameListButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RefreshGameListAsync();
|
||||
}
|
||||
|
||||
// In-memory icon cache (title_id -> ImageSource) so we don't refetch on every refresh
|
||||
private readonly Dictionary<string, System.Windows.Media.ImageSource> _iconCache = new();
|
||||
|
||||
private async Task RefreshGameListAsync()
|
||||
{
|
||||
if (!_protocol.IsConnected)
|
||||
{
|
||||
Log("❌ Not connected to PS5");
|
||||
return;
|
||||
}
|
||||
|
||||
Log("🎮 Fetching mounted games list...");
|
||||
|
||||
try
|
||||
{
|
||||
var games = await _protocol.GetGameListAsync();
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
MountedGamesListBox.ItemsSource = games;
|
||||
GameCountText.Text = $" ({games.Count} games)";
|
||||
});
|
||||
|
||||
if (games.Count > 0)
|
||||
{
|
||||
Log($"🎮 Found {games.Count} mounted games:");
|
||||
foreach (var game in games)
|
||||
{
|
||||
Log($" • {game.TitleId} - {game.Name} [{game.Region}]");
|
||||
}
|
||||
|
||||
// Fetch icons in background, one at a time to avoid swamping the payload
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
foreach (var game in games)
|
||||
{
|
||||
if (_iconCache.TryGetValue(game.TitleId, out var cached))
|
||||
{
|
||||
Dispatcher.Invoke(() => game.Icon = cached);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var iconBytes = await _protocol.GetGameIconAsync(game.TitleId);
|
||||
if (iconBytes != null && iconBytes.Length > 0)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var bitmap = new System.Windows.Media.Imaging.BitmapImage();
|
||||
bitmap.BeginInit();
|
||||
bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
|
||||
bitmap.StreamSource = new MemoryStream(iconBytes);
|
||||
bitmap.EndInit();
|
||||
bitmap.Freeze();
|
||||
_iconCache[game.TitleId] = bitmap;
|
||||
game.Icon = bitmap;
|
||||
}
|
||||
catch { /* corrupt/invalid image */ }
|
||||
});
|
||||
}
|
||||
}
|
||||
catch { /* skip failed icons */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("🎮 No mounted games found");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"❌ Error fetching game list: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void MountedGamesListBox_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (MountedGamesListBox.SelectedItem is PS5MountedGame game)
|
||||
{
|
||||
await ShowGameDetailsAsync(game);
|
||||
}
|
||||
}
|
||||
|
||||
private async void ViewGameDetailsMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (MountedGamesListBox.SelectedItem is PS5MountedGame game)
|
||||
{
|
||||
await ShowGameDetailsAsync(game);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowGameDetailsAsync(PS5MountedGame game)
|
||||
{
|
||||
if (!_protocol.IsConnected)
|
||||
{
|
||||
Log("❌ Not connected to PS5");
|
||||
return;
|
||||
}
|
||||
|
||||
Log($"ℹ️ Fetching details for {game.TitleId}...");
|
||||
var details = await _protocol.GetGameDetailsAsync(game.TitleId);
|
||||
if (details == null)
|
||||
{
|
||||
MessageBox.Show("Failed to fetch game details.", "Error",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var dlg = new GameDetailsWindow(game, details, _protocol) { Owner = this };
|
||||
dlg.ShowDialog();
|
||||
}
|
||||
|
||||
private async void UnmountGameMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (MountedGamesListBox.SelectedItem is PS5MountedGame game)
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
$"Unmount game {game.TitleId}?\n\n{game.Name}\n\nThis will remove the game from the PS5 home screen.",
|
||||
"Confirm Unmount",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
Log($"🗑️ Unmounting {game.TitleId}...");
|
||||
var (success, message) = await _protocol.UnmountGameAsync(game.TitleId);
|
||||
if (success)
|
||||
{
|
||||
Log($"✅ {message}");
|
||||
await RefreshGameListAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log($"❌ Failed: {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OpenGamePathMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (MountedGamesListBox.SelectedItem is PS5MountedGame game)
|
||||
{
|
||||
// Navigate to the game's path in the PS5 file browser
|
||||
string gamePath = game.Path;
|
||||
if (!string.IsNullOrEmpty(gamePath))
|
||||
{
|
||||
Log($"📂 Navigating to {gamePath}");
|
||||
_currentPS5Path = gamePath;
|
||||
await LoadPS5DirectoryAsync(gamePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LAUNCH GAME
|
||||
// ============================================================
|
||||
private async void LaunchGameMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (MountedGamesListBox.SelectedItem is not PS5MountedGame game) return;
|
||||
if (_protocol == null || !_protocol.IsConnected)
|
||||
{
|
||||
Log("❌ Not connected to PS5");
|
||||
return;
|
||||
}
|
||||
|
||||
Log($"▶ Launching {game.TitleId} ({game.Name})...");
|
||||
var (success, message) = await _protocol.LaunchGameAsync(game.TitleId);
|
||||
if (success)
|
||||
Log($"✅ {message}");
|
||||
else
|
||||
Log($"❌ Launch failed: {message}");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HARDWARE TAB
|
||||
// ============================================================
|
||||
private System.Windows.Threading.DispatcherTimer? _hwAutoTimer;
|
||||
private int _hwBusyFlag; // 0 = idle, 1 = refresh in progress (Interlocked)
|
||||
|
||||
private async void RefreshHardwareButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RefreshHardwareAsync();
|
||||
}
|
||||
|
||||
private void HwAutoRefresh_Changed(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (HwAutoRefreshCheckBox?.IsChecked == true)
|
||||
{
|
||||
_hwAutoTimer ??= new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(5) // 5s is plenty for sensor polling
|
||||
};
|
||||
_hwAutoTimer.Tick -= HwAutoTimer_Tick;
|
||||
_hwAutoTimer.Tick += HwAutoTimer_Tick;
|
||||
_hwAutoTimer.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
_hwAutoTimer?.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async void HwAutoTimer_Tick(object? sender, EventArgs e)
|
||||
{
|
||||
// Auto-stop if disconnected
|
||||
if (_protocol == null || !_protocol.IsConnected)
|
||||
{
|
||||
_hwAutoTimer?.Stop();
|
||||
HwAutoRefreshCheckBox.IsChecked = false;
|
||||
HwStatusText.Text = "Auto-refresh stopped: disconnected";
|
||||
return;
|
||||
}
|
||||
// Skip if previous refresh still running or app is busy (uploads, etc.)
|
||||
if (Interlocked.CompareExchange(ref _hwBusyFlag, 1, 0) != 0) return;
|
||||
if (_activeTaskCount > 0)
|
||||
{
|
||||
Interlocked.Exchange(ref _hwBusyFlag, 0);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await RefreshHardwareSensorsAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _hwBusyFlag, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshHardwareAsync()
|
||||
{
|
||||
if (_protocol == null || !_protocol.IsConnected)
|
||||
{
|
||||
Log("❌ Not connected to PS5");
|
||||
return;
|
||||
}
|
||||
if (Interlocked.CompareExchange(ref _hwBusyFlag, 1, 0) != 0)
|
||||
{
|
||||
Log("⏳ Hardware refresh already in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var hw = await _protocol.GetHardwareInfoAsync();
|
||||
if (hw != null)
|
||||
{
|
||||
HwModelText.Text = string.IsNullOrWhiteSpace(hw.Model) ? "PlayStation 5" : hw.Model;
|
||||
HwSerialText.Text = string.IsNullOrWhiteSpace(hw.Serial) ? "—" : hw.Serial;
|
||||
HwMachineText.Text = string.IsNullOrWhiteSpace(hw.HwMachine) ? "—" : hw.HwMachine;
|
||||
HwOsText.Text = string.IsNullOrWhiteSpace(hw.OsVersion) ? "—" : hw.OsVersion;
|
||||
HwCpuCoresText.Text = hw.NumCpu > 0 ? $"{hw.NumCpu} cores" : "—";
|
||||
HwPhysMemText.Text = hw.PhysMem > 0
|
||||
? $"{hw.PhysMem / (1024.0 * 1024.0 * 1024.0):0.0} GB"
|
||||
: "—";
|
||||
HwWlanBtText.Text = hw.HasWlanBt ? "✓ Present" : "✗ None";
|
||||
HwOpticalOutText.Text = hw.HasOpticalOut ? "✓ Present" : "✗ None";
|
||||
}
|
||||
|
||||
await RefreshHardwareSensorsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"❌ Hardware refresh failed: {ex.Message}");
|
||||
HwStatusText.Text = $"Error: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _hwBusyFlag, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshHardwareSensorsAsync()
|
||||
{
|
||||
if (_protocol == null || !_protocol.IsConnected) return;
|
||||
|
||||
try
|
||||
{
|
||||
var t = await _protocol.GetTemperatureInfoAsync();
|
||||
if (t == null)
|
||||
{
|
||||
HwStatusText.Text = "Sensors unavailable";
|
||||
return;
|
||||
}
|
||||
|
||||
HwCpuTempText.Text = $"{t.CpuTemp} °C";
|
||||
HwCpuTempBar.Value = Math.Clamp(t.CpuTemp, 0, 100);
|
||||
|
||||
HwSocTempText.Text = $"{t.SocTemp} °C";
|
||||
HwSocTempBar.Value = Math.Clamp(t.SocTemp, 0, 100);
|
||||
|
||||
HwCpuFreqText.Text = t.CpuFreqMhz > 0 ? $"{t.CpuFreqMhz} MHz" : "—";
|
||||
HwCpuFreqBar.Value = Math.Clamp(t.CpuFreqMhz, 0, 3500);
|
||||
|
||||
double watts = t.SocPowerMw / 1000.0;
|
||||
HwSocPowerText.Text = t.SocPowerMw > 0 ? $"{watts:0.0} W" : "—";
|
||||
HwSocPowerBar.Value = Math.Clamp(watts, 0, 250);
|
||||
|
||||
HwStatusText.Text = $"Last updated: {DateTime.Now:HH:mm:ss}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HwStatusText.Text = $"Error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SCREENSHOTS TAB
|
||||
// ============================================================
|
||||
private List<PS5Screenshot> _currentScreenshots = new();
|
||||
|
||||
private async void RefreshScreenshotsButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RefreshScreenshotsAsync();
|
||||
}
|
||||
|
||||
// Thumbnail cache: remote PS5 path -> BitmapImage (Frozen)
|
||||
private readonly Dictionary<string, System.Windows.Media.ImageSource> _screenshotThumbCache = new();
|
||||
|
||||
private async Task RefreshScreenshotsAsync()
|
||||
{
|
||||
if (_protocol == null || !_protocol.IsConnected)
|
||||
{
|
||||
Log("❌ Not connected to PS5");
|
||||
return;
|
||||
}
|
||||
|
||||
Log("📷 Fetching screenshots list...");
|
||||
try
|
||||
{
|
||||
var shots = await _protocol.ListScreenshotsAsync();
|
||||
_currentScreenshots = shots;
|
||||
|
||||
// Apply cached thumbnails immediately
|
||||
foreach (var s in shots)
|
||||
{
|
||||
if (_screenshotThumbCache.TryGetValue(s.FullPath, out var cached))
|
||||
s.Thumbnail = cached;
|
||||
}
|
||||
|
||||
ScreenshotsListBox.ItemsSource = null;
|
||||
ScreenshotsListBox.ItemsSource = shots;
|
||||
ScreenshotsCountText.Text = $"({shots.Count} items)";
|
||||
Log($"✅ Found {shots.Count} screenshots");
|
||||
|
||||
// Background: download thumbnails for items without one
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
string cacheDir = System.IO.Path.Combine(
|
||||
System.IO.Path.GetTempPath(), "PS5UploadCache", "ss_thumbs");
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
|
||||
foreach (var shot in shots)
|
||||
{
|
||||
if (shot.Thumbnail != null) continue;
|
||||
if (_protocol == null || !_protocol.IsConnected) break;
|
||||
|
||||
// Use hash of remote path as cache file name
|
||||
string hash = shot.FullPath.GetHashCode().ToString("X8");
|
||||
string ext = System.IO.Path.GetExtension(shot.FileName).ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(ext)) ext = ".jpg";
|
||||
string localCache = System.IO.Path.Combine(cacheDir, hash + ext);
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(localCache) || new FileInfo(localCache).Length == 0)
|
||||
{
|
||||
bool ok = await _protocol.DownloadFileAsync(shot.FullPath, localCache);
|
||||
if (!ok) continue;
|
||||
}
|
||||
|
||||
// Decode downsampled to save RAM (target ~200px wide)
|
||||
var bmp = new System.Windows.Media.Imaging.BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
|
||||
bmp.DecodePixelWidth = 200;
|
||||
bmp.UriSource = new Uri(localCache, UriKind.Absolute);
|
||||
bmp.EndInit();
|
||||
bmp.Freeze();
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_screenshotThumbCache[shot.FullPath] = bmp;
|
||||
shot.Thumbnail = bmp;
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip broken images; the emoji fallback stays
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"❌ Screenshots fetch failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void DownloadScreenshotButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_protocol == null || !_protocol.IsConnected)
|
||||
{
|
||||
Log("❌ Not connected to PS5");
|
||||
return;
|
||||
}
|
||||
var selected = ScreenshotsListBox.SelectedItems.Cast<PS5Screenshot>().ToList();
|
||||
if (selected.Count == 0)
|
||||
{
|
||||
Log("⚠️ No screenshots selected");
|
||||
return;
|
||||
}
|
||||
|
||||
var dlg = new System.Windows.Forms.FolderBrowserDialog
|
||||
{
|
||||
Description = "Select folder to save screenshots"
|
||||
};
|
||||
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
||||
string targetFolder = dlg.SelectedPath;
|
||||
|
||||
int ok = 0, fail = 0;
|
||||
foreach (var shot in selected)
|
||||
{
|
||||
string localPath = System.IO.Path.Combine(targetFolder, shot.FileName);
|
||||
Log($"⬇ Downloading {shot.FileName} ({shot.SizeDisplay})...");
|
||||
try
|
||||
{
|
||||
bool success = await _protocol.DownloadFileAsync(shot.FullPath, localPath);
|
||||
if (success) { ok++; } else { fail++; Log($"❌ Failed: {shot.FileName}"); }
|
||||
}
|
||||
catch (Exception ex) { fail++; Log($"❌ {shot.FileName}: {ex.Message}"); }
|
||||
}
|
||||
Log($"✅ Downloaded {ok} screenshot(s), {fail} failed → {targetFolder}");
|
||||
}
|
||||
|
||||
private async void DeleteScreenshotButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_protocol == null || !_protocol.IsConnected)
|
||||
{
|
||||
Log("❌ Not connected to PS5");
|
||||
return;
|
||||
}
|
||||
var selected = ScreenshotsListBox.SelectedItems.Cast<PS5Screenshot>().ToList();
|
||||
if (selected.Count == 0)
|
||||
{
|
||||
Log("⚠️ No screenshots selected");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show(
|
||||
$"Delete {selected.Count} screenshot(s) from PS5?\n\nThis cannot be undone.",
|
||||
"Confirm Delete", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
int ok = 0, fail = 0;
|
||||
foreach (var shot in selected)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use dedicated screenshot-delete which also clears PS5's parallel thumbnail
|
||||
var (success, _) = await _protocol.DeleteScreenshotAsync(shot.FullPath);
|
||||
if (success) ok++; else fail++;
|
||||
}
|
||||
catch { fail++; }
|
||||
}
|
||||
Log($"🗑️ Deleted {ok} screenshot(s) (+ their PS5 thumbnails), {fail} failed");
|
||||
await RefreshScreenshotsAsync();
|
||||
}
|
||||
|
||||
private async void ScreenshotsListBox_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (ScreenshotsListBox.SelectedItem is not PS5Screenshot shot) return;
|
||||
if (_protocol == null || !_protocol.IsConnected) return;
|
||||
|
||||
// Download to %TEMP% and open with default viewer
|
||||
string tempPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), shot.FileName);
|
||||
Log($"⬇ Opening preview: {shot.FileName}...");
|
||||
try
|
||||
{
|
||||
bool ok = await _protocol.DownloadFileAsync(shot.FullPath, tempPath);
|
||||
if (ok)
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = tempPath,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Log($"❌ Failed to download preview");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"❌ Preview failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyScreenshotPathMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ScreenshotsListBox.SelectedItem is PS5Screenshot shot)
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.SetText(shot.FullPath);
|
||||
Log($"📋 Copied path: {shot.FullPath}");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<ApplicationIcon>ps5_logo_fixed.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
+1342
-139
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,23 @@
|
||||
# PS5 Upload Server - Makefile
|
||||
# Uses the maintained ps5-payload-dev/sdk (2026 release)
|
||||
|
||||
PS5_HOST ?= 192.168.0.160
|
||||
PS5_PORT ?= 9021
|
||||
PS5_PAYLOAD_SDK := /opt/ps5-payload-sdk
|
||||
|
||||
CC := $(PS5_PAYLOAD_SDK)/bin/prospero-clang
|
||||
CXX := $(PS5_PAYLOAD_SDK)/bin/prospero-clang++
|
||||
LD := $(PS5_PAYLOAD_SDK)/bin/prospero-lld
|
||||
|
||||
ELF := ps5_upload_server.elf
|
||||
CFLAGS := -Wall -O3
|
||||
|
||||
all: $(ELF)
|
||||
|
||||
$(ELF): main.c
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lkernel_sys -lSceSystemService -lSceUserService -lSceAppInstUtil
|
||||
|
||||
clean:
|
||||
rm -f $(ELF)
|
||||
|
||||
.PHONY: all clean
|
||||
+1332
-70
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,142 @@
|
||||
# 🚀 PS5 Upload Suite v5.0.0 — Complete Management Suite
|
||||
|
||||
Complete all-in-one PS5 management platform — **104+ MB/s** file transfers, live hardware monitoring, full game/save/screenshot management, and deep PS5 integration through a single desktop client + a robust PS5 payload.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Downloads
|
||||
|
||||
| File | Size | Purpose |
|
||||
|------|------|---------|
|
||||
| `PS5Upload.exe` | 146 MB | Windows GUI client (self-contained, no .NET install required) |
|
||||
| `ps5_upload_server.elf` | 187 KB | PS5 payload — load via elfldr, listens on port 9113 |
|
||||
|
||||
**Load the payload on your PS5:** copy `ps5_upload_server.elf` to `/data/etaHEN/payloads/` and run it. You will see a notification `PS5 Upload Server: 192.168.0.XXX:9113 - By Manos` confirming it's ready.
|
||||
|
||||
---
|
||||
|
||||
## 🆕 What's New in v5.0.0
|
||||
|
||||
### 🖥️ NEW: Hardware Monitor Tab
|
||||
- **System Info** — Model (CFI-xxxx), Serial Number, Architecture, OS version
|
||||
- **Physical RAM detection** — 5-step fallback chain (PS5-specific API → sysctl chain → 16 GB default)
|
||||
- **Live Sensors** — CPU temperature, SoC temperature, CPU frequency, SoC power consumption
|
||||
- **Auto-refresh** — Configurable 5-second polling with automatic disconnect detection
|
||||
- **Payload hardened** — `pthread_mutex_t` + 1s cache for sensors, 5s throttle for power API, rejection of abnormal readings
|
||||
|
||||
### 📷 NEW: Screenshots Tab
|
||||
- **Visual Gallery** — Live thumbnail previews (200px downsampled for fast loading)
|
||||
- **Smart Cache** — Local cache in `%TEMP%` keyed by remote path hash — zero re-downloads on refresh
|
||||
- **Batch Download** — Multi-select and download multiple screenshots
|
||||
- **Clean Delete** — Removes ALL 3 files per screenshot (`.dat`, `.meta`, `.jpg.jpeg`) — **no more ghost entries in PS5 Media Gallery**
|
||||
- **Double-click Preview** — Opens in default image viewer
|
||||
- **Full Path Scanning** — 5-level recursive scan of `/user/av_contents/thumbnails/photo/…`
|
||||
|
||||
### ▶️ NEW: Launch Game
|
||||
- Right-click any mounted game → **Launch Game**
|
||||
- Uses low-level `sceLncUtilLaunchApp` with fallback to `sceSystemServiceLaunchApp`
|
||||
- **Works on pirated/sideloaded games** (bypasses `0x80940005` permission error)
|
||||
- Triple-strategy launcher with detailed diagnostic error messages
|
||||
|
||||
### 💾 NEW: Save Manager Tab
|
||||
- Per-game save browsing with metadata
|
||||
- Game icons fetched and cached from PSN/param.sfo
|
||||
- Batch download saves to local backup folder
|
||||
- Upload saves back to PS5
|
||||
- Size, date, and title ID display
|
||||
|
||||
### 🎮 NEW: Game Details Window
|
||||
- **PSN Cover Art** fetched from PlayStation Store
|
||||
- Full `param.json` metadata formatted nicely
|
||||
- Online PSN info: description, genres, age rating, publisher
|
||||
- Alternate title ID detection (regional variants)
|
||||
- Browser search fallback if API fails
|
||||
|
||||
### 🔧 Improved Storage Reporting
|
||||
- ✅ Matches **PS5 Settings "Console Storage"** formula exactly
|
||||
- ✅ Total: `/user` effective + `/system_data` + `/system_ex`
|
||||
- ✅ Free: proper `f_bavail` across all partitions
|
||||
- ✅ Accounts for reserved space & per-partition blocksize
|
||||
- Example real-world PS5: **848 GB total / 354 GB free** ✓
|
||||
|
||||
### 🎮 Improved Mount Games
|
||||
- ✅ Correct `sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0)` signature
|
||||
- ✅ Registration BEFORE `mount.lnk` (correct order)
|
||||
- ✅ Direct in-payload registration — no external `game_mounter.elf` dependency
|
||||
- ✅ Proper DRM patching & metadata copy
|
||||
|
||||
### 🛡️ Stability Overhaul
|
||||
- ✅ **Thread-safe sensor reads** — `pthread_mutex_t` prevents concurrent API crashes
|
||||
- ✅ **API call throttling** — SoC power read at most every 5s, sensor cache every 1s
|
||||
- ✅ **Cached static HW info** — Read once, serve forever (Model, Serial, OS)
|
||||
- ✅ **Unsafe APIs removed** — `sceKernelIccGet*`, `HwHasWlanBt`, `HwHasOpticalOut` (caused crashes)
|
||||
- ✅ **Client busy-flag guard** — `Interlocked` prevents overlapping refreshes
|
||||
- ✅ **Auto-stop timers** — Hardware auto-refresh stops on disconnect
|
||||
- ✅ **Graceful reconnect** — Connection lost → automatic reconnection attempt
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- **Launch failed with 0x80940005** — Fixed by using low-level `sceLncUtilLaunchApp` instead of `sceSystemServiceLaunchApp` for pirated/mounted games
|
||||
- **Payload crashes on Hardware Tab refresh** — Fixed with mutex, caching, and removal of unsafe `sceKernelIccGet*` APIs
|
||||
- **Connection freezes after auto-refresh enabled** — Fixed with busy-flag guard, auto-stop on disconnect, and longer polling interval
|
||||
- **Ghost screenshots in PS5 Media Gallery after delete** — Fixed by deleting all 3 associated files (`.dat`, `.meta`, `.jpg.jpeg`)
|
||||
- **Physical RAM showing "—"** — Fixed with 5-step detection chain including PS5-specific `sceKernelGetDirectMemorySize()`
|
||||
- **Wi-Fi / Bluetooth showing "✗ None"** — Fixed (hardcoded to `true` since all PS5 models have it)
|
||||
- **Screenshots showing 0 items** — Fixed by scanning correct path `/user/av_contents/thumbnails/photo/` (where viewable `.jpg.jpeg` files live)
|
||||
- **Storage reporting mismatch with PS5 Settings** — Fixed with correct aggregation formula
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Details
|
||||
|
||||
### New Payload Commands
|
||||
- `CMD_LAUNCH_GAME (0x44)` — Launch mounted games
|
||||
- `CMD_LIST_SCREENSHOTS (0x45)` — Recursive scan of screenshot directories
|
||||
- `CMD_DELETE_SCREENSHOT (0x46)` — Clean delete (original + thumbnail + metadata)
|
||||
|
||||
### New Sony Kernel APIs Used (verified safe)
|
||||
- `sceKernelGetHwModelName` / `sceKernelGetHwSerialNumber`
|
||||
- `sceKernelGetCpuTemperature` / `sceKernelGetSocSensorTemperature`
|
||||
- `sceKernelGetCpuFrequency`
|
||||
- `sceKernelGetDirectMemorySize`
|
||||
- `sceKernelGetSocPowerConsumption` (throttled to 5s)
|
||||
- `sceLncUtilLaunchApp` / `sceLncUtilInitialize`
|
||||
- `sceUserServiceInitialize` / `sceUserServiceGetForegroundUser`
|
||||
|
||||
### Removed APIs (destabilize payload)
|
||||
- `sceKernelIccGetPowerOperatingTime`
|
||||
- `sceKernelIccGetPowerNumberOfBootShutdown`
|
||||
- `sceKernelHwHasWlanBt` / `sceKernelHwHasOpticalOut`
|
||||
- `sceShellCoreUtilGetEffectiveTotalSizeOfUserPartition` (privileges issue)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Load payload on PS5:** Copy `ps5_upload_server.elf` to `/data/etaHEN/payloads/` and execute it
|
||||
2. **Run client on PC:** Launch `PS5Upload.exe` on Windows
|
||||
3. **Connect:** Enter your PS5 IP (e.g. `192.168.0.160`) and click Connect
|
||||
4. **Enjoy:** 8 tabs of full PS5 management — uploads, games, saves, screenshots, live hardware monitoring, and more!
|
||||
|
||||
---
|
||||
|
||||
## 📋 Full Feature List
|
||||
|
||||
See the [README](https://github.com/manos555555/PS5-Upload-Suite/blob/main/README.md) for the complete feature matrix.
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
- PS5 SDK by John Törnblom
|
||||
- etaHEN team for the kernel runtime
|
||||
- PS5 homebrew community
|
||||
- Inspired by `ps5upload` by PhantomPtr
|
||||
|
||||
---
|
||||
|
||||
**Tested on:** PS5 Slim (CFI-2016 A01Y), firmware with etaHEN — 16 GB RAM, 16 CPU cores, ~848 GB storage
|
||||
|
||||
🎮 **Enjoy the complete PS5 management experience!**
|
||||
Binary file not shown.
Reference in new issue
Block a user