mirror of
https://github.com/GreemDev/Ryujinx.git
synced 2024-11-30 20:54:30 +01:00
Add ability to trim and untrim XCI files from the application context menu AND in Bulk (#105)
This commit is contained in:
parent
47b8145809
commit
4831965404
@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging
|
|||||||
TamperMachine,
|
TamperMachine,
|
||||||
UI,
|
UI,
|
||||||
Vic,
|
Vic,
|
||||||
|
XCIFileTrimmer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
30
src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs
Normal file
30
src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Logging
|
||||||
|
{
|
||||||
|
public class XCIFileTrimmerLog : XCIFileTrimmer.ILog
|
||||||
|
{
|
||||||
|
public virtual void Progress(long current, long total, string text, bool complete)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(XCIFileTrimmer.LogType logType, string text)
|
||||||
|
{
|
||||||
|
switch (logType)
|
||||||
|
{
|
||||||
|
case XCIFileTrimmer.LogType.Info:
|
||||||
|
Logger.Notice.Print(LogClass.XCIFileTrimmer, text);
|
||||||
|
break;
|
||||||
|
case XCIFileTrimmer.LogType.Warn:
|
||||||
|
Logger.Warning?.Print(LogClass.XCIFileTrimmer, text);
|
||||||
|
break;
|
||||||
|
case XCIFileTrimmer.LogType.Error:
|
||||||
|
Logger.Error?.Print(LogClass.XCIFileTrimmer, text);
|
||||||
|
break;
|
||||||
|
case XCIFileTrimmer.LogType.Progress:
|
||||||
|
Logger.Info?.Print(LogClass.XCIFileTrimmer, text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
524
src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs
Normal file
524
src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs
Normal file
@ -0,0 +1,524 @@
|
|||||||
|
// Uncomment the line below to ensure XCIFileTrimmer does not modify files
|
||||||
|
//#define XCI_TRIMMER_READ_ONLY_MODE
|
||||||
|
|
||||||
|
using Gommon;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Utilities
|
||||||
|
{
|
||||||
|
public sealed class XCIFileTrimmer
|
||||||
|
{
|
||||||
|
private const long BytesInAMegabyte = 1024 * 1024;
|
||||||
|
private const int BufferSize = 8 * (int)BytesInAMegabyte;
|
||||||
|
|
||||||
|
private const long CartSizeMBinFormattedGB = 952;
|
||||||
|
private const int CartKeyAreaSize = 0x1000;
|
||||||
|
private const byte PaddingByte = 0xFF;
|
||||||
|
private const int HeaderFilePos = 0x100;
|
||||||
|
private const int CartSizeFilePos = 0x10D;
|
||||||
|
private const int DataSizeFilePos = 0x118;
|
||||||
|
private const string HeaderMagicValue = "HEAD";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cartridge Sizes (ByteIdentifier, SizeInGB)
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Dictionary<byte, long> _cartSizesGB = new()
|
||||||
|
{
|
||||||
|
{ 0xFA, 1 },
|
||||||
|
{ 0xF8, 2 },
|
||||||
|
{ 0xF0, 4 },
|
||||||
|
{ 0xE0, 8 },
|
||||||
|
{ 0xE1, 16 },
|
||||||
|
{ 0xE2, 32 }
|
||||||
|
};
|
||||||
|
|
||||||
|
private static long RecordsToByte(long records)
|
||||||
|
{
|
||||||
|
return 512 + (records * 512);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CanTrim(string filename, ILog log = null)
|
||||||
|
{
|
||||||
|
if (Path.GetExtension(filename).Equals(".XCI", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
var trimmer = new XCIFileTrimmer(filename, log);
|
||||||
|
return trimmer.CanBeTrimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CanUntrim(string filename, ILog log = null)
|
||||||
|
{
|
||||||
|
if (Path.GetExtension(filename).Equals(".XCI", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
var trimmer = new XCIFileTrimmer(filename, log);
|
||||||
|
return trimmer.CanBeUntrimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ILog _log;
|
||||||
|
private string _filename;
|
||||||
|
private FileStream _fileStream;
|
||||||
|
private BinaryReader _binaryReader;
|
||||||
|
private long _offsetB, _dataSizeB, _cartSizeB, _fileSizeB;
|
||||||
|
private bool _fileOK = true;
|
||||||
|
private bool _freeSpaceChecked = false;
|
||||||
|
private bool _freeSpaceValid = false;
|
||||||
|
|
||||||
|
public enum OperationOutcome
|
||||||
|
{
|
||||||
|
Undetermined,
|
||||||
|
InvalidXCIFile,
|
||||||
|
NoTrimNecessary,
|
||||||
|
NoUntrimPossible,
|
||||||
|
FreeSpaceCheckFailed,
|
||||||
|
FileIOWriteError,
|
||||||
|
ReadOnlyFileCannotFix,
|
||||||
|
FileSizeChanged,
|
||||||
|
Successful,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LogType
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error,
|
||||||
|
Progress
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ILog
|
||||||
|
{
|
||||||
|
public void Write(LogType logType, string text);
|
||||||
|
public void Progress(long current, long total, string text, bool complete);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FileOK => _fileOK;
|
||||||
|
public bool Trimmed => _fileOK && FileSizeB < UntrimmedFileSizeB;
|
||||||
|
public bool ContainsKeyArea => _offsetB != 0;
|
||||||
|
public bool CanBeTrimmed => _fileOK && FileSizeB > TrimmedFileSizeB;
|
||||||
|
public bool CanBeUntrimmed => _fileOK && FileSizeB < UntrimmedFileSizeB;
|
||||||
|
public bool FreeSpaceChecked => _fileOK && _freeSpaceChecked;
|
||||||
|
public bool FreeSpaceValid => _fileOK && _freeSpaceValid;
|
||||||
|
public long DataSizeB => _dataSizeB;
|
||||||
|
public long CartSizeB => _cartSizeB;
|
||||||
|
public long FileSizeB => _fileSizeB;
|
||||||
|
public long DiskSpaceSavedB => CartSizeB - FileSizeB;
|
||||||
|
public long DiskSpaceSavingsB => CartSizeB - DataSizeB;
|
||||||
|
public long TrimmedFileSizeB => _offsetB + _dataSizeB;
|
||||||
|
public long UntrimmedFileSizeB => _offsetB + _cartSizeB;
|
||||||
|
|
||||||
|
public ILog Log
|
||||||
|
{
|
||||||
|
get => _log;
|
||||||
|
set => _log = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String Filename
|
||||||
|
{
|
||||||
|
get => _filename;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_filename = value;
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long Pos
|
||||||
|
{
|
||||||
|
get => _fileStream.Position;
|
||||||
|
set => _fileStream.Position = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public XCIFileTrimmer(string path, ILog log = null)
|
||||||
|
{
|
||||||
|
Log = log;
|
||||||
|
Filename = path;
|
||||||
|
ReadHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckFreeSpace(CancellationToken? cancelToken = null)
|
||||||
|
{
|
||||||
|
if (FreeSpaceChecked)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (CanBeTrimmed)
|
||||||
|
{
|
||||||
|
_freeSpaceValid = false;
|
||||||
|
|
||||||
|
OpenReaders();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Pos = TrimmedFileSizeB;
|
||||||
|
bool freeSpaceValid = true;
|
||||||
|
long readSizeB = FileSizeB - TrimmedFileSizeB;
|
||||||
|
|
||||||
|
Stopwatch timedSw = Lambda.Timed(() =>
|
||||||
|
{
|
||||||
|
freeSpaceValid = CheckPadding(readSizeB, cancelToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (timedSw.Elapsed.TotalSeconds > 0)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Info, $"Checked at {readSizeB / (double)XCIFileTrimmer.BytesInAMegabyte / timedSw.Elapsed.TotalSeconds:N} Mb/sec");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freeSpaceValid)
|
||||||
|
Log?.Write(LogType.Info, "Free space is valid");
|
||||||
|
|
||||||
|
_freeSpaceValid = freeSpaceValid;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Warn, "There is no free space to check.");
|
||||||
|
_freeSpaceValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_freeSpaceChecked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckPadding(long readSizeB, CancellationToken? cancelToken = null)
|
||||||
|
{
|
||||||
|
long maxReads = readSizeB / XCIFileTrimmer.BufferSize;
|
||||||
|
long read = 0;
|
||||||
|
var buffer = new byte[BufferSize];
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bytes = _fileStream.Read(buffer, 0, XCIFileTrimmer.BufferSize);
|
||||||
|
if (bytes == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
Log?.Progress(read, maxReads, "Verifying file can be trimmed", false);
|
||||||
|
if (buffer.Take(bytes).AsParallel().Any(b => b != XCIFileTrimmer.PaddingByte))
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Warn, "Free space is NOT valid");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
read++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reset()
|
||||||
|
{
|
||||||
|
_freeSpaceChecked = false;
|
||||||
|
_freeSpaceValid = false;
|
||||||
|
ReadHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
public OperationOutcome Trim(CancellationToken? cancelToken = null)
|
||||||
|
{
|
||||||
|
if (!FileOK)
|
||||||
|
{
|
||||||
|
return OperationOutcome.InvalidXCIFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CanBeTrimmed)
|
||||||
|
{
|
||||||
|
return OperationOutcome.NoTrimNecessary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FreeSpaceChecked)
|
||||||
|
{
|
||||||
|
CheckFreeSpace(cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FreeSpaceValid)
|
||||||
|
{
|
||||||
|
if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return OperationOutcome.Cancelled;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return OperationOutcome.FreeSpaceCheckFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log?.Write(LogType.Info, "Trimming...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = new FileInfo(Filename);
|
||||||
|
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute");
|
||||||
|
File.SetAttributes(Filename, info.Attributes & ~FileAttributes.ReadOnly);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, e.ToString());
|
||||||
|
return OperationOutcome.ReadOnlyFileCannotFix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.Length != FileSizeB)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, "File size has changed, cannot safely trim.");
|
||||||
|
return OperationOutcome.FileSizeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outfileStream = new FileStream(_filename, FileMode.Open, FileAccess.Write, FileShare.Write);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
#if !XCI_TRIMMER_READ_ONLY_MODE
|
||||||
|
outfileStream.SetLength(TrimmedFileSizeB);
|
||||||
|
#endif
|
||||||
|
return OperationOutcome.Successful;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
outfileStream.Close();
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, e.ToString());
|
||||||
|
return OperationOutcome.FileIOWriteError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public OperationOutcome Untrim(CancellationToken? cancelToken = null)
|
||||||
|
{
|
||||||
|
if (!FileOK)
|
||||||
|
{
|
||||||
|
return OperationOutcome.InvalidXCIFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CanBeUntrimmed)
|
||||||
|
{
|
||||||
|
return OperationOutcome.NoUntrimPossible;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Info, "Untrimming...");
|
||||||
|
|
||||||
|
var info = new FileInfo(Filename);
|
||||||
|
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute");
|
||||||
|
File.SetAttributes(Filename, info.Attributes & ~FileAttributes.ReadOnly);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, e.ToString());
|
||||||
|
return OperationOutcome.ReadOnlyFileCannotFix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.Length != FileSizeB)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, "File size has changed, cannot safely untrim.");
|
||||||
|
return OperationOutcome.FileSizeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outfileStream = new FileStream(_filename, FileMode.Append, FileAccess.Write, FileShare.Write);
|
||||||
|
long bytesToWriteB = UntrimmedFileSizeB - FileSizeB;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Stopwatch timedSw = Lambda.Timed(() =>
|
||||||
|
{
|
||||||
|
WritePadding(outfileStream, bytesToWriteB, cancelToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (timedSw.Elapsed.TotalSeconds > 0)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Info, $"Wrote at {bytesToWriteB / (double)XCIFileTrimmer.BytesInAMegabyte / timedSw.Elapsed.TotalSeconds:N} Mb/sec");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return OperationOutcome.Cancelled;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return OperationOutcome.Successful;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
outfileStream.Close();
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, e.ToString());
|
||||||
|
return OperationOutcome.FileIOWriteError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WritePadding(FileStream outfileStream, long bytesToWriteB, CancellationToken? cancelToken = null)
|
||||||
|
{
|
||||||
|
long bytesLeftToWriteB = bytesToWriteB;
|
||||||
|
long writes = bytesLeftToWriteB / XCIFileTrimmer.BufferSize;
|
||||||
|
int write = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var buffer = new byte[BufferSize];
|
||||||
|
Array.Fill<byte>(buffer, XCIFileTrimmer.PaddingByte);
|
||||||
|
|
||||||
|
while (bytesLeftToWriteB > 0)
|
||||||
|
{
|
||||||
|
if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long bytesToWrite = Math.Min(XCIFileTrimmer.BufferSize, bytesLeftToWriteB);
|
||||||
|
|
||||||
|
#if !XCI_TRIMMER_READ_ONLY_MODE
|
||||||
|
outfileStream.Write(buffer, 0, (int)bytesToWrite);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bytesLeftToWriteB -= bytesToWrite;
|
||||||
|
Log?.Progress(write, writes, "Writing padding data...", false);
|
||||||
|
write++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Log?.Progress(write, writes, "Writing padding data...", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenReaders()
|
||||||
|
{
|
||||||
|
if (_binaryReader == null)
|
||||||
|
{
|
||||||
|
_fileStream = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
_binaryReader = new BinaryReader(_fileStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseReaders()
|
||||||
|
{
|
||||||
|
if (_binaryReader != null && _binaryReader.BaseStream != null)
|
||||||
|
_binaryReader.Close();
|
||||||
|
_binaryReader = null;
|
||||||
|
_fileStream = null;
|
||||||
|
GC.Collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReadHeader()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
OpenReaders();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Attempt without key area
|
||||||
|
bool success = CheckAndReadHeader(false);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
// Attempt with key area
|
||||||
|
success = CheckAndReadHeader(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileOK = success;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReaders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, ex.Message);
|
||||||
|
_fileOK = false;
|
||||||
|
_dataSizeB = 0;
|
||||||
|
_cartSizeB = 0;
|
||||||
|
_fileSizeB = 0;
|
||||||
|
_offsetB = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckAndReadHeader(bool assumeKeyArea)
|
||||||
|
{
|
||||||
|
// Read file size
|
||||||
|
_fileSizeB = _fileStream.Length;
|
||||||
|
if (_fileSizeB < 32 * 1024)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the data size is too small");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup offset
|
||||||
|
_offsetB = (long)(assumeKeyArea ? XCIFileTrimmer.CartKeyAreaSize : 0);
|
||||||
|
|
||||||
|
// Check header
|
||||||
|
Pos = _offsetB + XCIFileTrimmer.HeaderFilePos;
|
||||||
|
string head = System.Text.Encoding.ASCII.GetString(_binaryReader.ReadBytes(4));
|
||||||
|
if (head != XCIFileTrimmer.HeaderMagicValue)
|
||||||
|
{
|
||||||
|
if (!assumeKeyArea)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Warn, $"Incorrect header found, file mat contain a key area...");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the header is corrupted");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Cart Size
|
||||||
|
Pos = _offsetB + XCIFileTrimmer.CartSizeFilePos;
|
||||||
|
byte cartSizeId = _binaryReader.ReadByte();
|
||||||
|
if (!_cartSizesGB.TryGetValue(cartSizeId, out long cartSizeNGB))
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, $"The source file doesn't look like an XCI file as the Cartridge Size is incorrect (0x{cartSizeId:X2})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_cartSizeB = cartSizeNGB * XCIFileTrimmer.CartSizeMBinFormattedGB * XCIFileTrimmer.BytesInAMegabyte;
|
||||||
|
|
||||||
|
// Read data size
|
||||||
|
Pos = _offsetB + XCIFileTrimmer.DataSizeFilePos;
|
||||||
|
long records = (long)BitConverter.ToUInt32(_binaryReader.ReadBytes(4), 0);
|
||||||
|
_dataSizeB = RecordsToByte(records);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ namespace Ryujinx.HLE.Generators
|
|||||||
var syntaxReceiver = (ServiceSyntaxReceiver)context.SyntaxReceiver;
|
var syntaxReceiver = (ServiceSyntaxReceiver)context.SyntaxReceiver;
|
||||||
CodeGenerator generator = new CodeGenerator();
|
CodeGenerator generator = new CodeGenerator();
|
||||||
|
|
||||||
|
generator.AppendLine("#nullable enable");
|
||||||
generator.AppendLine("using System;");
|
generator.AppendLine("using System;");
|
||||||
generator.EnterScope($"namespace Ryujinx.HLE.HOS.Services.Sm");
|
generator.EnterScope($"namespace Ryujinx.HLE.HOS.Services.Sm");
|
||||||
generator.EnterScope($"partial class IUserInterface");
|
generator.EnterScope($"partial class IUserInterface");
|
||||||
@ -58,6 +59,7 @@ namespace Ryujinx.HLE.Generators
|
|||||||
|
|
||||||
generator.LeaveScope();
|
generator.LeaveScope();
|
||||||
generator.LeaveScope();
|
generator.LeaveScope();
|
||||||
|
generator.AppendLine("#nullable disable");
|
||||||
context.AddSource($"IUserInterface.g.cs", generator.ToString());
|
context.AddSource($"IUserInterface.g.cs", generator.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
55
src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs
Normal file
55
src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.UI.App.Common;
|
||||||
|
|
||||||
|
namespace Ryujinx.UI.Common.Models
|
||||||
|
{
|
||||||
|
public record XCITrimmerFileModel(
|
||||||
|
string Name,
|
||||||
|
string Path,
|
||||||
|
bool Trimmable,
|
||||||
|
bool Untrimmable,
|
||||||
|
long PotentialSavingsB,
|
||||||
|
long CurrentSavingsB,
|
||||||
|
int? PercentageProgress,
|
||||||
|
XCIFileTrimmer.OperationOutcome ProcessingOutcome)
|
||||||
|
{
|
||||||
|
public static XCITrimmerFileModel FromApplicationData(ApplicationData applicationData, XCIFileTrimmerLog logger)
|
||||||
|
{
|
||||||
|
var trimmer = new XCIFileTrimmer(applicationData.Path, logger);
|
||||||
|
|
||||||
|
return new XCITrimmerFileModel(
|
||||||
|
applicationData.Name,
|
||||||
|
applicationData.Path,
|
||||||
|
trimmer.CanBeTrimmed,
|
||||||
|
trimmer.CanBeUntrimmed,
|
||||||
|
trimmer.DiskSpaceSavingsB,
|
||||||
|
trimmer.DiskSpaceSavedB,
|
||||||
|
null,
|
||||||
|
XCIFileTrimmer.OperationOutcome.Undetermined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsFailed
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ProcessingOutcome != XCIFileTrimmer.OperationOutcome.Undetermined &&
|
||||||
|
ProcessingOutcome != XCIFileTrimmer.OperationOutcome.Successful;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool Equals(XCITrimmerFileModel obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
return false;
|
||||||
|
else
|
||||||
|
return this.Path == obj.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return this.Path.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,6 +33,7 @@
|
|||||||
"MenuBarToolsManageFileTypes": "Manage file types",
|
"MenuBarToolsManageFileTypes": "Manage file types",
|
||||||
"MenuBarToolsInstallFileTypes": "Install file types",
|
"MenuBarToolsInstallFileTypes": "Install file types",
|
||||||
"MenuBarToolsUninstallFileTypes": "Uninstall file types",
|
"MenuBarToolsUninstallFileTypes": "Uninstall file types",
|
||||||
|
"MenuBarToolsXCITrimmer": "Trim XCI Files",
|
||||||
"MenuBarView": "_View",
|
"MenuBarView": "_View",
|
||||||
"MenuBarViewWindow": "Window Size",
|
"MenuBarViewWindow": "Window Size",
|
||||||
"MenuBarViewWindow720": "720p",
|
"MenuBarViewWindow720": "720p",
|
||||||
@ -84,8 +85,11 @@
|
|||||||
"GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods",
|
"GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods",
|
||||||
"GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory",
|
"GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory",
|
||||||
"GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.",
|
"GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.",
|
||||||
|
"GameListContextMenuTrimXCI": "Check and Trim XCI File",
|
||||||
|
"GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space",
|
||||||
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
|
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
|
||||||
"StatusBarSystemVersion": "System Version: {0}",
|
"StatusBarSystemVersion": "System Version: {0}",
|
||||||
|
"StatusBarXCIFileTrimming": "Trimming XCI File '{0}'",
|
||||||
"LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
|
"LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
|
||||||
"LinuxVmMaxMapCountDialogTextPrimary": "Would you like to increase the value of vm.max_map_count to {0}",
|
"LinuxVmMaxMapCountDialogTextPrimary": "Would you like to increase the value of vm.max_map_count to {0}",
|
||||||
"LinuxVmMaxMapCountDialogTextSecondary": "Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.",
|
"LinuxVmMaxMapCountDialogTextSecondary": "Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.",
|
||||||
@ -400,6 +404,8 @@
|
|||||||
"InputDialogTitle": "Input Dialog",
|
"InputDialogTitle": "Input Dialog",
|
||||||
"InputDialogOk": "OK",
|
"InputDialogOk": "OK",
|
||||||
"InputDialogCancel": "Cancel",
|
"InputDialogCancel": "Cancel",
|
||||||
|
"InputDialogCancelling": "Cancelling",
|
||||||
|
"InputDialogClose": "Close",
|
||||||
"InputDialogAddNewProfileTitle": "Choose the Profile Name",
|
"InputDialogAddNewProfileTitle": "Choose the Profile Name",
|
||||||
"InputDialogAddNewProfileHeader": "Please Enter a Profile Name",
|
"InputDialogAddNewProfileHeader": "Please Enter a Profile Name",
|
||||||
"InputDialogAddNewProfileSubtext": "(Max Length: {0})",
|
"InputDialogAddNewProfileSubtext": "(Max Length: {0})",
|
||||||
@ -468,6 +474,7 @@
|
|||||||
"DialogUninstallFileTypesSuccessMessage": "Successfully uninstalled file types!",
|
"DialogUninstallFileTypesSuccessMessage": "Successfully uninstalled file types!",
|
||||||
"DialogUninstallFileTypesErrorMessage": "Failed to uninstall file types.",
|
"DialogUninstallFileTypesErrorMessage": "Failed to uninstall file types.",
|
||||||
"DialogOpenSettingsWindowLabel": "Open Settings Window",
|
"DialogOpenSettingsWindowLabel": "Open Settings Window",
|
||||||
|
"DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window",
|
||||||
"DialogControllerAppletTitle": "Controller Applet",
|
"DialogControllerAppletTitle": "Controller Applet",
|
||||||
"DialogMessageDialogErrorExceptionMessage": "Error displaying Message Dialog: {0}",
|
"DialogMessageDialogErrorExceptionMessage": "Error displaying Message Dialog: {0}",
|
||||||
"DialogSoftwareKeyboardErrorExceptionMessage": "Error displaying Software Keyboard: {0}",
|
"DialogSoftwareKeyboardErrorExceptionMessage": "Error displaying Software Keyboard: {0}",
|
||||||
@ -670,6 +677,12 @@
|
|||||||
"TitleUpdateVersionLabel": "Version {0}",
|
"TitleUpdateVersionLabel": "Version {0}",
|
||||||
"TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
|
"TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
|
||||||
"TitleBundledDlcLabel": "Bundled:",
|
"TitleBundledDlcLabel": "Bundled:",
|
||||||
|
"TitleXCIStatusPartialLabel": "Partial",
|
||||||
|
"TitleXCIStatusTrimmableLabel": "Untrimmed",
|
||||||
|
"TitleXCIStatusUntrimmableLabel": "Trimmed",
|
||||||
|
"TitleXCIStatusFailedLabel": "(Failed)",
|
||||||
|
"TitleXCICanSaveLabel": "Save {0:n0} Mb",
|
||||||
|
"TitleXCISavingLabel": "Saved {0:n0} Mb",
|
||||||
"RyujinxInfo": "Ryujinx - Info",
|
"RyujinxInfo": "Ryujinx - Info",
|
||||||
"RyujinxConfirm": "Ryujinx - Confirmation",
|
"RyujinxConfirm": "Ryujinx - Confirmation",
|
||||||
"FileDialogAllTypes": "All types",
|
"FileDialogAllTypes": "All types",
|
||||||
@ -722,11 +735,37 @@
|
|||||||
"SelectDlcDialogTitle": "Select DLC files",
|
"SelectDlcDialogTitle": "Select DLC files",
|
||||||
"SelectUpdateDialogTitle": "Select update files",
|
"SelectUpdateDialogTitle": "Select update files",
|
||||||
"SelectModDialogTitle": "Select mod directory",
|
"SelectModDialogTitle": "Select mod directory",
|
||||||
|
"TrimXCIFileDialogTitle": "Check and Trim XCI File",
|
||||||
|
"TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.",
|
||||||
|
"TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB",
|
||||||
|
"TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details",
|
||||||
|
"TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details",
|
||||||
|
"TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details",
|
||||||
|
"TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.",
|
||||||
|
"TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim",
|
||||||
|
"TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details",
|
||||||
|
"TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details",
|
||||||
|
"TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed",
|
||||||
|
"TrimXCIFileCancelled": "The operation was cancelled",
|
||||||
|
"TrimXCIFileFileUndertermined": "No operation was performed",
|
||||||
"UserProfileWindowTitle": "User Profiles Manager",
|
"UserProfileWindowTitle": "User Profiles Manager",
|
||||||
"CheatWindowTitle": "Cheats Manager",
|
"CheatWindowTitle": "Cheats Manager",
|
||||||
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
||||||
"ModWindowTitle": "Manage Mods for {0} ({1})",
|
"ModWindowTitle": "Manage Mods for {0} ({1})",
|
||||||
"UpdateWindowTitle": "Title Update Manager",
|
"UpdateWindowTitle": "Title Update Manager",
|
||||||
|
"XCITrimmerWindowTitle": "XCI File Trimmer",
|
||||||
|
"XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected",
|
||||||
|
"XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)",
|
||||||
|
"XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...",
|
||||||
|
"XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...",
|
||||||
|
"XCITrimmerTitleStatusFailed": "Failed",
|
||||||
|
"XCITrimmerPotentialSavings": "Potential Savings",
|
||||||
|
"XCITrimmerActualSavings": "Actual Savings",
|
||||||
|
"XCITrimmerSavingsMb": "{0:n0} Mb",
|
||||||
|
"XCITrimmerSelectDisplayed": "Select Shown",
|
||||||
|
"XCITrimmerDeselectDisplayed": "Deselect Shown",
|
||||||
|
"XCITrimmerSortName": "Title",
|
||||||
|
"XCITrimmerSortSaved": "Space Savings",
|
||||||
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
||||||
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
||||||
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
||||||
@ -740,6 +779,7 @@
|
|||||||
"AutoloadUpdateRemovedMessage": "{0} missing update(s) removed",
|
"AutoloadUpdateRemovedMessage": "{0} missing update(s) removed",
|
||||||
"ModWindowHeading": "{0} Mod(s)",
|
"ModWindowHeading": "{0} Mod(s)",
|
||||||
"UserProfilesEditProfile": "Edit Selected",
|
"UserProfilesEditProfile": "Edit Selected",
|
||||||
|
"Continue": "Continue",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Discard": "Discard",
|
"Discard": "Discard",
|
||||||
|
@ -43,6 +43,10 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Design.PreviewWith>
|
</Design.PreviewWith>
|
||||||
|
<Style Selector="DropDownButton">
|
||||||
|
<Setter Property="FontSize"
|
||||||
|
Value="12" />
|
||||||
|
</Style>
|
||||||
<Style Selector="Border.small">
|
<Style Selector="Border.small">
|
||||||
<Setter Property="Width"
|
<Setter Property="Width"
|
||||||
Value="100" />
|
Value="100" />
|
||||||
@ -231,6 +235,14 @@
|
|||||||
<Setter Property="MinWidth"
|
<Setter Property="MinWidth"
|
||||||
Value="80" />
|
Value="80" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="ProgressBar:horizontal">
|
||||||
|
<Setter Property="MinWidth" Value="0"/>
|
||||||
|
<Setter Property="MinHeight" Value="0"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ProgressBar:vertical">
|
||||||
|
<Setter Property="MinWidth" Value="0"/>
|
||||||
|
<Setter Property="MinHeight" Value="0"/>
|
||||||
|
</Style>
|
||||||
<Style Selector="ProgressBar /template/ Border#ProgressBarTrack">
|
<Style Selector="ProgressBar /template/ Border#ProgressBarTrack">
|
||||||
<Setter Property="IsVisible"
|
<Setter Property="IsVisible"
|
||||||
Value="False" />
|
Value="False" />
|
||||||
@ -389,7 +401,7 @@
|
|||||||
<x:Double x:Key="ControlContentThemeFontSize">13</x:Double>
|
<x:Double x:Key="ControlContentThemeFontSize">13</x:Double>
|
||||||
<x:Double x:Key="MenuItemHeight">26</x:Double>
|
<x:Double x:Key="MenuItemHeight">26</x:Double>
|
||||||
<x:Double x:Key="TabItemMinHeight">28</x:Double>
|
<x:Double x:Key="TabItemMinHeight">28</x:Double>
|
||||||
<x:Double x:Key="ContentDialogMaxWidth">600</x:Double>
|
<x:Double x:Key="ContentDialogMaxWidth">700</x:Double>
|
||||||
<x:Double x:Key="ContentDialogMaxHeight">756</x:Double>
|
<x:Double x:Key="ContentDialogMaxHeight">756</x:Double>
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
</Styles>
|
</Styles>
|
||||||
|
24
src/Ryujinx/Common/XCIFileTrimmerMainWindowLog.cs
Normal file
24
src/Ryujinx/Common/XCIFileTrimmerMainWindowLog.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using Avalonia.Threading;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Common
|
||||||
|
{
|
||||||
|
internal class XCIFileTrimmerMainWindowLog : Ryujinx.Common.Logging.XCIFileTrimmerLog
|
||||||
|
{
|
||||||
|
private readonly MainWindowViewModel _viewModel;
|
||||||
|
|
||||||
|
public XCIFileTrimmerMainWindowLog(MainWindowViewModel viewModel)
|
||||||
|
{
|
||||||
|
_viewModel = viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Progress(long current, long total, string text, bool complete)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
_viewModel.StatusBarProgressMaximum = (int)(total);
|
||||||
|
_viewModel.StatusBarProgressValue = (int)(current);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
src/Ryujinx/Common/XCIFileTrimmerWindowLog.cs
Normal file
23
src/Ryujinx/Common/XCIFileTrimmerWindowLog.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Avalonia.Threading;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Common
|
||||||
|
{
|
||||||
|
internal class XCIFileTrimmerWindowLog : Ryujinx.Common.Logging.XCIFileTrimmerLog
|
||||||
|
{
|
||||||
|
private readonly XCITrimmerViewModel _viewModel;
|
||||||
|
|
||||||
|
public XCIFileTrimmerWindowLog(XCITrimmerViewModel viewModel)
|
||||||
|
{
|
||||||
|
_viewModel = viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Progress(long current, long total, string text, bool complete)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
_viewModel.SetProgress((int)(current), (int)(total));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -69,6 +69,12 @@
|
|||||||
Header="{ext:Locale GameListContextMenuOpenSdModsDirectory}"
|
Header="{ext:Locale GameListContextMenuOpenSdModsDirectory}"
|
||||||
Icon="{ext:Icon mdi-folder-file}"
|
Icon="{ext:Icon mdi-folder-file}"
|
||||||
ToolTip.Tip="{ext:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
|
ToolTip.Tip="{ext:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem
|
||||||
|
Click="TrimXCI_Click"
|
||||||
|
Header="{ext:Locale GameListContextMenuTrimXCI}"
|
||||||
|
IsEnabled="{Binding TrimXCIEnabled}"
|
||||||
|
ToolTip.Tip="{ext:Locale GameListContextMenuTrimXCIToolTip}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon mdi-cached}">
|
<MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon mdi-cached}">
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -2,6 +2,7 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
using Avalonia.Threading;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
@ -17,6 +18,8 @@ using SkiaSharp;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
namespace Ryujinx.Ava.UI.Controls
|
||||||
@ -323,5 +326,15 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||||
await viewModel.LoadApplication(viewModel.SelectedApplication);
|
await viewModel.LoadApplication(viewModel.SelectedApplication);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void TrimXCI_Click(object sender, RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
|
||||||
|
|
||||||
|
if (viewModel?.SelectedApplication != null)
|
||||||
|
{
|
||||||
|
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
62
src/Ryujinx/UI/Helpers/AvaloniaListExtensions.cs
Normal file
62
src/Ryujinx/UI/Helpers/AvaloniaListExtensions.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
public static class AvaloniaListExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds or Replaces an item in an AvaloniaList irrespective of whether the item already exists
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the element in the AvaoloniaList</typeparam>
|
||||||
|
/// <param name="list">The list containing the item to replace</param>
|
||||||
|
/// <param name="item">The item to replace</param>
|
||||||
|
/// <param name="addIfNotFound">True to add the item if its not found</param>
|
||||||
|
/// <returns>True if the item was found and replaced, false if it was addded</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// The indexes on the AvaloniaList will only replace if the item does not match,
|
||||||
|
/// this causes the items to not be replaced if the Equality is customised on the
|
||||||
|
/// items. This method will instead find, remove and add the item to ensure it is
|
||||||
|
/// replaced correctly.
|
||||||
|
/// </remarks>
|
||||||
|
public static bool ReplaceWith<T>(this AvaloniaList<T> list, T item, bool addIfNotFound = true)
|
||||||
|
{
|
||||||
|
var index = list.IndexOf(item);
|
||||||
|
|
||||||
|
if (index != -1)
|
||||||
|
{
|
||||||
|
list.RemoveAt(index);
|
||||||
|
list.Insert(index, item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
list.Add(item);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds or Replaces items in an AvaloniaList from another list irrespective of whether the item already exists
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the element in the AvaoloniaList</typeparam>
|
||||||
|
/// <param name="list">The list containing the item to replace</param>
|
||||||
|
/// <param name="sourceList">The list of items to be actually added to `list`</param>
|
||||||
|
/// <param name="matchingList">The items to use as matching records to search for in the `sourceList', if not found this item will be added instead</params>
|
||||||
|
public static void AddOrReplaceMatching<T>(this AvaloniaList<T> list, IList<T> sourceList, IList<T> matchingList)
|
||||||
|
{
|
||||||
|
foreach (var match in matchingList)
|
||||||
|
{
|
||||||
|
var index = sourceList.IndexOf(match);
|
||||||
|
if (index != -1)
|
||||||
|
{
|
||||||
|
list.ReplaceWith(sourceList[index]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
list.ReplaceWith(match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Data;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.UI.Common.Models;
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
internal class XCITrimmerFileSpaceSavingsConverter : IValueConverter
|
||||||
|
{
|
||||||
|
private const long _bytesPerMB = 1024 * 1024;
|
||||||
|
public static XCITrimmerFileSpaceSavingsConverter Instance = new();
|
||||||
|
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is UnsetValueType)
|
||||||
|
{
|
||||||
|
return BindingOperations.DoNothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetType.IsAssignableFrom(typeof(string)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is not XCITrimmerFileModel app)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.CurrentSavingsB < app.PotentialSavingsB)
|
||||||
|
{
|
||||||
|
return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCICanSaveLabel, (app.PotentialSavingsB - app.CurrentSavingsB) / _bytesPerMB);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCISavingLabel, app.CurrentSavingsB / _bytesPerMB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
src/Ryujinx/UI/Helpers/XCITrimmerFileStatusConverter.cs
Normal file
46
src/Ryujinx/UI/Helpers/XCITrimmerFileStatusConverter.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Data;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.UI.Common.Models;
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using static Ryujinx.Common.Utilities.XCIFileTrimmer;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
internal class XCITrimmerFileStatusConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public static XCITrimmerFileStatusConverter Instance = new();
|
||||||
|
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is UnsetValueType)
|
||||||
|
{
|
||||||
|
return BindingOperations.DoNothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetType.IsAssignableFrom(typeof(string)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is not XCITrimmerFileModel app)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.PercentageProgress != null ? String.Empty :
|
||||||
|
app.ProcessingOutcome != OperationOutcome.Successful && app.ProcessingOutcome != OperationOutcome.Undetermined ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusFailedLabel] :
|
||||||
|
app.Trimmable & app.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusPartialLabel] :
|
||||||
|
app.Trimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusTrimmableLabel] :
|
||||||
|
app.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusUntrimmableLabel] :
|
||||||
|
String.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Data;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Ryujinx.UI.Common.Models;
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using static Ryujinx.Common.Utilities.XCIFileTrimmer;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
internal class XCITrimmerFileStatusDetailConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public static XCITrimmerFileStatusDetailConverter Instance = new();
|
||||||
|
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is UnsetValueType)
|
||||||
|
{
|
||||||
|
return BindingOperations.DoNothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetType.IsAssignableFrom(typeof(string)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is not XCITrimmerFileModel app)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.PercentageProgress != null ? null :
|
||||||
|
app.ProcessingOutcome != OperationOutcome.Successful && app.ProcessingOutcome != OperationOutcome.Undetermined ? app.ProcessingOutcome.ToLocalisedText() :
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
src/Ryujinx/UI/Helpers/XCITrimmerOperationOutcomeHelper.cs
Normal file
36
src/Ryujinx/UI/Helpers/XCITrimmerOperationOutcomeHelper.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using static Ryujinx.Common.Utilities.XCIFileTrimmer;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
public static class XCIFileTrimmerOperationOutcomeExtensions
|
||||||
|
{
|
||||||
|
public static string ToLocalisedText(this OperationOutcome operationOutcome)
|
||||||
|
{
|
||||||
|
switch (operationOutcome)
|
||||||
|
{
|
||||||
|
case OperationOutcome.NoTrimNecessary:
|
||||||
|
return LocaleManager.Instance[LocaleKeys.TrimXCIFileNoTrimNecessary];
|
||||||
|
case OperationOutcome.NoUntrimPossible:
|
||||||
|
return LocaleManager.Instance[LocaleKeys.TrimXCIFileNoUntrimPossible];
|
||||||
|
case OperationOutcome.ReadOnlyFileCannotFix:
|
||||||
|
return LocaleManager.Instance[LocaleKeys.TrimXCIFileReadOnlyFileCannotFix];
|
||||||
|
case OperationOutcome.FreeSpaceCheckFailed:
|
||||||
|
return LocaleManager.Instance[LocaleKeys.TrimXCIFileFreeSpaceCheckFailed];
|
||||||
|
case OperationOutcome.InvalidXCIFile:
|
||||||
|
return LocaleManager.Instance[LocaleKeys.TrimXCIFileInvalidXCIFile];
|
||||||
|
case OperationOutcome.FileIOWriteError:
|
||||||
|
return LocaleManager.Instance[LocaleKeys.TrimXCIFileFileIOWriteError];
|
||||||
|
case OperationOutcome.FileSizeChanged:
|
||||||
|
return LocaleManager.Instance[LocaleKeys.TrimXCIFileFileSizeChanged];
|
||||||
|
case OperationOutcome.Cancelled:
|
||||||
|
return LocaleManager.Instance[LocaleKeys.TrimXCIFileCancelled];
|
||||||
|
case OperationOutcome.Undetermined:
|
||||||
|
return LocaleManager.Instance[LocaleKeys.TrimXCIFileFileUndertermined];
|
||||||
|
case OperationOutcome.Successful:
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ using Ryujinx.Ava.UI.Windows;
|
|||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.Cpu;
|
using Ryujinx.Cpu;
|
||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
@ -84,6 +85,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
private bool _isAppletMenuActive;
|
private bool _isAppletMenuActive;
|
||||||
private int _statusBarProgressMaximum;
|
private int _statusBarProgressMaximum;
|
||||||
private int _statusBarProgressValue;
|
private int _statusBarProgressValue;
|
||||||
|
private string _statusBarProgressStatusText;
|
||||||
|
private bool _statusBarProgressStatusVisible;
|
||||||
private bool _isPaused;
|
private bool _isPaused;
|
||||||
private bool _showContent = true;
|
private bool _showContent = true;
|
||||||
private bool _isLoadingIndeterminate = true;
|
private bool _isLoadingIndeterminate = true;
|
||||||
@ -391,6 +394,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public bool OpenDeviceSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0;
|
public bool OpenDeviceSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0;
|
||||||
|
|
||||||
|
public bool TrimXCIEnabled => Ryujinx.Common.Utilities.XCIFileTrimmer.CanTrim(SelectedApplication.Path, new Common.XCIFileTrimmerMainWindowLog(this));
|
||||||
|
|
||||||
public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
||||||
|
|
||||||
public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild;
|
public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild;
|
||||||
@ -505,6 +510,28 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool StatusBarProgressStatusVisible
|
||||||
|
{
|
||||||
|
get => _statusBarProgressStatusVisible;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_statusBarProgressStatusVisible = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StatusBarProgressStatusText
|
||||||
|
{
|
||||||
|
get => _statusBarProgressStatusText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_statusBarProgressStatusText = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string FifoStatusText
|
public string FifoStatusText
|
||||||
{
|
{
|
||||||
get => _fifoStatusText;
|
get => _fifoStatusText;
|
||||||
@ -1834,6 +1861,98 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void ProcessTrimResult(String filename, Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome operationOutcome)
|
||||||
|
{
|
||||||
|
string notifyUser = operationOutcome.ToLocalisedText();
|
||||||
|
|
||||||
|
if (notifyUser != null)
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateWarningDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.TrimXCIFileFailedPrimaryText],
|
||||||
|
notifyUser
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (operationOutcome)
|
||||||
|
{
|
||||||
|
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.Successful:
|
||||||
|
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
if (desktop.MainWindow is MainWindow mainWindow)
|
||||||
|
mainWindow.LoadApplications();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task TrimXCIFile(string filename)
|
||||||
|
{
|
||||||
|
if (filename == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmer = new XCIFileTrimmer(filename, new Common.XCIFileTrimmerMainWindowLog(this));
|
||||||
|
|
||||||
|
if (trimmer.CanBeTrimmed)
|
||||||
|
{
|
||||||
|
var savings = (double)trimmer.DiskSpaceSavingsB / 1024.0 / 1024.0;
|
||||||
|
var currentFileSize = (double)trimmer.FileSizeB / 1024.0 / 1024.0;
|
||||||
|
var cartDataSize = (double)trimmer.DataSizeB / 1024.0 / 1024.0;
|
||||||
|
string secondaryText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TrimXCIFileDialogSecondaryText, currentFileSize, cartDataSize, savings);
|
||||||
|
|
||||||
|
var result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.TrimXCIFileDialogPrimaryText],
|
||||||
|
secondaryText,
|
||||||
|
LocaleManager.Instance[LocaleKeys.Continue],
|
||||||
|
LocaleManager.Instance[LocaleKeys.Cancel],
|
||||||
|
LocaleManager.Instance[LocaleKeys.TrimXCIFileDialogTitle]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == UserResult.Yes)
|
||||||
|
{
|
||||||
|
Thread XCIFileTrimThread = new(() =>
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
StatusBarProgressStatusText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarXCIFileTrimming, Path.GetFileName(filename));
|
||||||
|
StatusBarProgressStatusVisible = true;
|
||||||
|
StatusBarProgressMaximum = 1;
|
||||||
|
StatusBarProgressValue = 0;
|
||||||
|
StatusBarVisible = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
XCIFileTrimmer.OperationOutcome operationOutcome = trimmer.Trim();
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
ProcessTrimResult(filename, operationOutcome);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
StatusBarProgressStatusVisible = false;
|
||||||
|
StatusBarProgressStatusText = string.Empty;
|
||||||
|
StatusBarVisible = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Name = "GUI.XCIFileTrimmerThread",
|
||||||
|
IsBackground = true,
|
||||||
|
};
|
||||||
|
XCIFileTrimThread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
541
src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs
Normal file
541
src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs
Normal file
@ -0,0 +1,541 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using DynamicData;
|
||||||
|
using Gommon;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Ryujinx.Ava.Common;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.UI.App.Common;
|
||||||
|
using Ryujinx.UI.Common.Models;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using static Ryujinx.Common.Utilities.XCIFileTrimmer;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
{
|
||||||
|
public class XCITrimmerViewModel : BaseModel
|
||||||
|
{
|
||||||
|
private const long _bytesPerMB = 1024 * 1024;
|
||||||
|
private enum ProcessingMode
|
||||||
|
{
|
||||||
|
Trimming,
|
||||||
|
Untrimming
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SortField
|
||||||
|
{
|
||||||
|
Name,
|
||||||
|
Saved
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string _FileExtXCI = "XCI";
|
||||||
|
|
||||||
|
private readonly Ryujinx.Common.Logging.XCIFileTrimmerLog _logger;
|
||||||
|
private readonly ApplicationLibrary _applicationLibrary;
|
||||||
|
private Optional<XCITrimmerFileModel> _processingApplication = null;
|
||||||
|
private AvaloniaList<XCITrimmerFileModel> _allXCIFiles = new();
|
||||||
|
private AvaloniaList<XCITrimmerFileModel> _selectedXCIFiles = new();
|
||||||
|
private AvaloniaList<XCITrimmerFileModel> _displayedXCIFiles = new();
|
||||||
|
private MainWindowViewModel _mainWindowViewModel;
|
||||||
|
private CancellationTokenSource _cancellationTokenSource;
|
||||||
|
private string _search;
|
||||||
|
private ProcessingMode _processingMode;
|
||||||
|
private SortField _sortField = SortField.Name;
|
||||||
|
private bool _sortAscending = true;
|
||||||
|
|
||||||
|
public XCITrimmerViewModel(MainWindowViewModel mainWindowViewModel)
|
||||||
|
{
|
||||||
|
_logger = new XCIFileTrimmerWindowLog(this);
|
||||||
|
_mainWindowViewModel = mainWindowViewModel;
|
||||||
|
_applicationLibrary = _mainWindowViewModel.ApplicationLibrary;
|
||||||
|
LoadXCIApplications();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadXCIApplications()
|
||||||
|
{
|
||||||
|
var apps = _applicationLibrary.Applications.Items
|
||||||
|
.Where(app => app.FileExtension == _FileExtXCI);
|
||||||
|
|
||||||
|
foreach (var xciApp in apps)
|
||||||
|
AddOrUpdateXCITrimmerFile(CreateXCITrimmerFile(xciApp.Path));
|
||||||
|
|
||||||
|
ApplicationsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private XCITrimmerFileModel CreateXCITrimmerFile(
|
||||||
|
string path,
|
||||||
|
OperationOutcome operationOutcome = OperationOutcome.Undetermined)
|
||||||
|
{
|
||||||
|
var xciApp = _applicationLibrary.Applications.Items.First(app => app.FileExtension == _FileExtXCI && app.Path == path);
|
||||||
|
return XCITrimmerFileModel.FromApplicationData(xciApp, _logger) with { ProcessingOutcome = operationOutcome };
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool AddOrUpdateXCITrimmerFile(XCITrimmerFileModel xci, bool suppressChanged = true, bool autoSelect = true)
|
||||||
|
{
|
||||||
|
bool replaced = _allXCIFiles.ReplaceWith(xci);
|
||||||
|
_displayedXCIFiles.ReplaceWith(xci, Filter(xci));
|
||||||
|
_selectedXCIFiles.ReplaceWith(xci, xci.Trimmable && autoSelect);
|
||||||
|
|
||||||
|
if (!suppressChanged)
|
||||||
|
ApplicationsChanged();
|
||||||
|
|
||||||
|
return replaced;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FilteringChanged()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Search));
|
||||||
|
SortAndFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SortingChanged()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsSortedByName));
|
||||||
|
OnPropertyChanged(nameof(IsSortedBySaved));
|
||||||
|
OnPropertyChanged(nameof(SortingAscending));
|
||||||
|
OnPropertyChanged(nameof(SortingField));
|
||||||
|
OnPropertyChanged(nameof(SortingFieldName));
|
||||||
|
SortAndFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisplayedChanged()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Status));
|
||||||
|
OnPropertyChanged(nameof(DisplayedXCIFiles));
|
||||||
|
OnPropertyChanged(nameof(SelectedDisplayedXCIFiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplicationsChanged()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(AllXCIFiles));
|
||||||
|
OnPropertyChanged(nameof(Status));
|
||||||
|
OnPropertyChanged(nameof(PotentialSavings));
|
||||||
|
OnPropertyChanged(nameof(ActualSavings));
|
||||||
|
OnPropertyChanged(nameof(CanTrim));
|
||||||
|
OnPropertyChanged(nameof(CanUntrim));
|
||||||
|
DisplayedChanged();
|
||||||
|
SortAndFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectionChanged(bool displayedChanged = true)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Status));
|
||||||
|
OnPropertyChanged(nameof(CanTrim));
|
||||||
|
OnPropertyChanged(nameof(CanUntrim));
|
||||||
|
OnPropertyChanged(nameof(SelectedXCIFiles));
|
||||||
|
|
||||||
|
if (displayedChanged)
|
||||||
|
OnPropertyChanged(nameof(SelectedDisplayedXCIFiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessingChanged()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Processing));
|
||||||
|
OnPropertyChanged(nameof(Cancel));
|
||||||
|
OnPropertyChanged(nameof(Status));
|
||||||
|
OnPropertyChanged(nameof(CanTrim));
|
||||||
|
OnPropertyChanged(nameof(CanUntrim));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<XCITrimmerFileModel> GetSelectedDisplayedXCIFiles()
|
||||||
|
{
|
||||||
|
return _displayedXCIFiles.Where(xci => _selectedXCIFiles.Contains(xci));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PerformOperation(ProcessingMode processingMode)
|
||||||
|
{
|
||||||
|
if (Processing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_processingMode = processingMode;
|
||||||
|
Processing = true;
|
||||||
|
var cancellationToken = _cancellationTokenSource.Token;
|
||||||
|
|
||||||
|
Thread XCIFileTrimThread = new(() =>
|
||||||
|
{
|
||||||
|
var toProcess = Sort(SelectedXCIFiles
|
||||||
|
.Where(xci =>
|
||||||
|
(processingMode == ProcessingMode.Untrimming && xci.Untrimmable) ||
|
||||||
|
(processingMode == ProcessingMode.Trimming && xci.Trimmable)
|
||||||
|
)).ToList();
|
||||||
|
|
||||||
|
var viewsSaved = DisplayedXCIFiles.ToList();
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
_selectedXCIFiles.Clear();
|
||||||
|
_displayedXCIFiles.Clear();
|
||||||
|
_displayedXCIFiles.AddRange(toProcess);
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var xciApp in toProcess)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var trimmer = new XCIFileTrimmer(xciApp.Path, _logger);
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
ProcessingApplication = xciApp;
|
||||||
|
});
|
||||||
|
|
||||||
|
var outcome = OperationOutcome.Undetermined;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
switch (processingMode)
|
||||||
|
{
|
||||||
|
case ProcessingMode.Trimming:
|
||||||
|
outcome = trimmer.Trim(cancellationToken);
|
||||||
|
break;
|
||||||
|
case ProcessingMode.Untrimming:
|
||||||
|
outcome = trimmer.Untrim(cancellationToken);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outcome == OperationOutcome.Cancelled)
|
||||||
|
outcome = OperationOutcome.Undetermined;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
ProcessingApplication = CreateXCITrimmerFile(xciApp.Path);
|
||||||
|
AddOrUpdateXCITrimmerFile(ProcessingApplication, false, false);
|
||||||
|
ProcessingApplication = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
_displayedXCIFiles.AddOrReplaceMatching(_allXCIFiles, viewsSaved);
|
||||||
|
_selectedXCIFiles.AddOrReplaceMatching(_allXCIFiles, toProcess);
|
||||||
|
Processing = false;
|
||||||
|
ApplicationsChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Name = "GUI.XCIFilesTrimmerThread",
|
||||||
|
IsBackground = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
XCIFileTrimThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Filter<T>(T arg)
|
||||||
|
{
|
||||||
|
if (arg is XCITrimmerFileModel content)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(_search)
|
||||||
|
|| content.Name.ToLower().Contains(_search.ToLower())
|
||||||
|
|| content.Path.ToLower().Contains(_search.ToLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CompareXCITrimmerFiles : IComparer<XCITrimmerFileModel>
|
||||||
|
{
|
||||||
|
private XCITrimmerViewModel _viewModel;
|
||||||
|
|
||||||
|
public CompareXCITrimmerFiles(XCITrimmerViewModel ViewModel)
|
||||||
|
{
|
||||||
|
_viewModel = ViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Compare(XCITrimmerFileModel x, XCITrimmerFileModel y)
|
||||||
|
{
|
||||||
|
int result = 0;
|
||||||
|
|
||||||
|
switch (_viewModel.SortingField)
|
||||||
|
{
|
||||||
|
case SortField.Name:
|
||||||
|
result = x.Name.CompareTo(y.Name);
|
||||||
|
break;
|
||||||
|
case SortField.Saved:
|
||||||
|
result = x.PotentialSavingsB.CompareTo(y.PotentialSavingsB);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_viewModel.SortingAscending)
|
||||||
|
result = -result;
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
result = x.Path.CompareTo(y.Path);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IOrderedEnumerable<XCITrimmerFileModel> Sort(IEnumerable<XCITrimmerFileModel> list)
|
||||||
|
{
|
||||||
|
return list
|
||||||
|
.OrderBy(xci => xci, new CompareXCITrimmerFiles(this))
|
||||||
|
.ThenBy(it => it.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TrimSelected()
|
||||||
|
{
|
||||||
|
PerformOperation(ProcessingMode.Trimming);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UntrimSelected()
|
||||||
|
{
|
||||||
|
PerformOperation(ProcessingMode.Untrimming);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetProgress(int current, int maximum)
|
||||||
|
{
|
||||||
|
if (_processingApplication != null)
|
||||||
|
{
|
||||||
|
int percentageProgress = 100 * current / maximum;
|
||||||
|
if (!ProcessingApplication.HasValue || (ProcessingApplication.Value.PercentageProgress != percentageProgress))
|
||||||
|
ProcessingApplication = ProcessingApplication.Value with { PercentageProgress = percentageProgress };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SelectDisplayed()
|
||||||
|
{
|
||||||
|
SelectedXCIFiles.AddRange(DisplayedXCIFiles);
|
||||||
|
SelectionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeselectDisplayed()
|
||||||
|
{
|
||||||
|
SelectedXCIFiles.RemoveMany(DisplayedXCIFiles);
|
||||||
|
SelectionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Select(XCITrimmerFileModel model)
|
||||||
|
{
|
||||||
|
bool selectionChanged = !SelectedXCIFiles.Contains(model);
|
||||||
|
bool displayedSelectionChanged = !SelectedDisplayedXCIFiles.Contains(model);
|
||||||
|
SelectedXCIFiles.ReplaceOrAdd(model, model);
|
||||||
|
if (selectionChanged)
|
||||||
|
SelectionChanged(displayedSelectionChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Deselect(XCITrimmerFileModel model)
|
||||||
|
{
|
||||||
|
bool displayedSelectionChanged = !SelectedDisplayedXCIFiles.Contains(model);
|
||||||
|
if (SelectedXCIFiles.Remove(model))
|
||||||
|
SelectionChanged(displayedSelectionChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SortAndFilter()
|
||||||
|
{
|
||||||
|
if (Processing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Sort(AllXCIFiles)
|
||||||
|
.AsObservableChangeSet()
|
||||||
|
.Filter(Filter)
|
||||||
|
.Bind(out var view).AsObservableList();
|
||||||
|
|
||||||
|
_displayedXCIFiles.Clear();
|
||||||
|
_displayedXCIFiles.AddRange(view);
|
||||||
|
|
||||||
|
DisplayedChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<XCITrimmerFileModel> ProcessingApplication
|
||||||
|
{
|
||||||
|
get => _processingApplication;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!value.HasValue && _processingApplication.HasValue)
|
||||||
|
value = _processingApplication.Value with { PercentageProgress = null };
|
||||||
|
|
||||||
|
if (value.HasValue)
|
||||||
|
_displayedXCIFiles.ReplaceWith(value.Value);
|
||||||
|
|
||||||
|
_processingApplication = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Processing
|
||||||
|
{
|
||||||
|
get => _cancellationTokenSource != null;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (value && !Processing)
|
||||||
|
{
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
}
|
||||||
|
else if (!value && Processing)
|
||||||
|
{
|
||||||
|
_cancellationTokenSource.Dispose();
|
||||||
|
_cancellationTokenSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessingChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Cancel
|
||||||
|
{
|
||||||
|
get => _cancellationTokenSource != null && _cancellationTokenSource.IsCancellationRequested;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value)
|
||||||
|
{
|
||||||
|
if (!Processing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_cancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessingChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Status
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Processing)
|
||||||
|
{
|
||||||
|
return _processingMode switch
|
||||||
|
{
|
||||||
|
ProcessingMode.Trimming => string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusTrimming], DisplayedXCIFiles.Count),
|
||||||
|
ProcessingMode.Untrimming => string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusUntrimming], DisplayedXCIFiles.Count),
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(Search) ?
|
||||||
|
string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusCount], SelectedXCIFiles.Count, AllXCIFiles.Count) :
|
||||||
|
string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusCountWithFilter], SelectedXCIFiles.Count, AllXCIFiles.Count, DisplayedXCIFiles.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Search
|
||||||
|
{
|
||||||
|
get => _search;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_search = value;
|
||||||
|
FilteringChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortField SortingField
|
||||||
|
{
|
||||||
|
get => _sortField;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_sortField = value;
|
||||||
|
SortingChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SortingFieldName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return SortingField switch
|
||||||
|
{
|
||||||
|
SortField.Name => LocaleManager.Instance[LocaleKeys.XCITrimmerSortName],
|
||||||
|
SortField.Saved => LocaleManager.Instance[LocaleKeys.XCITrimmerSortSaved],
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool SortingAscending
|
||||||
|
{
|
||||||
|
get => _sortAscending;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_sortAscending = value;
|
||||||
|
SortingChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSortedByName
|
||||||
|
{
|
||||||
|
get => _sortField == SortField.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSortedBySaved
|
||||||
|
{
|
||||||
|
get => _sortField == SortField.Saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvaloniaList<XCITrimmerFileModel> SelectedXCIFiles
|
||||||
|
{
|
||||||
|
get => _selectedXCIFiles;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_selectedXCIFiles = value;
|
||||||
|
SelectionChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvaloniaList<XCITrimmerFileModel> AllXCIFiles
|
||||||
|
{
|
||||||
|
get => _allXCIFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvaloniaList<XCITrimmerFileModel> DisplayedXCIFiles
|
||||||
|
{
|
||||||
|
get => _displayedXCIFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PotentialSavings
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerSavingsMb], AllXCIFiles.Sum(xci => xci.PotentialSavingsB / _bytesPerMB));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ActualSavings
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerSavingsMb], AllXCIFiles.Sum(xci => xci.CurrentSavingsB / _bytesPerMB));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<XCITrimmerFileModel> SelectedDisplayedXCIFiles
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return GetSelectedDisplayedXCIFiles().ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanTrim
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return !Processing && _selectedXCIFiles.Any(xci => xci.Trimmable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanUntrim
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return !Processing && _selectedXCIFiles.Any(xci => xci.Untrimmable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -268,6 +268,8 @@
|
|||||||
<MenuItem Header="{ext:Locale MenuBarToolsInstallFileTypes}" Click="InstallFileTypes_Click"/>
|
<MenuItem Header="{ext:Locale MenuBarToolsInstallFileTypes}" Click="InstallFileTypes_Click"/>
|
||||||
<MenuItem Header="{ext:Locale MenuBarToolsUninstallFileTypes}" Click="UninstallFileTypes_Click"/>
|
<MenuItem Header="{ext:Locale MenuBarToolsUninstallFileTypes}" Click="UninstallFileTypes_Click"/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="{ext:Locale MenuBarToolsXCITrimmer}" Click="OpenXCITrimmerWindow" Icon="{ext:Icon fa-solid fa-scissors}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarView}">
|
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarView}">
|
||||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarViewWindow}">
|
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarViewWindow}">
|
||||||
|
@ -203,6 +203,8 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||||||
await Updater.BeginParse(Window, true);
|
await Updater.BeginParse(Window, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void OpenXCITrimmerWindow(object sender, RoutedEventArgs e) => await XCITrimmerWindow.Show(ViewModel);
|
||||||
|
|
||||||
public async void OpenAboutWindow(object sender, RoutedEventArgs e) => await AboutWindow.Show();
|
public async void OpenAboutWindow(object sender, RoutedEventArgs e) => await AboutWindow.Show();
|
||||||
|
|
||||||
public void CloseWindow(object sender, RoutedEventArgs e) => Window.Close();
|
public void CloseWindow(object sender, RoutedEventArgs e) => Window.Close();
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
Margin="5"
|
Margin="5"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
IsVisible="{Binding EnableNonGameRunningControls}">
|
IsVisible="{Binding EnableNonGameRunningControls}">
|
||||||
<Grid Margin="0" ColumnDefinitions="Auto,Auto,*">
|
<Grid Margin="0" ColumnDefinitions="Auto,Auto,Auto,*">
|
||||||
<Button
|
<Button
|
||||||
Width="25"
|
Width="25"
|
||||||
Height="25"
|
Height="25"
|
||||||
@ -50,9 +50,18 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
IsVisible="{Binding EnableNonGameRunningControls}"
|
IsVisible="{Binding EnableNonGameRunningControls}"
|
||||||
Text="{ext:Locale StatusBarGamesLoaded}" />
|
Text="{ext:Locale StatusBarGamesLoaded}" />
|
||||||
|
<TextBlock
|
||||||
|
Name="StatusBarProgressStatus"
|
||||||
|
Grid.Column="2"
|
||||||
|
MinWidth="200"
|
||||||
|
Margin="10,0,5,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding StatusBarProgressStatusVisible}"
|
||||||
|
Text="{Binding StatusBarProgressStatusText}" />
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
Name="LoadProgressBar"
|
Name="LoadProgressBar"
|
||||||
Grid.Column="2"
|
Grid.Column="3"
|
||||||
|
MinWidth="200"
|
||||||
Height="6"
|
Height="6"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Foreground="{DynamicResource SystemAccentColorLight2}"
|
Foreground="{DynamicResource SystemAccentColorLight2}"
|
||||||
|
354
src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml
Normal file
354
src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Ryujinx.Ava.UI.Windows.XCITrimmerWindow"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||||
|
xmlns:models="clr-namespace:Ryujinx.UI.Common.Models;assembly=Ryujinx.UI.Common"
|
||||||
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||||
|
Width="700"
|
||||||
|
Height="600"
|
||||||
|
x:DataType="viewModels:XCITrimmerViewModel"
|
||||||
|
Focusable="True"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<helpers:XCITrimmerFileStatusConverter x:Key="StatusLabel" />
|
||||||
|
<helpers:XCITrimmerFileStatusDetailConverter x:Key="StatusDetailLabel" />
|
||||||
|
<helpers:XCITrimmerFileSpaceSavingsConverter x:Key="SpaceSavingsLabel" />
|
||||||
|
</UserControl.Resources>
|
||||||
|
<Grid Margin="20 0 20 0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Panel
|
||||||
|
Margin="10 10 10 10"
|
||||||
|
Grid.Row="0">
|
||||||
|
<TextBlock Text="{Binding Status}" />
|
||||||
|
</Panel>
|
||||||
|
<Panel
|
||||||
|
Margin="0 0 10 10"
|
||||||
|
IsVisible="{Binding !Processing}"
|
||||||
|
Grid.Row="1">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="0"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<TextBlock
|
||||||
|
Margin="10,0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{ext:Locale CommonSort}" />
|
||||||
|
<DropDownButton
|
||||||
|
Width="150"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="{Binding SortingFieldName}">
|
||||||
|
<DropDownButton.Flyout>
|
||||||
|
<Flyout Placement="Bottom">
|
||||||
|
<StackPanel
|
||||||
|
Margin="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Orientation="Vertical">
|
||||||
|
<StackPanel>
|
||||||
|
<RadioButton
|
||||||
|
Checked="Sort_Checked"
|
||||||
|
Content="{ext:Locale XCITrimmerSortName}"
|
||||||
|
GroupName="Sort"
|
||||||
|
IsChecked="{Binding IsSortedByName, Mode=OneTime}"
|
||||||
|
Tag="Name" />
|
||||||
|
<RadioButton
|
||||||
|
Checked="Sort_Checked"
|
||||||
|
Content="{ext:Locale XCITrimmerSortSaved}"
|
||||||
|
GroupName="Sort"
|
||||||
|
IsChecked="{Binding IsSortedBySaved, Mode=OneTime}"
|
||||||
|
Tag="Saved" />
|
||||||
|
</StackPanel>
|
||||||
|
<Border
|
||||||
|
Width="60"
|
||||||
|
Height="2"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
BorderBrush="White"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<Separator Height="0" HorizontalAlignment="Stretch" />
|
||||||
|
</Border>
|
||||||
|
<RadioButton
|
||||||
|
Checked="Order_Checked"
|
||||||
|
Content="{ext:Locale OrderAscending}"
|
||||||
|
GroupName="Order"
|
||||||
|
IsChecked="{Binding SortingAscending, Mode=OneTime}"
|
||||||
|
Tag="Ascending" />
|
||||||
|
<RadioButton
|
||||||
|
Checked="Order_Checked"
|
||||||
|
Content="{ext:Locale OrderDescending}"
|
||||||
|
GroupName="Order"
|
||||||
|
IsChecked="{Binding !SortingAscending, Mode=OneTime}"
|
||||||
|
Tag="Descending" />
|
||||||
|
</StackPanel>
|
||||||
|
</Flyout>
|
||||||
|
</DropDownButton.Flyout>
|
||||||
|
</DropDownButton>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBox
|
||||||
|
Grid.Column="1"
|
||||||
|
MinHeight="29"
|
||||||
|
MaxHeight="29"
|
||||||
|
Margin="5 0 5 0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Watermark="{ext:Locale Search}"
|
||||||
|
Text="{Binding Search}" />
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="2"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<Button
|
||||||
|
Name="SelectDisplayedButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding SelectDisplayed}">
|
||||||
|
<TextBlock Text="{ext:Locale XCITrimmerSelectDisplayed}" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="DeselectDisplayedButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding DeselectDisplayed}">
|
||||||
|
<TextBlock Text="{ext:Locale XCITrimmerDeselectDisplayed}" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Panel>
|
||||||
|
<Border
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="0 0 0 10"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="5"
|
||||||
|
Padding="2.5">
|
||||||
|
<ListBox
|
||||||
|
AutoScrollToSelectedItem="{Binding Processing}"
|
||||||
|
SelectedItem="{Binding ProcessingApplication.Value}"
|
||||||
|
SelectionMode="Multiple, Toggle"
|
||||||
|
Background="Transparent"
|
||||||
|
SelectionChanged="OnSelectionChanged"
|
||||||
|
SelectedItems="{Binding SelectedDisplayedXCIFiles, Mode=OneWay}"
|
||||||
|
ItemsSource="{Binding DisplayedXCIFiles}"
|
||||||
|
IsEnabled="{Binding !Processing}">
|
||||||
|
<ListBox.DataTemplates>
|
||||||
|
<DataTemplate
|
||||||
|
DataType="models:XCITrimmerFileModel">
|
||||||
|
<Panel Margin="10">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="65*" />
|
||||||
|
<ColumnDefinition Width="35*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="10 0 10 0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxLines="2"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
Text="{Binding Name}">
|
||||||
|
</TextBlock>
|
||||||
|
<Grid Grid.Column="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="45*" />
|
||||||
|
<ColumnDefinition Width="55*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ProgressBar
|
||||||
|
Height="10"
|
||||||
|
Margin="10 0 10 0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
CornerRadius="5"
|
||||||
|
IsVisible="{Binding $parent[UserControl].((viewModels:XCITrimmerViewModel)DataContext).Processing}"
|
||||||
|
Maximum="100"
|
||||||
|
Minimum="0"
|
||||||
|
Value="{Binding PercentageProgress}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="10 0 10 0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxLines="1"
|
||||||
|
Text="{Binding ., Converter={StaticResource StatusLabel}}">
|
||||||
|
<ToolTip.Tip>
|
||||||
|
<StackPanel
|
||||||
|
IsVisible="{Binding IsFailed}">
|
||||||
|
<TextBlock
|
||||||
|
Classes="h1"
|
||||||
|
Text="{ext:Locale XCITrimmerTitleStatusFailed}" />
|
||||||
|
<TextBlock
|
||||||
|
Text="{Binding ., Converter={StaticResource StatusDetailLabel}}"
|
||||||
|
MaxLines="5"
|
||||||
|
MaxWidth="200"
|
||||||
|
MaxHeight="100"
|
||||||
|
TextTrimming="None"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ToolTip.Tip>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="10 0 10 0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxLines="1"
|
||||||
|
Text="{Binding ., Converter={StaticResource SpaceSavingsLabel}}">>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Panel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.DataTemplates>
|
||||||
|
<ListBox.Styles>
|
||||||
|
<Style Selector="ListBoxItem">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Grid.Row="3"
|
||||||
|
Margin="0 0 0 10"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="5"
|
||||||
|
Padding="2.5">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
|
Classes="h1"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxLines="1"
|
||||||
|
Text="{ext:Locale XCITrimmerPotentialSavings}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="1"
|
||||||
|
Classes="h1"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxLines="1"
|
||||||
|
Text="{ext:Locale XCITrimmerActualSavings}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxLines="1"
|
||||||
|
Text="{Binding PotentialSavings}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxLines="1"
|
||||||
|
Text="{Binding ActualSavings}" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<Panel
|
||||||
|
Grid.Row="4"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="0"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="10"
|
||||||
|
HorizontalAlignment="Left">
|
||||||
|
<Button
|
||||||
|
Name="TrimButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Click="Trim"
|
||||||
|
IsEnabled="{Binding CanTrim}">
|
||||||
|
<TextBlock Text="Trim" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="UntrimButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Click="Untrim"
|
||||||
|
IsEnabled="{Binding CanUntrim}">
|
||||||
|
<TextBlock Text="Untrim" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="10"
|
||||||
|
HorizontalAlignment="Right">
|
||||||
|
<Button
|
||||||
|
Name="CancellingButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Click="Cancel"
|
||||||
|
IsEnabled="False">
|
||||||
|
<Button.IsVisible>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="Processing" />
|
||||||
|
<Binding Path="Cancel" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsVisible>
|
||||||
|
<TextBlock Text="{ext:Locale InputDialogCancelling}" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="CancelButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Click="Cancel">
|
||||||
|
<Button.IsVisible>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="Processing" />
|
||||||
|
<Binding Path="!Cancel" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsVisible>
|
||||||
|
<TextBlock Text="{ext:Locale InputDialogCancel}" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="CloseButton"
|
||||||
|
MinWidth="90"
|
||||||
|
Margin="5"
|
||||||
|
Click="Close"
|
||||||
|
IsVisible="{Binding !Processing}">
|
||||||
|
<TextBlock Text="{ext:Locale InputDialogClose}" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Panel>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
101
src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs
Normal file
101
src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Styling;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using Ryujinx.UI.Common.Models;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Windows
|
||||||
|
{
|
||||||
|
public partial class XCITrimmerWindow : UserControl
|
||||||
|
{
|
||||||
|
public XCITrimmerViewModel ViewModel;
|
||||||
|
|
||||||
|
public XCITrimmerWindow()
|
||||||
|
{
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public XCITrimmerWindow(MainWindowViewModel mainWindowViewModel)
|
||||||
|
{
|
||||||
|
DataContext = ViewModel = new XCITrimmerViewModel(mainWindowViewModel);
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task Show(MainWindowViewModel mainWindowViewModel)
|
||||||
|
{
|
||||||
|
ContentDialog contentDialog = new()
|
||||||
|
{
|
||||||
|
PrimaryButtonText = "",
|
||||||
|
SecondaryButtonText = "",
|
||||||
|
CloseButtonText = "",
|
||||||
|
Content = new XCITrimmerWindow(mainWindowViewModel),
|
||||||
|
Title = string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerWindowTitle]),
|
||||||
|
};
|
||||||
|
|
||||||
|
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||||
|
bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false));
|
||||||
|
|
||||||
|
contentDialog.Styles.Add(bottomBorder);
|
||||||
|
|
||||||
|
await contentDialog.ShowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Trim(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel.TrimSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Untrim(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel.UntrimSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
((ContentDialog)Parent).Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel(Object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel.Cancel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Sort_Checked(object sender, RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is RadioButton { Tag: string sortField })
|
||||||
|
ViewModel.SortingField = Enum.Parse<XCITrimmerViewModel.SortField>(sortField);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Order_Checked(object sender, RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is RadioButton { Tag: string sortOrder })
|
||||||
|
ViewModel.SortingAscending = sortOrder is "Ascending";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
foreach (var content in e.AddedItems)
|
||||||
|
{
|
||||||
|
if (content is XCITrimmerFileModel applicationData)
|
||||||
|
{
|
||||||
|
ViewModel.Select(applicationData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var content in e.RemovedItems)
|
||||||
|
{
|
||||||
|
if (content is XCITrimmerFileModel applicationData)
|
||||||
|
{
|
||||||
|
ViewModel.Deselect(applicationData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user