Compare commits
59 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
296c4a3d01 | ||
|
e7cf4e6eaf | ||
|
a1a4771ac1 | ||
|
2fd819613f | ||
|
ad6ff6ce99 | ||
|
dc30d94852 | ||
|
4f293f8cbe | ||
|
32a1cd83fd | ||
|
e3d0ccf8d5 | ||
|
c14844d12c | ||
|
7fea26e97e | ||
|
7b7f62c776 | ||
|
423dbc8888 | ||
|
6adf15e479 | ||
|
2747f12591 | ||
|
a47824f961 | ||
|
8474d52778 | ||
|
dd7a924596 | ||
|
a76eaf9a9a | ||
|
009e6bcd1b | ||
|
eb2cc159fa | ||
|
bb89e36fd8 | ||
|
de3134adbe | ||
|
36d53819a4 | ||
|
ae4324032a | ||
|
f449895e6d | ||
|
410be95ab6 | ||
|
cff9046fc7 | ||
|
86fd0643c2 | ||
|
43a83a401e | ||
|
f0e27a23a5 | ||
|
e68650237d | ||
|
1faff14e73 | ||
|
784cf9d594 | ||
|
64263c5218 | ||
|
065c4e520d | ||
|
139a930407 | ||
|
719dc97bbd | ||
|
41bba5310a | ||
|
8071c8c8c0 | ||
|
b402b4e7f6 | ||
|
93df366b2c | ||
|
cd3a15aea5 | ||
|
070136b3f7 | ||
|
08ab47c6c0 | ||
|
85faa9d8fa | ||
|
dca5b14493 | ||
|
4d2c8e2a44 | ||
|
8fa248ceb4 | ||
|
30862b5ffd | ||
|
9f57747c57 | ||
|
fe29a2ff6e | ||
|
e9a173e00c | ||
|
a11784fcbf | ||
|
fd36c8deca | ||
|
70638340b3 | ||
|
4b495f3333 | ||
|
934b5a64e5 | ||
|
cee667b491 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -170,3 +170,6 @@ launchSettings.json
|
|||||||
|
|
||||||
# NetCore Publishing Profiles
|
# NetCore Publishing Profiles
|
||||||
PublishProfiles/
|
PublishProfiles/
|
||||||
|
|
||||||
|
# Glade backup files
|
||||||
|
*.glade~
|
||||||
|
@@ -16,4 +16,10 @@
|
|||||||
</ContentWithTargetPath>
|
</ContentWithTargetPath>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||||
|
<_Parameter1>Ryujinx.Tests</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using ARMeilleure.IntermediateRepresentation;
|
using ARMeilleure.IntermediateRepresentation;
|
||||||
using System;
|
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace ARMeilleure.CodeGen.Arm64
|
namespace ARMeilleure.CodeGen.Arm64
|
||||||
@@ -32,9 +31,12 @@ namespace ARMeilleure.CodeGen.Arm64
|
|||||||
|
|
||||||
public static bool TryEncodeBitMask(Operand operand, out int immN, out int immS, out int immR)
|
public static bool TryEncodeBitMask(Operand operand, out int immN, out int immS, out int immR)
|
||||||
{
|
{
|
||||||
ulong value = operand.Value;
|
return TryEncodeBitMask(operand.Type, operand.Value, out immN, out immS, out immR);
|
||||||
|
}
|
||||||
|
|
||||||
if (operand.Type == OperandType.I32)
|
public static bool TryEncodeBitMask(OperandType type, ulong value, out int immN, out int immS, out int immR)
|
||||||
|
{
|
||||||
|
if (type == OperandType.I32)
|
||||||
{
|
{
|
||||||
value |= value << 32;
|
value |= value << 32;
|
||||||
}
|
}
|
||||||
@@ -50,7 +52,7 @@ namespace ARMeilleure.CodeGen.Arm64
|
|||||||
// Any value AND all ones will be equal itself, so it's effectively a no-op.
|
// Any value AND all ones will be equal itself, so it's effectively a no-op.
|
||||||
// Any value OR all ones will be equal all ones, so one can just use MOV.
|
// Any value OR all ones will be equal all ones, so one can just use MOV.
|
||||||
// Any value XOR all ones will be equal its inverse, so one can just use MVN.
|
// Any value XOR all ones will be equal its inverse, so one can just use MVN.
|
||||||
if (value == ulong.MaxValue)
|
if (value == 0 || value == ulong.MaxValue)
|
||||||
{
|
{
|
||||||
immN = 0;
|
immN = 0;
|
||||||
immS = 0;
|
immS = 0;
|
||||||
@@ -59,79 +61,18 @@ namespace ARMeilleure.CodeGen.Arm64
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int bitLength = CountSequence(value);
|
// Normalize value, rotating it such that the LSB is 1: Ensures we get a complete element that has not
|
||||||
|
// been cut-in-half across the word boundary.
|
||||||
|
int rotation = BitOperations.TrailingZeroCount(value & (value + 1));
|
||||||
|
ulong rotatedValue = ulong.RotateRight(value, rotation);
|
||||||
|
|
||||||
if ((value >> bitLength) != 0)
|
// Now that we have a complete element in the LSB with the LSB = 1, determine size and number of ones
|
||||||
{
|
// in element.
|
||||||
bitLength += CountSequence(value >> bitLength);
|
int elementSize = BitOperations.TrailingZeroCount(rotatedValue & (rotatedValue + 1));
|
||||||
}
|
int onesInElement = BitOperations.TrailingZeroCount(~rotatedValue);
|
||||||
|
|
||||||
int bitLengthLog2 = BitOperations.Log2((uint)bitLength);
|
// Check the value is repeating; also ensures element size is a power of two.
|
||||||
int bitLengthPow2 = 1 << bitLengthLog2;
|
if (ulong.RotateRight(value, elementSize) != value)
|
||||||
|
|
||||||
if (bitLengthPow2 < bitLength)
|
|
||||||
{
|
|
||||||
bitLengthLog2++;
|
|
||||||
bitLengthPow2 <<= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int selectedESize = 64;
|
|
||||||
int repetitions = 1;
|
|
||||||
int onesCount = BitOperations.PopCount(value);
|
|
||||||
|
|
||||||
if (bitLengthPow2 < 64 && (value >> bitLengthPow2) != 0)
|
|
||||||
{
|
|
||||||
for (int eSizeLog2 = bitLengthLog2; eSizeLog2 < 6; eSizeLog2++)
|
|
||||||
{
|
|
||||||
bool match = true;
|
|
||||||
int eSize = 1 << eSizeLog2;
|
|
||||||
ulong mask = (1UL << eSize) - 1;
|
|
||||||
ulong eValue = value & mask;
|
|
||||||
|
|
||||||
for (int e = 1; e < 64 / eSize; e++)
|
|
||||||
{
|
|
||||||
if (((value >> (e * eSize)) & mask) != eValue)
|
|
||||||
{
|
|
||||||
match = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match)
|
|
||||||
{
|
|
||||||
selectedESize = eSize;
|
|
||||||
repetitions = 64 / eSize;
|
|
||||||
onesCount = BitOperations.PopCount(eValue);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find rotation. We have two cases, one where the highest bit is 0
|
|
||||||
// and one where it is 1.
|
|
||||||
// If it's 1, we just need to count the number of 1 bits on the MSB to find the right rotation.
|
|
||||||
// If it's 0, we just need to count the number of 0 bits on the LSB to find the left rotation,
|
|
||||||
// then we can convert it to the right rotation shift by subtracting the value from the element size.
|
|
||||||
int rotation;
|
|
||||||
long vHigh = (long)(value << (64 - selectedESize));
|
|
||||||
if (vHigh < 0)
|
|
||||||
{
|
|
||||||
rotation = BitOperations.LeadingZeroCount(~(ulong)vHigh);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
rotation = (selectedESize - BitOperations.TrailingZeroCount(value)) & (selectedESize - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconstruct value and see if it matches. If not, we can't encode.
|
|
||||||
ulong reconstructed = onesCount == 64 ? ulong.MaxValue : RotateRight((1UL << onesCount) - 1, rotation, selectedESize);
|
|
||||||
|
|
||||||
for (int bit = 32; bit >= selectedESize; bit >>= 1)
|
|
||||||
{
|
|
||||||
reconstructed |= reconstructed << bit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reconstructed != value || onesCount == 0)
|
|
||||||
{
|
{
|
||||||
immN = 0;
|
immN = 0;
|
||||||
immS = 0;
|
immS = 0;
|
||||||
@@ -140,34 +81,11 @@ namespace ARMeilleure.CodeGen.Arm64
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
immR = rotation;
|
immN = (elementSize >> 6) & 1;
|
||||||
|
immS = (((~elementSize + 1) << 1) | (onesInElement - 1)) & 0x3f;
|
||||||
// immN indicates that there are no repetitions.
|
immR = (elementSize - rotation) & (elementSize - 1);
|
||||||
// The MSB of immS indicates the amount of repetitions, and the LSB the number of bits set.
|
|
||||||
if (repetitions == 1)
|
|
||||||
{
|
|
||||||
immN = 1;
|
|
||||||
immS = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
immN = 0;
|
|
||||||
immS = (0xf80 >> BitOperations.Log2((uint)repetitions)) & 0x3f;
|
|
||||||
}
|
|
||||||
|
|
||||||
immS |= onesCount - 1;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CountSequence(ulong value)
|
|
||||||
{
|
|
||||||
return BitOperations.TrailingZeroCount(value) + BitOperations.TrailingZeroCount(~value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ulong RotateRight(ulong bits, int shift, int size)
|
|
||||||
{
|
|
||||||
return (bits >> shift) | ((bits << (size - shift)) & (size == 64 ? ulong.MaxValue : (1UL << size) - 1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1303,7 +1303,15 @@ namespace ARMeilleure.CodeGen.Arm64
|
|||||||
|
|
||||||
private static void GenerateConstantCopy(CodeGenContext context, Operand dest, ulong value)
|
private static void GenerateConstantCopy(CodeGenContext context, Operand dest, ulong value)
|
||||||
{
|
{
|
||||||
if (value != 0)
|
if (value == 0)
|
||||||
|
{
|
||||||
|
context.Assembler.Mov(dest, Register(ZrRegister, dest.Type));
|
||||||
|
}
|
||||||
|
else if (CodeGenCommon.TryEncodeBitMask(dest.Type, value, out _, out _, out _))
|
||||||
|
{
|
||||||
|
context.Assembler.Orr(dest, Register(ZrRegister, dest.Type), Const(dest.Type, (long)value));
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
int hw = 0;
|
int hw = 0;
|
||||||
bool first = true;
|
bool first = true;
|
||||||
@@ -1328,10 +1336,6 @@ namespace ARMeilleure.CodeGen.Arm64
|
|||||||
value >>= 16;
|
value >>= 16;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Assembler.Mov(dest, Register(ZrRegister, dest.Type));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void GenerateAtomicCas(
|
private static void GenerateAtomicCas(
|
||||||
|
185
ARMeilleure/CodeGen/Arm64/HardwareCapabilities.cs
Normal file
185
ARMeilleure/CodeGen/Arm64/HardwareCapabilities.cs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Intrinsics.Arm;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
|
||||||
|
namespace ARMeilleure.CodeGen.Arm64
|
||||||
|
{
|
||||||
|
static partial class HardwareCapabilities
|
||||||
|
{
|
||||||
|
static HardwareCapabilities()
|
||||||
|
{
|
||||||
|
if (!ArmBase.Arm64.IsSupported)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
LinuxFeatureInfoHwCap = (LinuxFeatureFlagsHwCap)getauxval(AT_HWCAP);
|
||||||
|
LinuxFeatureInfoHwCap2 = (LinuxFeatureFlagsHwCap2)getauxval(AT_HWCAP2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _sysctlNames.Length; i++)
|
||||||
|
{
|
||||||
|
if (CheckSysctlName(_sysctlNames[i]))
|
||||||
|
{
|
||||||
|
MacOsFeatureInfo |= (MacOsFeatureFlags)(1 << i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Linux
|
||||||
|
|
||||||
|
private const ulong AT_HWCAP = 16;
|
||||||
|
private const ulong AT_HWCAP2 = 26;
|
||||||
|
|
||||||
|
[LibraryImport("libc", SetLastError = true)]
|
||||||
|
private static partial ulong getauxval(ulong type);
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum LinuxFeatureFlagsHwCap : ulong
|
||||||
|
{
|
||||||
|
Fp = 1 << 0,
|
||||||
|
Asimd = 1 << 1,
|
||||||
|
Evtstrm = 1 << 2,
|
||||||
|
Aes = 1 << 3,
|
||||||
|
Pmull = 1 << 4,
|
||||||
|
Sha1 = 1 << 5,
|
||||||
|
Sha2 = 1 << 6,
|
||||||
|
Crc32 = 1 << 7,
|
||||||
|
Atomics = 1 << 8,
|
||||||
|
FpHp = 1 << 9,
|
||||||
|
AsimdHp = 1 << 10,
|
||||||
|
CpuId = 1 << 11,
|
||||||
|
AsimdRdm = 1 << 12,
|
||||||
|
Jscvt = 1 << 13,
|
||||||
|
Fcma = 1 << 14,
|
||||||
|
Lrcpc = 1 << 15,
|
||||||
|
DcpOp = 1 << 16,
|
||||||
|
Sha3 = 1 << 17,
|
||||||
|
Sm3 = 1 << 18,
|
||||||
|
Sm4 = 1 << 19,
|
||||||
|
AsimdDp = 1 << 20,
|
||||||
|
Sha512 = 1 << 21,
|
||||||
|
Sve = 1 << 22,
|
||||||
|
AsimdFhm = 1 << 23,
|
||||||
|
Dit = 1 << 24,
|
||||||
|
Uscat = 1 << 25,
|
||||||
|
Ilrcpc = 1 << 26,
|
||||||
|
FlagM = 1 << 27,
|
||||||
|
Ssbs = 1 << 28,
|
||||||
|
Sb = 1 << 29,
|
||||||
|
Paca = 1 << 30,
|
||||||
|
Pacg = 1UL << 31
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum LinuxFeatureFlagsHwCap2 : ulong
|
||||||
|
{
|
||||||
|
Dcpodp = 1 << 0,
|
||||||
|
Sve2 = 1 << 1,
|
||||||
|
SveAes = 1 << 2,
|
||||||
|
SvePmull = 1 << 3,
|
||||||
|
SveBitperm = 1 << 4,
|
||||||
|
SveSha3 = 1 << 5,
|
||||||
|
SveSm4 = 1 << 6,
|
||||||
|
FlagM2 = 1 << 7,
|
||||||
|
Frint = 1 << 8,
|
||||||
|
SveI8mm = 1 << 9,
|
||||||
|
SveF32mm = 1 << 10,
|
||||||
|
SveF64mm = 1 << 11,
|
||||||
|
SveBf16 = 1 << 12,
|
||||||
|
I8mm = 1 << 13,
|
||||||
|
Bf16 = 1 << 14,
|
||||||
|
Dgh = 1 << 15,
|
||||||
|
Rng = 1 << 16,
|
||||||
|
Bti = 1 << 17,
|
||||||
|
Mte = 1 << 18,
|
||||||
|
Ecv = 1 << 19,
|
||||||
|
Afp = 1 << 20,
|
||||||
|
Rpres = 1 << 21,
|
||||||
|
Mte3 = 1 << 22,
|
||||||
|
Sme = 1 << 23,
|
||||||
|
Sme_i16i64 = 1 << 24,
|
||||||
|
Sme_f64f64 = 1 << 25,
|
||||||
|
Sme_i8i32 = 1 << 26,
|
||||||
|
Sme_f16f32 = 1 << 27,
|
||||||
|
Sme_b16f32 = 1 << 28,
|
||||||
|
Sme_f32f32 = 1 << 29,
|
||||||
|
Sme_fa64 = 1 << 30,
|
||||||
|
Wfxt = 1UL << 31,
|
||||||
|
Ebf16 = 1UL << 32,
|
||||||
|
Sve_Ebf16 = 1UL << 33,
|
||||||
|
Cssc = 1UL << 34,
|
||||||
|
Rprfm = 1UL << 35,
|
||||||
|
Sve2p1 = 1UL << 36
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LinuxFeatureFlagsHwCap LinuxFeatureInfoHwCap { get; } = 0;
|
||||||
|
public static LinuxFeatureFlagsHwCap2 LinuxFeatureInfoHwCap2 { get; } = 0;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region macOS
|
||||||
|
|
||||||
|
[LibraryImport("libSystem.dylib", SetLastError = true)]
|
||||||
|
private static unsafe partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, out int oldValue, ref ulong oldSize, IntPtr newValue, ulong newValueSize);
|
||||||
|
|
||||||
|
[SupportedOSPlatform("macos")]
|
||||||
|
private static bool CheckSysctlName(string name)
|
||||||
|
{
|
||||||
|
ulong size = sizeof(int);
|
||||||
|
if (sysctlbyname(name, out int val, ref size, IntPtr.Zero, 0) == 0 && size == sizeof(int))
|
||||||
|
{
|
||||||
|
return val != 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] _sysctlNames = new string[]
|
||||||
|
{
|
||||||
|
"hw.optional.floatingpoint",
|
||||||
|
"hw.optional.AdvSIMD",
|
||||||
|
"hw.optional.arm.FEAT_FP16",
|
||||||
|
"hw.optional.arm.FEAT_AES",
|
||||||
|
"hw.optional.arm.FEAT_PMULL",
|
||||||
|
"hw.optional.arm.FEAT_LSE",
|
||||||
|
"hw.optional.armv8_crc32",
|
||||||
|
"hw.optional.arm.FEAT_SHA1",
|
||||||
|
"hw.optional.arm.FEAT_SHA256"
|
||||||
|
};
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum MacOsFeatureFlags
|
||||||
|
{
|
||||||
|
Fp = 1 << 0,
|
||||||
|
AdvSimd = 1 << 1,
|
||||||
|
Fp16 = 1 << 2,
|
||||||
|
Aes = 1 << 3,
|
||||||
|
Pmull = 1 << 4,
|
||||||
|
Lse = 1 << 5,
|
||||||
|
Crc32 = 1 << 6,
|
||||||
|
Sha1 = 1 << 7,
|
||||||
|
Sha256 = 1 << 8
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MacOsFeatureFlags MacOsFeatureInfo { get; } = 0;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public static bool SupportsAdvSimd => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Asimd) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.AdvSimd);
|
||||||
|
public static bool SupportsAes => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Aes) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Aes);
|
||||||
|
public static bool SupportsPmull => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Pmull) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Pmull);
|
||||||
|
public static bool SupportsLse => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Atomics) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Lse);
|
||||||
|
public static bool SupportsCrc32 => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Crc32) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Crc32);
|
||||||
|
public static bool SupportsSha1 => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Sha1) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Sha1);
|
||||||
|
public static bool SupportsSha256 => LinuxFeatureInfoHwCap.HasFlag(LinuxFeatureFlagsHwCap.Sha2) || MacOsFeatureInfo.HasFlag(MacOsFeatureFlags.Sha256);
|
||||||
|
}
|
||||||
|
}
|
@@ -48,9 +48,21 @@ namespace ARMeilleure.CodeGen
|
|||||||
/// <returns>A delegate of type <typeparamref name="T"/> pointing to the mapped function</returns>
|
/// <returns>A delegate of type <typeparamref name="T"/> pointing to the mapped function</returns>
|
||||||
public T Map<T>()
|
public T Map<T>()
|
||||||
{
|
{
|
||||||
IntPtr codePtr = JitCache.Map(this);
|
return MapWithPointer<T>(out _);
|
||||||
|
}
|
||||||
|
|
||||||
return Marshal.GetDelegateForFunctionPointer<T>(codePtr);
|
/// <summary>
|
||||||
|
/// Maps the <see cref="CompiledFunction"/> onto the <see cref="JitCache"/> and returns a delegate of type
|
||||||
|
/// <typeparamref name="T"/> pointing to the mapped function.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of delegate</typeparam>
|
||||||
|
/// <param name="codePointer">Pointer to the function code in memory</param>
|
||||||
|
/// <returns>A delegate of type <typeparamref name="T"/> pointing to the mapped function</returns>
|
||||||
|
public T MapWithPointer<T>(out IntPtr codePointer)
|
||||||
|
{
|
||||||
|
codePointer = JitCache.Map(this);
|
||||||
|
|
||||||
|
return Marshal.GetDelegateForFunctionPointer<T>(codePointer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1339,7 +1339,7 @@ namespace ARMeilleure.Decoders
|
|||||||
|
|
||||||
private static void SetT32(string encoding, InstName name, InstEmitter emitter, MakeOp makeOp)
|
private static void SetT32(string encoding, InstName name, InstEmitter emitter, MakeOp makeOp)
|
||||||
{
|
{
|
||||||
string reversedEncoding = encoding.Substring(16) + encoding.Substring(0, 16);
|
string reversedEncoding = $"{encoding.AsSpan(16)}{encoding.AsSpan(0, 16)}";
|
||||||
MakeOp reversedMakeOp =
|
MakeOp reversedMakeOp =
|
||||||
(inst, address, opCode)
|
(inst, address, opCode)
|
||||||
=> makeOp(inst, address, (int)BitOperations.RotateRight((uint)opCode, 16));
|
=> makeOp(inst, address, (int)BitOperations.RotateRight((uint)opCode, 16));
|
||||||
@@ -1353,7 +1353,7 @@ namespace ARMeilleure.Decoders
|
|||||||
string thumbEncoding = encoding;
|
string thumbEncoding = encoding;
|
||||||
if (thumbEncoding.StartsWith("<<<<"))
|
if (thumbEncoding.StartsWith("<<<<"))
|
||||||
{
|
{
|
||||||
thumbEncoding = "1110" + thumbEncoding.Substring(4);
|
thumbEncoding = $"1110{thumbEncoding.AsSpan(4)}";
|
||||||
}
|
}
|
||||||
SetT32(thumbEncoding, name, emitter, makeOpT32);
|
SetT32(thumbEncoding, name, emitter, makeOpT32);
|
||||||
}
|
}
|
||||||
@@ -1365,19 +1365,19 @@ namespace ARMeilleure.Decoders
|
|||||||
string thumbEncoding = encoding;
|
string thumbEncoding = encoding;
|
||||||
if (thumbEncoding.StartsWith("11110100"))
|
if (thumbEncoding.StartsWith("11110100"))
|
||||||
{
|
{
|
||||||
thumbEncoding = "11111001" + encoding.Substring(8);
|
thumbEncoding = $"11111001{encoding.AsSpan(8)}";
|
||||||
}
|
}
|
||||||
else if (thumbEncoding.StartsWith("1111001x"))
|
else if (thumbEncoding.StartsWith("1111001x"))
|
||||||
{
|
{
|
||||||
thumbEncoding = "111x1111" + encoding.Substring(8);
|
thumbEncoding = $"111x1111{encoding.AsSpan(8)}";
|
||||||
}
|
}
|
||||||
else if (thumbEncoding.StartsWith("11110010"))
|
else if (thumbEncoding.StartsWith("11110010"))
|
||||||
{
|
{
|
||||||
thumbEncoding = "11101111" + encoding.Substring(8);
|
thumbEncoding = $"11101111{encoding.AsSpan(8)}";
|
||||||
}
|
}
|
||||||
else if (thumbEncoding.StartsWith("11110011"))
|
else if (thumbEncoding.StartsWith("11110011"))
|
||||||
{
|
{
|
||||||
thumbEncoding = "11111111" + encoding.Substring(8);
|
thumbEncoding = $"11111111{encoding.AsSpan(8)}";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@@ -2556,7 +2556,7 @@ namespace ARMeilleure.Instructions
|
|||||||
{
|
{
|
||||||
OpCodeSimdReg op = (OpCodeSimdReg)context.CurrOp;
|
OpCodeSimdReg op = (OpCodeSimdReg)context.CurrOp;
|
||||||
|
|
||||||
if (Optimizations.UseAdvSimd && false) // Not supported by all Arm CPUs.
|
if (Optimizations.UseArm64Pmull)
|
||||||
{
|
{
|
||||||
InstEmitSimdHelperArm64.EmitVectorBinaryOp(context, Intrinsic.Arm64PmullV);
|
InstEmitSimdHelperArm64.EmitVectorBinaryOp(context, Intrinsic.Arm64PmullV);
|
||||||
}
|
}
|
||||||
|
@@ -191,7 +191,7 @@ namespace ARMeilleure.Instructions
|
|||||||
{
|
{
|
||||||
TranslatedFunction function = Context.Translator.GetOrTranslate(address, GetContext().ExecutionMode);
|
TranslatedFunction function = Context.Translator.GetOrTranslate(address, GetContext().ExecutionMode);
|
||||||
|
|
||||||
return (ulong)function.FuncPtr.ToInt64();
|
return (ulong)function.FuncPointer.ToInt64();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void InvalidateCacheLine(ulong address)
|
public static void InvalidateCacheLine(ulong address)
|
||||||
|
@@ -4,5 +4,7 @@
|
|||||||
{
|
{
|
||||||
IJitMemoryBlock Allocate(ulong size);
|
IJitMemoryBlock Allocate(ulong size);
|
||||||
IJitMemoryBlock Reserve(ulong size);
|
IJitMemoryBlock Reserve(ulong size);
|
||||||
|
|
||||||
|
ulong GetPageSize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
using ARMeilleure.CodeGen.X86;
|
|
||||||
using System.Runtime.Intrinsics.Arm;
|
using System.Runtime.Intrinsics.Arm;
|
||||||
|
|
||||||
namespace ARMeilleure
|
namespace ARMeilleure
|
||||||
{
|
{
|
||||||
|
using Arm64HardwareCapabilities = ARMeilleure.CodeGen.Arm64.HardwareCapabilities;
|
||||||
|
using X86HardwareCapabilities = ARMeilleure.CodeGen.X86.HardwareCapabilities;
|
||||||
|
|
||||||
public static class Optimizations
|
public static class Optimizations
|
||||||
{
|
{
|
||||||
public static bool FastFP { get; set; } = true;
|
public static bool FastFP { get; set; } = true;
|
||||||
@@ -10,7 +12,8 @@ namespace ARMeilleure
|
|||||||
public static bool AllowLcqInFunctionTable { get; set; } = true;
|
public static bool AllowLcqInFunctionTable { get; set; } = true;
|
||||||
public static bool UseUnmanagedDispatchLoop { get; set; } = true;
|
public static bool UseUnmanagedDispatchLoop { get; set; } = true;
|
||||||
|
|
||||||
public static bool UseAdvSimdIfAvailable { get; set; } = true;
|
public static bool UseAdvSimdIfAvailable { get; set; } = true;
|
||||||
|
public static bool UseArm64PmullIfAvailable { get; set; } = true;
|
||||||
|
|
||||||
public static bool UseSseIfAvailable { get; set; } = true;
|
public static bool UseSseIfAvailable { get; set; } = true;
|
||||||
public static bool UseSse2IfAvailable { get; set; } = true;
|
public static bool UseSse2IfAvailable { get; set; } = true;
|
||||||
@@ -29,25 +32,26 @@ namespace ARMeilleure
|
|||||||
|
|
||||||
public static bool ForceLegacySse
|
public static bool ForceLegacySse
|
||||||
{
|
{
|
||||||
get => HardwareCapabilities.ForceLegacySse;
|
get => X86HardwareCapabilities.ForceLegacySse;
|
||||||
set => HardwareCapabilities.ForceLegacySse = value;
|
set => X86HardwareCapabilities.ForceLegacySse = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool UseAdvSimd => UseAdvSimdIfAvailable && AdvSimd.IsSupported;
|
internal static bool UseAdvSimd => UseAdvSimdIfAvailable && Arm64HardwareCapabilities.SupportsAdvSimd;
|
||||||
|
internal static bool UseArm64Pmull => UseArm64PmullIfAvailable && Arm64HardwareCapabilities.SupportsPmull;
|
||||||
|
|
||||||
internal static bool UseSse => UseSseIfAvailable && HardwareCapabilities.SupportsSse;
|
internal static bool UseSse => UseSseIfAvailable && X86HardwareCapabilities.SupportsSse;
|
||||||
internal static bool UseSse2 => UseSse2IfAvailable && HardwareCapabilities.SupportsSse2;
|
internal static bool UseSse2 => UseSse2IfAvailable && X86HardwareCapabilities.SupportsSse2;
|
||||||
internal static bool UseSse3 => UseSse3IfAvailable && HardwareCapabilities.SupportsSse3;
|
internal static bool UseSse3 => UseSse3IfAvailable && X86HardwareCapabilities.SupportsSse3;
|
||||||
internal static bool UseSsse3 => UseSsse3IfAvailable && HardwareCapabilities.SupportsSsse3;
|
internal static bool UseSsse3 => UseSsse3IfAvailable && X86HardwareCapabilities.SupportsSsse3;
|
||||||
internal static bool UseSse41 => UseSse41IfAvailable && HardwareCapabilities.SupportsSse41;
|
internal static bool UseSse41 => UseSse41IfAvailable && X86HardwareCapabilities.SupportsSse41;
|
||||||
internal static bool UseSse42 => UseSse42IfAvailable && HardwareCapabilities.SupportsSse42;
|
internal static bool UseSse42 => UseSse42IfAvailable && X86HardwareCapabilities.SupportsSse42;
|
||||||
internal static bool UsePopCnt => UsePopCntIfAvailable && HardwareCapabilities.SupportsPopcnt;
|
internal static bool UsePopCnt => UsePopCntIfAvailable && X86HardwareCapabilities.SupportsPopcnt;
|
||||||
internal static bool UseAvx => UseAvxIfAvailable && HardwareCapabilities.SupportsAvx && !ForceLegacySse;
|
internal static bool UseAvx => UseAvxIfAvailable && X86HardwareCapabilities.SupportsAvx && !ForceLegacySse;
|
||||||
internal static bool UseF16c => UseF16cIfAvailable && HardwareCapabilities.SupportsF16c;
|
internal static bool UseF16c => UseF16cIfAvailable && X86HardwareCapabilities.SupportsF16c;
|
||||||
internal static bool UseFma => UseFmaIfAvailable && HardwareCapabilities.SupportsFma;
|
internal static bool UseFma => UseFmaIfAvailable && X86HardwareCapabilities.SupportsFma;
|
||||||
internal static bool UseAesni => UseAesniIfAvailable && HardwareCapabilities.SupportsAesni;
|
internal static bool UseAesni => UseAesniIfAvailable && X86HardwareCapabilities.SupportsAesni;
|
||||||
internal static bool UsePclmulqdq => UsePclmulqdqIfAvailable && HardwareCapabilities.SupportsPclmulqdq;
|
internal static bool UsePclmulqdq => UsePclmulqdqIfAvailable && X86HardwareCapabilities.SupportsPclmulqdq;
|
||||||
internal static bool UseSha => UseShaIfAvailable && HardwareCapabilities.SupportsSha;
|
internal static bool UseSha => UseShaIfAvailable && X86HardwareCapabilities.SupportsSha;
|
||||||
internal static bool UseGfni => UseGfniIfAvailable && HardwareCapabilities.SupportsGfni;
|
internal static bool UseGfni => UseGfniIfAvailable && X86HardwareCapabilities.SupportsGfni;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -71,8 +71,8 @@ namespace ARMeilleure.Signal
|
|||||||
|
|
||||||
private const uint EXCEPTION_ACCESS_VIOLATION = 0xc0000005;
|
private const uint EXCEPTION_ACCESS_VIOLATION = 0xc0000005;
|
||||||
|
|
||||||
private static ulong _pageSize = GetPageSize();
|
private static ulong _pageSize;
|
||||||
private static ulong _pageMask = _pageSize - 1;
|
private static ulong _pageMask;
|
||||||
|
|
||||||
private static IntPtr _handlerConfig;
|
private static IntPtr _handlerConfig;
|
||||||
private static IntPtr _signalHandlerPtr;
|
private static IntPtr _signalHandlerPtr;
|
||||||
@@ -81,19 +81,6 @@ namespace ARMeilleure.Signal
|
|||||||
private static readonly object _lock = new object();
|
private static readonly object _lock = new object();
|
||||||
private static bool _initialized;
|
private static bool _initialized;
|
||||||
|
|
||||||
private static ulong GetPageSize()
|
|
||||||
{
|
|
||||||
// TODO: This needs to be based on the current memory manager configuration.
|
|
||||||
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
|
||||||
{
|
|
||||||
return 1UL << 14;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return 1UL << 12;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static NativeSignalHandler()
|
static NativeSignalHandler()
|
||||||
{
|
{
|
||||||
_handlerConfig = Marshal.AllocHGlobal(Unsafe.SizeOf<SignalHandlerConfig>());
|
_handlerConfig = Marshal.AllocHGlobal(Unsafe.SizeOf<SignalHandlerConfig>());
|
||||||
@@ -102,12 +89,12 @@ namespace ARMeilleure.Signal
|
|||||||
config = new SignalHandlerConfig();
|
config = new SignalHandlerConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void InitializeJitCache(IJitMemoryAllocator allocator)
|
public static void Initialize(IJitMemoryAllocator allocator)
|
||||||
{
|
{
|
||||||
JitCache.Initialize(allocator);
|
JitCache.Initialize(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void InitializeSignalHandler(Func<IntPtr, IntPtr, IntPtr> customSignalHandlerFactory = null)
|
public static void InitializeSignalHandler(ulong pageSize, Func<IntPtr, IntPtr, IntPtr> customSignalHandlerFactory = null)
|
||||||
{
|
{
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
|
|
||||||
@@ -115,16 +102,13 @@ namespace ARMeilleure.Signal
|
|||||||
{
|
{
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
|
|
||||||
|
_pageSize = pageSize;
|
||||||
|
_pageMask = pageSize - 1;
|
||||||
|
|
||||||
ref SignalHandlerConfig config = ref GetConfigRef();
|
ref SignalHandlerConfig config = ref GetConfigRef();
|
||||||
|
|
||||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
// Unix siginfo struct locations.
|
|
||||||
// NOTE: These are incredibly likely to be different between kernel version and architectures.
|
|
||||||
|
|
||||||
config.StructAddressOffset = OperatingSystem.IsMacOS() ? 24 : 16; // si_addr
|
|
||||||
config.StructWriteOffset = 8; // si_code
|
|
||||||
|
|
||||||
_signalHandlerPtr = Marshal.GetFunctionPointerForDelegate(GenerateUnixSignalHandler(_handlerConfig));
|
_signalHandlerPtr = Marshal.GetFunctionPointerForDelegate(GenerateUnixSignalHandler(_handlerConfig));
|
||||||
|
|
||||||
if (customSignalHandlerFactory != null)
|
if (customSignalHandlerFactory != null)
|
||||||
@@ -261,18 +245,88 @@ namespace ARMeilleure.Signal
|
|||||||
return context.Copy(inRegionLocal);
|
return context.Copy(inRegionLocal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Operand GenerateUnixFaultAddress(EmitterContext context, Operand sigInfoPtr)
|
||||||
|
{
|
||||||
|
ulong structAddressOffset = OperatingSystem.IsMacOS() ? 24ul : 16ul; // si_addr
|
||||||
|
return context.Load(OperandType.I64, context.Add(sigInfoPtr, Const(structAddressOffset)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Operand GenerateUnixWriteFlag(EmitterContext context, Operand ucontextPtr)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
const ulong mcontextOffset = 48; // uc_mcontext
|
||||||
|
Operand ctxPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(mcontextOffset)));
|
||||||
|
|
||||||
|
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
|
{
|
||||||
|
const ulong esrOffset = 8; // __es.__esr
|
||||||
|
Operand esr = context.Load(OperandType.I64, context.Add(ctxPtr, Const(esrOffset)));
|
||||||
|
return context.BitwiseAnd(esr, Const(0x40ul));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||||
|
{
|
||||||
|
const ulong errOffset = 4; // __es.__err
|
||||||
|
Operand err = context.Load(OperandType.I64, context.Add(ctxPtr, Const(errOffset)));
|
||||||
|
return context.BitwiseAnd(err, Const(2ul));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
|
{
|
||||||
|
Operand auxPtr = context.AllocateLocal(OperandType.I64);
|
||||||
|
|
||||||
|
Operand loopLabel = Label();
|
||||||
|
Operand successLabel = Label();
|
||||||
|
|
||||||
|
const ulong auxOffset = 464; // uc_mcontext.__reserved
|
||||||
|
const uint esrMagic = 0x45535201;
|
||||||
|
|
||||||
|
context.Copy(auxPtr, context.Add(ucontextPtr, Const(auxOffset)));
|
||||||
|
|
||||||
|
context.MarkLabel(loopLabel);
|
||||||
|
|
||||||
|
// _aarch64_ctx::magic
|
||||||
|
Operand magic = context.Load(OperandType.I32, auxPtr);
|
||||||
|
// _aarch64_ctx::size
|
||||||
|
Operand size = context.Load(OperandType.I32, context.Add(auxPtr, Const(4ul)));
|
||||||
|
|
||||||
|
context.BranchIf(successLabel, magic, Const(esrMagic), Comparison.Equal);
|
||||||
|
|
||||||
|
context.Copy(auxPtr, context.Add(auxPtr, context.ZeroExtend32(OperandType.I64, size)));
|
||||||
|
|
||||||
|
context.Branch(loopLabel);
|
||||||
|
|
||||||
|
context.MarkLabel(successLabel);
|
||||||
|
|
||||||
|
// esr_context::esr
|
||||||
|
Operand esr = context.Load(OperandType.I64, context.Add(auxPtr, Const(8ul)));
|
||||||
|
return context.BitwiseAnd(esr, Const(0x40ul));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||||
|
{
|
||||||
|
const int errOffset = 192; // uc_mcontext.gregs[REG_ERR]
|
||||||
|
Operand err = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(errOffset)));
|
||||||
|
return context.BitwiseAnd(err, Const(2ul));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PlatformNotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
private static UnixExceptionHandler GenerateUnixSignalHandler(IntPtr signalStructPtr)
|
private static UnixExceptionHandler GenerateUnixSignalHandler(IntPtr signalStructPtr)
|
||||||
{
|
{
|
||||||
EmitterContext context = new EmitterContext();
|
EmitterContext context = new EmitterContext();
|
||||||
|
|
||||||
// (int sig, SigInfo* sigInfo, void* ucontext)
|
// (int sig, SigInfo* sigInfo, void* ucontext)
|
||||||
Operand sigInfoPtr = context.LoadArgument(OperandType.I64, 1);
|
Operand sigInfoPtr = context.LoadArgument(OperandType.I64, 1);
|
||||||
|
Operand ucontextPtr = context.LoadArgument(OperandType.I64, 2);
|
||||||
|
|
||||||
Operand structAddressOffset = context.Load(OperandType.I64, Const((ulong)signalStructPtr + StructAddressOffset));
|
Operand faultAddress = GenerateUnixFaultAddress(context, sigInfoPtr);
|
||||||
Operand structWriteOffset = context.Load(OperandType.I64, Const((ulong)signalStructPtr + StructWriteOffset));
|
Operand writeFlag = GenerateUnixWriteFlag(context, ucontextPtr);
|
||||||
|
|
||||||
Operand faultAddress = context.Load(OperandType.I64, context.Add(sigInfoPtr, context.ZeroExtend32(OperandType.I64, structAddressOffset)));
|
|
||||||
Operand writeFlag = context.Load(OperandType.I64, context.Add(sigInfoPtr, context.ZeroExtend32(OperandType.I64, structWriteOffset)));
|
|
||||||
|
|
||||||
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
|
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
using ARMeilleure.CodeGen;
|
using ARMeilleure.CodeGen;
|
||||||
using ARMeilleure.CodeGen.Linking;
|
using ARMeilleure.CodeGen.Linking;
|
||||||
using ARMeilleure.CodeGen.Unwinding;
|
using ARMeilleure.CodeGen.Unwinding;
|
||||||
using ARMeilleure.CodeGen.X86;
|
|
||||||
using ARMeilleure.Common;
|
using ARMeilleure.Common;
|
||||||
using ARMeilleure.Memory;
|
using ARMeilleure.Memory;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
@@ -22,12 +21,15 @@ using static ARMeilleure.Translation.PTC.PtcFormatter;
|
|||||||
|
|
||||||
namespace ARMeilleure.Translation.PTC
|
namespace ARMeilleure.Translation.PTC
|
||||||
{
|
{
|
||||||
|
using Arm64HardwareCapabilities = ARMeilleure.CodeGen.Arm64.HardwareCapabilities;
|
||||||
|
using X86HardwareCapabilities = ARMeilleure.CodeGen.X86.HardwareCapabilities;
|
||||||
|
|
||||||
class Ptc : IPtcLoadState
|
class Ptc : IPtcLoadState
|
||||||
{
|
{
|
||||||
private const string OuterHeaderMagicString = "PTCohd\0\0";
|
private const string OuterHeaderMagicString = "PTCohd\0\0";
|
||||||
private const string InnerHeaderMagicString = "PTCihd\0\0";
|
private const string InnerHeaderMagicString = "PTCihd\0\0";
|
||||||
|
|
||||||
private const uint InternalVersion = 4114; //! To be incremented manually for each change to the ARMeilleure project.
|
private const uint InternalVersion = 4328; //! To be incremented manually for each change to the ARMeilleure project.
|
||||||
|
|
||||||
private const string ActualDir = "0";
|
private const string ActualDir = "0";
|
||||||
private const string BackupDir = "1";
|
private const string BackupDir = "1";
|
||||||
@@ -181,8 +183,8 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
|
|
||||||
private void PreLoad()
|
private void PreLoad()
|
||||||
{
|
{
|
||||||
string fileNameActual = string.Concat(CachePathActual, ".cache");
|
string fileNameActual = $"{CachePathActual}.cache";
|
||||||
string fileNameBackup = string.Concat(CachePathBackup, ".cache");
|
string fileNameBackup = $"{CachePathBackup}.cache";
|
||||||
|
|
||||||
FileInfo fileInfoActual = new FileInfo(fileNameActual);
|
FileInfo fileInfoActual = new FileInfo(fileNameActual);
|
||||||
FileInfo fileInfoBackup = new FileInfo(fileNameBackup);
|
FileInfo fileInfoBackup = new FileInfo(fileNameBackup);
|
||||||
@@ -259,6 +261,13 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (outerHeader.Architecture != (uint)RuntimeInformation.ProcessArchitecture)
|
||||||
|
{
|
||||||
|
InvalidateCompressedStream(compressedStream);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
IntPtr intPtr = IntPtr.Zero;
|
IntPtr intPtr = IntPtr.Zero;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -391,8 +400,8 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string fileNameActual = string.Concat(CachePathActual, ".cache");
|
string fileNameActual = $"{CachePathActual}.cache";
|
||||||
string fileNameBackup = string.Concat(CachePathBackup, ".cache");
|
string fileNameBackup = $"{CachePathBackup}.cache";
|
||||||
|
|
||||||
FileInfo fileInfoActual = new FileInfo(fileNameActual);
|
FileInfo fileInfoActual = new FileInfo(fileNameActual);
|
||||||
|
|
||||||
@@ -435,6 +444,7 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
outerHeader.FeatureInfo = GetFeatureInfo();
|
outerHeader.FeatureInfo = GetFeatureInfo();
|
||||||
outerHeader.MemoryManagerMode = GetMemoryManagerMode();
|
outerHeader.MemoryManagerMode = GetMemoryManagerMode();
|
||||||
outerHeader.OSPlatform = GetOSPlatform();
|
outerHeader.OSPlatform = GetOSPlatform();
|
||||||
|
outerHeader.Architecture = (uint)RuntimeInformation.ProcessArchitecture;
|
||||||
|
|
||||||
outerHeader.UncompressedStreamSize =
|
outerHeader.UncompressedStreamSize =
|
||||||
(long)Unsafe.SizeOf<InnerHeader>() +
|
(long)Unsafe.SizeOf<InnerHeader>() +
|
||||||
@@ -735,9 +745,9 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
bool highCq)
|
bool highCq)
|
||||||
{
|
{
|
||||||
var cFunc = new CompiledFunction(code, unwindInfo, RelocInfo.Empty);
|
var cFunc = new CompiledFunction(code, unwindInfo, RelocInfo.Empty);
|
||||||
var gFunc = cFunc.Map<GuestFunction>();
|
var gFunc = cFunc.MapWithPointer<GuestFunction>(out IntPtr gFuncPointer);
|
||||||
|
|
||||||
return new TranslatedFunction(gFunc, callCounter, guestSize, highCq);
|
return new TranslatedFunction(gFunc, gFuncPointer, callCounter, guestSize, highCq);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateInfo(InfoEntry infoEntry)
|
private void UpdateInfo(InfoEntry infoEntry)
|
||||||
@@ -952,11 +962,26 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
|
|
||||||
private static FeatureInfo GetFeatureInfo()
|
private static FeatureInfo GetFeatureInfo()
|
||||||
{
|
{
|
||||||
return new FeatureInfo(
|
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
(uint)HardwareCapabilities.FeatureInfo1Ecx,
|
{
|
||||||
(uint)HardwareCapabilities.FeatureInfo1Edx,
|
return new FeatureInfo(
|
||||||
(uint)HardwareCapabilities.FeatureInfo7Ebx,
|
(ulong)Arm64HardwareCapabilities.LinuxFeatureInfoHwCap,
|
||||||
(uint)HardwareCapabilities.FeatureInfo7Ecx);
|
(ulong)Arm64HardwareCapabilities.LinuxFeatureInfoHwCap2,
|
||||||
|
(ulong)Arm64HardwareCapabilities.MacOsFeatureInfo,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||||
|
{
|
||||||
|
return new FeatureInfo(
|
||||||
|
(ulong)X86HardwareCapabilities.FeatureInfo1Ecx,
|
||||||
|
(ulong)X86HardwareCapabilities.FeatureInfo1Edx,
|
||||||
|
(ulong)X86HardwareCapabilities.FeatureInfo7Ebx,
|
||||||
|
(ulong)X86HardwareCapabilities.FeatureInfo7Ecx);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new FeatureInfo(0, 0, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte GetMemoryManagerMode()
|
private byte GetMemoryManagerMode()
|
||||||
@@ -976,7 +1001,7 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
return osPlatform;
|
return osPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 58*/)]
|
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 78*/)]
|
||||||
private struct OuterHeader
|
private struct OuterHeader
|
||||||
{
|
{
|
||||||
public ulong Magic;
|
public ulong Magic;
|
||||||
@@ -987,6 +1012,7 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
public FeatureInfo FeatureInfo;
|
public FeatureInfo FeatureInfo;
|
||||||
public byte MemoryManagerMode;
|
public byte MemoryManagerMode;
|
||||||
public uint OSPlatform;
|
public uint OSPlatform;
|
||||||
|
public uint Architecture;
|
||||||
|
|
||||||
public long UncompressedStreamSize;
|
public long UncompressedStreamSize;
|
||||||
|
|
||||||
@@ -1007,8 +1033,8 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 16*/)]
|
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 32*/)]
|
||||||
private record struct FeatureInfo(uint FeatureInfo0, uint FeatureInfo1, uint FeatureInfo2, uint FeatureInfo3);
|
private record struct FeatureInfo(ulong FeatureInfo0, ulong FeatureInfo1, ulong FeatureInfo2, ulong FeatureInfo3);
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 128*/)]
|
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 128*/)]
|
||||||
private struct InnerHeader
|
private struct InnerHeader
|
||||||
|
@@ -125,8 +125,8 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
{
|
{
|
||||||
_lastHash = default;
|
_lastHash = default;
|
||||||
|
|
||||||
string fileNameActual = string.Concat(_ptc.CachePathActual, ".info");
|
string fileNameActual = $"{_ptc.CachePathActual}.info";
|
||||||
string fileNameBackup = string.Concat(_ptc.CachePathBackup, ".info");
|
string fileNameBackup = $"{_ptc.CachePathBackup}.info";
|
||||||
|
|
||||||
FileInfo fileInfoActual = new FileInfo(fileNameActual);
|
FileInfo fileInfoActual = new FileInfo(fileNameActual);
|
||||||
FileInfo fileInfoBackup = new FileInfo(fileNameBackup);
|
FileInfo fileInfoBackup = new FileInfo(fileNameBackup);
|
||||||
@@ -246,8 +246,8 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
{
|
{
|
||||||
_waitEvent.Reset();
|
_waitEvent.Reset();
|
||||||
|
|
||||||
string fileNameActual = string.Concat(_ptc.CachePathActual, ".info");
|
string fileNameActual = $"{_ptc.CachePathActual}.info";
|
||||||
string fileNameBackup = string.Concat(_ptc.CachePathBackup, ".info");
|
string fileNameBackup = $"{_ptc.CachePathBackup}.info";
|
||||||
|
|
||||||
FileInfo fileInfoActual = new FileInfo(fileNameActual);
|
FileInfo fileInfoActual = new FileInfo(fileNameActual);
|
||||||
|
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
using ARMeilleure.Common;
|
using ARMeilleure.Common;
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace ARMeilleure.Translation
|
namespace ARMeilleure.Translation
|
||||||
{
|
{
|
||||||
@@ -8,18 +7,18 @@ namespace ARMeilleure.Translation
|
|||||||
{
|
{
|
||||||
private readonly GuestFunction _func; // Ensure that this delegate will not be garbage collected.
|
private readonly GuestFunction _func; // Ensure that this delegate will not be garbage collected.
|
||||||
|
|
||||||
|
public IntPtr FuncPointer { get; }
|
||||||
public Counter<uint> CallCounter { get; }
|
public Counter<uint> CallCounter { get; }
|
||||||
public ulong GuestSize { get; }
|
public ulong GuestSize { get; }
|
||||||
public bool HighCq { get; }
|
public bool HighCq { get; }
|
||||||
public IntPtr FuncPtr { get; }
|
|
||||||
|
|
||||||
public TranslatedFunction(GuestFunction func, Counter<uint> callCounter, ulong guestSize, bool highCq)
|
public TranslatedFunction(GuestFunction func, IntPtr funcPointer, Counter<uint> callCounter, ulong guestSize, bool highCq)
|
||||||
{
|
{
|
||||||
_func = func;
|
_func = func;
|
||||||
|
FuncPointer = funcPointer;
|
||||||
CallCounter = callCounter;
|
CallCounter = callCounter;
|
||||||
GuestSize = guestSize;
|
GuestSize = guestSize;
|
||||||
HighCq = highCq;
|
HighCq = highCq;
|
||||||
FuncPtr = Marshal.GetFunctionPointerForDelegate(func);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong Execute(State.ExecutionContext context)
|
public ulong Execute(State.ExecutionContext context)
|
||||||
|
@@ -81,7 +81,7 @@ namespace ARMeilleure.Translation
|
|||||||
|
|
||||||
if (memory.Type.IsHostMapped())
|
if (memory.Type.IsHostMapped())
|
||||||
{
|
{
|
||||||
NativeSignalHandler.InitializeSignalHandler();
|
NativeSignalHandler.InitializeSignalHandler(allocator.GetPageSize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ namespace ARMeilleure.Translation
|
|||||||
|
|
||||||
if (oldFunc != func)
|
if (oldFunc != func)
|
||||||
{
|
{
|
||||||
JitCache.Unmap(func.FuncPtr);
|
JitCache.Unmap(func.FuncPointer);
|
||||||
func = oldFunc;
|
func = oldFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ namespace ARMeilleure.Translation
|
|||||||
{
|
{
|
||||||
if (FunctionTable.IsValid(guestAddress) && (Optimizations.AllowLcqInFunctionTable || func.HighCq))
|
if (FunctionTable.IsValid(guestAddress) && (Optimizations.AllowLcqInFunctionTable || func.HighCq))
|
||||||
{
|
{
|
||||||
Volatile.Write(ref FunctionTable.GetValue(guestAddress), (ulong)func.FuncPtr);
|
Volatile.Write(ref FunctionTable.GetValue(guestAddress), (ulong)func.FuncPointer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,11 +292,11 @@ namespace ARMeilleure.Translation
|
|||||||
_ptc.WriteCompiledFunction(address, funcSize, hash, highCq, compiledFunc);
|
_ptc.WriteCompiledFunction(address, funcSize, hash, highCq, compiledFunc);
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestFunction func = compiledFunc.Map<GuestFunction>();
|
GuestFunction func = compiledFunc.MapWithPointer<GuestFunction>(out IntPtr funcPointer);
|
||||||
|
|
||||||
Allocators.ResetAll();
|
Allocators.ResetAll();
|
||||||
|
|
||||||
return new TranslatedFunction(func, counter, funcSize, highCq);
|
return new TranslatedFunction(func, funcPointer, counter, funcSize, highCq);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BackgroundTranslate()
|
private void BackgroundTranslate()
|
||||||
@@ -537,7 +537,7 @@ namespace ARMeilleure.Translation
|
|||||||
|
|
||||||
foreach (var func in functions)
|
foreach (var func in functions)
|
||||||
{
|
{
|
||||||
JitCache.Unmap(func.FuncPtr);
|
JitCache.Unmap(func.FuncPointer);
|
||||||
|
|
||||||
func.CallCounter?.Dispose();
|
func.CallCounter?.Dispose();
|
||||||
}
|
}
|
||||||
@@ -546,7 +546,7 @@ namespace ARMeilleure.Translation
|
|||||||
|
|
||||||
while (_oldFuncs.TryDequeue(out var kv))
|
while (_oldFuncs.TryDequeue(out var kv))
|
||||||
{
|
{
|
||||||
JitCache.Unmap(kv.Value.FuncPtr);
|
JitCache.Unmap(kv.Value.FuncPointer);
|
||||||
|
|
||||||
kv.Value.CallCounter?.Dispose();
|
kv.Value.CallCounter?.Dispose();
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@
|
|||||||
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
|
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
|
||||||
<PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.2.0" />
|
<PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.2.0" />
|
||||||
<PackageVersion Include="LibHac" Version="0.17.0" />
|
<PackageVersion Include="LibHac" Version="0.17.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||||
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
|
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
|
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
|
||||||
<PackageVersion Include="SPB" Version="0.0.4-build28" />
|
<PackageVersion Include="SPB" Version="0.0.4-build28" />
|
||||||
<PackageVersion Include="System.Drawing.Common" Version="7.0.0" />
|
<PackageVersion Include="System.Drawing.Common" Version="7.0.0" />
|
||||||
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="6.25.1" />
|
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="6.26.0" />
|
||||||
<PackageVersion Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
|
<PackageVersion Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
|
||||||
<PackageVersion Include="System.Management" Version="7.0.0" />
|
<PackageVersion Include="System.Management" Version="7.0.0" />
|
||||||
<PackageVersion Include="System.Net.NameResolution" Version="4.3.0" />
|
<PackageVersion Include="System.Net.NameResolution" Version="4.3.0" />
|
||||||
|
@@ -75,9 +75,12 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
|||||||
return SampleFormat.PcmFloat;
|
return SampleFormat.PcmFloat;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement PCM24 conversion.
|
if (_realDriver.SupportsSampleFormat(SampleFormat.PcmInt24))
|
||||||
|
{
|
||||||
|
return SampleFormat.PcmInt24;
|
||||||
|
}
|
||||||
|
|
||||||
// If nothing is truly supported, attempt PCM8 at the cost of loosing quality.
|
// If nothing is truly supported, attempt PCM8 at the cost of losing quality.
|
||||||
if (_realDriver.SupportsSampleFormat(SampleFormat.PcmInt8))
|
if (_realDriver.SupportsSampleFormat(SampleFormat.PcmInt8))
|
||||||
{
|
{
|
||||||
return SampleFormat.PcmInt8;
|
return SampleFormat.PcmInt8;
|
||||||
|
@@ -58,10 +58,13 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
|||||||
switch (realSampleFormat)
|
switch (realSampleFormat)
|
||||||
{
|
{
|
||||||
case SampleFormat.PcmInt8:
|
case SampleFormat.PcmInt8:
|
||||||
PcmHelper.Convert(MemoryMarshal.Cast<byte, sbyte>(convertedSamples), samples);
|
PcmHelper.ConvertSampleToPcm8(MemoryMarshal.Cast<byte, sbyte>(convertedSamples), samples);
|
||||||
|
break;
|
||||||
|
case SampleFormat.PcmInt24:
|
||||||
|
PcmHelper.ConvertSampleToPcm24(convertedSamples, samples);
|
||||||
break;
|
break;
|
||||||
case SampleFormat.PcmInt32:
|
case SampleFormat.PcmInt32:
|
||||||
PcmHelper.Convert(MemoryMarshal.Cast<byte, int>(convertedSamples), samples);
|
PcmHelper.ConvertSampleToPcm32(MemoryMarshal.Cast<byte, int>(convertedSamples), samples);
|
||||||
break;
|
break;
|
||||||
case SampleFormat.PcmFloat:
|
case SampleFormat.PcmFloat:
|
||||||
PcmHelper.ConvertSampleToPcmFloat(MemoryMarshal.Cast<byte, float>(convertedSamples), samples);
|
PcmHelper.ConvertSampleToPcmFloat(MemoryMarshal.Cast<byte, float>(convertedSamples), samples);
|
||||||
|
@@ -40,6 +40,12 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
|||||||
info.InputBufferIndices[i] = (ushort)(bufferOffset + inputBufferOffset[i]);
|
info.InputBufferIndices[i] = (ushort)(bufferOffset + inputBufferOffset[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (info.BufferStates?.Length != (int)inputCount)
|
||||||
|
{
|
||||||
|
// Keep state if possible.
|
||||||
|
info.BufferStates = new UpsamplerBufferState[(int)inputCount];
|
||||||
|
}
|
||||||
|
|
||||||
UpsamplerInfo = info;
|
UpsamplerInfo = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +56,6 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
|||||||
|
|
||||||
public void Process(CommandList context)
|
public void Process(CommandList context)
|
||||||
{
|
{
|
||||||
float ratio = (float)InputSampleRate / Constants.TargetSampleRate;
|
|
||||||
|
|
||||||
uint bufferCount = Math.Min(BufferCount, UpsamplerInfo.SourceSampleCount);
|
uint bufferCount = Math.Min(BufferCount, UpsamplerInfo.SourceSampleCount);
|
||||||
|
|
||||||
for (int i = 0; i < bufferCount; i++)
|
for (int i = 0; i < bufferCount; i++)
|
||||||
@@ -59,9 +63,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
|||||||
Span<float> inputBuffer = context.GetBuffer(UpsamplerInfo.InputBufferIndices[i]);
|
Span<float> inputBuffer = context.GetBuffer(UpsamplerInfo.InputBufferIndices[i]);
|
||||||
Span<float> outputBuffer = GetBuffer(UpsamplerInfo.InputBufferIndices[i], (int)UpsamplerInfo.SampleCount);
|
Span<float> outputBuffer = GetBuffer(UpsamplerInfo.InputBufferIndices[i], (int)UpsamplerInfo.SampleCount);
|
||||||
|
|
||||||
float fraction = 0.0f;
|
UpsamplerHelper.Upsample(outputBuffer, inputBuffer, (int)UpsamplerInfo.SampleCount, (int)InputSampleCount, ref UpsamplerInfo.BufferStates[i]);
|
||||||
|
|
||||||
ResamplerHelper.ResampleForUpsampler(outputBuffer, inputBuffer, ratio, ref fraction, (int)(InputSampleCount / ratio));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -37,19 +37,32 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static TOutput ConvertSample<TInput, TOutput>(TInput value) where TInput: INumber<TInput>, IMinMaxValue<TInput> where TOutput : INumber<TOutput>, IMinMaxValue<TOutput>
|
public static void ConvertSampleToPcm8(Span<sbyte> output, ReadOnlySpan<short> input)
|
||||||
{
|
|
||||||
TInput conversionRate = TInput.CreateSaturating(TOutput.MaxValue / TOutput.CreateSaturating(TInput.MaxValue));
|
|
||||||
|
|
||||||
return TOutput.CreateSaturating(value * conversionRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static void Convert<TInput, TOutput>(Span<TOutput> output, ReadOnlySpan<TInput> input) where TInput : INumber<TInput>, IMinMaxValue<TInput> where TOutput : INumber<TOutput>, IMinMaxValue<TOutput>
|
|
||||||
{
|
{
|
||||||
for (int i = 0; i < input.Length; i++)
|
for (int i = 0; i < input.Length; i++)
|
||||||
{
|
{
|
||||||
output[i] = ConvertSample<TInput, TOutput>(input[i]);
|
// Output most significant byte
|
||||||
|
output[i] = (sbyte)(input[i] >> 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static void ConvertSampleToPcm24(Span<byte> output, ReadOnlySpan<short> input)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < input.Length; i++)
|
||||||
|
{
|
||||||
|
output[i * 3 + 2] = (byte)(input[i] >> 8);
|
||||||
|
output[i * 3 + 1] = (byte)(input[i] & 0xff);
|
||||||
|
output[i * 3 + 0] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static void ConvertSampleToPcm32(Span<int> output, ReadOnlySpan<short> input)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < input.Length; i++)
|
||||||
|
{
|
||||||
|
output[i] = ((int)input[i]) << 16;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -579,52 +579,5 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
|||||||
fraction -= (int)fraction;
|
fraction -= (int)fraction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static void ResampleForUpsampler(Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, float ratio, ref float fraction, int sampleCount)
|
|
||||||
{
|
|
||||||
// Currently a simple cubic interpolation, assuming duplicated values at edges.
|
|
||||||
// TODO: Discover and use algorithm that the switch uses.
|
|
||||||
|
|
||||||
int inputBufferIndex = 0;
|
|
||||||
int maxIndex = inputBuffer.Length - 1;
|
|
||||||
int cubicEnd = inputBuffer.Length - 3;
|
|
||||||
|
|
||||||
for (int i = 0; i < sampleCount; i++)
|
|
||||||
{
|
|
||||||
float s0, s1, s2, s3;
|
|
||||||
|
|
||||||
s1 = inputBuffer[inputBufferIndex];
|
|
||||||
|
|
||||||
if (inputBufferIndex == 0 || inputBufferIndex > cubicEnd)
|
|
||||||
{
|
|
||||||
// Clamp interplation values at the ends of the input buffer.
|
|
||||||
s0 = inputBuffer[Math.Max(0, inputBufferIndex - 1)];
|
|
||||||
s2 = inputBuffer[Math.Min(maxIndex, inputBufferIndex + 1)];
|
|
||||||
s3 = inputBuffer[Math.Min(maxIndex, inputBufferIndex + 2)];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
s0 = inputBuffer[inputBufferIndex - 1];
|
|
||||||
s2 = inputBuffer[inputBufferIndex + 1];
|
|
||||||
s3 = inputBuffer[inputBufferIndex + 2];
|
|
||||||
}
|
|
||||||
|
|
||||||
float a = s3 - s2 - s0 + s1;
|
|
||||||
float b = s0 - s1 - a;
|
|
||||||
float c = s2 - s0;
|
|
||||||
float d = s1;
|
|
||||||
|
|
||||||
float f2 = fraction * fraction;
|
|
||||||
float f3 = f2 * fraction;
|
|
||||||
|
|
||||||
outputBuffer[i] = a * f3 + b * f2 + c * fraction + d;
|
|
||||||
|
|
||||||
fraction += ratio;
|
|
||||||
inputBufferIndex += (int)MathF.Truncate(fraction);
|
|
||||||
|
|
||||||
fraction -= (int)fraction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
175
Ryujinx.Audio/Renderer/Dsp/UpsamplerHelper.cs
Normal file
175
Ryujinx.Audio/Renderer/Dsp/UpsamplerHelper.cs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
using Ryujinx.Audio.Renderer.Server.Upsampler;
|
||||||
|
using Ryujinx.Common.Memory;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.Audio.Renderer.Dsp
|
||||||
|
{
|
||||||
|
public class UpsamplerHelper
|
||||||
|
{
|
||||||
|
private const int HistoryLength = UpsamplerBufferState.HistoryLength;
|
||||||
|
private const int FilterBankLength = 20;
|
||||||
|
// Bank0 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
private const int Bank0CenterIndex = 9;
|
||||||
|
private static readonly Array20<float> Bank1 = PrecomputeFilterBank(1.0f / 6.0f);
|
||||||
|
private static readonly Array20<float> Bank2 = PrecomputeFilterBank(2.0f / 6.0f);
|
||||||
|
private static readonly Array20<float> Bank3 = PrecomputeFilterBank(3.0f / 6.0f);
|
||||||
|
private static readonly Array20<float> Bank4 = PrecomputeFilterBank(4.0f / 6.0f);
|
||||||
|
private static readonly Array20<float> Bank5 = PrecomputeFilterBank(5.0f / 6.0f);
|
||||||
|
|
||||||
|
private static Array20<float> PrecomputeFilterBank(float offset)
|
||||||
|
{
|
||||||
|
float Sinc(float x)
|
||||||
|
{
|
||||||
|
if (x == 0)
|
||||||
|
{
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
return (MathF.Sin(MathF.PI * x) / (MathF.PI * x));
|
||||||
|
}
|
||||||
|
|
||||||
|
float BlackmanWindow(float x)
|
||||||
|
{
|
||||||
|
const float a = 0.18f;
|
||||||
|
const float a0 = 0.5f - 0.5f * a;
|
||||||
|
const float a1 = -0.5f;
|
||||||
|
const float a2 = 0.5f * a;
|
||||||
|
return a0 + a1 * MathF.Cos(2 * MathF.PI * x) + a2 * MathF.Cos(4 * MathF.PI * x);
|
||||||
|
}
|
||||||
|
|
||||||
|
Array20<float> result = new Array20<float>();
|
||||||
|
|
||||||
|
for (int i = 0; i < FilterBankLength; i++)
|
||||||
|
{
|
||||||
|
float x = (Bank0CenterIndex - i) + offset;
|
||||||
|
result[i] = Sinc(x) * BlackmanWindow(x / FilterBankLength + 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polyphase upsampling algorithm
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static void Upsample(Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, int outputSampleCount, int inputSampleCount, ref UpsamplerBufferState state)
|
||||||
|
{
|
||||||
|
if (!state.Initialized)
|
||||||
|
{
|
||||||
|
state.Scale = inputSampleCount switch
|
||||||
|
{
|
||||||
|
40 => 6.0f,
|
||||||
|
80 => 3.0f,
|
||||||
|
160 => 1.5f,
|
||||||
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
|
};
|
||||||
|
state.Initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputSampleCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
float DoFilterBank(ref UpsamplerBufferState state, in Array20<float> bank)
|
||||||
|
{
|
||||||
|
float result = 0.0f;
|
||||||
|
|
||||||
|
Debug.Assert(state.History.Length == HistoryLength);
|
||||||
|
Debug.Assert(bank.Length == FilterBankLength);
|
||||||
|
for (int j = 0; j < FilterBankLength; j++)
|
||||||
|
{
|
||||||
|
result += bank[j] * state.History[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
void NextInput(ref UpsamplerBufferState state, float input)
|
||||||
|
{
|
||||||
|
state.History.AsSpan().Slice(1).CopyTo(state.History.AsSpan());
|
||||||
|
state.History[HistoryLength - 1] = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
int inputBufferIndex = 0;
|
||||||
|
|
||||||
|
switch (state.Scale)
|
||||||
|
{
|
||||||
|
case 6.0f:
|
||||||
|
for (int i = 0; i < outputSampleCount; i++)
|
||||||
|
{
|
||||||
|
switch (state.Phase)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
NextInput(ref state, inputBuffer[inputBufferIndex++]);
|
||||||
|
outputBuffer[i] = state.History[Bank0CenterIndex];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
outputBuffer[i] = DoFilterBank(ref state, Bank1);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
outputBuffer[i] = DoFilterBank(ref state, Bank2);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
outputBuffer[i] = DoFilterBank(ref state, Bank3);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
outputBuffer[i] = DoFilterBank(ref state, Bank4);
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
outputBuffer[i] = DoFilterBank(ref state, Bank5);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Phase = (state.Phase + 1) % 6;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3.0f:
|
||||||
|
for (int i = 0; i < outputSampleCount; i++)
|
||||||
|
{
|
||||||
|
switch (state.Phase)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
NextInput(ref state, inputBuffer[inputBufferIndex++]);
|
||||||
|
outputBuffer[i] = state.History[Bank0CenterIndex];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
outputBuffer[i] = DoFilterBank(ref state, Bank2);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
outputBuffer[i] = DoFilterBank(ref state, Bank4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Phase = (state.Phase + 1) % 3;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1.5f:
|
||||||
|
// Upsample by 3 then decimate by 2.
|
||||||
|
for (int i = 0; i < outputSampleCount; i++)
|
||||||
|
{
|
||||||
|
switch (state.Phase)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
NextInput(ref state, inputBuffer[inputBufferIndex++]);
|
||||||
|
outputBuffer[i] = state.History[Bank0CenterIndex];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
outputBuffer[i] = DoFilterBank(ref state, Bank4);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
NextInput(ref state, inputBuffer[inputBufferIndex++]);
|
||||||
|
outputBuffer[i] = DoFilterBank(ref state, Bank2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Phase = (state.Phase + 1) % 3;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
using Ryujinx.Common.Memory;
|
||||||
|
|
||||||
|
namespace Ryujinx.Audio.Renderer.Server.Upsampler
|
||||||
|
{
|
||||||
|
public struct UpsamplerBufferState
|
||||||
|
{
|
||||||
|
public const int HistoryLength = 20;
|
||||||
|
|
||||||
|
public float Scale;
|
||||||
|
public Array20<float> History;
|
||||||
|
public bool Initialized;
|
||||||
|
public int Phase;
|
||||||
|
}
|
||||||
|
}
|
@@ -37,6 +37,11 @@ namespace Ryujinx.Audio.Renderer.Server.Upsampler
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort[] InputBufferIndices;
|
public ushort[] InputBufferIndices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// State of each input buffer index kept across invocations of the upsampler.
|
||||||
|
/// </summary>
|
||||||
|
public UpsamplerBufferState[] BufferStates;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="UpsamplerState"/>.
|
/// Create a new <see cref="UpsamplerState"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@@ -12,9 +12,9 @@ using Ryujinx.Audio.Integration;
|
|||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Input;
|
using Ryujinx.Ava.Input;
|
||||||
using Ryujinx.Ava.UI.Controls;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using Ryujinx.Ava.UI.Renderer;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.Ava.UI.Windows;
|
using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
@@ -46,6 +46,7 @@ using System.Diagnostics;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
||||||
using Image = SixLabors.ImageSharp.Image;
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
using InputManager = Ryujinx.Input.HLE.InputManager;
|
using InputManager = Ryujinx.Input.HLE.InputManager;
|
||||||
using Key = Ryujinx.Input.Key;
|
using Key = Ryujinx.Input.Key;
|
||||||
@@ -58,12 +59,14 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
internal class AppHost
|
internal class AppHost
|
||||||
{
|
{
|
||||||
private const int CursorHideIdleTime = 8; // Hide Cursor seconds.
|
private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
|
||||||
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
|
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
|
||||||
private const int TargetFps = 60;
|
private const int TargetFps = 60;
|
||||||
private const float VolumeDelta = 0.05f;
|
private const float VolumeDelta = 0.05f;
|
||||||
|
|
||||||
private static readonly Cursor InvisibleCursor = new(StandardCursorType.None);
|
private static readonly Cursor InvisibleCursor = new(StandardCursorType.None);
|
||||||
|
private readonly IntPtr InvisibleCursorWin;
|
||||||
|
private readonly IntPtr DefaultCursorWin;
|
||||||
|
|
||||||
private readonly long _ticksPerFrame;
|
private readonly long _ticksPerFrame;
|
||||||
private readonly Stopwatch _chrono;
|
private readonly Stopwatch _chrono;
|
||||||
@@ -76,12 +79,12 @@ namespace Ryujinx.Ava
|
|||||||
private readonly MainWindowViewModel _viewModel;
|
private readonly MainWindowViewModel _viewModel;
|
||||||
private readonly IKeyboard _keyboardInterface;
|
private readonly IKeyboard _keyboardInterface;
|
||||||
private readonly TopLevel _topLevel;
|
private readonly TopLevel _topLevel;
|
||||||
|
public RendererHost _rendererHost;
|
||||||
|
|
||||||
private readonly GraphicsDebugLevel _glLogLevel;
|
private readonly GraphicsDebugLevel _glLogLevel;
|
||||||
private float _newVolume;
|
private float _newVolume;
|
||||||
private KeyboardHotkeyState _prevHotkeyState;
|
private KeyboardHotkeyState _prevHotkeyState;
|
||||||
|
|
||||||
private bool _hideCursorOnIdle;
|
|
||||||
private long _lastCursorMoveTime;
|
private long _lastCursorMoveTime;
|
||||||
private bool _isCursorInRenderer;
|
private bool _isCursorInRenderer;
|
||||||
|
|
||||||
@@ -102,7 +105,6 @@ namespace Ryujinx.Ava
|
|||||||
public event EventHandler AppExit;
|
public event EventHandler AppExit;
|
||||||
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
|
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
|
||||||
|
|
||||||
public RendererHost Renderer { get; }
|
|
||||||
public VirtualFileSystem VirtualFileSystem { get; }
|
public VirtualFileSystem VirtualFileSystem { get; }
|
||||||
public ContentManager ContentManager { get; }
|
public ContentManager ContentManager { get; }
|
||||||
public NpadManager NpadManager { get; }
|
public NpadManager NpadManager { get; }
|
||||||
@@ -114,7 +116,6 @@ namespace Ryujinx.Ava
|
|||||||
public string ApplicationPath { get; private set; }
|
public string ApplicationPath { get; private set; }
|
||||||
public bool ScreenshotRequested { get; set; }
|
public bool ScreenshotRequested { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public AppHost(
|
public AppHost(
|
||||||
RendererHost renderer,
|
RendererHost renderer,
|
||||||
InputManager inputManager,
|
InputManager inputManager,
|
||||||
@@ -131,7 +132,6 @@ namespace Ryujinx.Ava
|
|||||||
_accountManager = accountManager;
|
_accountManager = accountManager;
|
||||||
_userChannelPersistence = userChannelPersistence;
|
_userChannelPersistence = userChannelPersistence;
|
||||||
_renderingThread = new Thread(RenderLoop, 1 * 1024 * 1024) { Name = "GUI.RenderThread" };
|
_renderingThread = new Thread(RenderLoop, 1 * 1024 * 1024) { Name = "GUI.RenderThread" };
|
||||||
_hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
|
|
||||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||||
_glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
|
_glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
|
||||||
_topLevel = topLevel;
|
_topLevel = topLevel;
|
||||||
@@ -142,11 +142,12 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
NpadManager = _inputManager.CreateNpadManager();
|
NpadManager = _inputManager.CreateNpadManager();
|
||||||
TouchScreenManager = _inputManager.CreateTouchScreenManager();
|
TouchScreenManager = _inputManager.CreateTouchScreenManager();
|
||||||
Renderer = renderer;
|
|
||||||
ApplicationPath = applicationPath;
|
ApplicationPath = applicationPath;
|
||||||
VirtualFileSystem = virtualFileSystem;
|
VirtualFileSystem = virtualFileSystem;
|
||||||
ContentManager = contentManager;
|
ContentManager = contentManager;
|
||||||
|
|
||||||
|
_rendererHost = renderer;
|
||||||
|
|
||||||
_chrono = new Stopwatch();
|
_chrono = new Stopwatch();
|
||||||
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
|
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
|
||||||
|
|
||||||
@@ -159,9 +160,14 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
|
ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
|
||||||
|
|
||||||
_topLevel.PointerLeave += TopLevel_PointerLeave;
|
|
||||||
_topLevel.PointerMoved += TopLevel_PointerMoved;
|
_topLevel.PointerMoved += TopLevel_PointerMoved;
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
InvisibleCursorWin = CreateEmptyCursor();
|
||||||
|
DefaultCursorWin = CreateArrowCursor();
|
||||||
|
}
|
||||||
|
|
||||||
ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
|
ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
|
||||||
ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
|
ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
|
||||||
ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
|
ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
|
||||||
@@ -172,20 +178,47 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
private void TopLevel_PointerMoved(object sender, PointerEventArgs e)
|
private void TopLevel_PointerMoved(object sender, PointerEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is Control visual)
|
if (sender is MainWindow window)
|
||||||
{
|
{
|
||||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
var point = e.GetCurrentPoint(visual).Position;
|
if (_rendererHost.EmbeddedWindow.TransformedBounds != null)
|
||||||
|
{
|
||||||
|
var point = e.GetCurrentPoint(window).Position;
|
||||||
|
var bounds = _rendererHost.EmbeddedWindow.TransformedBounds.Value.Clip;
|
||||||
|
|
||||||
_isCursorInRenderer = Equals(visual.InputHitTest(point), Renderer);
|
_isCursorInRenderer = point.X >= bounds.X &&
|
||||||
|
point.X <= bounds.Width + bounds.X &&
|
||||||
|
point.Y >= bounds.Y &&
|
||||||
|
point.Y <= bounds.Height + bounds.Y;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TopLevel_PointerLeave(object sender, PointerEventArgs e)
|
private void ShowCursor()
|
||||||
{
|
{
|
||||||
_isCursorInRenderer = false;
|
Dispatcher.UIThread.Post(() =>
|
||||||
_viewModel.Cursor = Cursor.Default;
|
{
|
||||||
|
_viewModel.Cursor = Cursor.Default;
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
SetCursor(DefaultCursorWin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideCursor()
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
_viewModel.Cursor = InvisibleCursor;
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
SetCursor(InvisibleCursorWin);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetRendererWindowSize(Size size)
|
private void SetRendererWindowSize(Size size)
|
||||||
@@ -198,7 +231,7 @@ namespace Ryujinx.Ava
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e)
|
private void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e)
|
||||||
{
|
{
|
||||||
if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0)
|
if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0)
|
||||||
{
|
{
|
||||||
@@ -207,7 +240,7 @@ namespace Ryujinx.Ava
|
|||||||
lock (_lockObject)
|
lock (_lockObject)
|
||||||
{
|
{
|
||||||
DateTime currentTime = DateTime.Now;
|
DateTime currentTime = DateTime.Now;
|
||||||
string filename = $"ryujinx_capture_{currentTime}-{currentTime:D2}-{currentTime:D2}_{currentTime:D2}-{currentTime:D2}-{currentTime:D2}.png";
|
string filename = $"ryujinx_capture_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png";
|
||||||
|
|
||||||
string directory = AppDataManager.Mode switch
|
string directory = AppDataManager.Mode switch
|
||||||
{
|
{
|
||||||
@@ -284,7 +317,7 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
_viewModel.SetUIProgressHandlers(Device);
|
_viewModel.SetUIProgressHandlers(Device);
|
||||||
|
|
||||||
Renderer.SizeChanged += Window_SizeChanged;
|
_rendererHost.SizeChanged += Window_SizeChanged;
|
||||||
|
|
||||||
_isActive = true;
|
_isActive = true;
|
||||||
|
|
||||||
@@ -380,7 +413,6 @@ namespace Ryujinx.Ava
|
|||||||
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
|
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
|
||||||
ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
|
ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
|
||||||
|
|
||||||
_topLevel.PointerLeave -= TopLevel_PointerLeave;
|
|
||||||
_topLevel.PointerMoved -= TopLevel_PointerMoved;
|
_topLevel.PointerMoved -= TopLevel_PointerMoved;
|
||||||
|
|
||||||
_gpuCancellationTokenSource.Cancel();
|
_gpuCancellationTokenSource.Cancel();
|
||||||
@@ -397,28 +429,19 @@ namespace Ryujinx.Ava
|
|||||||
_windowsMultimediaTimerResolution = null;
|
_windowsMultimediaTimerResolution = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Renderer?.MakeCurrent();
|
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent();
|
||||||
|
|
||||||
Device.DisposeGpu();
|
Device.DisposeGpu();
|
||||||
|
|
||||||
Renderer?.MakeCurrent(null);
|
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state)
|
private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.InvokeAsync(delegate
|
if (state.NewValue)
|
||||||
{
|
{
|
||||||
_hideCursorOnIdle = state.NewValue;
|
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||||
|
}
|
||||||
if (_hideCursorOnIdle)
|
|
||||||
{
|
|
||||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_viewModel.Cursor = Cursor.Default;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> LoadGuestApplication()
|
public async Task<bool> LoadGuestApplication()
|
||||||
@@ -439,8 +462,7 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||||
LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
|
LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
|
||||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedMessage],
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedMessage, firmwareVersion.VersionString),
|
||||||
firmwareVersion.VersionString),
|
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
"");
|
"");
|
||||||
@@ -470,10 +492,8 @@ namespace Ryujinx.Ava
|
|||||||
_viewModel.RefreshFirmwareStatus();
|
_viewModel.RefreshFirmwareStatus();
|
||||||
|
|
||||||
await ContentDialogHelper.CreateInfoDialog(
|
await ContentDialogHelper.CreateInfoDialog(
|
||||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstalledMessage],
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstalledMessage, firmwareVersion.VersionString),
|
||||||
firmwareVersion.VersionString),
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage, firmwareVersion.VersionString),
|
||||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage],
|
|
||||||
firmwareVersion.VersionString),
|
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||||
"",
|
"",
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
||||||
@@ -611,11 +631,12 @@ namespace Ryujinx.Ava
|
|||||||
// Initialize Renderer.
|
// Initialize Renderer.
|
||||||
IRenderer renderer;
|
IRenderer renderer;
|
||||||
|
|
||||||
if (Renderer.IsVulkan)
|
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
|
||||||
{
|
{
|
||||||
string preferredGpu = ConfigurationState.Instance.Graphics.PreferredGpu.Value;
|
renderer = new VulkanRenderer(
|
||||||
|
(_rendererHost.EmbeddedWindow as EmbeddedWindowVulkan).CreateSurface,
|
||||||
renderer = new VulkanRenderer(Renderer.CreateVulkanSurface, VulkanHelper.GetRequiredInstanceExtensions, preferredGpu);
|
VulkanHelper.GetRequiredInstanceExtensions,
|
||||||
|
ConfigurationState.Instance.Graphics.PreferredGpu.Value);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -763,14 +784,12 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
_renderer.ScreenCaptured += Renderer_ScreenCaptured;
|
_renderer.ScreenCaptured += Renderer_ScreenCaptured;
|
||||||
|
|
||||||
(_renderer as OpenGLRenderer)?.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(Renderer.GetContext()));
|
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.InitializeBackgroundContext(_renderer);
|
||||||
|
|
||||||
Renderer.MakeCurrent();
|
|
||||||
|
|
||||||
Device.Gpu.Renderer.Initialize(_glLogLevel);
|
Device.Gpu.Renderer.Initialize(_glLogLevel);
|
||||||
|
|
||||||
Width = (int)Renderer.Bounds.Width;
|
Width = (int)_rendererHost.Bounds.Width;
|
||||||
Height = (int)Renderer.Bounds.Height;
|
Height = (int)_rendererHost.Bounds.Height;
|
||||||
|
|
||||||
_renderer.Window.SetSize((int)(Width * _topLevel.PlatformImpl.RenderScaling), (int)(Height * _topLevel.PlatformImpl.RenderScaling));
|
_renderer.Window.SetSize((int)(Width * _topLevel.PlatformImpl.RenderScaling), (int)(Height * _topLevel.PlatformImpl.RenderScaling));
|
||||||
|
|
||||||
@@ -803,7 +822,7 @@ namespace Ryujinx.Ava
|
|||||||
_viewModel.SwitchToRenderer(false);
|
_viewModel.SwitchToRenderer(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Device.PresentFrame(() => Renderer?.SwapBuffers());
|
Device.PresentFrame(() => (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_ticks >= _ticksPerFrame)
|
if (_ticks >= _ticksPerFrame)
|
||||||
@@ -813,7 +832,7 @@ namespace Ryujinx.Ava
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Renderer?.MakeCurrent(null);
|
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateStatus()
|
public void UpdateStatus()
|
||||||
@@ -829,7 +848,7 @@ namespace Ryujinx.Ava
|
|||||||
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
|
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
|
||||||
Device.EnableDeviceVsync,
|
Device.EnableDeviceVsync,
|
||||||
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
|
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
|
||||||
Renderer.IsVulkan ? "Vulkan" : "OpenGL",
|
ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan ? "Vulkan" : "OpenGL",
|
||||||
dockedMode,
|
dockedMode,
|
||||||
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
|
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
|
||||||
LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
|
LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
|
||||||
@@ -860,29 +879,6 @@ namespace Ryujinx.Ava
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleScreenState()
|
|
||||||
{
|
|
||||||
if (ConfigurationState.Instance.Hid.EnableMouse)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
_viewModel.Cursor = _isCursorInRenderer ? InvisibleCursor : Cursor.Default;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_hideCursorOnIdle)
|
|
||||||
{
|
|
||||||
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
_viewModel.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool UpdateFrame()
|
private bool UpdateFrame()
|
||||||
{
|
{
|
||||||
if (!_isActive)
|
if (!_isActive)
|
||||||
@@ -890,23 +886,44 @@ namespace Ryujinx.Ava
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
||||||
|
|
||||||
if (_viewModel.IsActive)
|
if (_viewModel.IsActive)
|
||||||
{
|
{
|
||||||
|
if (ConfigurationState.Instance.Hid.EnableMouse)
|
||||||
|
{
|
||||||
|
if (_isCursorInRenderer)
|
||||||
|
{
|
||||||
|
HideCursor();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ShowCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ConfigurationState.Instance.HideCursorOnIdle)
|
||||||
|
{
|
||||||
|
if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency)
|
||||||
|
{
|
||||||
|
HideCursor();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ShowCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
HandleScreenState();
|
|
||||||
|
|
||||||
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
|
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
|
||||||
{
|
{
|
||||||
Device.Application.DiskCacheLoadState?.Cancel();
|
Device.Application.DiskCacheLoadState?.Cancel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
|
||||||
|
|
||||||
if (_viewModel.IsActive)
|
|
||||||
{
|
|
||||||
KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
|
KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
|
||||||
|
|
||||||
if (currentHotkeyState != _prevHotkeyState)
|
if (currentHotkeyState != _prevHotkeyState)
|
||||||
|
@@ -260,7 +260,7 @@
|
|||||||
"UserProfilesChangeProfileImage": "Profilbild ändern",
|
"UserProfilesChangeProfileImage": "Profilbild ändern",
|
||||||
"UserProfilesAvailableUserProfiles": "Verfügbare Profile:",
|
"UserProfilesAvailableUserProfiles": "Verfügbare Profile:",
|
||||||
"UserProfilesAddNewProfile": "Neues Profil",
|
"UserProfilesAddNewProfile": "Neues Profil",
|
||||||
"UserProfilesDeleteSelectedProfile": "Profil löschen",
|
"UserProfilesDelete": "Löschen",
|
||||||
"UserProfilesClose": "Schließen",
|
"UserProfilesClose": "Schließen",
|
||||||
"ProfileImageSelectionTitle": "Auswahl des Profilbildes",
|
"ProfileImageSelectionTitle": "Auswahl des Profilbildes",
|
||||||
"ProfileImageSelectionHeader": "Wähle ein Profilbild aus",
|
"ProfileImageSelectionHeader": "Wähle ein Profilbild aus",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "Möchtest du das ausgewählte Profil löschen?",
|
"DialogUserProfileDeletionConfirmMessage": "Möchtest du das ausgewählte Profil löschen?",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "Die aktuellen Controller-Einstellungen wurden aktualisiert.",
|
"DialogControllerSettingsModifiedConfirmMessage": "Die aktuellen Controller-Einstellungen wurden aktualisiert.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Controller-Einstellungen speichern?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "Controller-Einstellungen speichern?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. Fehlerhafte Datei: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. Fehlerhafte Datei: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "Die angegebene Datei enthält keinen DLC für den ausgewählten Titel!",
|
"DialogDlcNoDlcErrorMessage": "Die angegebene Datei enthält keinen DLC für den ausgewählten Titel!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "Es wurde die Debug Protokollierung aktiviert",
|
"DialogPerformanceCheckLoggingEnabledMessage": "Es wurde die Debug Protokollierung aktiviert",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Um eine optimale Leistung zu erzielen, wird empfohlen, die Debug Protokollierung zu deaktivieren. Debug Protokollierung jetzt deaktivieren?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Um eine optimale Leistung zu erzielen, wird empfohlen, die Debug Protokollierung zu deaktivieren. Debug Protokollierung jetzt deaktivieren?",
|
||||||
|
@@ -260,7 +260,7 @@
|
|||||||
"UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ",
|
"UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ",
|
||||||
"UserProfilesAvailableUserProfiles": "Διαθέσιμα Προφίλ Χρηστών:",
|
"UserProfilesAvailableUserProfiles": "Διαθέσιμα Προφίλ Χρηστών:",
|
||||||
"UserProfilesAddNewProfile": "Προσθήκη Νέου Προφίλ",
|
"UserProfilesAddNewProfile": "Προσθήκη Νέου Προφίλ",
|
||||||
"UserProfilesDeleteSelectedProfile": "Διαγραφή Επιλεγμένου Προφίλ",
|
"UserProfilesDelete": "Διαγράφω",
|
||||||
"UserProfilesClose": "Κλείσιμο",
|
"UserProfilesClose": "Κλείσιμο",
|
||||||
"ProfileImageSelectionTitle": "Επιλογή Εικόνας Προφίλ",
|
"ProfileImageSelectionTitle": "Επιλογή Εικόνας Προφίλ",
|
||||||
"ProfileImageSelectionHeader": "Επιλέξτε μία Εικόνα Προφίλ",
|
"ProfileImageSelectionHeader": "Επιλέξτε μία Εικόνα Προφίλ",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "Θέλετε να διαγράψετε το επιλεγμένο προφίλ",
|
"DialogUserProfileDeletionConfirmMessage": "Θέλετε να διαγράψετε το επιλεγμένο προφίλ",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "Οι τρέχουσες ρυθμίσεις χειρισμού έχουν ενημερωθεί.",
|
"DialogControllerSettingsModifiedConfirmMessage": "Οι τρέχουσες ρυθμίσεις χειρισμού έχουν ενημερωθεί.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Θέλετε να αποθηκεύσετε;",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "Θέλετε να αποθηκεύσετε;",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. Σφάλμα Αρχείου: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. Σφάλμα Αρχείου: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "Το αρχείο δεν περιέχει DLC για τον επιλεγμένο τίτλο!",
|
"DialogDlcNoDlcErrorMessage": "Το αρχείο δεν περιέχει DLC για τον επιλεγμένο τίτλο!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "Έχετε ενεργοποιημένη την καταγραφή εντοπισμού σφαλμάτων, η οποία έχει σχεδιαστεί για χρήση μόνο από προγραμματιστές.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "Έχετε ενεργοποιημένη την καταγραφή εντοπισμού σφαλμάτων, η οποία έχει σχεδιαστεί για χρήση μόνο από προγραμματιστές.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Για βέλτιστη απόδοση, συνιστάται η απενεργοποίηση καταγραφής εντοπισμού σφαλμάτων. Θέλετε να απενεργοποιήσετε την καταγραφή τώρα;",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Για βέλτιστη απόδοση, συνιστάται η απενεργοποίηση καταγραφής εντοπισμού σφαλμάτων. Θέλετε να απενεργοποιήσετε την καταγραφή τώρα;",
|
||||||
|
@@ -26,6 +26,9 @@
|
|||||||
"MenuBarToolsInstallFirmware": "Install Firmware",
|
"MenuBarToolsInstallFirmware": "Install Firmware",
|
||||||
"MenuBarFileToolsInstallFirmwareFromFile": "Install a firmware from XCI or ZIP",
|
"MenuBarFileToolsInstallFirmwareFromFile": "Install a firmware from XCI or ZIP",
|
||||||
"MenuBarFileToolsInstallFirmwareFromDirectory": "Install a firmware from a directory",
|
"MenuBarFileToolsInstallFirmwareFromDirectory": "Install a firmware from a directory",
|
||||||
|
"MenuBarToolsManageFileTypes": "Manage file types",
|
||||||
|
"MenuBarToolsInstallFileTypes": "Install file types",
|
||||||
|
"MenuBarToolsUninstallFileTypes": "Uninstall file types",
|
||||||
"MenuBarHelp": "Help",
|
"MenuBarHelp": "Help",
|
||||||
"MenuBarHelpCheckForUpdates": "Check for Updates",
|
"MenuBarHelpCheckForUpdates": "Check for Updates",
|
||||||
"MenuBarHelpAbout": "About",
|
"MenuBarHelpAbout": "About",
|
||||||
@@ -119,7 +122,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " (may cause instability)",
|
"SettingsTabSystemHacksNote": "May cause instability",
|
||||||
"SettingsTabSystemExpandDramSize": "Use alternative memory layout (Developers)",
|
"SettingsTabSystemExpandDramSize": "Use alternative memory layout (Developers)",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignore Missing Services",
|
"SettingsTabSystemIgnoreMissingServices": "Ignore Missing Services",
|
||||||
"SettingsTabGraphics": "Graphics",
|
"SettingsTabGraphics": "Graphics",
|
||||||
@@ -157,7 +160,8 @@
|
|||||||
"SettingsTabLoggingEnableGuestLogs": "Enable Guest Logs",
|
"SettingsTabLoggingEnableGuestLogs": "Enable Guest Logs",
|
||||||
"SettingsTabLoggingEnableFsAccessLogs": "Enable Fs Access Logs",
|
"SettingsTabLoggingEnableFsAccessLogs": "Enable Fs Access Logs",
|
||||||
"SettingsTabLoggingFsGlobalAccessLogMode": "Fs Global Access Log Mode:",
|
"SettingsTabLoggingFsGlobalAccessLogMode": "Fs Global Access Log Mode:",
|
||||||
"SettingsTabLoggingDeveloperOptions": "Developer Options (WARNING: Will reduce performance)",
|
"SettingsTabLoggingDeveloperOptions": "Developer Options",
|
||||||
|
"SettingsTabLoggingDeveloperOptionsNote": "WARNING: Will reduce performance",
|
||||||
"SettingsTabLoggingGraphicsBackendLogLevel": "Graphics Backend Log Level:",
|
"SettingsTabLoggingGraphicsBackendLogLevel": "Graphics Backend Log Level:",
|
||||||
"SettingsTabLoggingGraphicsBackendLogLevelNone": "None",
|
"SettingsTabLoggingGraphicsBackendLogLevelNone": "None",
|
||||||
"SettingsTabLoggingGraphicsBackendLogLevelError": "Error",
|
"SettingsTabLoggingGraphicsBackendLogLevelError": "Error",
|
||||||
@@ -260,8 +264,9 @@
|
|||||||
"UserProfilesChangeProfileImage": "Change Profile Image",
|
"UserProfilesChangeProfileImage": "Change Profile Image",
|
||||||
"UserProfilesAvailableUserProfiles": "Available User Profiles:",
|
"UserProfilesAvailableUserProfiles": "Available User Profiles:",
|
||||||
"UserProfilesAddNewProfile": "Create Profile",
|
"UserProfilesAddNewProfile": "Create Profile",
|
||||||
"UserProfilesDeleteSelectedProfile": "Delete Selected",
|
"UserProfilesDelete": "Delete",
|
||||||
"UserProfilesClose": "Close",
|
"UserProfilesClose": "Close",
|
||||||
|
"ProfileNameSelectionWatermark": "Choose a nickname",
|
||||||
"ProfileImageSelectionTitle": "Profile Image Selection",
|
"ProfileImageSelectionTitle": "Profile Image Selection",
|
||||||
"ProfileImageSelectionHeader": "Choose a profile Image",
|
"ProfileImageSelectionHeader": "Choose a profile Image",
|
||||||
"ProfileImageSelectionNote": "You may import a custom profile image, or select an avatar from system firmware",
|
"ProfileImageSelectionNote": "You may import a custom profile image, or select an avatar from system firmware",
|
||||||
@@ -273,7 +278,7 @@
|
|||||||
"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})",
|
||||||
"AvatarChoose": "Choose",
|
"AvatarChoose": "Choose Avatar",
|
||||||
"AvatarSetBackgroundColor": "Set Background Color",
|
"AvatarSetBackgroundColor": "Set Background Color",
|
||||||
"AvatarClose": "Close",
|
"AvatarClose": "Close",
|
||||||
"ControllerSettingsLoadProfileToolTip": "Load Profile",
|
"ControllerSettingsLoadProfileToolTip": "Load Profile",
|
||||||
@@ -337,6 +342,10 @@
|
|||||||
"DialogFirmwareInstallEmbeddedSuccessMessage": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\\nThe emulator will now start.",
|
"DialogFirmwareInstallEmbeddedSuccessMessage": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\\nThe emulator will now start.",
|
||||||
"DialogFirmwareNoFirmwareInstalledMessage": "No Firmware Installed",
|
"DialogFirmwareNoFirmwareInstalledMessage": "No Firmware Installed",
|
||||||
"DialogFirmwareInstalledMessage": "Firmware {0} was installed",
|
"DialogFirmwareInstalledMessage": "Firmware {0} was installed",
|
||||||
|
"DialogInstallFileTypesSuccessMessage": "Successfully installed file types!",
|
||||||
|
"DialogInstallFileTypesErrorMessage": "Failed to install file types.",
|
||||||
|
"DialogUninstallFileTypesSuccessMessage": "Successfully uninstalled file types!",
|
||||||
|
"DialogUninstallFileTypesErrorMessage": "Failed to uninstall file types.",
|
||||||
"DialogOpenSettingsWindowLabel": "Open Settings Window",
|
"DialogOpenSettingsWindowLabel": "Open Settings Window",
|
||||||
"DialogControllerAppletTitle": "Controller Applet",
|
"DialogControllerAppletTitle": "Controller Applet",
|
||||||
"DialogMessageDialogErrorExceptionMessage": "Error displaying Message Dialog: {0}",
|
"DialogMessageDialogErrorExceptionMessage": "Error displaying Message Dialog: {0}",
|
||||||
@@ -368,9 +377,12 @@
|
|||||||
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "System version {0} successfully installed.",
|
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "System version {0} successfully installed.",
|
||||||
"DialogUserProfileDeletionWarningMessage": "There would be no other profiles to be opened if selected profile is deleted",
|
"DialogUserProfileDeletionWarningMessage": "There would be no other profiles to be opened if selected profile is deleted",
|
||||||
"DialogUserProfileDeletionConfirmMessage": "Do you want to delete the selected profile",
|
"DialogUserProfileDeletionConfirmMessage": "Do you want to delete the selected profile",
|
||||||
|
"DialogUserProfileUnsavedChangesTitle": "Warning - Unsaved Changes",
|
||||||
|
"DialogUserProfileUnsavedChangesMessage": "You have made changes to this user profile that have not been saved.",
|
||||||
|
"DialogUserProfileUnsavedChangesSubMessage": "Do you want to discard your changes?",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.",
|
"DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. Errored File: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. Errored File: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "The specified file does not contain a DLC for the selected title!",
|
"DialogDlcNoDlcErrorMessage": "The specified file does not contain a DLC for the selected title!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "You have trace logging enabled, which is designed to be used by developers only.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "You have trace logging enabled, which is designed to be used by developers only.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "For optimal performance, it's recommended to disable trace logging. Would you like to disable trace logging now?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "For optimal performance, it's recommended to disable trace logging. Would you like to disable trace logging now?",
|
||||||
@@ -519,7 +531,7 @@
|
|||||||
"UserErrorUndefinedDescription": "An undefined error occured! This shouldn't happen, please contact a dev!",
|
"UserErrorUndefinedDescription": "An undefined error occured! This shouldn't happen, please contact a dev!",
|
||||||
"OpenSetupGuideMessage": "Open the Setup Guide",
|
"OpenSetupGuideMessage": "Open the Setup Guide",
|
||||||
"NoUpdate": "No Update",
|
"NoUpdate": "No Update",
|
||||||
"TitleUpdateVersionLabel": "Version {0} - {1}",
|
"TitleUpdateVersionLabel": "Version {0}",
|
||||||
"RyujinxInfo": "Ryujinx - Info",
|
"RyujinxInfo": "Ryujinx - Info",
|
||||||
"RyujinxConfirm": "Ryujinx - Confirmation",
|
"RyujinxConfirm": "Ryujinx - Confirmation",
|
||||||
"FileDialogAllTypes": "All types",
|
"FileDialogAllTypes": "All types",
|
||||||
@@ -580,11 +592,11 @@
|
|||||||
"UserProfilesSetProfileImage": "Set Profile Image",
|
"UserProfilesSetProfileImage": "Set Profile Image",
|
||||||
"UserProfileEmptyNameError": "Name is required",
|
"UserProfileEmptyNameError": "Name is required",
|
||||||
"UserProfileNoImageError": "Profile image must be set",
|
"UserProfileNoImageError": "Profile image must be set",
|
||||||
"GameUpdateWindowHeading": "{0} Update(s) available for {1} ({2})",
|
"GameUpdateWindowHeading": "Manage Updates for {0} ({1})",
|
||||||
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
|
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
|
||||||
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
|
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
|
||||||
"UserProfilesName": "Name:",
|
"UserProfilesName": "Name:",
|
||||||
"UserProfilesUserId": "User Id:",
|
"UserProfilesUserId": "User ID:",
|
||||||
"SettingsTabGraphicsBackend": "Graphics Backend",
|
"SettingsTabGraphicsBackend": "Graphics Backend",
|
||||||
"SettingsTabGraphicsBackendTooltip": "Graphics Backend to use",
|
"SettingsTabGraphicsBackendTooltip": "Graphics Backend to use",
|
||||||
"SettingsEnableTextureRecompression": "Enable Texture Recompression",
|
"SettingsEnableTextureRecompression": "Enable Texture Recompression",
|
||||||
@@ -603,13 +615,15 @@
|
|||||||
"UserProfilesManageSaves": "Manage Saves",
|
"UserProfilesManageSaves": "Manage Saves",
|
||||||
"DeleteUserSave": "Do you want to delete user save for this game?",
|
"DeleteUserSave": "Do you want to delete user save for this game?",
|
||||||
"IrreversibleActionNote": "This action is not reversible.",
|
"IrreversibleActionNote": "This action is not reversible.",
|
||||||
"SaveManagerHeading": "Manage Saves for {0}",
|
"SaveManagerHeading": "Manage Saves for {0} ({1})",
|
||||||
"SaveManagerTitle": "Save Manager",
|
"SaveManagerTitle": "Save Manager",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Size": "Size",
|
"Size": "Size",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"UserProfilesRecoverLostAccounts": "Recover Lost Accounts",
|
"UserProfilesRecoverLostAccounts": "Recover Lost Accounts",
|
||||||
"Recover": "Recover",
|
"Recover": "Recover",
|
||||||
"UserProfilesRecoverHeading" : "Saves were found for the following accounts"
|
"UserProfilesRecoverHeading" : "Saves were found for the following accounts",
|
||||||
|
"UserProfilesRecoverEmptyList": "No profiles to recover",
|
||||||
|
"UserEditorTitle" : "Edit User",
|
||||||
|
"UserEditorTitleCreate" : "Create User"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -260,7 +260,7 @@
|
|||||||
"UserProfilesChangeProfileImage": "Cambiar imagen de perfil",
|
"UserProfilesChangeProfileImage": "Cambiar imagen de perfil",
|
||||||
"UserProfilesAvailableUserProfiles": "Perfiles de usuario disponibles:",
|
"UserProfilesAvailableUserProfiles": "Perfiles de usuario disponibles:",
|
||||||
"UserProfilesAddNewProfile": "Añadir nuevo perfil",
|
"UserProfilesAddNewProfile": "Añadir nuevo perfil",
|
||||||
"UserProfilesDeleteSelectedProfile": "Eliminar perfil seleccionado",
|
"UserProfilesDelete": "Eliminar",
|
||||||
"UserProfilesClose": "Cerrar",
|
"UserProfilesClose": "Cerrar",
|
||||||
"ProfileImageSelectionTitle": "Selección de imagen de perfil",
|
"ProfileImageSelectionTitle": "Selección de imagen de perfil",
|
||||||
"ProfileImageSelectionHeader": "Elige una imagen de perfil",
|
"ProfileImageSelectionHeader": "Elige una imagen de perfil",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "¿Quieres eliminar el perfil seleccionado?",
|
"DialogUserProfileDeletionConfirmMessage": "¿Quieres eliminar el perfil seleccionado?",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "Se ha actualizado la configuración del mando actual.",
|
"DialogControllerSettingsModifiedConfirmMessage": "Se ha actualizado la configuración del mando actual.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "¿Guardar cambios?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "¿Guardar cambios?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. Archivo con error: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. Archivo con error: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "¡Ese archivo no contiene contenido descargable para el título seleccionado!",
|
"DialogDlcNoDlcErrorMessage": "¡Ese archivo no contiene contenido descargable para el título seleccionado!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "Has habilitado los registros debug, diseñados solo para uso de los desarrolladores.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "Has habilitado los registros debug, diseñados solo para uso de los desarrolladores.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Para un rendimiento óptimo, se recomienda deshabilitar los registros debug. ¿Quieres deshabilitarlos ahora?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Para un rendimiento óptimo, se recomienda deshabilitar los registros debug. ¿Quieres deshabilitarlos ahora?",
|
||||||
|
@@ -260,7 +260,7 @@
|
|||||||
"UserProfilesChangeProfileImage": "Changer l'image du profil",
|
"UserProfilesChangeProfileImage": "Changer l'image du profil",
|
||||||
"UserProfilesAvailableUserProfiles": "Profils utilisateurs disponible:",
|
"UserProfilesAvailableUserProfiles": "Profils utilisateurs disponible:",
|
||||||
"UserProfilesAddNewProfile": "Ajouter un nouveau profil",
|
"UserProfilesAddNewProfile": "Ajouter un nouveau profil",
|
||||||
"UserProfilesDeleteSelectedProfile": "Supprimer le profil sélectionné",
|
"UserProfilesDelete": "Supprimer",
|
||||||
"UserProfilesClose": "Fermer",
|
"UserProfilesClose": "Fermer",
|
||||||
"ProfileImageSelectionTitle": "Sélection de l'image du profil",
|
"ProfileImageSelectionTitle": "Sélection de l'image du profil",
|
||||||
"ProfileImageSelectionHeader": "Choisir l'image du profil",
|
"ProfileImageSelectionHeader": "Choisir l'image du profil",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "Voulez-vous supprimer le profil sélectionné ?",
|
"DialogUserProfileDeletionConfirmMessage": "Voulez-vous supprimer le profil sélectionné ?",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "Les paramètres actuels du contrôleur ont été mis à jour.",
|
"DialogControllerSettingsModifiedConfirmMessage": "Les paramètres actuels du contrôleur ont été mis à jour.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Voulez-vous sauvegarder?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "Voulez-vous sauvegarder?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. Fichier erroné : {1}",
|
"DialogLoadNcaErrorMessage": "{0}. Fichier erroné : {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "Le fichier spécifié ne contient pas de DLC pour le titre sélectionné !",
|
"DialogDlcNoDlcErrorMessage": "Le fichier spécifié ne contient pas de DLC pour le titre sélectionné !",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "Vous avez activé la journalisation des traces, conçue pour être utilisée uniquement par les développeurs.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "Vous avez activé la journalisation des traces, conçue pour être utilisée uniquement par les développeurs.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Pour des performances optimales, il est recommandé de désactiver la journalisation des traces. Souhaitez-vous désactiver la journalisation des traces maintenant ?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Pour des performances optimales, il est recommandé de désactiver la journalisation des traces. Souhaitez-vous désactiver la journalisation des traces maintenant ?",
|
||||||
|
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "Vuoi eliminare il profilo selezionato?",
|
"DialogUserProfileDeletionConfirmMessage": "Vuoi eliminare il profilo selezionato?",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "Le attuali impostazioni del controller sono state aggiornate.",
|
"DialogControllerSettingsModifiedConfirmMessage": "Le attuali impostazioni del controller sono state aggiornate.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Vuoi salvare?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "Vuoi salvare?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. File errato: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. File errato: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "Il file specificato non contiene un DLC per il titolo selezionato!",
|
"DialogDlcNoDlcErrorMessage": "Il file specificato non contiene un DLC per il titolo selezionato!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "Hai abilitato il trace logging, che è progettato per essere usato solo dagli sviluppatori.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "Hai abilitato il trace logging, che è progettato per essere usato solo dagli sviluppatori.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Per prestazioni ottimali, si raccomanda di disabilitare il trace logging. Vuoi disabilitare il debug logging adesso?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Per prestazioni ottimali, si raccomanda di disabilitare il trace logging. Vuoi disabilitare il debug logging adesso?",
|
||||||
|
@@ -260,7 +260,7 @@
|
|||||||
"UserProfilesChangeProfileImage": "プロファイル画像を変更",
|
"UserProfilesChangeProfileImage": "プロファイル画像を変更",
|
||||||
"UserProfilesAvailableUserProfiles": "利用可能なユーザプロファイル:",
|
"UserProfilesAvailableUserProfiles": "利用可能なユーザプロファイル:",
|
||||||
"UserProfilesAddNewProfile": "プロファイルを作成",
|
"UserProfilesAddNewProfile": "プロファイルを作成",
|
||||||
"UserProfilesDeleteSelectedProfile": "削除",
|
"UserProfilesDelete": "削除",
|
||||||
"UserProfilesClose": "閉じる",
|
"UserProfilesClose": "閉じる",
|
||||||
"ProfileImageSelectionTitle": "プロファイル画像選択",
|
"ProfileImageSelectionTitle": "プロファイル画像選択",
|
||||||
"ProfileImageSelectionHeader": "プロファイル画像を選択",
|
"ProfileImageSelectionHeader": "プロファイル画像を選択",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "選択されたプロファイルを削除しますか",
|
"DialogUserProfileDeletionConfirmMessage": "選択されたプロファイルを削除しますか",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "現在のコントローラ設定が更新されました.",
|
"DialogControllerSettingsModifiedConfirmMessage": "現在のコントローラ設定が更新されました.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "セーブしますか?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "セーブしますか?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. エラー発生ファイル: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. エラー発生ファイル: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "選択されたファイルはこのタイトル用の DLC ではありません!",
|
"DialogDlcNoDlcErrorMessage": "選択されたファイルはこのタイトル用の DLC ではありません!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "トレースロギングを有効にします. これは開発者のみに有用な機能です.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "トレースロギングを有効にします. これは開発者のみに有用な機能です.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "パフォーマンス最適化のためには,トレースロギングを無効にすることを推奨します. トレースロギングを無効にしてよろしいですか?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "パフォーマンス最適化のためには,トレースロギングを無効にすることを推奨します. トレースロギングを無効にしてよろしいですか?",
|
||||||
|
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "선택한 프로파일을 삭제하겠습니까?",
|
"DialogUserProfileDeletionConfirmMessage": "선택한 프로파일을 삭제하겠습니까?",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "현재 컨트롤러 설정이 업데이트되었습니다.",
|
"DialogControllerSettingsModifiedConfirmMessage": "현재 컨트롤러 설정이 업데이트되었습니다.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "저장하겠습니까?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "저장하겠습니까?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}입니다. 오류 발생 파일: {1}",
|
"DialogLoadNcaErrorMessage": "{0}입니다. 오류 발생 파일: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "지정된 파일에 선택한 타이틀에 대한 DLC가 포함되어 있지 않습니다!",
|
"DialogDlcNoDlcErrorMessage": "지정된 파일에 선택한 타이틀에 대한 DLC가 포함되어 있지 않습니다!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "개발자만 사용하도록 설계된 추적 로깅이 활성화되어 있습니다.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "개발자만 사용하도록 설계된 추적 로깅이 활성화되어 있습니다.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "최적의 성능을 위해 추적 로깅을 비활성화하는 것이 좋습니다. 지금 추적 로깅을 비활성화하겠습니까?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "최적의 성능을 위해 추적 로깅을 비활성화하는 것이 좋습니다. 지금 추적 로깅을 비활성화하겠습니까?",
|
||||||
|
@@ -260,7 +260,7 @@
|
|||||||
"UserProfilesChangeProfileImage": "Zmień Obraz Profilu",
|
"UserProfilesChangeProfileImage": "Zmień Obraz Profilu",
|
||||||
"UserProfilesAvailableUserProfiles": "Dostępne Profile Użytkowników:",
|
"UserProfilesAvailableUserProfiles": "Dostępne Profile Użytkowników:",
|
||||||
"UserProfilesAddNewProfile": "Utwórz Profil",
|
"UserProfilesAddNewProfile": "Utwórz Profil",
|
||||||
"UserProfilesDeleteSelectedProfile": "Usuń Zaznaczone",
|
"UserProfilesDelete": "Usuwać",
|
||||||
"UserProfilesClose": "Zamknij",
|
"UserProfilesClose": "Zamknij",
|
||||||
"ProfileImageSelectionTitle": "Wybór Obrazu Profilu",
|
"ProfileImageSelectionTitle": "Wybór Obrazu Profilu",
|
||||||
"ProfileImageSelectionHeader": "Wybierz zdjęcie profilowe",
|
"ProfileImageSelectionHeader": "Wybierz zdjęcie profilowe",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "Czy chcesz usunąć wybrany profil",
|
"DialogUserProfileDeletionConfirmMessage": "Czy chcesz usunąć wybrany profil",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "Aktualne ustawienia kontrolera zostały zaktualizowane.",
|
"DialogControllerSettingsModifiedConfirmMessage": "Aktualne ustawienia kontrolera zostały zaktualizowane.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Czy chcesz zapisać?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "Czy chcesz zapisać?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. Błędny Plik: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. Błędny Plik: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "Określony plik nie zawiera DLC dla wybranego tytułu!",
|
"DialogDlcNoDlcErrorMessage": "Określony plik nie zawiera DLC dla wybranego tytułu!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "Masz włączone rejestrowanie śledzenia, które jest przeznaczone tylko dla programistów.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "Masz włączone rejestrowanie śledzenia, które jest przeznaczone tylko dla programistów.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Aby uzyskać optymalną wydajność, zaleca się wyłączenie rejestrowania śledzenia. Czy chcesz teraz wyłączyć rejestrowanie śledzenia?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Aby uzyskać optymalną wydajność, zaleca się wyłączenie rejestrowania śledzenia. Czy chcesz teraz wyłączyć rejestrowanie śledzenia?",
|
||||||
|
@@ -260,7 +260,7 @@
|
|||||||
"UserProfilesChangeProfileImage": "Mudar imagem de perfil",
|
"UserProfilesChangeProfileImage": "Mudar imagem de perfil",
|
||||||
"UserProfilesAvailableUserProfiles": "Perfis de usuário disponíveis:",
|
"UserProfilesAvailableUserProfiles": "Perfis de usuário disponíveis:",
|
||||||
"UserProfilesAddNewProfile": "Adicionar novo perfil",
|
"UserProfilesAddNewProfile": "Adicionar novo perfil",
|
||||||
"UserProfilesDeleteSelectedProfile": "Apagar perfil selecionado",
|
"UserProfilesDelete": "Apagar",
|
||||||
"UserProfilesClose": "Fechar",
|
"UserProfilesClose": "Fechar",
|
||||||
"ProfileImageSelectionTitle": "Seleção da imagem de perfil",
|
"ProfileImageSelectionTitle": "Seleção da imagem de perfil",
|
||||||
"ProfileImageSelectionHeader": "Escolha uma imagem de perfil",
|
"ProfileImageSelectionHeader": "Escolha uma imagem de perfil",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "Deseja deletar o perfil selecionado",
|
"DialogUserProfileDeletionConfirmMessage": "Deseja deletar o perfil selecionado",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "As configurações de controle atuais foram atualizadas.",
|
"DialogControllerSettingsModifiedConfirmMessage": "As configurações de controle atuais foram atualizadas.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Deseja salvar?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "Deseja salvar?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. Arquivo com erro: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. Arquivo com erro: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "O arquivo especificado não contém DLCs para o título selecionado!",
|
"DialogDlcNoDlcErrorMessage": "O arquivo especificado não contém DLCs para o título selecionado!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "Os logs de depuração estão ativos, esse recurso é feito para ser usado apenas por desenvolvedores.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "Os logs de depuração estão ativos, esse recurso é feito para ser usado apenas por desenvolvedores.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Para melhor performance, é recomendável desabilitar os logs de depuração. Gostaria de desabilitar os logs de depuração agora?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Para melhor performance, é recomendável desabilitar os logs de depuração. Gostaria de desabilitar os logs de depuração agora?",
|
||||||
|
@@ -260,7 +260,7 @@
|
|||||||
"UserProfilesChangeProfileImage": "Изменить изображение профиля",
|
"UserProfilesChangeProfileImage": "Изменить изображение профиля",
|
||||||
"UserProfilesAvailableUserProfiles": "Доступные профили пользователей:",
|
"UserProfilesAvailableUserProfiles": "Доступные профили пользователей:",
|
||||||
"UserProfilesAddNewProfile": "Добавить новый профиль",
|
"UserProfilesAddNewProfile": "Добавить новый профиль",
|
||||||
"UserProfilesDeleteSelectedProfile": "Удалить выбранный профиль",
|
"UserProfilesDelete": "Удалить",
|
||||||
"UserProfilesClose": "Закрыть",
|
"UserProfilesClose": "Закрыть",
|
||||||
"ProfileImageSelectionTitle": "Выбор изображения профиля",
|
"ProfileImageSelectionTitle": "Выбор изображения профиля",
|
||||||
"ProfileImageSelectionHeader": "Выберите изображение профиля",
|
"ProfileImageSelectionHeader": "Выберите изображение профиля",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "Вы хотите удалить выбранный профиль",
|
"DialogUserProfileDeletionConfirmMessage": "Вы хотите удалить выбранный профиль",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "Текущие настройки контроллера обновлены.",
|
"DialogControllerSettingsModifiedConfirmMessage": "Текущие настройки контроллера обновлены.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Вы хотите сохранить?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "Вы хотите сохранить?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. Файл с ошибкой: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. Файл с ошибкой: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "Указанный файл не содержит DLC для выбранной игры!",
|
"DialogDlcNoDlcErrorMessage": "Указанный файл не содержит DLC для выбранной игры!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "У вас включено ведение журнала отладки, предназначенное только для разработчиков.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "У вас включено ведение журнала отладки, предназначенное только для разработчиков.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Для оптимальной производительности рекомендуется отключить ведение журнала отладки. Вы хотите отключить ведение журнала отладки сейчас?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Для оптимальной производительности рекомендуется отключить ведение журнала отладки. Вы хотите отключить ведение журнала отладки сейчас?",
|
||||||
|
@@ -260,7 +260,7 @@
|
|||||||
"UserProfilesChangeProfileImage": "Profil Resmini Değiştir",
|
"UserProfilesChangeProfileImage": "Profil Resmini Değiştir",
|
||||||
"UserProfilesAvailableUserProfiles": "Mevcut Kullanıcı Profilleri:",
|
"UserProfilesAvailableUserProfiles": "Mevcut Kullanıcı Profilleri:",
|
||||||
"UserProfilesAddNewProfile": "Yeni Profil Ekle",
|
"UserProfilesAddNewProfile": "Yeni Profil Ekle",
|
||||||
"UserProfilesDeleteSelectedProfile": "Seçili Profili Sil",
|
"UserProfilesDelete": "Sil",
|
||||||
"UserProfilesClose": "Kapat",
|
"UserProfilesClose": "Kapat",
|
||||||
"ProfileImageSelectionTitle": "Profil Resmi Seçimi",
|
"ProfileImageSelectionTitle": "Profil Resmi Seçimi",
|
||||||
"ProfileImageSelectionHeader": "Profil Resmi Seç",
|
"ProfileImageSelectionHeader": "Profil Resmi Seç",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "Seçilen profili silmek istiyor musunuz",
|
"DialogUserProfileDeletionConfirmMessage": "Seçilen profili silmek istiyor musunuz",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "Güncel kontrolcü seçenekleri güncellendi.",
|
"DialogControllerSettingsModifiedConfirmMessage": "Güncel kontrolcü seçenekleri güncellendi.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Kaydetmek istiyor musunuz?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "Kaydetmek istiyor musunuz?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. Hatalı Dosya: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. Hatalı Dosya: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "Belirtilen dosya seçilen oyun için DLC içermiyor!",
|
"DialogDlcNoDlcErrorMessage": "Belirtilen dosya seçilen oyun için DLC içermiyor!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "Sadece geliştiriler için dizayn edilen Trace Loglama seçeneği etkin.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "Sadece geliştiriler için dizayn edilen Trace Loglama seçeneği etkin.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "En iyi performans için trace loglama'nın devre dışı bırakılması tavsiye edilir. Trace loglama seçeneğini şimdi devre dışı bırakmak ister misiniz?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "En iyi performans için trace loglama'nın devre dışı bırakılması tavsiye edilir. Trace loglama seçeneğini şimdi devre dışı bırakmak ister misiniz?",
|
||||||
|
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "Ви хочете видалити вибраний профіль",
|
"DialogUserProfileDeletionConfirmMessage": "Ви хочете видалити вибраний профіль",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "Поточні налаштування контролера оновлено.",
|
"DialogControllerSettingsModifiedConfirmMessage": "Поточні налаштування контролера оновлено.",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Ви хочете зберегти?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "Ви хочете зберегти?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. Файл з помилкою: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. Файл з помилкою: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "Зазначений файл не містить DLC для вибраного заголовку!",
|
"DialogDlcNoDlcErrorMessage": "Зазначений файл не містить DLC для вибраного заголовку!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "Ви увімкнули журнал налагодження, призначений лише для розробників.",
|
"DialogPerformanceCheckLoggingEnabledMessage": "Ви увімкнули журнал налагодження, призначений лише для розробників.",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Для оптимальної продуктивності рекомендується вимкнути ведення журналу налагодження. Ви хочете вимкнути ведення журналу налагодження зараз?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "Для оптимальної продуктивності рекомендується вимкнути ведення журналу налагодження. Ви хочете вимкнути ведення журналу налагодження зараз?",
|
||||||
|
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "是否删除选择的账户",
|
"DialogUserProfileDeletionConfirmMessage": "是否删除选择的账户",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "目前的输入预设已更新",
|
"DialogControllerSettingsModifiedConfirmMessage": "目前的输入预设已更新",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "要保存吗?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "要保存吗?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. 错误的文件: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. 错误的文件: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "选择的文件不包含所选游戏的 DLC!",
|
"DialogDlcNoDlcErrorMessage": "选择的文件不包含所选游戏的 DLC!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "您启用了跟踪日志,仅供开发人员使用。",
|
"DialogPerformanceCheckLoggingEnabledMessage": "您启用了跟踪日志,仅供开发人员使用。",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "为了获得最佳性能,建议禁用跟踪日志记录。您是否要立即禁用?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "为了获得最佳性能,建议禁用跟踪日志记录。您是否要立即禁用?",
|
||||||
|
@@ -260,7 +260,7 @@
|
|||||||
"UserProfilesChangeProfileImage": "更換頭貼",
|
"UserProfilesChangeProfileImage": "更換頭貼",
|
||||||
"UserProfilesAvailableUserProfiles": "現有的帳號:",
|
"UserProfilesAvailableUserProfiles": "現有的帳號:",
|
||||||
"UserProfilesAddNewProfile": "建立帳號",
|
"UserProfilesAddNewProfile": "建立帳號",
|
||||||
"UserProfilesDeleteSelectedProfile": "刪除選擇的帳號",
|
"UserProfilesDelete": "刪除",
|
||||||
"UserProfilesClose": "關閉",
|
"UserProfilesClose": "關閉",
|
||||||
"ProfileImageSelectionTitle": "頭貼選擇",
|
"ProfileImageSelectionTitle": "頭貼選擇",
|
||||||
"ProfileImageSelectionHeader": "選擇合適的頭貼圖片",
|
"ProfileImageSelectionHeader": "選擇合適的頭貼圖片",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"DialogUserProfileDeletionConfirmMessage": "是否刪除選擇的帳號",
|
"DialogUserProfileDeletionConfirmMessage": "是否刪除選擇的帳號",
|
||||||
"DialogControllerSettingsModifiedConfirmMessage": "目前的輸入預設已更新",
|
"DialogControllerSettingsModifiedConfirmMessage": "目前的輸入預設已更新",
|
||||||
"DialogControllerSettingsModifiedConfirmSubMessage": "要儲存嗎?",
|
"DialogControllerSettingsModifiedConfirmSubMessage": "要儲存嗎?",
|
||||||
"DialogDlcLoadNcaErrorMessage": "{0}. 錯誤的檔案: {1}",
|
"DialogLoadNcaErrorMessage": "{0}. 錯誤的檔案: {1}",
|
||||||
"DialogDlcNoDlcErrorMessage": "選擇的檔案不包含所選遊戲的 DLC!",
|
"DialogDlcNoDlcErrorMessage": "選擇的檔案不包含所選遊戲的 DLC!",
|
||||||
"DialogPerformanceCheckLoggingEnabledMessage": "您啟用了跟蹤日誌,僅供開發人員使用。",
|
"DialogPerformanceCheckLoggingEnabledMessage": "您啟用了跟蹤日誌,僅供開發人員使用。",
|
||||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "為了獲得最佳效能,建議停用跟蹤日誌記錄。您是否要立即停用?",
|
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "為了獲得最佳效能,建議停用跟蹤日誌記錄。您是否要立即停用?",
|
||||||
|
@@ -60,5 +60,6 @@
|
|||||||
<Color x:Key="MenuFlyoutPresenterBorderColor">#3D3D3D</Color>
|
<Color x:Key="MenuFlyoutPresenterBorderColor">#3D3D3D</Color>
|
||||||
<Color x:Key="AppListBackgroundColor">#0FFFFFFF</Color>
|
<Color x:Key="AppListBackgroundColor">#0FFFFFFF</Color>
|
||||||
<Color x:Key="AppListHoverBackgroundColor">#1EFFFFFF</Color>
|
<Color x:Key="AppListHoverBackgroundColor">#1EFFFFFF</Color>
|
||||||
|
<Color x:Key="SecondaryTextColor">#A0FFFFFF</Color>
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
</Styles>
|
</Styles>
|
@@ -52,5 +52,6 @@
|
|||||||
<Color x:Key="MenuFlyoutPresenterBorderColor">#C1C1C1</Color>
|
<Color x:Key="MenuFlyoutPresenterBorderColor">#C1C1C1</Color>
|
||||||
<Color x:Key="AppListBackgroundColor">#b3ffffff</Color>
|
<Color x:Key="AppListBackgroundColor">#b3ffffff</Color>
|
||||||
<Color x:Key="AppListHoverBackgroundColor">#80cccccc</Color>
|
<Color x:Key="AppListHoverBackgroundColor">#80cccccc</Color>
|
||||||
|
<Color x:Key="SecondaryTextColor">#A0000000</Color>
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
</Styles>
|
</Styles>
|
@@ -56,8 +56,8 @@
|
|||||||
<Style Selector="Border.settings">
|
<Style Selector="Border.settings">
|
||||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkColor}" />
|
<Setter Property="Background" Value="{DynamicResource ThemeDarkColor}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderColor}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderColor}" />
|
||||||
<Setter Property="BorderThickness" Value="2" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="3" />
|
<Setter Property="CornerRadius" Value="5" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Image.small">
|
<Style Selector="Image.small">
|
||||||
<Setter Property="Width" Value="50" />
|
<Setter Property="Width" Value="50" />
|
||||||
@@ -179,6 +179,9 @@
|
|||||||
<Style Selector="Button">
|
<Style Selector="Button">
|
||||||
<Setter Property="MinWidth" Value="80" />
|
<Setter Property="MinWidth" Value="80" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="ProgressBar /template/ Border#ProgressBarTrack">
|
||||||
|
<Setter Property="IsVisible" Value="False" />
|
||||||
|
</Style>
|
||||||
<Style Selector="ToggleButton">
|
<Style Selector="ToggleButton">
|
||||||
<Setter Property="Padding" Value="0,-5,0,0" />
|
<Setter Property="Padding" Value="0,-5,0,0" />
|
||||||
</Style>
|
</Style>
|
||||||
@@ -231,9 +234,41 @@
|
|||||||
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}" />
|
<Setter Property="BorderThickness" Value="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="TextBox">
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
<Style Selector="TextBox.NumberBoxTextBoxStyle">
|
<Style Selector="TextBox.NumberBoxTextBoxStyle">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
|
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="ListBox ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
<Setter Property="Margin" Value="0" />
|
||||||
|
<Setter Property="CornerRadius" Value="5" />
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
||||||
|
<Setter Property="BorderThickness" Value="2"/>
|
||||||
|
<Style.Animations>
|
||||||
|
<Animation Duration="0:0:0.7">
|
||||||
|
<KeyFrame Cue="0%">
|
||||||
|
<Setter Property="MaxHeight" Value="0" />
|
||||||
|
<Setter Property="Opacity" Value="0.0" />
|
||||||
|
</KeyFrame>
|
||||||
|
<KeyFrame Cue="50%">
|
||||||
|
<Setter Property="MaxHeight" Value="1000" />
|
||||||
|
<Setter Property="Opacity" Value="0.3" />
|
||||||
|
</KeyFrame>
|
||||||
|
<KeyFrame Cue="100%">
|
||||||
|
<Setter Property="MaxHeight" Value="1000" />
|
||||||
|
<Setter Property="Opacity" Value="1.0" />
|
||||||
|
</KeyFrame>
|
||||||
|
</Animation>
|
||||||
|
</Style.Animations>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBox ListBoxItem:selected /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBox ListBoxItem:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
|
||||||
|
</Style>
|
||||||
<Styles.Resources>
|
<Styles.Resources>
|
||||||
<SolidColorBrush x:Key="ThemeAccentColorBrush" Color="{DynamicResource SystemAccentColor}" />
|
<SolidColorBrush x:Key="ThemeAccentColorBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||||
<StaticResource x:Key="ListViewItemBackgroundSelected" ResourceKey="ThemeAccentColorBrush" />
|
<StaticResource x:Key="ListViewItemBackgroundSelected" ResourceKey="ThemeAccentColorBrush" />
|
||||||
@@ -271,6 +306,9 @@
|
|||||||
<Color x:Key="ThemeControlBorderColor">#FF505050</Color>
|
<Color x:Key="ThemeControlBorderColor">#FF505050</Color>
|
||||||
<Color x:Key="VsyncEnabled">#FF2EEAC9</Color>
|
<Color x:Key="VsyncEnabled">#FF2EEAC9</Color>
|
||||||
<Color x:Key="VsyncDisabled">#FFFF4554</Color>
|
<Color x:Key="VsyncDisabled">#FFFF4554</Color>
|
||||||
|
<Color x:Key="AppListBackgroundColor">#0FFFFFFF</Color>
|
||||||
|
<Color x:Key="AppListHoverBackgroundColor">#1EFFFFFF</Color>
|
||||||
|
<Color x:Key="SecondaryTextColor">#A0FFFFFF</Color>
|
||||||
<x:Double x:Key="ScrollBarThickness">15</x:Double>
|
<x:Double x:Key="ScrollBarThickness">15</x:Double>
|
||||||
<x:Double x:Key="FontSizeSmall">8</x:Double>
|
<x:Double x:Key="FontSizeSmall">8</x:Double>
|
||||||
<x:Double x:Key="FontSizeNormal">10</x:Double>
|
<x:Double x:Key="FontSizeNormal">10</x:Double>
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Notifications;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LibHac;
|
using LibHac;
|
||||||
using LibHac.Account;
|
using LibHac.Account;
|
||||||
@@ -12,7 +13,6 @@ using LibHac.Tools.Fs;
|
|||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Controls;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.Windows;
|
using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
@@ -44,14 +44,11 @@ namespace Ryujinx.Ava.Common
|
|||||||
_accountManager = accountManager;
|
_accountManager = accountManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryFindSaveData(string titleName, ulong titleId,
|
private static bool TryFindSaveData(string titleName, ulong titleId, BlitStruct<ApplicationControlProperty> controlHolder, in SaveDataFilter filter, out ulong saveDataId)
|
||||||
BlitStruct<ApplicationControlProperty> controlHolder, in SaveDataFilter filter, out ulong saveDataId)
|
|
||||||
{
|
{
|
||||||
saveDataId = default;
|
saveDataId = default;
|
||||||
|
|
||||||
Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo,
|
Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, in filter);
|
||||||
SaveDataSpaceId.User, in filter);
|
|
||||||
|
|
||||||
if (ResultFs.TargetNotFound.Includes(result))
|
if (ResultFs.TargetNotFound.Includes(result))
|
||||||
{
|
{
|
||||||
ref ApplicationControlProperty control = ref controlHolder.Value;
|
ref ApplicationControlProperty control = ref controlHolder.Value;
|
||||||
@@ -68,20 +65,17 @@ namespace Ryujinx.Ava.Common
|
|||||||
control.UserAccountSaveDataSize = 0x4000;
|
control.UserAccountSaveDataSize = 0x4000;
|
||||||
control.UserAccountSaveDataJournalSize = 0x4000;
|
control.UserAccountSaveDataJournalSize = 0x4000;
|
||||||
|
|
||||||
Logger.Warning?.Print(LogClass.Application,
|
Logger.Warning?.Print(LogClass.Application, "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
|
||||||
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Uid user = new Uid((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
|
Uid user = new((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
|
||||||
|
|
||||||
result = _horizonClient.Fs.EnsureApplicationSaveData(out _, new LibHac.Ncm.ApplicationId(titleId), in control, in user);
|
result = _horizonClient.Fs.EnsureApplicationSaveData(out _, new LibHac.Ncm.ApplicationId(titleId), in control, in user);
|
||||||
|
|
||||||
if (result.IsFailure())
|
if (result.IsFailure())
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(async () =>
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateErrorDialog(
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageCreateSaveErrorMessage, result.ToStringWithName()));
|
||||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogMessageCreateSaveErrorMessage], result.ToStringWithName()));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -98,16 +92,15 @@ namespace Ryujinx.Ava.Common
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(async () =>
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogMessageFindSaveErrorMessage], result.ToStringWithName()));
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageFindSaveErrorMessage, result.ToStringWithName()));
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void OpenSaveDir(in SaveDataFilter saveDataFilter, ulong titleId,
|
public static void OpenSaveDir(in SaveDataFilter saveDataFilter, ulong titleId, BlitStruct<ApplicationControlProperty> controlData, string titleName)
|
||||||
BlitStruct<ApplicationControlProperty> controlData, string titleName)
|
|
||||||
{
|
{
|
||||||
if (!TryFindSaveData(titleName, titleId, controlData, in saveDataFilter, out ulong saveDataId))
|
if (!TryFindSaveData(titleName, titleId, controlData, in saveDataFilter, out ulong saveDataId))
|
||||||
{
|
{
|
||||||
@@ -148,14 +141,15 @@ namespace Ryujinx.Ava.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task ExtractSection(NcaSectionType ncaSectionType, string titleFilePath,
|
public static async Task ExtractSection(NcaSectionType ncaSectionType, string titleFilePath, string titleName, int programIndex = 0)
|
||||||
int programIndex = 0)
|
|
||||||
{
|
{
|
||||||
OpenFolderDialog folderDialog = new() { Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle] };
|
OpenFolderDialog folderDialog = new()
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle]
|
||||||
|
};
|
||||||
|
|
||||||
string destination = await folderDialog.ShowAsync(_owner);
|
string destination = await folderDialog.ShowAsync(_owner);
|
||||||
|
var cancellationToken = new CancellationTokenSource();
|
||||||
var cancellationToken = new CancellationTokenSource();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(destination))
|
if (!string.IsNullOrWhiteSpace(destination))
|
||||||
{
|
{
|
||||||
@@ -164,7 +158,7 @@ namespace Ryujinx.Ava.Common
|
|||||||
Dispatcher.UIThread.Post(async () =>
|
Dispatcher.UIThread.Post(async () =>
|
||||||
{
|
{
|
||||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMessage], ncaSectionType, Path.GetFileName(titleFilePath)),
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, ncaSectionType, Path.GetFileName(titleFilePath)),
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogCancel],
|
LocaleManager.Instance[LocaleKeys.InputDialogCancel],
|
||||||
@@ -175,133 +169,122 @@ namespace Ryujinx.Ava.Common
|
|||||||
cancellationToken.Cancel();
|
cancellationToken.Cancel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
Thread.Sleep(1000);
|
Nca mainNca = null;
|
||||||
|
Nca patchNca = null;
|
||||||
|
|
||||||
using (FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read))
|
string extension = Path.GetExtension(titleFilePath).ToLower();
|
||||||
|
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
|
||||||
{
|
{
|
||||||
Nca mainNca = null;
|
PartitionFileSystem pfs;
|
||||||
Nca patchNca = null;
|
|
||||||
|
|
||||||
string extension = Path.GetExtension(titleFilePath).ToLower();
|
if (extension == ".xci")
|
||||||
|
|
||||||
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
|
|
||||||
{
|
{
|
||||||
PartitionFileSystem pfs;
|
pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pfs = new PartitionFileSystem(file.AsStorage());
|
||||||
|
}
|
||||||
|
|
||||||
if (extension == ".xci")
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||||
|
{
|
||||||
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
||||||
|
if (nca.Header.ContentType == NcaContentType.Program)
|
||||||
{
|
{
|
||||||
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
|
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||||
|
if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
|
||||||
pfs = xci.OpenPartition(XciPartitionType.Secure);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pfs = new PartitionFileSystem(file.AsStorage());
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
|
||||||
|
|
||||||
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
|
||||||
|
|
||||||
if (nca.Header.ContentType == NcaContentType.Program)
|
|
||||||
{
|
{
|
||||||
int dataIndex =
|
patchNca = nca;
|
||||||
Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
}
|
||||||
if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
|
else
|
||||||
{
|
{
|
||||||
patchNca = nca;
|
mainNca = nca;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mainNca = nca;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (extension == ".nca")
|
}
|
||||||
{
|
else if (extension == ".nca")
|
||||||
mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
|
{
|
||||||
}
|
mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
|
||||||
|
}
|
||||||
|
|
||||||
if (mainNca == null)
|
if (mainNca == null)
|
||||||
|
{
|
||||||
|
Logger.Error?.Print(LogClass.Application, "Extraction failure. The main NCA was not present in the selected file");
|
||||||
|
|
||||||
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application,
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]);
|
||||||
"Extraction failure. The main NCA was not present in the selected file");
|
});
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
|
||||||
|
if (updatePatchNca != null)
|
||||||
|
{
|
||||||
|
patchNca = updatePatchNca;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IFileSystem ncaFileSystem = patchNca != null
|
||||||
|
? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid)
|
||||||
|
: mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
|
FileSystemClient fsClient = _horizonClient.Fs;
|
||||||
|
|
||||||
|
string source = DateTime.Now.ToFileTime().ToString()[10..];
|
||||||
|
string output = DateTime.Now.ToFileTime().ToString()[10..];
|
||||||
|
|
||||||
|
using var uniqueSourceFs = new UniqueRef<IFileSystem>(ncaFileSystem);
|
||||||
|
using var uniqueOutputFs = new UniqueRef<IFileSystem>(new LocalFileSystem(destination));
|
||||||
|
|
||||||
|
fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref());
|
||||||
|
fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref());
|
||||||
|
|
||||||
|
(Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/", cancellationToken.Token);
|
||||||
|
|
||||||
|
if (!canceled)
|
||||||
|
{
|
||||||
|
if (resultCode.Value.IsFailure())
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]);
|
Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}");
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem,
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
|
|
||||||
if (updatePatchNca != null)
|
|
||||||
{
|
|
||||||
patchNca = updatePatchNca;
|
|
||||||
}
|
|
||||||
|
|
||||||
int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IFileSystem ncaFileSystem = patchNca != null
|
|
||||||
? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid)
|
|
||||||
: mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid);
|
|
||||||
|
|
||||||
FileSystemClient fsClient = _horizonClient.Fs;
|
|
||||||
|
|
||||||
string source = DateTime.Now.ToFileTime().ToString()[10..];
|
|
||||||
string output = DateTime.Now.ToFileTime().ToString()[10..];
|
|
||||||
|
|
||||||
using var uniqueSourceFs = new UniqueRef<IFileSystem>(ncaFileSystem);
|
|
||||||
using var uniqueOutputFs = new UniqueRef<IFileSystem>(new LocalFileSystem(destination));
|
|
||||||
|
|
||||||
fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref());
|
|
||||||
fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref());
|
|
||||||
|
|
||||||
(Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/", cancellationToken.Token);
|
|
||||||
|
|
||||||
if (!canceled)
|
|
||||||
{
|
|
||||||
if (resultCode.Value.IsFailure())
|
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application,
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]);
|
||||||
$"LibHac returned error code: {resultCode.Value.ErrorCode}");
|
});
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (resultCode.Value.IsSuccess())
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateInfoDialog(
|
|
||||||
LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage],
|
|
||||||
"",
|
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
|
||||||
"",
|
|
||||||
LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else if (resultCode.Value.IsSuccess())
|
||||||
fsClient.Unmount(source.ToU8Span());
|
|
||||||
fsClient.Unmount(output.ToU8Span());
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateErrorDialog(ex.Message);
|
NotificationHelper.Show(
|
||||||
});
|
LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle],
|
||||||
|
$"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}",
|
||||||
|
NotificationType.Information);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fsClient.Unmount(source.ToU8Span());
|
||||||
|
fsClient.Unmount(output.ToU8Span());
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
Logger.Error?.Print(LogClass.Application, $"{ex.Message}");
|
||||||
|
|
||||||
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(ex.Message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -20,7 +20,7 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
|
|
||||||
ReflectionBindingExtension binding = new($"[{keyToUse}]")
|
ReflectionBindingExtension binding = new($"[{keyToUse}]")
|
||||||
{
|
{
|
||||||
Mode = BindingMode.OneWay,
|
Mode = BindingMode.OneWay,
|
||||||
Source = LocaleManager.Instance
|
Source = LocaleManager.Instance
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -13,56 +13,87 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
{
|
{
|
||||||
private const string DefaultLanguageCode = "en_US";
|
private const string DefaultLanguageCode = "en_US";
|
||||||
|
|
||||||
private Dictionary<LocaleKeys, string> _localeStrings;
|
private Dictionary<LocaleKeys, string> _localeStrings;
|
||||||
private ConcurrentDictionary<LocaleKeys, object[]> _dynamicValues;
|
private Dictionary<LocaleKeys, string> _localeDefaultStrings;
|
||||||
|
private readonly ConcurrentDictionary<LocaleKeys, object[]> _dynamicValues;
|
||||||
|
|
||||||
public static LocaleManager Instance { get; } = new LocaleManager();
|
public static LocaleManager Instance { get; } = new LocaleManager();
|
||||||
public Dictionary<LocaleKeys, string> LocaleStrings { get => _localeStrings; set => _localeStrings = value; }
|
|
||||||
|
|
||||||
|
|
||||||
public LocaleManager()
|
public LocaleManager()
|
||||||
{
|
{
|
||||||
_localeStrings = new Dictionary<LocaleKeys, string>();
|
_localeStrings = new Dictionary<LocaleKeys, string>();
|
||||||
_dynamicValues = new ConcurrentDictionary<LocaleKeys, object[]>();
|
_localeDefaultStrings = new Dictionary<LocaleKeys, string>();
|
||||||
|
_dynamicValues = new ConcurrentDictionary<LocaleKeys, object[]>();
|
||||||
|
|
||||||
Load();
|
Load();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Load()
|
public void Load()
|
||||||
{
|
{
|
||||||
|
// Load the system Language Code.
|
||||||
string localeLanguageCode = CultureInfo.CurrentCulture.Name.Replace('-', '_');
|
string localeLanguageCode = CultureInfo.CurrentCulture.Name.Replace('-', '_');
|
||||||
|
|
||||||
|
// If the view is loaded with the UI Previewer detached, then override it with the saved one or default.
|
||||||
if (Program.PreviewerDetached)
|
if (Program.PreviewerDetached)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(ConfigurationState.Instance.Ui.LanguageCode.Value))
|
if (!string.IsNullOrEmpty(ConfigurationState.Instance.Ui.LanguageCode.Value))
|
||||||
{
|
{
|
||||||
localeLanguageCode = ConfigurationState.Instance.Ui.LanguageCode.Value;
|
localeLanguageCode = ConfigurationState.Instance.Ui.LanguageCode.Value;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
localeLanguageCode = DefaultLanguageCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load english first, if the target language translation is incomplete, we default to english.
|
// Load en_US as default, if the target language translation is incomplete.
|
||||||
LoadDefaultLanguage();
|
LoadDefaultLanguage();
|
||||||
|
|
||||||
if (localeLanguageCode != DefaultLanguageCode)
|
LoadLanguage(localeLanguageCode);
|
||||||
{
|
|
||||||
LoadLanguage(localeLanguageCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string this[LocaleKeys key]
|
public string this[LocaleKeys key]
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
// Check if the locale contains the key.
|
||||||
if (_localeStrings.TryGetValue(key, out string value))
|
if (_localeStrings.TryGetValue(key, out string value))
|
||||||
{
|
{
|
||||||
|
// Check if the localized string needs to be formatted.
|
||||||
if (_dynamicValues.TryGetValue(key, out var dynamicValue))
|
if (_dynamicValues.TryGetValue(key, out var dynamicValue))
|
||||||
{
|
{
|
||||||
return string.Format(value, dynamicValue);
|
try
|
||||||
|
{
|
||||||
|
return string.Format(value, dynamicValue);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// If formatting failed use the default text instead.
|
||||||
|
if (_localeDefaultStrings.TryGetValue(key, out value))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return string.Format(value, dynamicValue);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// If formatting the default text failed return the key.
|
||||||
|
return key.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the locale doesn't contain the key return the default one.
|
||||||
|
if (_localeDefaultStrings.TryGetValue(key, out string defaultValue))
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the locale text doesn't exist return the key.
|
||||||
return key.ToString();
|
return key.ToString();
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
@@ -73,42 +104,43 @@ namespace Ryujinx.Ava.Common.Locale
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateDynamicValue(LocaleKeys key, params object[] values)
|
public string UpdateAndGetDynamicValue(LocaleKeys key, params object[] values)
|
||||||
{
|
{
|
||||||
_dynamicValues[key] = values;
|
_dynamicValues[key] = values;
|
||||||
|
|
||||||
OnPropertyChanged("Item");
|
OnPropertyChanged("Item");
|
||||||
|
|
||||||
|
return this[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadDefaultLanguage()
|
private void LoadDefaultLanguage()
|
||||||
{
|
{
|
||||||
LoadLanguage(DefaultLanguageCode);
|
_localeDefaultStrings = LoadJsonLanguage();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadLanguage(string languageCode)
|
public void LoadLanguage(string languageCode)
|
||||||
{
|
{
|
||||||
string languageJson = EmbeddedResources.ReadAllText($"Ryujinx.Ava/Assets/Locales/{languageCode}.json");
|
foreach (var item in LoadJsonLanguage(languageCode))
|
||||||
|
|
||||||
if (languageJson == null)
|
|
||||||
{
|
{
|
||||||
return;
|
this[item.Key] = item.Value;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var strings = JsonHelper.Deserialize<Dictionary<string, string>>(languageJson);
|
private Dictionary<LocaleKeys, string> LoadJsonLanguage(string languageCode = DefaultLanguageCode)
|
||||||
|
{
|
||||||
|
var localeStrings = new Dictionary<LocaleKeys, string>();
|
||||||
|
string languageJson = EmbeddedResources.ReadAllText($"Ryujinx.Ava/Assets/Locales/{languageCode}.json");
|
||||||
|
var strings = JsonHelper.Deserialize<Dictionary<string, string>>(languageJson);
|
||||||
|
|
||||||
foreach (var item in strings)
|
foreach (var item in strings)
|
||||||
{
|
{
|
||||||
if (Enum.TryParse<LocaleKeys>(item.Key, out var key))
|
if (Enum.TryParse<LocaleKeys>(item.Key, out var key))
|
||||||
{
|
{
|
||||||
this[key] = item.Value;
|
localeStrings[key] = item.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Program.PreviewerDetached)
|
return localeStrings;
|
||||||
{
|
|
||||||
ConfigurationState.Instance.Ui.LanguageCode.Value = languageCode;
|
|
||||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
using System;
|
using System;
|
||||||
@@ -30,6 +31,7 @@ namespace Ryujinx.Ava.Input
|
|||||||
_control.KeyDown += OnKeyPress;
|
_control.KeyDown += OnKeyPress;
|
||||||
_control.KeyUp += OnKeyRelease;
|
_control.KeyUp += OnKeyRelease;
|
||||||
_control.TextInput += Control_TextInput;
|
_control.TextInput += Control_TextInput;
|
||||||
|
_control.AddHandler(InputElement.TextInputEvent, Control_LastChanceTextInput, RoutingStrategies.Bubble);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Control_TextInput(object sender, TextInputEventArgs e)
|
private void Control_TextInput(object sender, TextInputEventArgs e)
|
||||||
@@ -37,6 +39,12 @@ namespace Ryujinx.Ava.Input
|
|||||||
TextInput?.Invoke(this, e.Text);
|
TextInput?.Invoke(this, e.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Control_LastChanceTextInput(object sender, TextInputEventArgs e)
|
||||||
|
{
|
||||||
|
// Swallow event
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
public event Action<string> OnGamepadConnected
|
public event Action<string> OnGamepadConnected
|
||||||
{
|
{
|
||||||
add { }
|
add { }
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
using FluentAvalonia.Core;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
@@ -69,12 +70,22 @@ namespace Ryujinx.Ava.Input
|
|||||||
|
|
||||||
private void Parent_PointerReleaseEvent(object o, PointerReleasedEventArgs args)
|
private void Parent_PointerReleaseEvent(object o, PointerReleasedEventArgs args)
|
||||||
{
|
{
|
||||||
PressedButtons[(int)args.InitialPressMouseButton - 1] = false;
|
int button = (int)args.InitialPressMouseButton - 1;
|
||||||
|
|
||||||
|
if (PressedButtons.Count() >= button)
|
||||||
|
{
|
||||||
|
PressedButtons[button] = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Parent_PointerPressEvent(object o, PointerPressedEventArgs args)
|
private void Parent_PointerPressEvent(object o, PointerPressedEventArgs args)
|
||||||
{
|
{
|
||||||
PressedButtons[(int)args.GetCurrentPoint(_widget).Properties.PointerUpdateKind] = true;
|
int button = (int)args.GetCurrentPoint(_widget).Properties.PointerUpdateKind;
|
||||||
|
|
||||||
|
if (PressedButtons.Count() >= button)
|
||||||
|
{
|
||||||
|
PressedButtons[button] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Parent_PointerMovedEvent(object o, PointerEventArgs args)
|
private void Parent_PointerMovedEvent(object o, PointerEventArgs args)
|
||||||
@@ -86,12 +97,18 @@ namespace Ryujinx.Ava.Input
|
|||||||
|
|
||||||
public void SetMousePressed(MouseButton button)
|
public void SetMousePressed(MouseButton button)
|
||||||
{
|
{
|
||||||
PressedButtons[(int)button] = true;
|
if (PressedButtons.Count() >= (int)button)
|
||||||
|
{
|
||||||
|
PressedButtons[(int)button] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetMouseReleased(MouseButton button)
|
public void SetMouseReleased(MouseButton button)
|
||||||
{
|
{
|
||||||
PressedButtons[(int)button] = false;
|
if (PressedButtons.Count() >= (int)button)
|
||||||
|
{
|
||||||
|
PressedButtons[(int)button] = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetPosition(double x, double y)
|
public void SetPosition(double x, double y)
|
||||||
@@ -101,7 +118,12 @@ namespace Ryujinx.Ava.Input
|
|||||||
|
|
||||||
public bool IsButtonPressed(MouseButton button)
|
public bool IsButtonPressed(MouseButton button)
|
||||||
{
|
{
|
||||||
return PressedButtons[(int)button];
|
if (PressedButtons.Count() >= (int)button)
|
||||||
|
{
|
||||||
|
return PressedButtons[(int)button];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Size GetClientSize()
|
public Size GetClientSize()
|
||||||
|
@@ -7,9 +7,7 @@ using ICSharpCode.SharpZipLib.Zip;
|
|||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Ryujinx.Ava;
|
using Ryujinx.Ava;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Controls;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.Windows;
|
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
@@ -32,31 +30,29 @@ namespace Ryujinx.Modules
|
|||||||
internal static class Updater
|
internal static class Updater
|
||||||
{
|
{
|
||||||
private const string GitHubApiURL = "https://api.github.com";
|
private const string GitHubApiURL = "https://api.github.com";
|
||||||
internal static bool Running;
|
|
||||||
|
|
||||||
private static readonly string HomeDir = AppDomain.CurrentDomain.BaseDirectory;
|
private static readonly string HomeDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
|
private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
|
||||||
private static readonly string UpdatePublishDir = Path.Combine(UpdateDir, "publish");
|
private static readonly string UpdatePublishDir = Path.Combine(UpdateDir, "publish");
|
||||||
private static readonly int ConnectionCount = 4;
|
private static readonly int ConnectionCount = 4;
|
||||||
|
|
||||||
private static string _buildVer;
|
private static string _buildVer;
|
||||||
private static string _platformExt;
|
private static string _platformExt;
|
||||||
private static string _buildUrl;
|
private static string _buildUrl;
|
||||||
private static long _buildSize;
|
private static long _buildSize;
|
||||||
|
private static bool _updateSuccessful;
|
||||||
|
private static bool _running;
|
||||||
|
|
||||||
private static readonly string[] WindowsDependencyDirs = Array.Empty<string>();
|
private static readonly string[] WindowsDependencyDirs = Array.Empty<string>();
|
||||||
|
|
||||||
public static bool UpdateSuccessful { get; private set; }
|
public static async Task BeginParse(Window mainWindow, bool showVersionUpToDate)
|
||||||
|
|
||||||
public static async Task BeginParse(MainWindow mainWindow, bool showVersionUpToDate)
|
|
||||||
{
|
{
|
||||||
if (Running)
|
if (_running)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Running = true;
|
_running = true;
|
||||||
mainWindow.ViewModel.CanUpdate = false;
|
|
||||||
|
|
||||||
// Detect current platform
|
// Detect current platform
|
||||||
if (OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsMacOS())
|
||||||
@@ -82,77 +78,87 @@ namespace Ryujinx.Modules
|
|||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
|
Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(async () =>
|
Dispatcher.UIThread.Post(async () =>
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage], LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
await ContentDialogHelper.CreateWarningDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_running = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get latest version number from GitHub API
|
// Get latest version number from GitHub API
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (HttpClient jsonClient = ConstructHttpClient())
|
using HttpClient jsonClient = ConstructHttpClient();
|
||||||
|
|
||||||
|
string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
|
||||||
|
string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL);
|
||||||
|
JObject jsonRoot = JObject.Parse(fetchedJson);
|
||||||
|
JToken assets = jsonRoot["assets"];
|
||||||
|
|
||||||
|
_buildVer = (string)jsonRoot["name"];
|
||||||
|
|
||||||
|
foreach (JToken asset in assets)
|
||||||
{
|
{
|
||||||
string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
|
string assetName = (string)asset["name"];
|
||||||
|
string assetState = (string)asset["state"];
|
||||||
|
string downloadURL = (string)asset["browser_download_url"];
|
||||||
|
|
||||||
string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL);
|
if (assetName.StartsWith("test-ava-ryujinx") && assetName.EndsWith(_platformExt))
|
||||||
JObject jsonRoot = JObject.Parse(fetchedJson);
|
|
||||||
JToken assets = jsonRoot["assets"];
|
|
||||||
|
|
||||||
_buildVer = (string)jsonRoot["name"];
|
|
||||||
|
|
||||||
foreach (JToken asset in assets)
|
|
||||||
{
|
{
|
||||||
string assetName = (string)asset["name"];
|
_buildUrl = downloadURL;
|
||||||
string assetState = (string)asset["state"];
|
|
||||||
string downloadURL = (string)asset["browser_download_url"];
|
|
||||||
|
|
||||||
if (assetName.StartsWith("test-ava-ryujinx") && assetName.EndsWith(_platformExt))
|
if (assetState != "uploaded")
|
||||||
{
|
{
|
||||||
_buildUrl = downloadURL;
|
if (showVersionUpToDate)
|
||||||
|
|
||||||
if (assetState != "uploaded")
|
|
||||||
{
|
{
|
||||||
if (showVersionUpToDate)
|
Dispatcher.UIThread.Post(async () =>
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(async () =>
|
await ContentDialogHelper.CreateUpdaterInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], "");
|
||||||
{
|
});
|
||||||
await ContentDialogHelper.CreateUpdaterInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], "");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
_running = false;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If build not done, assume no new update are availaible.
|
return;
|
||||||
if (_buildUrl == null)
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If build not done, assume no new update are availaible.
|
||||||
|
if (_buildUrl == null)
|
||||||
|
{
|
||||||
|
if (showVersionUpToDate)
|
||||||
{
|
{
|
||||||
if (showVersionUpToDate)
|
Dispatcher.UIThread.Post(async () =>
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(async () =>
|
await ContentDialogHelper.CreateUpdaterInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], "");
|
||||||
{
|
});
|
||||||
await ContentDialogHelper.CreateUpdaterInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], "");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_running = false;
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, exception.Message);
|
Logger.Error?.Print(LogClass.Application, exception.Message);
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(async () =>
|
Dispatcher.UIThread.Post(async () =>
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_running = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,11 +169,16 @@ namespace Ryujinx.Modules
|
|||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!");
|
Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!");
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(async () =>
|
Dispatcher.UIThread.Post(async () =>
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage], LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
await ContentDialogHelper.CreateWarningDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_running = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,8 +192,7 @@ namespace Ryujinx.Modules
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Running = false;
|
_running = false;
|
||||||
mainWindow.ViewModel.CanUpdate = true;
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -210,7 +220,8 @@ namespace Ryujinx.Modules
|
|||||||
Dispatcher.UIThread.Post(async () =>
|
Dispatcher.UIThread.Post(async () =>
|
||||||
{
|
{
|
||||||
// Show a message asking the user if they want to update
|
// Show a message asking the user if they want to update
|
||||||
var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxUpdaterMessage],
|
LocaleManager.Instance[LocaleKeys.RyujinxUpdaterMessage],
|
||||||
$"{Program.Version} -> {newVersion}");
|
$"{Program.Version} -> {newVersion}");
|
||||||
|
|
||||||
@@ -218,12 +229,16 @@ namespace Ryujinx.Modules
|
|||||||
{
|
{
|
||||||
UpdateRyujinx(mainWindow, _buildUrl);
|
UpdateRyujinx(mainWindow, _buildUrl);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_running = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpClient ConstructHttpClient()
|
private static HttpClient ConstructHttpClient()
|
||||||
{
|
{
|
||||||
HttpClient result = new HttpClient();
|
HttpClient result = new();
|
||||||
|
|
||||||
// Required by GitHub to interract with APIs.
|
// Required by GitHub to interract with APIs.
|
||||||
result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
|
result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
|
||||||
@@ -233,7 +248,7 @@ namespace Ryujinx.Modules
|
|||||||
|
|
||||||
public static async void UpdateRyujinx(Window parent, string downloadUrl)
|
public static async void UpdateRyujinx(Window parent, string downloadUrl)
|
||||||
{
|
{
|
||||||
UpdateSuccessful = false;
|
_updateSuccessful = false;
|
||||||
|
|
||||||
// Empty update dir, although it shouldn't ever have anything inside it
|
// Empty update dir, although it shouldn't ever have anything inside it
|
||||||
if (Directory.Exists(UpdateDir))
|
if (Directory.Exists(UpdateDir))
|
||||||
@@ -245,17 +260,16 @@ namespace Ryujinx.Modules
|
|||||||
|
|
||||||
string updateFile = Path.Combine(UpdateDir, "update.bin");
|
string updateFile = Path.Combine(UpdateDir, "update.bin");
|
||||||
|
|
||||||
var taskDialog = new TaskDialog()
|
TaskDialog taskDialog = new()
|
||||||
{
|
{
|
||||||
Header = LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
Header = LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
||||||
SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterDownloading],
|
SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterDownloading],
|
||||||
IconSource = new SymbolIconSource { Symbol = Symbol.Download },
|
IconSource = new SymbolIconSource { Symbol = Symbol.Download },
|
||||||
Buttons = { },
|
Buttons = { },
|
||||||
ShowProgressBar = true
|
ShowProgressBar = true,
|
||||||
|
XamlRoot = parent
|
||||||
};
|
};
|
||||||
|
|
||||||
taskDialog.XamlRoot = parent;
|
|
||||||
|
|
||||||
taskDialog.Opened += (s, e) =>
|
taskDialog.Opened += (s, e) =>
|
||||||
{
|
{
|
||||||
if (_buildSize >= 0)
|
if (_buildSize >= 0)
|
||||||
@@ -270,7 +284,7 @@ namespace Ryujinx.Modules
|
|||||||
|
|
||||||
await taskDialog.ShowAsync(true);
|
await taskDialog.ShowAsync(true);
|
||||||
|
|
||||||
if (UpdateSuccessful)
|
if (_updateSuccessful)
|
||||||
{
|
{
|
||||||
var shouldRestart = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
var shouldRestart = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
||||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterCompleteMessage],
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterCompleteMessage],
|
||||||
@@ -279,7 +293,7 @@ namespace Ryujinx.Modules
|
|||||||
if (shouldRestart)
|
if (shouldRestart)
|
||||||
{
|
{
|
||||||
string ryuName = Path.GetFileName(Environment.ProcessPath);
|
string ryuName = Path.GetFileName(Environment.ProcessPath);
|
||||||
string ryuExe = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ryuName);
|
string ryuExe = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ryuName);
|
||||||
|
|
||||||
if (!Path.Exists(ryuExe))
|
if (!Path.Exists(ryuExe))
|
||||||
{
|
{
|
||||||
@@ -298,15 +312,15 @@ namespace Ryujinx.Modules
|
|||||||
private static void DoUpdateWithMultipleThreads(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
private static void DoUpdateWithMultipleThreads(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
||||||
{
|
{
|
||||||
// Multi-Threaded Updater
|
// Multi-Threaded Updater
|
||||||
long chunkSize = _buildSize / ConnectionCount;
|
long chunkSize = _buildSize / ConnectionCount;
|
||||||
long remainderChunk = _buildSize % ConnectionCount;
|
long remainderChunk = _buildSize % ConnectionCount;
|
||||||
|
|
||||||
int completedRequests = 0;
|
int completedRequests = 0;
|
||||||
int totalProgressPercentage = 0;
|
int totalProgressPercentage = 0;
|
||||||
int[] progressPercentage = new int[ConnectionCount];
|
int[] progressPercentage = new int[ConnectionCount];
|
||||||
|
|
||||||
List<byte[]> list = new List<byte[]>(ConnectionCount);
|
List<byte[]> list = new(ConnectionCount);
|
||||||
List<WebClient> webClients = new List<WebClient>(ConnectionCount);
|
List<WebClient> webClients = new(ConnectionCount);
|
||||||
|
|
||||||
for (int i = 0; i < ConnectionCount; i++)
|
for (int i = 0; i < ConnectionCount; i++)
|
||||||
{
|
{
|
||||||
@@ -317,133 +331,129 @@ namespace Ryujinx.Modules
|
|||||||
{
|
{
|
||||||
#pragma warning disable SYSLIB0014
|
#pragma warning disable SYSLIB0014
|
||||||
// TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
|
// TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
|
||||||
using (WebClient client = new WebClient())
|
using WebClient client = new();
|
||||||
#pragma warning restore SYSLIB0014
|
#pragma warning restore SYSLIB0014
|
||||||
|
|
||||||
|
webClients.Add(client);
|
||||||
|
|
||||||
|
if (i == ConnectionCount - 1)
|
||||||
{
|
{
|
||||||
webClients.Add(client);
|
client.Headers.Add("Range", $"bytes={chunkSize * i}-{(chunkSize * (i + 1) - 1) + remainderChunk}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
client.Headers.Add("Range", $"bytes={chunkSize * i}-{chunkSize * (i + 1) - 1}");
|
||||||
|
}
|
||||||
|
|
||||||
if (i == ConnectionCount - 1)
|
client.DownloadProgressChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
int index = (int)args.UserState;
|
||||||
|
|
||||||
|
Interlocked.Add(ref totalProgressPercentage, -1 * progressPercentage[index]);
|
||||||
|
Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
|
||||||
|
Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
|
||||||
|
|
||||||
|
taskDialog.SetProgressBarState(totalProgressPercentage / ConnectionCount, TaskDialogProgressState.Normal);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.DownloadDataCompleted += (_, args) =>
|
||||||
|
{
|
||||||
|
int index = (int)args.UserState;
|
||||||
|
|
||||||
|
if (args.Cancelled)
|
||||||
{
|
{
|
||||||
client.Headers.Add("Range", $"bytes={chunkSize * i}-{(chunkSize * (i + 1) - 1) + remainderChunk}");
|
webClients[index].Dispose();
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.Headers.Add("Range", $"bytes={chunkSize * i}-{chunkSize * (i + 1) - 1}");
|
|
||||||
}
|
|
||||||
|
|
||||||
client.DownloadProgressChanged += (_, args) =>
|
taskDialog.Hide();
|
||||||
{
|
|
||||||
int index = (int)args.UserState;
|
|
||||||
|
|
||||||
Interlocked.Add(ref totalProgressPercentage, -1 * progressPercentage[index]);
|
|
||||||
Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
|
|
||||||
Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
|
|
||||||
|
|
||||||
taskDialog.SetProgressBarState(totalProgressPercentage / ConnectionCount, TaskDialogProgressState.Normal);
|
|
||||||
};
|
|
||||||
|
|
||||||
client.DownloadDataCompleted += (_, args) =>
|
|
||||||
{
|
|
||||||
int index = (int)args.UserState;
|
|
||||||
|
|
||||||
if (args.Cancelled)
|
|
||||||
{
|
|
||||||
webClients[index].Dispose();
|
|
||||||
|
|
||||||
taskDialog.Hide();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
list[index] = args.Result;
|
|
||||||
Interlocked.Increment(ref completedRequests);
|
|
||||||
|
|
||||||
if (Equals(completedRequests, ConnectionCount))
|
|
||||||
{
|
|
||||||
byte[] mergedFileBytes = new byte[_buildSize];
|
|
||||||
for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < ConnectionCount; connectionIndex++)
|
|
||||||
{
|
|
||||||
Array.Copy(list[connectionIndex], 0, mergedFileBytes, destinationOffset, list[connectionIndex].Length);
|
|
||||||
destinationOffset += list[connectionIndex].Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
File.WriteAllBytes(updateFile, mergedFileBytes);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
InstallUpdate(taskDialog, updateFile);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.Warning?.Print(LogClass.Application, e.Message);
|
|
||||||
Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
|
|
||||||
|
|
||||||
DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
client.DownloadDataAsync(new Uri(downloadUrl), i);
|
|
||||||
}
|
|
||||||
catch (WebException ex)
|
|
||||||
{
|
|
||||||
Logger.Warning?.Print(LogClass.Application, ex.Message);
|
|
||||||
Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
|
|
||||||
|
|
||||||
for (int j = 0; j < webClients.Count; j++)
|
|
||||||
{
|
|
||||||
webClients[j].CancelAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list[index] = args.Result;
|
||||||
|
Interlocked.Increment(ref completedRequests);
|
||||||
|
|
||||||
|
if (Equals(completedRequests, ConnectionCount))
|
||||||
|
{
|
||||||
|
byte[] mergedFileBytes = new byte[_buildSize];
|
||||||
|
for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < ConnectionCount; connectionIndex++)
|
||||||
|
{
|
||||||
|
Array.Copy(list[connectionIndex], 0, mergedFileBytes, destinationOffset, list[connectionIndex].Length);
|
||||||
|
destinationOffset += list[connectionIndex].Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllBytes(updateFile, mergedFileBytes);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InstallUpdate(taskDialog, updateFile);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, e.Message);
|
||||||
|
Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
|
||||||
|
|
||||||
|
DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
client.DownloadDataAsync(new Uri(downloadUrl), i);
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, ex.Message);
|
||||||
|
Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
|
||||||
|
|
||||||
|
for (int j = 0; j < webClients.Count; j++)
|
||||||
|
{
|
||||||
|
webClients[j].CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DoUpdateWithSingleThreadWorker(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
private static void DoUpdateWithSingleThreadWorker(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
||||||
{
|
{
|
||||||
using (HttpClient client = new HttpClient())
|
using HttpClient client = new();
|
||||||
|
// We do not want to timeout while downloading
|
||||||
|
client.Timeout = TimeSpan.FromDays(1);
|
||||||
|
|
||||||
|
using (HttpResponseMessage response = client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead).Result)
|
||||||
|
using (Stream remoteFileStream = response.Content.ReadAsStreamAsync().Result)
|
||||||
{
|
{
|
||||||
// We do not want to timeout while downloading
|
using Stream updateFileStream = File.Open(updateFile, FileMode.Create);
|
||||||
client.Timeout = TimeSpan.FromDays(1);
|
|
||||||
|
|
||||||
using (HttpResponseMessage response = client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead).Result)
|
long totalBytes = response.Content.Headers.ContentLength.Value;
|
||||||
using (Stream remoteFileStream = response.Content.ReadAsStreamAsync().Result)
|
long byteWritten = 0;
|
||||||
|
|
||||||
|
byte[] buffer = new byte[32 * 1024];
|
||||||
|
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
using (Stream updateFileStream = File.Open(updateFile, FileMode.Create))
|
int readSize = remoteFileStream.Read(buffer);
|
||||||
|
|
||||||
|
if (readSize == 0)
|
||||||
{
|
{
|
||||||
long totalBytes = response.Content.Headers.ContentLength.Value;
|
break;
|
||||||
long byteWritten = 0;
|
|
||||||
|
|
||||||
byte[] buffer = new byte[32 * 1024];
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
int readSize = remoteFileStream.Read(buffer);
|
|
||||||
|
|
||||||
if (readSize == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
byteWritten += readSize;
|
|
||||||
|
|
||||||
taskDialog.SetProgressBarState(GetPercentage(byteWritten, totalBytes), TaskDialogProgressState.Normal);
|
|
||||||
|
|
||||||
updateFileStream.Write(buffer, 0, readSize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
InstallUpdate(taskDialog, updateFile);
|
byteWritten += readSize;
|
||||||
|
|
||||||
|
taskDialog.SetProgressBarState(GetPercentage(byteWritten, totalBytes), TaskDialogProgressState.Normal);
|
||||||
|
|
||||||
|
updateFileStream.Write(buffer, 0, readSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InstallUpdate(taskDialog, updateFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
@@ -454,8 +464,11 @@ namespace Ryujinx.Modules
|
|||||||
|
|
||||||
private static void DoUpdateWithSingleThread(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
private static void DoUpdateWithSingleThread(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
||||||
{
|
{
|
||||||
Thread worker = new Thread(() => DoUpdateWithSingleThreadWorker(taskDialog, downloadUrl, updateFile));
|
Thread worker = new(() => DoUpdateWithSingleThreadWorker(taskDialog, downloadUrl, updateFile))
|
||||||
worker.Name = "Updater.SingleThreadWorker";
|
{
|
||||||
|
Name = "Updater.SingleThreadWorker"
|
||||||
|
};
|
||||||
|
|
||||||
worker.Start();
|
worker.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,72 +496,70 @@ namespace Ryujinx.Modules
|
|||||||
|
|
||||||
if (OperatingSystem.IsLinux())
|
if (OperatingSystem.IsLinux())
|
||||||
{
|
{
|
||||||
using (Stream inStream = File.OpenRead(updateFile))
|
using Stream inStream = File.OpenRead(updateFile);
|
||||||
using (Stream gzipStream = new GZipInputStream(inStream))
|
using GZipInputStream gzipStream = new(inStream);
|
||||||
using (TarInputStream tarStream = new TarInputStream(gzipStream, Encoding.ASCII))
|
using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
TarEntry tarEntry;
|
||||||
|
while ((tarEntry = tarStream.GetNextEntry()) != null)
|
||||||
{
|
{
|
||||||
TarEntry tarEntry;
|
if (tarEntry.IsDirectory) continue;
|
||||||
while ((tarEntry = tarStream.GetNextEntry()) != null)
|
|
||||||
|
string outPath = Path.Combine(UpdateDir, tarEntry.Name);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||||
|
|
||||||
|
using (FileStream outStream = File.OpenWrite(outPath))
|
||||||
{
|
{
|
||||||
if (tarEntry.IsDirectory) continue;
|
tarStream.CopyEntryContents(outStream);
|
||||||
|
|
||||||
string outPath = Path.Combine(UpdateDir, tarEntry.Name);
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
|
||||||
|
|
||||||
using (FileStream outStream = File.OpenWrite(outPath))
|
|
||||||
{
|
|
||||||
tarStream.CopyEntryContents(outStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
|
|
||||||
|
|
||||||
TarEntry entry = tarEntry;
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
taskDialog.SetProgressBarState(GetPercentage(entry.Size, inStream.Length), TaskDialogProgressState.Normal);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
taskDialog.SetProgressBarState(100, TaskDialogProgressState.Normal);
|
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
|
||||||
}
|
|
||||||
|
TarEntry entry = tarEntry;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
taskDialog.SetProgressBarState(GetPercentage(entry.Size, inStream.Length), TaskDialogProgressState.Normal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
taskDialog.SetProgressBarState(100, TaskDialogProgressState.Normal);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
using (Stream inStream = File.OpenRead(updateFile))
|
using Stream inStream = File.OpenRead(updateFile);
|
||||||
using (ZipFile zipFile = new ZipFile(inStream))
|
using ZipFile zipFile = new(inStream);
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
double count = 0;
|
||||||
|
foreach (ZipEntry zipEntry in zipFile)
|
||||||
{
|
{
|
||||||
double count = 0;
|
count++;
|
||||||
foreach (ZipEntry zipEntry in zipFile)
|
if (zipEntry.IsDirectory) continue;
|
||||||
|
|
||||||
|
string outPath = Path.Combine(UpdateDir, zipEntry.Name);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||||
|
|
||||||
|
using (Stream zipStream = zipFile.GetInputStream(zipEntry))
|
||||||
|
using (FileStream outStream = File.OpenWrite(outPath))
|
||||||
{
|
{
|
||||||
count++;
|
zipStream.CopyTo(outStream);
|
||||||
if (zipEntry.IsDirectory) continue;
|
|
||||||
|
|
||||||
string outPath = Path.Combine(UpdateDir, zipEntry.Name);
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
|
||||||
|
|
||||||
using (Stream zipStream = zipFile.GetInputStream(zipEntry))
|
|
||||||
using (FileStream outStream = File.OpenWrite(outPath))
|
|
||||||
{
|
|
||||||
zipStream.CopyTo(outStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete downloaded zip
|
// Delete downloaded zip
|
||||||
@@ -577,7 +588,7 @@ namespace Ryujinx.Modules
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Application, string.Format(LocaleManager.Instance[LocaleKeys.UpdaterRenameFailed], file));
|
Logger.Warning?.Print(LogClass.Application, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UpdaterRenameFailed, file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,21 +605,24 @@ namespace Ryujinx.Modules
|
|||||||
|
|
||||||
SetFileExecutable(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx"));
|
SetFileExecutable(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx"));
|
||||||
|
|
||||||
UpdateSuccessful = true;
|
_updateSuccessful = true;
|
||||||
|
|
||||||
taskDialog.Hide();
|
taskDialog.Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
public static bool CanUpdate(bool showWarnings)
|
||||||
public static bool CanUpdate(bool showWarnings, StyleableWindow parent)
|
|
||||||
{
|
{
|
||||||
#if !DISABLE_UPDATER
|
#if !DISABLE_UPDATER
|
||||||
if (RuntimeInformation.OSArchitecture != Architecture.X64)
|
if (RuntimeInformation.OSArchitecture != Architecture.X64)
|
||||||
{
|
{
|
||||||
if (showWarnings)
|
if (showWarnings)
|
||||||
{
|
{
|
||||||
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedMessage],
|
Dispatcher.UIThread.Post(async () =>
|
||||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedSubMessage]);
|
{
|
||||||
|
await ContentDialogHelper.CreateWarningDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedMessage],
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterArchNotSupportedSubMessage]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -618,8 +632,12 @@ namespace Ryujinx.Modules
|
|||||||
{
|
{
|
||||||
if (showWarnings)
|
if (showWarnings)
|
||||||
{
|
{
|
||||||
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage],
|
Dispatcher.UIThread.Post(async () =>
|
||||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage]);
|
{
|
||||||
|
await ContentDialogHelper.CreateWarningDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage],
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -629,8 +647,12 @@ namespace Ryujinx.Modules
|
|||||||
{
|
{
|
||||||
if (showWarnings)
|
if (showWarnings)
|
||||||
{
|
{
|
||||||
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage],
|
Dispatcher.UIThread.Post(async () =>
|
||||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]);
|
{
|
||||||
|
await ContentDialogHelper.CreateWarningDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage],
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -642,18 +664,27 @@ namespace Ryujinx.Modules
|
|||||||
{
|
{
|
||||||
if (ReleaseInformation.IsFlatHubBuild())
|
if (ReleaseInformation.IsFlatHubBuild())
|
||||||
{
|
{
|
||||||
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage]);
|
Dispatcher.UIThread.Post(async () =>
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateWarningDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]);
|
Dispatcher.UIThread.Post(async () =>
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateWarningDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
|
||||||
|
|
||||||
// NOTE: This method should always reflect the latest build layout.s
|
// NOTE: This method should always reflect the latest build layout.s
|
||||||
private static IEnumerable<string> EnumerateFilesToDelete()
|
private static IEnumerable<string> EnumerateFilesToDelete()
|
||||||
@@ -677,7 +708,7 @@ namespace Ryujinx.Modules
|
|||||||
|
|
||||||
private static void MoveAllFilesOver(string root, string dest, TaskDialog taskDialog)
|
private static void MoveAllFilesOver(string root, string dest, TaskDialog taskDialog)
|
||||||
{
|
{
|
||||||
var total = Directory.GetFiles(root, "*", SearchOption.AllDirectories).Length;
|
int total = Directory.GetFiles(root, "*", SearchOption.AllDirectories).Length;
|
||||||
foreach (string directory in Directory.GetDirectories(root))
|
foreach (string directory in Directory.GetDirectories(root))
|
||||||
{
|
{
|
||||||
string dirName = Path.GetFileName(directory);
|
string dirName = Path.GetFileName(directory);
|
||||||
@@ -694,6 +725,7 @@ namespace Ryujinx.Modules
|
|||||||
foreach (string file in Directory.GetFiles(root))
|
foreach (string file in Directory.GetFiles(root))
|
||||||
{
|
{
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true);
|
File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true);
|
||||||
|
|
||||||
Dispatcher.UIThread.InvokeAsync(() =>
|
Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Ryujinx.Ava.UI.Helper;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.Windows;
|
using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
@@ -14,10 +14,8 @@ using Ryujinx.Ui.Common;
|
|||||||
using Ryujinx.Ui.Common.Configuration;
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Versioning;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Ryujinx.Ava
|
namespace Ryujinx.Ava
|
||||||
@@ -28,55 +26,13 @@ namespace Ryujinx.Ava
|
|||||||
public static double DesktopScaleFactor { get; set; } = 1.0;
|
public static double DesktopScaleFactor { get; set; } = 1.0;
|
||||||
public static string Version { get; private set; }
|
public static string Version { get; private set; }
|
||||||
public static string ConfigurationPath { get; private set; }
|
public static string ConfigurationPath { get; private set; }
|
||||||
public static bool PreviewerDetached { get; private set; }
|
public static bool PreviewerDetached { get; private set; }
|
||||||
|
|
||||||
[LibraryImport("user32.dll", SetLastError = true)]
|
[LibraryImport("user32.dll", SetLastError = true)]
|
||||||
public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
|
public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
|
||||||
|
|
||||||
private const uint MB_ICONWARNING = 0x30;
|
private const uint MB_ICONWARNING = 0x30;
|
||||||
|
|
||||||
[SupportedOSPlatform("linux")]
|
|
||||||
static void RegisterMimeTypes()
|
|
||||||
{
|
|
||||||
if (ReleaseInformation.IsFlatHubBuild())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string mimeDbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "mime");
|
|
||||||
|
|
||||||
if (!File.Exists(Path.Combine(mimeDbPath, "packages", "Ryujinx.xml")))
|
|
||||||
{
|
|
||||||
string mimeTypesFile = Path.Combine(ReleaseInformation.GetBaseApplicationDirectory(), "mime", "Ryujinx.xml");
|
|
||||||
using Process mimeProcess = new();
|
|
||||||
|
|
||||||
mimeProcess.StartInfo.FileName = "xdg-mime";
|
|
||||||
mimeProcess.StartInfo.Arguments = $"install --novendor --mode user {mimeTypesFile}";
|
|
||||||
|
|
||||||
mimeProcess.Start();
|
|
||||||
mimeProcess.WaitForExit();
|
|
||||||
|
|
||||||
if (mimeProcess.ExitCode != 0)
|
|
||||||
{
|
|
||||||
Logger.Error?.PrintMsg(LogClass.Application, $"Unable to install mime types. Make sure xdg-utils is installed. Process exited with code: {mimeProcess.ExitCode}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using Process updateMimeProcess = new();
|
|
||||||
|
|
||||||
updateMimeProcess.StartInfo.FileName = "update-mime-database";
|
|
||||||
updateMimeProcess.StartInfo.Arguments = mimeDbPath;
|
|
||||||
|
|
||||||
updateMimeProcess.Start();
|
|
||||||
updateMimeProcess.WaitForExit();
|
|
||||||
|
|
||||||
if (updateMimeProcess.ExitCode != 0)
|
|
||||||
{
|
|
||||||
Logger.Error?.PrintMsg(LogClass.Application, $"Could not update local mime database. Process exited with code: {updateMimeProcess.ExitCode}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
Version = ReleaseInformation.GetVersion();
|
Version = ReleaseInformation.GetVersion();
|
||||||
@@ -139,12 +95,6 @@ namespace Ryujinx.Ava
|
|||||||
// Initialize the logger system.
|
// Initialize the logger system.
|
||||||
LoggerModule.Initialize();
|
LoggerModule.Initialize();
|
||||||
|
|
||||||
// Register mime types on linux.
|
|
||||||
if (OperatingSystem.IsLinux())
|
|
||||||
{
|
|
||||||
RegisterMimeTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Discord integration.
|
// Initialize Discord integration.
|
||||||
DiscordIntegrationModule.Initialize();
|
DiscordIntegrationModule.Initialize();
|
||||||
|
|
||||||
@@ -276,4 +226,4 @@ namespace Ryujinx.Ava
|
|||||||
Logger.Shutdown();
|
Logger.Shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -130,6 +130,18 @@
|
|||||||
<DependentUpon>GameListView.axaml</DependentUpon>
|
<DependentUpon>GameListView.axaml</DependentUpon>
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Update="UI\Views\User\UserEditorView.axaml.cs">
|
||||||
|
<DependentUpon>UserEditor.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="UI\Views\User\UserRecovererView.axaml.cs">
|
||||||
|
<DependentUpon>UserRecoverer.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="UI\Views\User\UserSelectorView.axaml.cs">
|
||||||
|
<DependentUpon>UserSelector.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -29,17 +29,12 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
|
|
||||||
public bool DisplayMessageDialog(ControllerAppletUiArgs args)
|
public bool DisplayMessageDialog(ControllerAppletUiArgs args)
|
||||||
{
|
{
|
||||||
string playerCount = args.PlayerCountMin == args.PlayerCountMax
|
string message = LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||||
? args.PlayerCountMin.ToString()
|
args.PlayerCountMin == args.PlayerCountMax ? LocaleKeys.DialogControllerAppletMessage : LocaleKeys.DialogControllerAppletMessagePlayerRange,
|
||||||
: $"{args.PlayerCountMin}-{args.PlayerCountMax}";
|
args.PlayerCountMin == args.PlayerCountMax ? args.PlayerCountMin.ToString() : $"{args.PlayerCountMin}-{args.PlayerCountMax}",
|
||||||
|
args.SupportedStyles,
|
||||||
LocaleKeys key = args.PlayerCountMin == args.PlayerCountMax ? LocaleKeys.DialogControllerAppletMessage : LocaleKeys.DialogControllerAppletMessagePlayerRange;
|
string.Join(", ", args.SupportedPlayers),
|
||||||
|
args.IsDocked ? LocaleManager.Instance[LocaleKeys.DialogControllerAppletDockModeSet] : "");
|
||||||
string message = string.Format(LocaleManager.Instance[key],
|
|
||||||
playerCount,
|
|
||||||
args.SupportedStyles,
|
|
||||||
string.Join(", ", args.SupportedPlayers),
|
|
||||||
args.IsDocked ? LocaleManager.Instance[LocaleKeys.DialogControllerAppletDockModeSet] : "");
|
|
||||||
|
|
||||||
return DisplayMessageDialog(LocaleManager.Instance[LocaleKeys.DialogControllerAppletTitle], message);
|
return DisplayMessageDialog(LocaleManager.Instance[LocaleKeys.DialogControllerAppletTitle], message);
|
||||||
}
|
}
|
||||||
@@ -92,7 +87,7 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogMessageDialogErrorExceptionMessage], ex));
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageDialogErrorExceptionMessage, ex));
|
||||||
|
|
||||||
dialogCloseEvent.Set();
|
dialogCloseEvent.Set();
|
||||||
}
|
}
|
||||||
@@ -126,7 +121,8 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
error = true;
|
error = true;
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogSoftwareKeyboardErrorExceptionMessage], ex));
|
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogSoftwareKeyboardErrorExceptionMessage, ex));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -181,7 +177,8 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
dialogCloseEvent.Set();
|
dialogCloseEvent.Set();
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogErrorAppletErrorExceptionMessage], ex));
|
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogErrorAppletErrorExceptionMessage, ex));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -136,7 +136,7 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
_hiddenTextBox.Clear();
|
_hiddenTextBox.Clear();
|
||||||
_parent.ViewModel.RendererControl.Focus();
|
_parent.ViewModel.RendererHostControl.Focus();
|
||||||
|
|
||||||
_parent = null;
|
_parent = null;
|
||||||
});
|
});
|
||||||
|
@@ -139,14 +139,16 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
else if (_inputMin > 0 && _inputMax == int.MaxValue)
|
else if (_inputMin > 0 && _inputMax == int.MaxValue)
|
||||||
{
|
{
|
||||||
Error.IsVisible = true;
|
Error.IsVisible = true;
|
||||||
Error.Text = string.Format(LocaleManager.Instance[LocaleKeys.SwkbdMinCharacters], _inputMin);
|
|
||||||
|
Error.Text = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SwkbdMinCharacters, _inputMin);
|
||||||
|
|
||||||
_checkLength = length => _inputMin <= length;
|
_checkLength = length => _inputMin <= length;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Error.IsVisible = true;
|
Error.IsVisible = true;
|
||||||
Error.Text = string.Format(LocaleManager.Instance[LocaleKeys.SwkbdMinRangeCharacters], _inputMin, _inputMax);
|
|
||||||
|
Error.Text = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SwkbdMinRangeCharacters, _inputMin, _inputMax);
|
||||||
|
|
||||||
_checkLength = length => _inputMin <= length && length <= _inputMax;
|
_checkLength = length => _inputMin <= length && length <= _inputMax;
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
Focusable="True">
|
Focusable="True">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||||
<MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
|
<MenuFlyout x:Key="GameContextMenu">
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding ToggleFavorite}"
|
Command="{Binding ToggleFavorite}"
|
||||||
Header="{locale:Locale GameListContextMenuToggleFavorite}"
|
Header="{locale:Locale GameListContextMenuToggleFavorite}"
|
||||||
@@ -22,14 +22,17 @@
|
|||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding OpenUserSaveDirectory}"
|
Command="{Binding OpenUserSaveDirectory}"
|
||||||
|
IsEnabled="{Binding EnabledUserSaveDirectory}"
|
||||||
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
|
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding OpenDeviceSaveDirectory}"
|
Command="{Binding OpenDeviceSaveDirectory}"
|
||||||
|
IsEnabled="{Binding EnabledDeviceSaveDirectory}"
|
||||||
Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
|
Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding OpenBcatSaveDirectory}"
|
Command="{Binding OpenBcatSaveDirectory}"
|
||||||
|
IsEnabled="{Binding EnabledBcatSaveDirectory}"
|
||||||
Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
|
Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -112,32 +115,8 @@
|
|||||||
</ListBox.ItemsPanel>
|
</ListBox.ItemsPanel>
|
||||||
<ListBox.Styles>
|
<ListBox.Styles>
|
||||||
<Style Selector="ListBoxItem">
|
<Style Selector="ListBoxItem">
|
||||||
<Setter Property="Padding" Value="0" />
|
|
||||||
<Setter Property="Margin" Value="5" />
|
<Setter Property="Margin" Value="5" />
|
||||||
<Setter Property="CornerRadius" Value="4" />
|
<Setter Property="CornerRadius" Value="4" />
|
||||||
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
|
||||||
<Style.Animations>
|
|
||||||
<Animation Duration="0:0:0.7">
|
|
||||||
<KeyFrame Cue="0%">
|
|
||||||
<Setter Property="MaxWidth" Value="0" />
|
|
||||||
<Setter Property="Opacity" Value="0.0" />
|
|
||||||
</KeyFrame>
|
|
||||||
<KeyFrame Cue="50%">
|
|
||||||
<Setter Property="MaxWidth" Value="1000" />
|
|
||||||
<Setter Property="Opacity" Value="0.3" />
|
|
||||||
</KeyFrame>
|
|
||||||
<KeyFrame Cue="100%">
|
|
||||||
<Setter Property="MaxWidth" Value="1000" />
|
|
||||||
<Setter Property="Opacity" Value="1.0" />
|
|
||||||
</KeyFrame>
|
|
||||||
</Animation>
|
|
||||||
</Style.Animations>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
|
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
|
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
|
||||||
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.GridItemSelectorSize}" />
|
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.GridItemSelectorSize}" />
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
using Avalonia.Collections;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using LibHac.Common;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.Ui.App.Common;
|
||||||
@@ -13,16 +11,25 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
{
|
{
|
||||||
public partial class GameGridView : UserControl
|
public partial class GameGridView : UserControl
|
||||||
{
|
{
|
||||||
private ApplicationData _selectedApplication;
|
|
||||||
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
|
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
|
||||||
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
|
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
|
||||||
|
|
||||||
public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
|
public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
|
||||||
{
|
{
|
||||||
add { AddHandler(ApplicationOpenedEvent, value); }
|
add { AddHandler(ApplicationOpenedEvent, value); }
|
||||||
remove { RemoveHandler(ApplicationOpenedEvent, value); }
|
remove { RemoveHandler(ApplicationOpenedEvent, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GameGridView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
|
public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
|
||||||
{
|
{
|
||||||
if (sender is ListBox listBox)
|
if (sender is ListBox listBox)
|
||||||
@@ -38,46 +45,13 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
{
|
{
|
||||||
if (sender is ListBox listBox)
|
if (sender is ListBox listBox)
|
||||||
{
|
{
|
||||||
_selectedApplication = listBox.SelectedItem as ApplicationData;
|
(DataContext as MainWindowViewModel).GridSelectedApplication = listBox.SelectedItem as ApplicationData;
|
||||||
|
|
||||||
(DataContext as MainWindowViewModel).GridSelectedApplication = _selectedApplication;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApplicationData SelectedApplication => _selectedApplication;
|
|
||||||
|
|
||||||
public GameGridView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
|
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
|
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MenuBase_OnMenuOpened(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var selection = SelectedApplication;
|
|
||||||
|
|
||||||
if (selection != null)
|
|
||||||
{
|
|
||||||
if (sender is ContextMenu menu)
|
|
||||||
{
|
|
||||||
bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
|
||||||
bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0;
|
|
||||||
bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
|
||||||
|
|
||||||
((menu.Items as AvaloniaList<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
|
|
||||||
((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
|
|
||||||
((menu.Items as AvaloniaList<object>)[4] as MenuItem).IsEnabled = canHaveBcatSave;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,17 +3,17 @@
|
|||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
|
||||||
d:DesignHeight="450"
|
d:DesignHeight="450"
|
||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
mc:Ignorable="d"
|
Focusable="True"
|
||||||
Focusable="True">
|
mc:Ignorable="d">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||||
<MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
|
<MenuFlyout x:Key="GameContextMenu">
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding ToggleFavorite}"
|
Command="{Binding ToggleFavorite}"
|
||||||
Header="{locale:Locale GameListContextMenuToggleFavorite}"
|
Header="{locale:Locale GameListContextMenuToggleFavorite}"
|
||||||
@@ -21,14 +21,17 @@
|
|||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding OpenUserSaveDirectory}"
|
Command="{Binding OpenUserSaveDirectory}"
|
||||||
|
IsEnabled="{Binding EnabledUserSaveDirectory}"
|
||||||
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
|
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding OpenDeviceSaveDirectory}"
|
Command="{Binding OpenDeviceSaveDirectory}"
|
||||||
|
IsEnabled="{Binding EnabledDeviceSaveDirectory}"
|
||||||
Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
|
Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding OpenBcatSaveDirectory}"
|
Command="{Binding OpenBcatSaveDirectory}"
|
||||||
|
IsEnabled="{Binding EnabledBcatSaveDirectory}"
|
||||||
Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
|
Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -111,35 +114,6 @@
|
|||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ListBox.ItemsPanel>
|
</ListBox.ItemsPanel>
|
||||||
<ListBox.Styles>
|
<ListBox.Styles>
|
||||||
<Style Selector="ListBoxItem">
|
|
||||||
<Setter Property="Padding" Value="0" />
|
|
||||||
<Setter Property="Margin" Value="0" />
|
|
||||||
<Setter Property="CornerRadius" Value="5" />
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
|
||||||
<Setter Property="BorderThickness" Value="2"/>
|
|
||||||
<Style.Animations>
|
|
||||||
<Animation Duration="0:0:0.7">
|
|
||||||
<KeyFrame Cue="0%">
|
|
||||||
<Setter Property="MaxHeight" Value="0" />
|
|
||||||
<Setter Property="Opacity" Value="0.0" />
|
|
||||||
</KeyFrame>
|
|
||||||
<KeyFrame Cue="50%">
|
|
||||||
<Setter Property="MaxHeight" Value="1000" />
|
|
||||||
<Setter Property="Opacity" Value="0.3" />
|
|
||||||
</KeyFrame>
|
|
||||||
<KeyFrame Cue="100%">
|
|
||||||
<Setter Property="MaxHeight" Value="1000" />
|
|
||||||
<Setter Property="Opacity" Value="1.0" />
|
|
||||||
</KeyFrame>
|
|
||||||
</Animation>
|
|
||||||
</Style.Animations>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
|
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
|
||||||
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.ListItemSelectorSize}" />
|
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.ListItemSelectorSize}" />
|
||||||
</Style>
|
</Style>
|
||||||
@@ -159,7 +133,8 @@
|
|||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="10" />
|
<ColumnDefinition Width="10" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="150" />
|
||||||
|
<ColumnDefinition Width="100" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Image
|
<Image
|
||||||
Grid.RowSpan="3"
|
Grid.RowSpan="3"
|
||||||
@@ -170,30 +145,54 @@
|
|||||||
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
|
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
|
||||||
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
|
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
|
||||||
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
|
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
|
||||||
<StackPanel
|
<Border
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
|
Margin="0,0,5,0"
|
||||||
|
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||||
|
BorderThickness="0,0,1,0">
|
||||||
|
<StackPanel
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Orientation="Vertical"
|
||||||
|
Spacing="5">
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Text="{Binding TitleName}"
|
||||||
|
TextAlignment="Left"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Text="{Binding Developer}"
|
||||||
|
TextAlignment="Left"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Text="{Binding Version}"
|
||||||
|
TextAlignment="Left"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="3"
|
||||||
|
Margin="10,0,0,0"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Orientation="Vertical"
|
Orientation="Vertical"
|
||||||
Spacing="5" >
|
Spacing="5">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Text="{Binding TitleName}"
|
Text="{Binding TitleId}"
|
||||||
TextAlignment="Left"
|
TextAlignment="Left"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Text="{Binding Developer}"
|
Text="{Binding FileExtension}"
|
||||||
TextAlignment="Left"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
<TextBlock
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Text="{Binding Version}"
|
|
||||||
TextAlignment="Left"
|
TextAlignment="Left"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Column="3"
|
Grid.Column="4"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Orientation="Vertical"
|
Orientation="Vertical"
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
using Avalonia.Collections;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using LibHac.Common;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.Ui.App.Common;
|
||||||
@@ -13,16 +11,25 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
{
|
{
|
||||||
public partial class GameListView : UserControl
|
public partial class GameListView : UserControl
|
||||||
{
|
{
|
||||||
private ApplicationData _selectedApplication;
|
|
||||||
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
|
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
|
||||||
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
|
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
|
||||||
|
|
||||||
public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
|
public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
|
||||||
{
|
{
|
||||||
add { AddHandler(ApplicationOpenedEvent, value); }
|
add { AddHandler(ApplicationOpenedEvent, value); }
|
||||||
remove { RemoveHandler(ApplicationOpenedEvent, value); }
|
remove { RemoveHandler(ApplicationOpenedEvent, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GameListView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
|
public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
|
||||||
{
|
{
|
||||||
if (sender is ListBox listBox)
|
if (sender is ListBox listBox)
|
||||||
@@ -38,46 +45,13 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
{
|
{
|
||||||
if (sender is ListBox listBox)
|
if (sender is ListBox listBox)
|
||||||
{
|
{
|
||||||
_selectedApplication = listBox.SelectedItem as ApplicationData;
|
(DataContext as MainWindowViewModel).ListSelectedApplication = listBox.SelectedItem as ApplicationData;
|
||||||
|
|
||||||
(DataContext as MainWindowViewModel).ListSelectedApplication = _selectedApplication;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApplicationData SelectedApplication => _selectedApplication;
|
|
||||||
|
|
||||||
public GameListView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
|
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
|
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MenuBase_OnMenuOpened(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var selection = SelectedApplication;
|
|
||||||
|
|
||||||
if (selection != null)
|
|
||||||
{
|
|
||||||
if (sender is ContextMenu menu)
|
|
||||||
{
|
|
||||||
bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
|
||||||
bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0;
|
|
||||||
bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
|
||||||
|
|
||||||
((menu.Items as AvaloniaList<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
|
|
||||||
((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
|
|
||||||
((menu.Items as AvaloniaList<object>)[4] as MenuItem).IsEnabled = canHaveBcatSave;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,5 +12,6 @@
|
|||||||
<ui:Frame
|
<ui:Frame
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
x:Name="ContentFrame" />
|
x:Name="ContentFrame">
|
||||||
|
</ui:Frame>
|
||||||
</UserControl>
|
</UserControl>
|
@@ -1,13 +1,25 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Styling;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using FluentAvalonia.Core;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using LibHac;
|
using LibHac;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Shim;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using Ryujinx.Ava.UI.Views.User;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
@@ -31,14 +43,14 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
ContentManager = contentManager;
|
ContentManager = contentManager;
|
||||||
VirtualFileSystem = virtualFileSystem;
|
VirtualFileSystem = virtualFileSystem;
|
||||||
HorizonClient = horizonClient;
|
HorizonClient = horizonClient;
|
||||||
ViewModel = new UserProfileViewModel(this);
|
ViewModel = new UserProfileViewModel();
|
||||||
|
LoadProfiles();
|
||||||
|
|
||||||
if (contentManager.GetCurrentFirmwareVersion() != null)
|
if (contentManager.GetCurrentFirmwareVersion() != null)
|
||||||
{
|
{
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem);
|
UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -51,7 +63,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
ContentFrame.GoBack();
|
ContentFrame.GoBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewModel.LoadProfiles();
|
LoadProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Navigate(Type sourcePageType, object parameter)
|
public void Navigate(Type sourcePageType, object parameter)
|
||||||
@@ -68,7 +80,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
|
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
|
||||||
PrimaryButtonText = "",
|
PrimaryButtonText = "",
|
||||||
SecondaryButtonText = "",
|
SecondaryButtonText = "",
|
||||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
|
CloseButtonText = "",
|
||||||
Content = content,
|
Content = content,
|
||||||
Padding = new Thickness(0)
|
Padding = new Thickness(0)
|
||||||
};
|
};
|
||||||
@@ -78,6 +90,11 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
content.ViewModel.Dispose();
|
content.ViewModel.Dispose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Style footer = new(x => x.Name("DialogSpace").Child().OfType<Border>());
|
||||||
|
footer.Setters.Add(new Setter(IsVisibleProperty, false));
|
||||||
|
|
||||||
|
contentDialog.Styles.Add(footer);
|
||||||
|
|
||||||
await contentDialog.ShowAsync();
|
await contentDialog.ShowAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +102,117 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
{
|
{
|
||||||
base.OnAttachedToVisualTree(e);
|
base.OnAttachedToVisualTree(e);
|
||||||
|
|
||||||
Navigate(typeof(UserSelector), this);
|
Navigate(typeof(UserSelectorViews), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadProfiles()
|
||||||
|
{
|
||||||
|
ViewModel.Profiles.Clear();
|
||||||
|
ViewModel.LostProfiles.Clear();
|
||||||
|
|
||||||
|
var profiles = AccountManager.GetAllUsers().OrderBy(x => x.Name);
|
||||||
|
|
||||||
|
foreach (var profile in profiles)
|
||||||
|
{
|
||||||
|
ViewModel.Profiles.Add(new UserProfile(profile, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account, default, saveDataId: default, index: default);
|
||||||
|
|
||||||
|
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
|
||||||
|
|
||||||
|
HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
|
||||||
|
|
||||||
|
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
|
||||||
|
|
||||||
|
HashSet<HLE.HOS.Services.Account.Acc.UserId> lostAccounts = new();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
|
||||||
|
|
||||||
|
if (readCount == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < readCount; i++)
|
||||||
|
{
|
||||||
|
var save = saveDataInfo[i];
|
||||||
|
var id = new HLE.HOS.Services.Account.Acc.UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High);
|
||||||
|
if (ViewModel.Profiles.Cast<UserProfile>().FirstOrDefault( x=> x.UserId == id) == null)
|
||||||
|
{
|
||||||
|
lostAccounts.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(var account in lostAccounts)
|
||||||
|
{
|
||||||
|
ViewModel.LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), this));
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewModel.Profiles.Add(new BaseModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void DeleteUser(UserProfile userProfile)
|
||||||
|
{
|
||||||
|
var lastUserId = AccountManager.LastOpenedUser.UserId;
|
||||||
|
|
||||||
|
if (userProfile.UserId == lastUserId)
|
||||||
|
{
|
||||||
|
// If we are deleting the currently open profile, then we must open something else before deleting.
|
||||||
|
var profile = ViewModel.Profiles.Cast<UserProfile>().FirstOrDefault(x => x.UserId != lastUserId);
|
||||||
|
|
||||||
|
if (profile == null)
|
||||||
|
{
|
||||||
|
async void Action()
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(Action);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountManager.OpenUser(profile.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage],
|
||||||
|
"",
|
||||||
|
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||||
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
|
"");
|
||||||
|
|
||||||
|
if (result == UserResult.Yes)
|
||||||
|
{
|
||||||
|
GoBack();
|
||||||
|
AccountManager.DeleteUser(userProfile.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddUser()
|
||||||
|
{
|
||||||
|
Navigate(typeof(UserEditorView), (this, (UserProfile)null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EditUser(UserProfile userProfile)
|
||||||
|
{
|
||||||
|
Navigate(typeof(UserEditorView), (this, userProfile, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecoverLostAccounts()
|
||||||
|
{
|
||||||
|
Navigate(typeof(UserRecovererView), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ManageSaves()
|
||||||
|
{
|
||||||
|
Navigate(typeof(UserSaveManagerView), (this, AccountManager, HorizonClient, VirtualFileSystem));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,57 +0,0 @@
|
|||||||
<UserControl
|
|
||||||
xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
x:Class="Ryujinx.Ava.UI.Controls.ProfileImageSelectionDialog"
|
|
||||||
Focusable="True">
|
|
||||||
<Grid
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="5,10,5, 5">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="70" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<TextBlock
|
|
||||||
FontWeight="Bold"
|
|
||||||
FontSize="18"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Grid.Row="1"
|
|
||||||
Text="{locale:Locale ProfileImageSelectionHeader}" />
|
|
||||||
<TextBlock
|
|
||||||
FontWeight="Bold"
|
|
||||||
Grid.Row="2"
|
|
||||||
Margin="10"
|
|
||||||
MaxWidth="400"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
TextAlignment="Center"
|
|
||||||
Text="{locale:Locale ProfileImageSelectionNote}" />
|
|
||||||
<StackPanel
|
|
||||||
Margin="5,0"
|
|
||||||
Spacing="10"
|
|
||||||
Grid.Row="4"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Orientation="Horizontal">
|
|
||||||
<Button
|
|
||||||
Name="Import"
|
|
||||||
Click="Import_OnClick"
|
|
||||||
Width="200">
|
|
||||||
<TextBlock Text="{locale:Locale ProfileImageSelectionImportImage}" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
Name="SelectFirmwareImage"
|
|
||||||
IsEnabled="{Binding FirmwareFound}"
|
|
||||||
Click="SelectFirmwareImage_OnClick"
|
|
||||||
Width="200">
|
|
||||||
<TextBlock Text="{locale:Locale ProfileImageSelectionSelectAvatar}" />
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
@@ -1,127 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
|
||||||
using Ryujinx.Common.Configuration;
|
|
||||||
using Silk.NET.Vulkan;
|
|
||||||
using SPB.Graphics.OpenGL;
|
|
||||||
using SPB.Windowing;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
|
||||||
{
|
|
||||||
public partial class RendererHost : UserControl, IDisposable
|
|
||||||
{
|
|
||||||
private readonly GraphicsDebugLevel _graphicsDebugLevel;
|
|
||||||
private EmbeddedWindow _currentWindow;
|
|
||||||
|
|
||||||
public bool IsVulkan { get; private set; }
|
|
||||||
|
|
||||||
public RendererHost(GraphicsDebugLevel graphicsDebugLevel)
|
|
||||||
{
|
|
||||||
_graphicsDebugLevel = graphicsDebugLevel;
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public RendererHost()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CreateOpenGL()
|
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
|
|
||||||
_currentWindow = new OpenGLEmbeddedWindow(3, 3, _graphicsDebugLevel);
|
|
||||||
Initialize();
|
|
||||||
|
|
||||||
IsVulkan = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Initialize()
|
|
||||||
{
|
|
||||||
_currentWindow.WindowCreated += CurrentWindow_WindowCreated;
|
|
||||||
_currentWindow.SizeChanged += CurrentWindow_SizeChanged;
|
|
||||||
Content = _currentWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CreateVulkan()
|
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
|
|
||||||
_currentWindow = new VulkanEmbeddedWindow();
|
|
||||||
Initialize();
|
|
||||||
|
|
||||||
IsVulkan = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OpenGLContextBase GetContext()
|
|
||||||
{
|
|
||||||
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
|
|
||||||
{
|
|
||||||
return openGlEmbeddedWindow.Context;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnDetachedFromVisualTree(e);
|
|
||||||
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CurrentWindow_SizeChanged(object sender, Size e)
|
|
||||||
{
|
|
||||||
SizeChanged?.Invoke(sender, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CurrentWindow_WindowCreated(object sender, IntPtr e)
|
|
||||||
{
|
|
||||||
RendererInitialized?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MakeCurrent()
|
|
||||||
{
|
|
||||||
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
|
|
||||||
{
|
|
||||||
openGlEmbeddedWindow.MakeCurrent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MakeCurrent(SwappableNativeWindowBase window)
|
|
||||||
{
|
|
||||||
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
|
|
||||||
{
|
|
||||||
openGlEmbeddedWindow.MakeCurrent(window);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SwapBuffers()
|
|
||||||
{
|
|
||||||
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
|
|
||||||
{
|
|
||||||
openGlEmbeddedWindow.SwapBuffers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler<EventArgs> RendererInitialized;
|
|
||||||
public event Action<object, Size> SizeChanged;
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_currentWindow != null)
|
|
||||||
{
|
|
||||||
_currentWindow.WindowCreated -= CurrentWindow_WindowCreated;
|
|
||||||
_currentWindow.SizeChanged -= CurrentWindow_SizeChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SurfaceKHR CreateVulkanSurface(Instance instance, Vk api)
|
|
||||||
{
|
|
||||||
return (_currentWindow is VulkanEmbeddedWindow vulkanEmbeddedWindow)
|
|
||||||
? vulkanEmbeddedWindow.CreateSurface(instance)
|
|
||||||
: default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,175 +0,0 @@
|
|||||||
<UserControl
|
|
||||||
xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
|
||||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
|
||||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
d:DesignWidth="800"
|
|
||||||
d:DesignHeight="450"
|
|
||||||
Height="400"
|
|
||||||
Width="550"
|
|
||||||
x:Class="Ryujinx.Ava.UI.Controls.SaveManager"
|
|
||||||
Focusable="True">
|
|
||||||
<UserControl.Resources>
|
|
||||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
|
||||||
</UserControl.Resources>
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid
|
|
||||||
Grid.Row="0"
|
|
||||||
HorizontalAlignment="Stretch">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<StackPanel
|
|
||||||
Spacing="10"
|
|
||||||
Orientation="Horizontal"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
VerticalAlignment="Center">
|
|
||||||
<Label
|
|
||||||
Content="{locale:Locale CommonSort}"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<ComboBox SelectedIndex="{Binding SortIndex}" Width="100">
|
|
||||||
<ComboBoxItem>
|
|
||||||
<Label
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalContentAlignment="Left"
|
|
||||||
Content="{locale:Locale Name}" />
|
|
||||||
</ComboBoxItem>
|
|
||||||
<ComboBoxItem>
|
|
||||||
<Label
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalContentAlignment="Left"
|
|
||||||
Content="{locale:Locale Size}" />
|
|
||||||
</ComboBoxItem>
|
|
||||||
</ComboBox>
|
|
||||||
<ComboBox SelectedIndex="{Binding OrderIndex}" Width="150">
|
|
||||||
<ComboBoxItem>
|
|
||||||
<Label
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalContentAlignment="Left"
|
|
||||||
Content="{locale:Locale OrderAscending}" />
|
|
||||||
</ComboBoxItem>
|
|
||||||
<ComboBoxItem>
|
|
||||||
<Label
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalContentAlignment="Left"
|
|
||||||
Content="{locale:Locale OrderDescending}" />
|
|
||||||
</ComboBoxItem>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
<Grid
|
|
||||||
Grid.Column="1"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Margin="10,0, 0, 0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Label
|
|
||||||
Content="{locale:Locale Search}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<TextBox
|
|
||||||
Margin="5,0,0,0"
|
|
||||||
Grid.Column="1"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Text="{Binding Search}"/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<Border
|
|
||||||
Grid.Row="1"
|
|
||||||
Margin="0,5"
|
|
||||||
BorderThickness="1"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch">
|
|
||||||
<ListBox
|
|
||||||
Name="SaveList"
|
|
||||||
Items="{Binding View}"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="models:SaveModel">
|
|
||||||
<Grid HorizontalAlignment="Stretch" Margin="0,5">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
|
||||||
<Border
|
|
||||||
Height="42"
|
|
||||||
Margin="2"
|
|
||||||
Width="42"
|
|
||||||
Padding="10"
|
|
||||||
IsVisible="{Binding !InGameList}">
|
|
||||||
<ui:SymbolIcon
|
|
||||||
Symbol="Help"
|
|
||||||
FontSize="30"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
</Border>
|
|
||||||
<Image
|
|
||||||
IsVisible="{Binding InGameList}"
|
|
||||||
Margin="2"
|
|
||||||
Width="42"
|
|
||||||
Height="42"
|
|
||||||
Source="{Binding Icon,
|
|
||||||
Converter={StaticResource ByteImage}}" />
|
|
||||||
<TextBlock
|
|
||||||
MaxLines="3"
|
|
||||||
Width="320"
|
|
||||||
Margin="5"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Text="{Binding Title}" VerticalAlignment="Center" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel
|
|
||||||
Grid.Column="1"
|
|
||||||
Spacing="10"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Orientation="Horizontal">
|
|
||||||
<Label
|
|
||||||
Content="{Binding SizeString}"
|
|
||||||
IsVisible="{Binding SizeAvailable}"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalAlignment="Right" />
|
|
||||||
<Button
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Padding="10"
|
|
||||||
MinWidth="0"
|
|
||||||
MinHeight="0"
|
|
||||||
Name="OpenLocation"
|
|
||||||
Command="{Binding OpenLocation}">
|
|
||||||
<ui:SymbolIcon
|
|
||||||
Symbol="OpenFolder"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Padding="10"
|
|
||||||
MinWidth="0"
|
|
||||||
MinHeight="0"
|
|
||||||
Name="Delete"
|
|
||||||
Command="{Binding Delete}">
|
|
||||||
<ui:SymbolIcon
|
|
||||||
Symbol="Delete"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
@@ -1,160 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using DynamicData;
|
|
||||||
using DynamicData.Binding;
|
|
||||||
using LibHac;
|
|
||||||
using LibHac.Common;
|
|
||||||
using LibHac.Fs;
|
|
||||||
using LibHac.Fs.Shim;
|
|
||||||
using Ryujinx.Ava.Common;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
|
||||||
using Ryujinx.Ava.UI.Models;
|
|
||||||
using Ryujinx.HLE.FileSystem;
|
|
||||||
using Ryujinx.Ui.App.Common;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
|
||||||
{
|
|
||||||
public partial class SaveManager : UserControl
|
|
||||||
{
|
|
||||||
private readonly UserProfile _userProfile;
|
|
||||||
private readonly HorizonClient _horizonClient;
|
|
||||||
private readonly VirtualFileSystem _virtualFileSystem;
|
|
||||||
private int _sortIndex;
|
|
||||||
private int _orderIndex;
|
|
||||||
private ObservableCollection<SaveModel> _view = new ObservableCollection<SaveModel>();
|
|
||||||
private string _search;
|
|
||||||
|
|
||||||
public ObservableCollection<SaveModel> Saves { get; set; } = new ObservableCollection<SaveModel>();
|
|
||||||
|
|
||||||
public ObservableCollection<SaveModel> View
|
|
||||||
{
|
|
||||||
get => _view;
|
|
||||||
set => _view = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int SortIndex
|
|
||||||
{
|
|
||||||
get => _sortIndex;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_sortIndex = value;
|
|
||||||
Sort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int OrderIndex
|
|
||||||
{
|
|
||||||
get => _orderIndex;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_orderIndex = value;
|
|
||||||
Sort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Search
|
|
||||||
{
|
|
||||||
get => _search;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_search = value;
|
|
||||||
Sort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SaveManager()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SaveManager(UserProfile userProfile, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem)
|
|
||||||
{
|
|
||||||
_userProfile = userProfile;
|
|
||||||
_horizonClient = horizonClient;
|
|
||||||
_virtualFileSystem = virtualFileSystem;
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
DataContext = this;
|
|
||||||
|
|
||||||
Task.Run(LoadSaves);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadSaves()
|
|
||||||
{
|
|
||||||
Saves.Clear();
|
|
||||||
var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account,
|
|
||||||
new UserId((ulong)_userProfile.UserId.High, (ulong)_userProfile.UserId.Low), saveDataId: default, index: default);
|
|
||||||
|
|
||||||
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
|
|
||||||
|
|
||||||
_horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
|
|
||||||
|
|
||||||
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
|
|
||||||
|
|
||||||
if (readCount == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < readCount; i++)
|
|
||||||
{
|
|
||||||
var save = saveDataInfo[i];
|
|
||||||
if (save.ProgramId.Value != 0)
|
|
||||||
{
|
|
||||||
var saveModel = new SaveModel(save, _horizonClient, _virtualFileSystem);
|
|
||||||
Saves.Add(saveModel);
|
|
||||||
saveModel.DeleteAction = () => { Saves.Remove(saveModel); };
|
|
||||||
}
|
|
||||||
|
|
||||||
Sort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Sort()
|
|
||||||
{
|
|
||||||
Saves.AsObservableChangeSet()
|
|
||||||
.Filter(Filter)
|
|
||||||
.Sort(GetComparer())
|
|
||||||
.Bind(out var view).AsObservableList();
|
|
||||||
|
|
||||||
_view.Clear();
|
|
||||||
_view.AddRange(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IComparer<SaveModel> GetComparer()
|
|
||||||
{
|
|
||||||
switch (SortIndex)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return OrderIndex == 0
|
|
||||||
? SortExpressionComparer<SaveModel>.Ascending(save => save.Title)
|
|
||||||
: SortExpressionComparer<SaveModel>.Descending(save => save.Title);
|
|
||||||
case 1:
|
|
||||||
return OrderIndex == 0
|
|
||||||
? SortExpressionComparer<SaveModel>.Ascending(save => save.Size)
|
|
||||||
: SortExpressionComparer<SaveModel>.Descending(save => save.Size);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool Filter(object arg)
|
|
||||||
{
|
|
||||||
if (arg is SaveModel save)
|
|
||||||
{
|
|
||||||
return string.IsNullOrWhiteSpace(_search) || save.Title.ToLower().Contains(_search.ToLower());
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,72 +0,0 @@
|
|||||||
<UserControl
|
|
||||||
xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
d:DesignWidth="800"
|
|
||||||
d:DesignHeight="450"
|
|
||||||
MinWidth="500"
|
|
||||||
MinHeight="400"
|
|
||||||
x:Class="Ryujinx.Ava.UI.Controls.UserRecoverer"
|
|
||||||
Focusable="True">
|
|
||||||
<Design.DataContext>
|
|
||||||
<viewModels:UserProfileViewModel />
|
|
||||||
</Design.DataContext>
|
|
||||||
<Grid HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Button Grid.Row="0"
|
|
||||||
Margin="5"
|
|
||||||
Height="30"
|
|
||||||
Width="50"
|
|
||||||
MinWidth="50"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Command="{Binding GoBack}">
|
|
||||||
<ui:SymbolIcon Symbol="Back"/>
|
|
||||||
</Button>
|
|
||||||
<TextBlock Grid.Row="1"
|
|
||||||
Text="{locale:Locale UserProfilesRecoverHeading}"/>
|
|
||||||
<ListBox
|
|
||||||
Margin="5"
|
|
||||||
Grid.Row="2"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
Items="{Binding LostProfiles}">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Border
|
|
||||||
Margin="2"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
ClipToBounds="True"
|
|
||||||
CornerRadius="5">
|
|
||||||
<Grid Margin="0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Text="{Binding UserId}"
|
|
||||||
TextAlignment="Left"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
<Button Grid.Column="1"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Command="{Binding Recover}"
|
|
||||||
CommandParameter="{Binding}"
|
|
||||||
Content="{locale:Locale Recover}"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
@@ -1,44 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using FluentAvalonia.UI.Controls;
|
|
||||||
using FluentAvalonia.UI.Navigation;
|
|
||||||
using Ryujinx.Ava.UI.Models;
|
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
|
||||||
{
|
|
||||||
public partial class UserRecoverer : UserControl
|
|
||||||
{
|
|
||||||
private UserProfileViewModel _viewModel;
|
|
||||||
private NavigationDialogHost _parent;
|
|
||||||
|
|
||||||
public UserRecoverer()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
|
||||||
{
|
|
||||||
NavigatedTo(e);
|
|
||||||
}, RoutingStrategies.Direct);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NavigatedTo(NavigationEventArgs arg)
|
|
||||||
{
|
|
||||||
if (Program.PreviewerDetached)
|
|
||||||
{
|
|
||||||
switch (arg.NavigationMode)
|
|
||||||
{
|
|
||||||
case NavigationMode.New:
|
|
||||||
var args = ((NavigationDialogHost parent, UserProfileViewModel viewModel))arg.Parameter;
|
|
||||||
|
|
||||||
_viewModel = args.viewModel;
|
|
||||||
_parent = args.parent;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
DataContext = _viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,145 +0,0 @@
|
|||||||
<UserControl
|
|
||||||
x:Class="Ryujinx.Ava.UI.Controls.UserSelector"
|
|
||||||
xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
|
||||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
|
||||||
d:DesignHeight="450"
|
|
||||||
MinWidth="500"
|
|
||||||
d:DesignWidth="800"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Focusable="True">
|
|
||||||
<UserControl.Resources>
|
|
||||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
|
||||||
</UserControl.Resources>
|
|
||||||
<Design.DataContext>
|
|
||||||
<viewModels:UserProfileViewModel />
|
|
||||||
</Design.DataContext>
|
|
||||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<ListBox
|
|
||||||
Margin="5"
|
|
||||||
MaxHeight="300"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
DoubleTapped="ProfilesList_DoubleTapped"
|
|
||||||
Items="{Binding Profiles}"
|
|
||||||
SelectionChanged="SelectingItemsControl_SelectionChanged">
|
|
||||||
<ListBox.ItemsPanel>
|
|
||||||
<ItemsPanelTemplate>
|
|
||||||
<flex:FlexPanel
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
AlignContent="FlexStart"
|
|
||||||
JustifyContent="Center" />
|
|
||||||
</ItemsPanelTemplate>
|
|
||||||
</ListBox.ItemsPanel>
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Grid>
|
|
||||||
<Border
|
|
||||||
Margin="2"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
ClipToBounds="True"
|
|
||||||
CornerRadius="5">
|
|
||||||
<Grid Margin="0">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Image
|
|
||||||
Grid.Row="0"
|
|
||||||
Width="96"
|
|
||||||
Height="96"
|
|
||||||
Margin="0"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
|
||||||
<StackPanel
|
|
||||||
Grid.Row="1"
|
|
||||||
Height="30"
|
|
||||||
Margin="5"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch">
|
|
||||||
<TextBlock
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Text="{Binding Name}"
|
|
||||||
TextAlignment="Center"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
<Border
|
|
||||||
Width="10"
|
|
||||||
Height="10"
|
|
||||||
Margin="5"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Background="LimeGreen"
|
|
||||||
CornerRadius="5"
|
|
||||||
IsVisible="{Binding IsOpened}" />
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
<Grid
|
|
||||||
Grid.Row="1"
|
|
||||||
HorizontalAlignment="Center">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Button
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="0"
|
|
||||||
Margin="2"
|
|
||||||
Command="{Binding AddUser}"
|
|
||||||
Content="{locale:Locale UserProfilesAddNewProfile}" />
|
|
||||||
<Button
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Grid.Row="0"
|
|
||||||
Margin="2"
|
|
||||||
Grid.Column="1"
|
|
||||||
Command="{Binding EditUser}"
|
|
||||||
Content="{locale:Locale UserProfilesEditProfile}"
|
|
||||||
IsEnabled="{Binding IsSelectedProfiledEditable}" />
|
|
||||||
<Button
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
Margin="2"
|
|
||||||
Content="{locale:Locale UserProfilesManageSaves}"
|
|
||||||
Command="{Binding ManageSaves}" />
|
|
||||||
<Button
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="1"
|
|
||||||
Margin="2"
|
|
||||||
Command="{Binding DeleteUser}"
|
|
||||||
Content="{locale:Locale UserProfilesDeleteSelectedProfile}"
|
|
||||||
IsEnabled="{Binding IsSelectedProfileDeletable}" />
|
|
||||||
<Button
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.ColumnSpan="2"
|
|
||||||
Grid.Column="0"
|
|
||||||
Margin="2"
|
|
||||||
Command="{Binding RecoverLostAccounts}"
|
|
||||||
Content="{locale:Locale UserProfilesRecoverLostAccounts}" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
@@ -1,77 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using FluentAvalonia.UI.Controls;
|
|
||||||
using FluentAvalonia.UI.Navigation;
|
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
|
||||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
|
||||||
{
|
|
||||||
public partial class UserSelector : UserControl
|
|
||||||
{
|
|
||||||
private NavigationDialogHost _parent;
|
|
||||||
public UserProfileViewModel ViewModel { get; set; }
|
|
||||||
|
|
||||||
public UserSelector()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
if (Program.PreviewerDetached)
|
|
||||||
{
|
|
||||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
|
||||||
{
|
|
||||||
NavigatedTo(e);
|
|
||||||
}, RoutingStrategies.Direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NavigatedTo(NavigationEventArgs arg)
|
|
||||||
{
|
|
||||||
if (Program.PreviewerDetached)
|
|
||||||
{
|
|
||||||
if (arg.NavigationMode == NavigationMode.New)
|
|
||||||
{
|
|
||||||
_parent = (NavigationDialogHost)arg.Parameter;
|
|
||||||
ViewModel = _parent.ViewModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
DataContext = ViewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProfilesList_DoubleTapped(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is ListBox listBox)
|
|
||||||
{
|
|
||||||
int selectedIndex = listBox.SelectedIndex;
|
|
||||||
|
|
||||||
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
|
|
||||||
{
|
|
||||||
ViewModel.SelectedProfile = ViewModel.Profiles[selectedIndex];
|
|
||||||
|
|
||||||
_parent?.AccountManager?.OpenUser(ViewModel.SelectedProfile.UserId);
|
|
||||||
|
|
||||||
ViewModel.LoadProfiles();
|
|
||||||
|
|
||||||
foreach (UserProfile profile in ViewModel.Profiles)
|
|
||||||
{
|
|
||||||
profile.UpdateState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SelectingItemsControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is ListBox listBox)
|
|
||||||
{
|
|
||||||
int selectedIndex = listBox.SelectedIndex;
|
|
||||||
|
|
||||||
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
|
|
||||||
{
|
|
||||||
ViewModel.HighlightedProfile = ViewModel.Profiles[selectedIndex];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
using SPB.Graphics;
|
|
||||||
using System;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
|
||||||
{
|
|
||||||
[SupportedOSPlatform("linux")]
|
|
||||||
internal class AvaloniaGlxContext : SPB.Platform.GLX.GLXOpenGLContext
|
|
||||||
{
|
|
||||||
public AvaloniaGlxContext(IntPtr handle)
|
|
||||||
: base(FramebufferFormat.Default, 0, 0, 0, false, null)
|
|
||||||
{
|
|
||||||
ContextHandle = handle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
using SPB.Graphics;
|
|
||||||
using System;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
|
||||||
{
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
internal class AvaloniaWglContext : SPB.Platform.WGL.WGLOpenGLContext
|
|
||||||
{
|
|
||||||
public AvaloniaWglContext(IntPtr handle)
|
|
||||||
: base(FramebufferFormat.Default, 0, 0, 0, false, null)
|
|
||||||
{
|
|
||||||
ContextHandle = handle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,233 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Platform;
|
|
||||||
using Ryujinx.Ava.UI.Helper;
|
|
||||||
using SPB.Graphics;
|
|
||||||
using SPB.Platform;
|
|
||||||
using SPB.Platform.GLX;
|
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
|
||||||
{
|
|
||||||
public class EmbeddedWindow : NativeControlHost
|
|
||||||
{
|
|
||||||
private WindowProc _wndProcDelegate;
|
|
||||||
private string _className;
|
|
||||||
|
|
||||||
protected GLXWindow X11Window { get; set; }
|
|
||||||
protected IntPtr WindowHandle { get; set; }
|
|
||||||
protected IntPtr X11Display { get; set; }
|
|
||||||
protected IntPtr NsView { get; set; }
|
|
||||||
protected IntPtr MetalLayer { get; set; }
|
|
||||||
|
|
||||||
private UpdateBoundsCallbackDelegate _updateBoundsCallback;
|
|
||||||
|
|
||||||
public event EventHandler<IntPtr> WindowCreated;
|
|
||||||
public event EventHandler<Size> SizeChanged;
|
|
||||||
|
|
||||||
protected virtual void OnWindowDestroyed() { }
|
|
||||||
protected virtual void OnWindowDestroying()
|
|
||||||
{
|
|
||||||
WindowHandle = IntPtr.Zero;
|
|
||||||
X11Display = IntPtr.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EmbeddedWindow()
|
|
||||||
{
|
|
||||||
var stateObserverable = this.GetObservable(BoundsProperty);
|
|
||||||
|
|
||||||
stateObserverable.Subscribe(StateChanged);
|
|
||||||
|
|
||||||
this.Initialized += NativeEmbeddedWindow_Initialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void OnWindowCreated() { }
|
|
||||||
|
|
||||||
private void NativeEmbeddedWindow_Initialized(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
OnWindowCreated();
|
|
||||||
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
WindowCreated?.Invoke(this, WindowHandle);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StateChanged(Rect rect)
|
|
||||||
{
|
|
||||||
SizeChanged?.Invoke(this, rect.Size);
|
|
||||||
_updateBoundsCallback?.Invoke(rect);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
|
|
||||||
{
|
|
||||||
if (OperatingSystem.IsLinux())
|
|
||||||
{
|
|
||||||
return CreateLinux(parent);
|
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return CreateWin32(parent);
|
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsMacOS())
|
|
||||||
{
|
|
||||||
return CreateMacOs(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.CreateNativeControlCore(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void DestroyNativeControlCore(IPlatformHandle control)
|
|
||||||
{
|
|
||||||
OnWindowDestroying();
|
|
||||||
|
|
||||||
if (OperatingSystem.IsLinux())
|
|
||||||
{
|
|
||||||
DestroyLinux();
|
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
DestroyWin32(control);
|
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsMacOS())
|
|
||||||
{
|
|
||||||
DestroyMacOS();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
base.DestroyNativeControlCore(control);
|
|
||||||
}
|
|
||||||
|
|
||||||
OnWindowDestroyed();
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("linux")]
|
|
||||||
protected virtual IPlatformHandle CreateLinux(IPlatformHandle parent)
|
|
||||||
{
|
|
||||||
X11Window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100) as GLXWindow;
|
|
||||||
WindowHandle = X11Window.WindowHandle.RawHandle;
|
|
||||||
X11Display = X11Window.DisplayHandle.RawHandle;
|
|
||||||
|
|
||||||
return new PlatformHandle(WindowHandle, "X11");
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
IPlatformHandle CreateWin32(IPlatformHandle parent)
|
|
||||||
{
|
|
||||||
_className = "NativeWindow-" + Guid.NewGuid();
|
|
||||||
_wndProcDelegate = WndProc;
|
|
||||||
var wndClassEx = new WNDCLASSEX
|
|
||||||
{
|
|
||||||
cbSize = Marshal.SizeOf<WNDCLASSEX>(),
|
|
||||||
hInstance = GetModuleHandle(null),
|
|
||||||
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate),
|
|
||||||
style = ClassStyles.CS_OWNDC,
|
|
||||||
lpszClassName = Marshal.StringToHGlobalUni(_className),
|
|
||||||
hCursor = LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW)
|
|
||||||
};
|
|
||||||
|
|
||||||
var atom = RegisterClassEx(ref wndClassEx);
|
|
||||||
|
|
||||||
var handle = CreateWindowEx(
|
|
||||||
0,
|
|
||||||
_className,
|
|
||||||
"NativeWindow",
|
|
||||||
WindowStyles.WS_CHILD,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
640,
|
|
||||||
480,
|
|
||||||
parent.Handle,
|
|
||||||
IntPtr.Zero,
|
|
||||||
IntPtr.Zero,
|
|
||||||
IntPtr.Zero);
|
|
||||||
|
|
||||||
WindowHandle = handle;
|
|
||||||
|
|
||||||
Marshal.FreeHGlobal(wndClassEx.lpszClassName);
|
|
||||||
|
|
||||||
return new PlatformHandle(WindowHandle, "HWND");
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
IntPtr WndProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam)
|
|
||||||
{
|
|
||||||
var point = new Point((long)lParam & 0xFFFF, ((long)lParam >> 16) & 0xFFFF);
|
|
||||||
var root = VisualRoot as Window;
|
|
||||||
bool isLeft = false;
|
|
||||||
switch (msg)
|
|
||||||
{
|
|
||||||
case WindowsMessages.LBUTTONDOWN:
|
|
||||||
case WindowsMessages.RBUTTONDOWN:
|
|
||||||
isLeft = msg == WindowsMessages.LBUTTONDOWN;
|
|
||||||
this.RaiseEvent(new PointerPressedEventArgs(
|
|
||||||
this,
|
|
||||||
new Pointer(0, PointerType.Mouse, true),
|
|
||||||
root,
|
|
||||||
this.TranslatePoint(point, root).Value,
|
|
||||||
(ulong)Environment.TickCount64,
|
|
||||||
new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonPressed : PointerUpdateKind.RightButtonPressed),
|
|
||||||
KeyModifiers.None));
|
|
||||||
break;
|
|
||||||
case WindowsMessages.LBUTTONUP:
|
|
||||||
case WindowsMessages.RBUTTONUP:
|
|
||||||
isLeft = msg == WindowsMessages.LBUTTONUP;
|
|
||||||
this.RaiseEvent(new PointerReleasedEventArgs(
|
|
||||||
this,
|
|
||||||
new Pointer(0, PointerType.Mouse, true),
|
|
||||||
root,
|
|
||||||
this.TranslatePoint(point, root).Value,
|
|
||||||
(ulong)Environment.TickCount64,
|
|
||||||
new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonReleased : PointerUpdateKind.RightButtonReleased),
|
|
||||||
KeyModifiers.None,
|
|
||||||
isLeft ? MouseButton.Left : MouseButton.Right));
|
|
||||||
break;
|
|
||||||
case WindowsMessages.MOUSEMOVE:
|
|
||||||
this.RaiseEvent(new PointerEventArgs(
|
|
||||||
PointerMovedEvent,
|
|
||||||
this,
|
|
||||||
new Pointer(0, PointerType.Mouse, true),
|
|
||||||
root,
|
|
||||||
this.TranslatePoint(point, root).Value,
|
|
||||||
(ulong)Environment.TickCount64,
|
|
||||||
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other),
|
|
||||||
KeyModifiers.None));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return DefWindowProc(hWnd, msg, wParam, lParam);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("macos")]
|
|
||||||
IPlatformHandle CreateMacOs(IPlatformHandle parent)
|
|
||||||
{
|
|
||||||
MetalLayer = MetalHelper.GetMetalLayer(out IntPtr nsView, out _updateBoundsCallback);
|
|
||||||
|
|
||||||
NsView = nsView;
|
|
||||||
|
|
||||||
return new PlatformHandle(nsView, "NSView");
|
|
||||||
}
|
|
||||||
|
|
||||||
void DestroyLinux()
|
|
||||||
{
|
|
||||||
X11Window?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
void DestroyWin32(IPlatformHandle handle)
|
|
||||||
{
|
|
||||||
DestroyWindow(handle.Handle);
|
|
||||||
UnregisterClass(_className, GetModuleHandle(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("macos")]
|
|
||||||
void DestroyMacOS()
|
|
||||||
{
|
|
||||||
MetalHelper.DestroyMetalLayer(NsView, MetalLayer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
using Avalonia.OpenGL;
|
|
||||||
using SPB.Graphics.OpenGL;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
|
||||||
{
|
|
||||||
internal static class IGlContextExtension
|
|
||||||
{
|
|
||||||
public static OpenGLContextBase AsOpenGLContextBase(this IGlContext context)
|
|
||||||
{
|
|
||||||
var handle = (IntPtr)context.GetType().GetProperty("Handle").GetValue(context);
|
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return new AvaloniaWglContext(handle);
|
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsLinux())
|
|
||||||
{
|
|
||||||
return new AvaloniaGlxContext(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,11 +2,11 @@ using Avalonia.Utilities;
|
|||||||
using System;
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helper
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
{
|
{
|
||||||
using AvaLogger = Avalonia.Logging.Logger;
|
using AvaLogger = Avalonia.Logging.Logger;
|
||||||
using AvaLogLevel = Avalonia.Logging.LogEventLevel;
|
using AvaLogLevel = Avalonia.Logging.LogEventLevel;
|
||||||
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
||||||
using RyuLogClass = Ryujinx.Common.Logging.LogClass;
|
using RyuLogClass = Ryujinx.Common.Logging.LogClass;
|
||||||
|
|
||||||
internal class LoggerAdapter : Avalonia.Logging.ILogSink
|
internal class LoggerAdapter : Avalonia.Logging.ILogSink
|
||||||
@@ -20,12 +20,12 @@ namespace Ryujinx.Ava.UI.Helper
|
|||||||
{
|
{
|
||||||
return level switch
|
return level switch
|
||||||
{
|
{
|
||||||
AvaLogLevel.Verbose => RyuLogger.Trace,
|
AvaLogLevel.Verbose => RyuLogger.Debug,
|
||||||
AvaLogLevel.Debug => RyuLogger.Debug,
|
AvaLogLevel.Debug => RyuLogger.Debug,
|
||||||
AvaLogLevel.Information => RyuLogger.Info,
|
AvaLogLevel.Information => RyuLogger.Debug,
|
||||||
AvaLogLevel.Warning => RyuLogger.Warning,
|
AvaLogLevel.Warning => RyuLogger.Debug,
|
||||||
AvaLogLevel.Error => RyuLogger.Error,
|
AvaLogLevel.Error => RyuLogger.Error,
|
||||||
AvaLogLevel.Fatal => RyuLogger.Notice,
|
AvaLogLevel.Fatal => RyuLogger.Error,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
|
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -37,34 +37,38 @@ namespace Ryujinx.Ava.UI.Helper
|
|||||||
|
|
||||||
public void Log(AvaLogLevel level, string area, object source, string messageTemplate)
|
public void Log(AvaLogLevel level, string area, object source, string messageTemplate)
|
||||||
{
|
{
|
||||||
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, null));
|
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Log<T0>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0)
|
public void Log<T0>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0)
|
||||||
{
|
{
|
||||||
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0 }));
|
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, new object[] { propertyValue0 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Log<T0, T1>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1)
|
public void Log<T0, T1>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1)
|
||||||
{
|
{
|
||||||
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0, propertyValue1 }));
|
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, new object[] { propertyValue0, propertyValue1 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Log<T0, T1, T2>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2)
|
public void Log<T0, T1, T2>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2)
|
||||||
{
|
{
|
||||||
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0, propertyValue1, propertyValue2 }));
|
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, new object[] { propertyValue0, propertyValue1, propertyValue2 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Log(AvaLogLevel level, string area, object source, string messageTemplate, params object[] propertyValues)
|
public void Log(AvaLogLevel level, string area, object source, string messageTemplate, params object[] propertyValues)
|
||||||
{
|
{
|
||||||
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, propertyValues));
|
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, propertyValues));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Format(string area, string template, object source, object[] v)
|
private static string Format(AvaLogLevel level, string area, string template, object source, object[] v)
|
||||||
{
|
{
|
||||||
var result = new StringBuilder();
|
var result = new StringBuilder();
|
||||||
var r = new CharacterReader(template.AsSpan());
|
var r = new CharacterReader(template.AsSpan());
|
||||||
var i = 0;
|
int i = 0;
|
||||||
|
|
||||||
|
result.Append('[');
|
||||||
|
result.Append(level);
|
||||||
|
result.Append("] ");
|
||||||
|
|
||||||
result.Append('[');
|
result.Append('[');
|
||||||
result.Append(area);
|
result.Append(area);
|
@@ -3,7 +3,7 @@ using System.Runtime.Versioning;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helper
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
{
|
{
|
||||||
public delegate void UpdateBoundsCallbackDelegate(Rect rect);
|
public delegate void UpdateBoundsCallbackDelegate(Rect rect);
|
||||||
|
|
65
Ryujinx.Ava/UI/Helpers/NotificationHelper.cs
Normal file
65
Ryujinx.Ava/UI/Helpers/NotificationHelper.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Notifications;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
public static class NotificationHelper
|
||||||
|
{
|
||||||
|
private const int MaxNotifications = 4;
|
||||||
|
private const int NotificationDelayInMs = 5000;
|
||||||
|
|
||||||
|
private static WindowNotificationManager _notificationManager;
|
||||||
|
|
||||||
|
private static readonly ManualResetEvent _templateAppliedEvent = new(false);
|
||||||
|
private static readonly BlockingCollection<Notification> _notifications = new();
|
||||||
|
|
||||||
|
public static void SetNotificationManager(Window host)
|
||||||
|
{
|
||||||
|
_notificationManager = new WindowNotificationManager(host)
|
||||||
|
{
|
||||||
|
Position = NotificationPosition.BottomRight,
|
||||||
|
MaxItems = MaxNotifications,
|
||||||
|
Margin = new Thickness(0, 0, 15, 40)
|
||||||
|
};
|
||||||
|
|
||||||
|
_notificationManager.TemplateApplied += (sender, args) =>
|
||||||
|
{
|
||||||
|
_templateAppliedEvent.Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
_templateAppliedEvent.WaitOne();
|
||||||
|
|
||||||
|
foreach (var notification in _notifications.GetConsumingEnumerable())
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
_notificationManager.Show(notification);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.Delay(NotificationDelayInMs / MaxNotifications);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Show(string title, string text, NotificationType type, bool waitingExit = false, Action onClick = null, Action onClose = null)
|
||||||
|
{
|
||||||
|
var delay = waitingExit ? TimeSpan.FromMilliseconds(0) : TimeSpan.FromMilliseconds(NotificationDelayInMs);
|
||||||
|
|
||||||
|
_notifications.Add(new Notification(title, text, type, delay, onClick, onClose));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ShowError(string message)
|
||||||
|
{
|
||||||
|
Show(LocaleManager.Instance[LocaleKeys.DialogErrorTitle], $"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}", NotificationType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -76,11 +76,11 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
string setupButtonLabel = isInSetupGuide ? LocaleManager.Instance[LocaleKeys.OpenSetupGuideMessage] : "";
|
string setupButtonLabel = isInSetupGuide ? LocaleManager.Instance[LocaleKeys.OpenSetupGuideMessage] : "";
|
||||||
|
|
||||||
var result = await ContentDialogHelper.CreateInfoDialog(
|
var result = await ContentDialogHelper.CreateInfoDialog(
|
||||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogUserErrorDialogMessage], errorCode, GetErrorTitle(error)),
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogUserErrorDialogMessage, errorCode, GetErrorTitle(error)),
|
||||||
GetErrorDescription(error) + (isInSetupGuide
|
GetErrorDescription(error) + (isInSetupGuide
|
||||||
? LocaleManager.Instance[LocaleKeys.DialogUserErrorDialogInfoMessage]
|
? LocaleManager.Instance[LocaleKeys.DialogUserErrorDialogInfoMessage]
|
||||||
: ""), setupButtonLabel, LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
: ""), setupButtonLabel, LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogUserErrorDialogTitle], errorCode));
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogUserErrorDialogTitle, errorCode));
|
||||||
|
|
||||||
if (result == UserResult.Ok)
|
if (result == UserResult.Ok)
|
||||||
{
|
{
|
||||||
|
@@ -1,52 +0,0 @@
|
|||||||
using Avalonia.Platform;
|
|
||||||
using Silk.NET.Vulkan;
|
|
||||||
using SPB.Graphics.Vulkan;
|
|
||||||
using SPB.Platform.GLX;
|
|
||||||
using SPB.Platform.Metal;
|
|
||||||
using SPB.Platform.Win32;
|
|
||||||
using SPB.Platform.X11;
|
|
||||||
using SPB.Windowing;
|
|
||||||
using System;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
|
||||||
{
|
|
||||||
public class VulkanEmbeddedWindow : EmbeddedWindow
|
|
||||||
{
|
|
||||||
private NativeWindowBase _window;
|
|
||||||
|
|
||||||
[SupportedOSPlatform("linux")]
|
|
||||||
protected override IPlatformHandle CreateLinux(IPlatformHandle parent)
|
|
||||||
{
|
|
||||||
X11Window = new GLXWindow(new NativeHandle(X11.DefaultDisplay), new NativeHandle(parent.Handle));
|
|
||||||
WindowHandle = X11Window.WindowHandle.RawHandle;
|
|
||||||
X11Display = X11Window.DisplayHandle.RawHandle;
|
|
||||||
|
|
||||||
X11Window.Hide();
|
|
||||||
|
|
||||||
return new PlatformHandle(WindowHandle, "X11");
|
|
||||||
}
|
|
||||||
|
|
||||||
public SurfaceKHR CreateSurface(Instance instance)
|
|
||||||
{
|
|
||||||
if (OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
_window = new SimpleWin32Window(new NativeHandle(WindowHandle));
|
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsLinux())
|
|
||||||
{
|
|
||||||
_window = new SimpleX11Window(new NativeHandle(X11Display), new NativeHandle(WindowHandle));
|
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsMacOS())
|
|
||||||
{
|
|
||||||
_window = new SimpleMetalWindow(new NativeHandle(NsView), new NativeHandle(MetalLayer));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new PlatformNotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SurfaceKHR((ulong?)VulkanHelper.CreateWindowSurface(instance.Handle, _window));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -70,6 +70,22 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IntPtr CreateEmptyCursor()
|
||||||
|
{
|
||||||
|
return CreateCursor(IntPtr.Zero, 0, 0, 1, 1, new byte[] { 0xFF }, new byte[] { 0x00 });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IntPtr CreateArrowCursor()
|
||||||
|
{
|
||||||
|
return LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW);
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
public static partial IntPtr SetCursor(IntPtr handle);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, byte[] pvANDPlane, byte[] pvXORPlane);
|
||||||
|
|
||||||
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
|
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
|
||||||
public static partial ushort RegisterClassEx(ref WNDCLASSEX param);
|
public static partial ushort RegisterClassEx(ref WNDCLASSEX param);
|
||||||
|
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
|
using Avalonia.Media;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Models
|
namespace Ryujinx.Ava.UI.Models
|
||||||
{
|
{
|
||||||
public class ProfileImageModel
|
public class ProfileImageModel : BaseModel
|
||||||
{
|
{
|
||||||
public ProfileImageModel(string name, byte[] data)
|
public ProfileImageModel(string name, byte[] data)
|
||||||
{
|
{
|
||||||
@@ -10,5 +13,20 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public byte[] Data { get; set; }
|
public byte[] Data { get; set; }
|
||||||
|
|
||||||
|
private SolidColorBrush _backgroundColor = new(Colors.White);
|
||||||
|
|
||||||
|
public SolidColorBrush BackgroundColor
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _backgroundColor;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_backgroundColor = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,15 +1,8 @@
|
|||||||
using LibHac;
|
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Shim;
|
|
||||||
using LibHac.Ncm;
|
using LibHac.Ncm;
|
||||||
using Ryujinx.Ava.Common;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
|
||||||
using Ryujinx.Ava.UI.Controls;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.Ava.UI.Windows;
|
using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.Ui.App.Common;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -19,10 +12,8 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
{
|
{
|
||||||
public class SaveModel : BaseModel
|
public class SaveModel : BaseModel
|
||||||
{
|
{
|
||||||
private readonly HorizonClient _horizonClient;
|
|
||||||
private long _size;
|
private long _size;
|
||||||
|
|
||||||
public Action DeleteAction { get; set; }
|
|
||||||
public ulong SaveId { get; }
|
public ulong SaveId { get; }
|
||||||
public ProgramId TitleId { get; }
|
public ProgramId TitleId { get; }
|
||||||
public string TitleIdString => $"{TitleId.Value:X16}";
|
public string TitleIdString => $"{TitleId.Value:X16}";
|
||||||
@@ -45,11 +36,29 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
|
|
||||||
public bool SizeAvailable { get; set; }
|
public bool SizeAvailable { get; set; }
|
||||||
|
|
||||||
public string SizeString => $"{((float)_size * 0.000000954):0.###}MB";
|
public string SizeString => GetSizeString();
|
||||||
|
|
||||||
public SaveModel(SaveDataInfo info, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem)
|
private string GetSizeString()
|
||||||
|
{
|
||||||
|
const int scale = 1024;
|
||||||
|
string[] orders = { "GiB", "MiB", "KiB" };
|
||||||
|
long max = (long)Math.Pow(scale, orders.Length);
|
||||||
|
|
||||||
|
foreach (string order in orders)
|
||||||
|
{
|
||||||
|
if (Size > max)
|
||||||
|
{
|
||||||
|
return $"{decimal.Divide(Size, max):##.##} {order}";
|
||||||
|
}
|
||||||
|
|
||||||
|
max /= scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "0 KiB";
|
||||||
|
}
|
||||||
|
|
||||||
|
public SaveModel(SaveDataInfo info, VirtualFileSystem virtualFileSystem)
|
||||||
{
|
{
|
||||||
_horizonClient = horizonClient;
|
|
||||||
SaveId = info.SaveDataId;
|
SaveId = info.SaveDataId;
|
||||||
TitleId = info.ProgramId;
|
TitleId = info.ProgramId;
|
||||||
UserId = info.UserId;
|
UserId = info.UserId;
|
||||||
@@ -99,25 +108,5 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenLocation()
|
|
||||||
{
|
|
||||||
ApplicationHelper.OpenSaveDir(SaveId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void Delete()
|
|
||||||
{
|
|
||||||
var result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DeleteUserSave],
|
|
||||||
LocaleManager.Instance[LocaleKeys.IrreversibleActionNote],
|
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo], "");
|
|
||||||
|
|
||||||
if (result == UserResult.Yes)
|
|
||||||
{
|
|
||||||
_horizonClient.Fs.DeleteSaveData(SaveDataSpaceId.User, SaveId);
|
|
||||||
|
|
||||||
DeleteAction?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -7,10 +7,12 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
public class TempProfile : BaseModel
|
public class TempProfile : BaseModel
|
||||||
{
|
{
|
||||||
private readonly UserProfile _profile;
|
private readonly UserProfile _profile;
|
||||||
private byte[] _image = null;
|
private byte[] _image;
|
||||||
private string _name = String.Empty;
|
private string _name = String.Empty;
|
||||||
private UserId _userId;
|
private UserId _userId;
|
||||||
|
|
||||||
|
public uint MaxProfileNameLength => 0x20;
|
||||||
|
|
||||||
public byte[] Image
|
public byte[] Image
|
||||||
{
|
{
|
||||||
get => _image;
|
get => _image;
|
||||||
@@ -28,9 +30,12 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
{
|
{
|
||||||
_userId = value;
|
_userId = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(UserIdString));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string UserIdString => _userId.ToString();
|
||||||
|
|
||||||
public string Name
|
public string Name
|
||||||
{
|
{
|
||||||
get => _name;
|
get => _name;
|
||||||
@@ -52,7 +57,5 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
UserId = profile.UserId;
|
UserId = profile.UserId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TempProfile(){}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -3,23 +3,17 @@ using Ryujinx.Ava.Common.Locale;
|
|||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Models
|
namespace Ryujinx.Ava.UI.Models
|
||||||
{
|
{
|
||||||
internal class TitleUpdateModel
|
public class TitleUpdateModel
|
||||||
{
|
{
|
||||||
public bool IsEnabled { get; set; }
|
|
||||||
public bool IsNoUpdate { get; }
|
|
||||||
public ApplicationControlProperty Control { get; }
|
public ApplicationControlProperty Control { get; }
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
|
|
||||||
public string Label => IsNoUpdate
|
public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString());
|
||||||
? LocaleManager.Instance[LocaleKeys.NoUpdate]
|
|
||||||
: string.Format(LocaleManager.Instance[LocaleKeys.TitleUpdateVersionLabel], Control.DisplayVersionString.ToString(),
|
|
||||||
Path);
|
|
||||||
|
|
||||||
public TitleUpdateModel(ApplicationControlProperty control, string path, bool isNoUpdate = false)
|
public TitleUpdateModel(ApplicationControlProperty control, string path)
|
||||||
{
|
{
|
||||||
Control = control;
|
Control = control;
|
||||||
Path = path;
|
Path = path;
|
||||||
IsNoUpdate = isNoUpdate;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,7 @@
|
|||||||
|
using Avalonia.Media;
|
||||||
using Ryujinx.Ava.UI.Controls;
|
using Ryujinx.Ava.UI.Controls;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using Ryujinx.Ava.UI.Views.User;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
|
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
|
||||||
|
|
||||||
@@ -12,6 +14,8 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
private byte[] _image;
|
private byte[] _image;
|
||||||
private string _name;
|
private string _name;
|
||||||
private UserId _userId;
|
private UserId _userId;
|
||||||
|
private bool _isPointerOver;
|
||||||
|
private IBrush _backgroundColor;
|
||||||
|
|
||||||
public byte[] Image
|
public byte[] Image
|
||||||
{
|
{
|
||||||
@@ -43,27 +47,57 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsPointerOver
|
||||||
|
{
|
||||||
|
get => _isPointerOver;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isPointerOver = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IBrush BackgroundColor
|
||||||
|
{
|
||||||
|
get => _backgroundColor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_backgroundColor = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public UserProfile(Profile profile, NavigationDialogHost owner)
|
public UserProfile(Profile profile, NavigationDialogHost owner)
|
||||||
{
|
{
|
||||||
_profile = profile;
|
_profile = profile;
|
||||||
_owner = owner;
|
_owner = owner;
|
||||||
|
|
||||||
|
UpdateBackground();
|
||||||
|
|
||||||
Image = profile.Image;
|
Image = profile.Image;
|
||||||
Name = profile.Name;
|
Name = profile.Name;
|
||||||
UserId = profile.UserId;
|
UserId = profile.UserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsOpened => _profile.AccountState == AccountState.Open;
|
|
||||||
|
|
||||||
public void UpdateState()
|
public void UpdateState()
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsOpened));
|
UpdateBackground();
|
||||||
OnPropertyChanged(nameof(Name));
|
OnPropertyChanged(nameof(Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateBackground()
|
||||||
|
{
|
||||||
|
Avalonia.Application.Current.Styles.TryGetResource("ControlFillColorSecondary", out object color);
|
||||||
|
|
||||||
|
if (color is not null)
|
||||||
|
{
|
||||||
|
BackgroundColor = _profile.AccountState == AccountState.Open ? new SolidColorBrush((Color)color) : Brushes.Transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Recover(UserProfile userProfile)
|
public void Recover(UserProfile userProfile)
|
||||||
{
|
{
|
||||||
_owner.Navigate(typeof(UserEditor), (_owner, userProfile, true));
|
_owner.Navigate(typeof(UserEditorView), (_owner, userProfile, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
266
Ryujinx.Ava/UI/Renderer/EmbeddedWindow.cs
Normal file
266
Ryujinx.Ava/UI/Renderer/EmbeddedWindow.cs
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Platform;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
|
using SPB.Graphics;
|
||||||
|
using SPB.Platform;
|
||||||
|
using SPB.Platform.GLX;
|
||||||
|
using SPB.Platform.X11;
|
||||||
|
using SPB.Windowing;
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Renderer
|
||||||
|
{
|
||||||
|
public class EmbeddedWindow : NativeControlHost
|
||||||
|
{
|
||||||
|
private WindowProc _wndProcDelegate;
|
||||||
|
private string _className;
|
||||||
|
|
||||||
|
protected GLXWindow X11Window { get; set; }
|
||||||
|
|
||||||
|
protected IntPtr WindowHandle { get; set; }
|
||||||
|
protected IntPtr X11Display { get; set; }
|
||||||
|
protected IntPtr NsView { get; set; }
|
||||||
|
protected IntPtr MetalLayer { get; set; }
|
||||||
|
|
||||||
|
private UpdateBoundsCallbackDelegate _updateBoundsCallback;
|
||||||
|
|
||||||
|
public event EventHandler<IntPtr> WindowCreated;
|
||||||
|
public event EventHandler<Size> SizeChanged;
|
||||||
|
|
||||||
|
public EmbeddedWindow()
|
||||||
|
{
|
||||||
|
this.GetObservable(BoundsProperty).Subscribe(StateChanged);
|
||||||
|
|
||||||
|
Initialized += OnNativeEmbeddedWindowCreated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void OnWindowCreated() { }
|
||||||
|
|
||||||
|
protected virtual void OnWindowDestroyed() { }
|
||||||
|
|
||||||
|
protected virtual void OnWindowDestroying()
|
||||||
|
{
|
||||||
|
WindowHandle = IntPtr.Zero;
|
||||||
|
X11Display = IntPtr.Zero;
|
||||||
|
NsView = IntPtr.Zero;
|
||||||
|
MetalLayer = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNativeEmbeddedWindowCreated(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
OnWindowCreated();
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
WindowCreated?.Invoke(this, WindowHandle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StateChanged(Rect rect)
|
||||||
|
{
|
||||||
|
SizeChanged?.Invoke(this, rect.Size);
|
||||||
|
_updateBoundsCallback?.Invoke(rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle control)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return CreateLinux(control);
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return CreateWin32(control);
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
return CreateMacOS();
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.CreateNativeControlCore(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DestroyNativeControlCore(IPlatformHandle control)
|
||||||
|
{
|
||||||
|
OnWindowDestroying();
|
||||||
|
|
||||||
|
if (OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
DestroyLinux();
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
DestroyWin32(control);
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
DestroyMacOS();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
base.DestroyNativeControlCore(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnWindowDestroyed();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("linux")]
|
||||||
|
private IPlatformHandle CreateLinux(IPlatformHandle control)
|
||||||
|
{
|
||||||
|
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
|
||||||
|
{
|
||||||
|
X11Window = new GLXWindow(new NativeHandle(X11.DefaultDisplay), new NativeHandle(control.Handle));
|
||||||
|
X11Window.Hide();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
X11Window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100) as GLXWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowHandle = X11Window.WindowHandle.RawHandle;
|
||||||
|
X11Display = X11Window.DisplayHandle.RawHandle;
|
||||||
|
|
||||||
|
return new PlatformHandle(WindowHandle, "X11");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
IPlatformHandle CreateWin32(IPlatformHandle control)
|
||||||
|
{
|
||||||
|
_className = "NativeWindow-" + Guid.NewGuid();
|
||||||
|
|
||||||
|
_wndProcDelegate = delegate (IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam)
|
||||||
|
{
|
||||||
|
if (VisualRoot != null)
|
||||||
|
{
|
||||||
|
if (msg == WindowsMessages.LBUTTONDOWN ||
|
||||||
|
msg == WindowsMessages.RBUTTONDOWN ||
|
||||||
|
msg == WindowsMessages.LBUTTONUP ||
|
||||||
|
msg == WindowsMessages.RBUTTONUP ||
|
||||||
|
msg == WindowsMessages.MOUSEMOVE)
|
||||||
|
{
|
||||||
|
Point rootVisualPosition = this.TranslatePoint(new Point((long)lParam & 0xFFFF, (long)lParam >> 16 & 0xFFFF), VisualRoot).Value;
|
||||||
|
Pointer pointer = new(0, PointerType.Mouse, true);
|
||||||
|
|
||||||
|
switch (msg)
|
||||||
|
{
|
||||||
|
case WindowsMessages.LBUTTONDOWN:
|
||||||
|
case WindowsMessages.RBUTTONDOWN:
|
||||||
|
{
|
||||||
|
bool isLeft = msg == WindowsMessages.LBUTTONDOWN;
|
||||||
|
RawInputModifiers pointerPointModifier = isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton;
|
||||||
|
PointerPointProperties properties = new(pointerPointModifier, isLeft ? PointerUpdateKind.LeftButtonPressed : PointerUpdateKind.RightButtonPressed);
|
||||||
|
|
||||||
|
var evnt = new PointerPressedEventArgs(
|
||||||
|
this,
|
||||||
|
pointer,
|
||||||
|
VisualRoot,
|
||||||
|
rootVisualPosition,
|
||||||
|
(ulong)Environment.TickCount64,
|
||||||
|
properties,
|
||||||
|
KeyModifiers.None);
|
||||||
|
|
||||||
|
RaiseEvent(evnt);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WindowsMessages.LBUTTONUP:
|
||||||
|
case WindowsMessages.RBUTTONUP:
|
||||||
|
{
|
||||||
|
bool isLeft = msg == WindowsMessages.LBUTTONUP;
|
||||||
|
RawInputModifiers pointerPointModifier = isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton;
|
||||||
|
PointerPointProperties properties = new(pointerPointModifier, isLeft ? PointerUpdateKind.LeftButtonReleased : PointerUpdateKind.RightButtonReleased);
|
||||||
|
|
||||||
|
var evnt = new PointerReleasedEventArgs(
|
||||||
|
this,
|
||||||
|
pointer,
|
||||||
|
VisualRoot,
|
||||||
|
rootVisualPosition,
|
||||||
|
(ulong)Environment.TickCount64,
|
||||||
|
properties,
|
||||||
|
KeyModifiers.None,
|
||||||
|
isLeft ? MouseButton.Left : MouseButton.Right);
|
||||||
|
|
||||||
|
RaiseEvent(evnt);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WindowsMessages.MOUSEMOVE:
|
||||||
|
{
|
||||||
|
var evnt = new PointerEventArgs(
|
||||||
|
PointerMovedEvent,
|
||||||
|
this,
|
||||||
|
pointer,
|
||||||
|
VisualRoot,
|
||||||
|
rootVisualPosition,
|
||||||
|
(ulong)Environment.TickCount64,
|
||||||
|
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other),
|
||||||
|
KeyModifiers.None);
|
||||||
|
|
||||||
|
RaiseEvent(evnt);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(hWnd, msg, wParam, lParam);
|
||||||
|
};
|
||||||
|
|
||||||
|
WNDCLASSEX wndClassEx = new()
|
||||||
|
{
|
||||||
|
cbSize = Marshal.SizeOf<WNDCLASSEX>(),
|
||||||
|
hInstance = GetModuleHandle(null),
|
||||||
|
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate),
|
||||||
|
style = ClassStyles.CS_OWNDC,
|
||||||
|
lpszClassName = Marshal.StringToHGlobalUni(_className),
|
||||||
|
hCursor = CreateArrowCursor()
|
||||||
|
};
|
||||||
|
|
||||||
|
RegisterClassEx(ref wndClassEx);
|
||||||
|
|
||||||
|
WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WS_CHILD, 0, 0, 640, 480, control.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
||||||
|
|
||||||
|
Marshal.FreeHGlobal(wndClassEx.lpszClassName);
|
||||||
|
|
||||||
|
return new PlatformHandle(WindowHandle, "HWND");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("macos")]
|
||||||
|
IPlatformHandle CreateMacOS()
|
||||||
|
{
|
||||||
|
MetalLayer = MetalHelper.GetMetalLayer(out IntPtr nsView, out _updateBoundsCallback);
|
||||||
|
|
||||||
|
NsView = nsView;
|
||||||
|
|
||||||
|
return new PlatformHandle(nsView, "NSView");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("Linux")]
|
||||||
|
void DestroyLinux()
|
||||||
|
{
|
||||||
|
X11Window?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
void DestroyWin32(IPlatformHandle handle)
|
||||||
|
{
|
||||||
|
DestroyWindow(handle.Handle);
|
||||||
|
UnregisterClass(_className, GetModuleHandle(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("macos")]
|
||||||
|
void DestroyMacOS()
|
||||||
|
{
|
||||||
|
MetalHelper.DestroyMetalLayer(NsView, MetalLayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,8 @@
|
|||||||
using OpenTK.Graphics.OpenGL;
|
using OpenTK.Graphics.OpenGL;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Graphics.GAL;
|
||||||
|
using Ryujinx.Graphics.OpenGL;
|
||||||
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using SPB.Graphics;
|
using SPB.Graphics;
|
||||||
using SPB.Graphics.OpenGL;
|
using SPB.Graphics.OpenGL;
|
||||||
using SPB.Platform;
|
using SPB.Platform;
|
||||||
@@ -7,26 +10,20 @@ using SPB.Platform.WGL;
|
|||||||
using SPB.Windowing;
|
using SPB.Windowing;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
namespace Ryujinx.Ava.UI.Renderer
|
||||||
{
|
{
|
||||||
public class OpenGLEmbeddedWindow : EmbeddedWindow
|
public class EmbeddedWindowOpenGL : EmbeddedWindow
|
||||||
{
|
{
|
||||||
private readonly int _major;
|
|
||||||
private readonly int _minor;
|
|
||||||
private readonly GraphicsDebugLevel _graphicsDebugLevel;
|
|
||||||
private SwappableNativeWindowBase _window;
|
private SwappableNativeWindowBase _window;
|
||||||
|
|
||||||
public OpenGLContextBase Context { get; set; }
|
public OpenGLContextBase Context { get; set; }
|
||||||
|
|
||||||
public OpenGLEmbeddedWindow(int major, int minor, GraphicsDebugLevel graphicsDebugLevel)
|
public EmbeddedWindowOpenGL() { }
|
||||||
{
|
|
||||||
_major = major;
|
|
||||||
_minor = minor;
|
|
||||||
_graphicsDebugLevel = graphicsDebugLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnWindowDestroying()
|
protected override void OnWindowDestroying()
|
||||||
{
|
{
|
||||||
Context.Dispose();
|
Context.Dispose();
|
||||||
|
|
||||||
base.OnWindowDestroying();
|
base.OnWindowDestroying();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,19 +45,20 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var flags = OpenGLContextFlags.Compat;
|
var flags = OpenGLContextFlags.Compat;
|
||||||
if (_graphicsDebugLevel != GraphicsDebugLevel.None)
|
if (ConfigurationState.Instance.Logger.GraphicsDebugLevel != GraphicsDebugLevel.None)
|
||||||
{
|
{
|
||||||
flags |= OpenGLContextFlags.Debug;
|
flags |= OpenGLContextFlags.Debug;
|
||||||
}
|
}
|
||||||
|
|
||||||
Context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, _major, _minor, flags);
|
var graphicsMode = Environment.OSVersion.Platform == PlatformID.Unix ? new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false) : FramebufferFormat.Default;
|
||||||
|
|
||||||
|
Context = PlatformHelper.CreateOpenGLContext(graphicsMode, 3, 3, flags);
|
||||||
|
|
||||||
Context.Initialize(_window);
|
Context.Initialize(_window);
|
||||||
Context.MakeCurrent(_window);
|
Context.MakeCurrent(_window);
|
||||||
|
|
||||||
var bindingsContext = new OpenToolkitBindingsContext(Context.GetProcAddress);
|
GL.LoadBindings(new OpenTKBindingsContext(Context.GetProcAddress));
|
||||||
|
|
||||||
GL.LoadBindings(bindingsContext);
|
|
||||||
Context.MakeCurrent(null);
|
Context.MakeCurrent(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +74,14 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
|
|
||||||
public void SwapBuffers()
|
public void SwapBuffers()
|
||||||
{
|
{
|
||||||
_window.SwapBuffers();
|
_window?.SwapBuffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitializeBackgroundContext(IRenderer renderer)
|
||||||
|
{
|
||||||
|
(renderer as OpenGLRenderer)?.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(Context));
|
||||||
|
|
||||||
|
MakeCurrent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
42
Ryujinx.Ava/UI/Renderer/EmbeddedWindowVulkan.cs
Normal file
42
Ryujinx.Ava/UI/Renderer/EmbeddedWindowVulkan.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Silk.NET.Vulkan;
|
||||||
|
using SPB.Graphics.Vulkan;
|
||||||
|
using SPB.Platform.Metal;
|
||||||
|
using SPB.Platform.Win32;
|
||||||
|
using SPB.Platform.X11;
|
||||||
|
using SPB.Windowing;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Renderer
|
||||||
|
{
|
||||||
|
public class EmbeddedWindowVulkan : EmbeddedWindow
|
||||||
|
{
|
||||||
|
public SurfaceKHR CreateSurface(Instance instance)
|
||||||
|
{
|
||||||
|
NativeWindowBase nativeWindowBase;
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
nativeWindowBase = new SimpleWin32Window(new NativeHandle(WindowHandle));
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
nativeWindowBase = new SimpleX11Window(new NativeHandle(X11Display), new NativeHandle(WindowHandle));
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
nativeWindowBase = new SimpleMetalWindow(new NativeHandle(NsView), new NativeHandle(MetalLayer));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new PlatformNotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SurfaceKHR((ulong?)VulkanHelper.CreateWindowSurface(instance.Handle, nativeWindowBase));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SurfaceKHR CreateSurface(Instance instance, Vk api)
|
||||||
|
{
|
||||||
|
return CreateSurface(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,13 @@
|
|||||||
using OpenTK;
|
using OpenTK;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
namespace Ryujinx.Ava.UI.Renderer
|
||||||
{
|
{
|
||||||
internal class OpenToolkitBindingsContext : IBindingsContext
|
internal class OpenTKBindingsContext : IBindingsContext
|
||||||
{
|
{
|
||||||
private readonly Func<string, IntPtr> _getProcAddress;
|
private readonly Func<string, IntPtr> _getProcAddress;
|
||||||
|
|
||||||
public OpenToolkitBindingsContext(Func<string, IntPtr> getProcAddress)
|
public OpenTKBindingsContext(Func<string, IntPtr> getProcAddress)
|
||||||
{
|
{
|
||||||
_getProcAddress = getProcAddress;
|
_getProcAddress = getProcAddress;
|
||||||
}
|
}
|
@@ -6,6 +6,6 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
d:DesignHeight="450"
|
d:DesignHeight="450"
|
||||||
x:Class="Ryujinx.Ava.UI.Controls.RendererHost"
|
x:Class="Ryujinx.Ava.UI.Renderer.RendererHost"
|
||||||
Focusable="True">
|
Focusable="True">
|
||||||
</UserControl>
|
</UserControl>
|
68
Ryujinx.Ava/UI/Renderer/RendererHost.axaml.cs
Normal file
68
Ryujinx.Ava/UI/Renderer/RendererHost.axaml.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Renderer
|
||||||
|
{
|
||||||
|
public partial class RendererHost : UserControl, IDisposable
|
||||||
|
{
|
||||||
|
public readonly EmbeddedWindow EmbeddedWindow;
|
||||||
|
|
||||||
|
public event EventHandler<EventArgs> WindowCreated;
|
||||||
|
public event Action<object, Size> SizeChanged;
|
||||||
|
|
||||||
|
public RendererHost()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl)
|
||||||
|
{
|
||||||
|
EmbeddedWindow = new EmbeddedWindowOpenGL();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EmbeddedWindow = new EmbeddedWindowVulkan();
|
||||||
|
}
|
||||||
|
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Initialize()
|
||||||
|
{
|
||||||
|
EmbeddedWindow.WindowCreated += CurrentWindow_WindowCreated;
|
||||||
|
EmbeddedWindow.SizeChanged += CurrentWindow_SizeChanged;
|
||||||
|
|
||||||
|
Content = EmbeddedWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (EmbeddedWindow != null)
|
||||||
|
{
|
||||||
|
EmbeddedWindow.WindowCreated -= CurrentWindow_WindowCreated;
|
||||||
|
EmbeddedWindow.SizeChanged -= CurrentWindow_SizeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDetachedFromVisualTree(e);
|
||||||
|
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CurrentWindow_SizeChanged(object sender, Size e)
|
||||||
|
{
|
||||||
|
SizeChanged?.Invoke(sender, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CurrentWindow_WindowCreated(object sender, IntPtr e)
|
||||||
|
{
|
||||||
|
WindowCreated?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -5,17 +5,17 @@ using SPB.Graphics.OpenGL;
|
|||||||
using SPB.Platform;
|
using SPB.Platform;
|
||||||
using SPB.Windowing;
|
using SPB.Windowing;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
namespace Ryujinx.Ava.UI.Renderer
|
||||||
{
|
{
|
||||||
class SPBOpenGLContext : IOpenGLContext
|
class SPBOpenGLContext : IOpenGLContext
|
||||||
{
|
{
|
||||||
private OpenGLContextBase _context;
|
private readonly OpenGLContextBase _context;
|
||||||
private NativeWindowBase _window;
|
private readonly NativeWindowBase _window;
|
||||||
|
|
||||||
private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window)
|
private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_window = window;
|
_window = window;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -32,12 +32,12 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext)
|
public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext)
|
||||||
{
|
{
|
||||||
OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext);
|
OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext);
|
||||||
NativeWindowBase window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
|
NativeWindowBase window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
|
||||||
|
|
||||||
context.Initialize(window);
|
context.Initialize(window);
|
||||||
context.MakeCurrent(window);
|
context.MakeCurrent(window);
|
||||||
|
|
||||||
GL.LoadBindings(new OpenToolkitBindingsContext(context.GetProcAddress));
|
GL.LoadBindings(new OpenTKBindingsContext(context.GetProcAddress));
|
||||||
|
|
||||||
context.MakeCurrent(null);
|
context.MakeCurrent(null);
|
||||||
|
|
@@ -81,10 +81,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Developers
|
public string Developers => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.AboutPageDeveloperListMore, "gdkchan, Ac_K, marysaka, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, GoffyDude, TSRBerry, IsaacMarovitz");
|
||||||
{
|
|
||||||
get => string.Format(LocaleManager.Instance[LocaleKeys.AboutPageDeveloperListMore], "gdkchan, Ac_K, marysaka, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, GoffyDude, TSRBerry, IsaacMarovitz");
|
|
||||||
}
|
|
||||||
|
|
||||||
public AboutWindowViewModel()
|
public AboutWindowViewModel()
|
||||||
{
|
{
|
||||||
|
@@ -3,6 +3,8 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Svg.Skia;
|
using Avalonia.Svg.Skia;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using LibHac.Bcat;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Input;
|
using Ryujinx.Ava.Input;
|
||||||
using Ryujinx.Ava.UI.Controls;
|
using Ryujinx.Ava.UI.Controls;
|
||||||
@@ -435,7 +437,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
if (str.Length > MaxSize)
|
if (str.Length > MaxSize)
|
||||||
{
|
{
|
||||||
return str.Substring(0, MaxSize - Ellipsis.Length) + Ellipsis;
|
return $"{str.AsSpan(0, MaxSize - Ellipsis.Length)}{Ellipsis}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return str;
|
return str;
|
||||||
@@ -717,7 +719,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Configuration, $"Profile {ProfileName} is incompatible with the current input configuration system.");
|
Logger.Error?.Print(LogClass.Configuration, $"Profile {ProfileName} is incompatible with the current input configuration system.");
|
||||||
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileErrorMessage], ProfileName));
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogProfileInvalidProfileErrorMessage, ProfileName));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -5,14 +5,17 @@ using Avalonia.Media;
|
|||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Binding;
|
using DynamicData.Binding;
|
||||||
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Input;
|
using Ryujinx.Ava.Input;
|
||||||
using Ryujinx.Ava.UI.Controls;
|
using Ryujinx.Ava.UI.Controls;
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using Ryujinx.Ava.UI.Renderer;
|
||||||
using Ryujinx.Ava.UI.Windows;
|
using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
@@ -87,7 +90,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
private float _volume;
|
private float _volume;
|
||||||
private string _backendText;
|
private string _backendText;
|
||||||
|
|
||||||
private bool _canUpdate;
|
private bool _canUpdate = true;
|
||||||
private Cursor _cursor;
|
private Cursor _cursor;
|
||||||
private string _title;
|
private string _title;
|
||||||
private string _currentEmulatedGamePath;
|
private string _currentEmulatedGamePath;
|
||||||
@@ -176,11 +179,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public bool CanUpdate
|
public bool CanUpdate
|
||||||
{
|
{
|
||||||
get => _canUpdate;
|
get => _canUpdate && EnableNonGameRunningControls && Modules.Updater.CanUpdate(false);
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_canUpdate = value;
|
_canUpdate = value;
|
||||||
|
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,6 +344,12 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool EnabledUserSaveDirectory => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
||||||
|
|
||||||
|
public bool EnabledDeviceSaveDirectory => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0;
|
||||||
|
|
||||||
|
public bool EnabledBcatSaveDirectory => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
||||||
|
|
||||||
public string LoadHeading
|
public string LoadHeading
|
||||||
{
|
{
|
||||||
get => _loadHeading;
|
get => _loadHeading;
|
||||||
@@ -675,6 +683,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
get => ConsoleHelper.SetConsoleWindowStateSupported;
|
get => ConsoleHelper.SetConsoleWindowStateSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ManageFileTypesVisible
|
||||||
|
{
|
||||||
|
get => FileAssociationHelper.IsTypeAssociationSupported;
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableCollection<ApplicationData> Applications
|
public ObservableCollection<ApplicationData> Applications
|
||||||
{
|
{
|
||||||
get => _applications;
|
get => _applications;
|
||||||
@@ -733,19 +746,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
switch (ConfigurationState.Instance.Ui.GridSize)
|
return ConfigurationState.Instance.Ui.GridSize.Value switch
|
||||||
{
|
{
|
||||||
case 1:
|
1 => 78,
|
||||||
return 78;
|
2 => 100,
|
||||||
case 2:
|
3 => 120,
|
||||||
return 100;
|
4 => 140,
|
||||||
case 3:
|
_ => 16,
|
||||||
return 120;
|
};
|
||||||
case 4:
|
|
||||||
return 140;
|
|
||||||
default:
|
|
||||||
return 16;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,19 +761,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
switch (ConfigurationState.Instance.Ui.GridSize)
|
return ConfigurationState.Instance.Ui.GridSize.Value switch
|
||||||
{
|
{
|
||||||
case 1:
|
1 => 120,
|
||||||
return 120;
|
2 => ShowNames ? 210 : 150,
|
||||||
case 2:
|
3 => ShowNames ? 240 : 180,
|
||||||
return ShowNames ? 210 : 150;
|
4 => ShowNames ? 280 : 220,
|
||||||
case 3:
|
_ => 16,
|
||||||
return ShowNames ? 240 : 180;
|
};
|
||||||
case 4:
|
|
||||||
return ShowNames ? 280 : 220;
|
|
||||||
default:
|
|
||||||
return 16;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,7 +873,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
public Action<bool> SwitchToGameControl { get; private set; }
|
public Action<bool> SwitchToGameControl { get; private set; }
|
||||||
public Action<Control> SetMainContent { get; private set; }
|
public Action<Control> SetMainContent { get; private set; }
|
||||||
public TopLevel TopLevel { get; private set; }
|
public TopLevel TopLevel { get; private set; }
|
||||||
public RendererHost RendererControl { get; private set; }
|
public RendererHost RendererHostControl { get; private set; }
|
||||||
public bool IsClosing { get; set; }
|
public bool IsClosing { get; set; }
|
||||||
public LibHacHorizonManager LibHacHorizonManager { get; internal set; }
|
public LibHacHorizonManager LibHacHorizonManager { get; internal set; }
|
||||||
public IHostUiHandler UiHandler { get; internal set; }
|
public IHostUiHandler UiHandler { get; internal set; }
|
||||||
@@ -947,20 +950,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
if (firmwareVersion == null)
|
if (firmwareVersion == null)
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareNotFoundErrorMessage], filename));
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallerFirmwareNotFoundErrorMessage, filename));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string dialogTitle = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallTitle], firmwareVersion.VersionString);
|
string dialogTitle = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallerFirmwareInstallTitle, firmwareVersion.VersionString);
|
||||||
|
string dialogMessage = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallerFirmwareInstallMessage, firmwareVersion.VersionString);
|
||||||
|
|
||||||
SystemVersion currentVersion = ContentManager.GetCurrentFirmwareVersion();
|
SystemVersion currentVersion = ContentManager.GetCurrentFirmwareVersion();
|
||||||
|
|
||||||
string dialogMessage = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallMessage], firmwareVersion.VersionString);
|
|
||||||
|
|
||||||
if (currentVersion != null)
|
if (currentVersion != null)
|
||||||
{
|
{
|
||||||
dialogMessage += string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallSubMessage], currentVersion.VersionString);
|
dialogMessage += LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallerFirmwareInstallSubMessage, currentVersion.VersionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogMessage += LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallConfirmMessage];
|
dialogMessage += LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallConfirmMessage];
|
||||||
@@ -993,7 +994,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
waitingDialog.Close();
|
waitingDialog.Close();
|
||||||
|
|
||||||
string message = string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallSuccessMessage], firmwareVersion.VersionString);
|
string message = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallerFirmwareInstallSuccessMessage, firmwareVersion.VersionString);
|
||||||
|
|
||||||
await ContentDialogHelper.CreateInfoDialog(dialogTitle, message, LocaleManager.Instance[LocaleKeys.InputDialogOk], "", LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
await ContentDialogHelper.CreateInfoDialog(dialogTitle, message, LocaleManager.Instance[LocaleKeys.InputDialogOk], "", LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
||||||
|
|
||||||
@@ -1063,7 +1064,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
IsLoadingIndeterminate = false;
|
IsLoadingIndeterminate = false;
|
||||||
break;
|
break;
|
||||||
case LoadState.Loaded:
|
case LoadState.Loaded:
|
||||||
LoadHeading = string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], TitleName);
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
|
||||||
IsLoadingIndeterminate = true;
|
IsLoadingIndeterminate = true;
|
||||||
CacheLoadStatus = "";
|
CacheLoadStatus = "";
|
||||||
break;
|
break;
|
||||||
@@ -1079,7 +1080,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
IsLoadingIndeterminate = false;
|
IsLoadingIndeterminate = false;
|
||||||
break;
|
break;
|
||||||
case ShaderCacheLoadingState.Loaded:
|
case ShaderCacheLoadingState.Loaded:
|
||||||
LoadHeading = string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], TitleName);
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
|
||||||
IsLoadingIndeterminate = true;
|
IsLoadingIndeterminate = true;
|
||||||
CacheLoadStatus = "";
|
CacheLoadStatus = "";
|
||||||
break;
|
break;
|
||||||
@@ -1091,35 +1092,27 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenSaveDirectory(in SaveDataFilter filter, ApplicationData data, ulong titleId)
|
|
||||||
{
|
|
||||||
ApplicationHelper.OpenSaveDir(in filter, titleId, data.ControlHolder, data.TitleName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void ExtractLogo()
|
private async void ExtractLogo()
|
||||||
{
|
{
|
||||||
var selection = SelectedApplication;
|
if (SelectedApplication != null)
|
||||||
if (selection != null)
|
|
||||||
{
|
{
|
||||||
await ApplicationHelper.ExtractSection(NcaSectionType.Logo, selection.Path);
|
await ApplicationHelper.ExtractSection(NcaSectionType.Logo, SelectedApplication.Path, SelectedApplication.TitleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ExtractRomFs()
|
private async void ExtractRomFs()
|
||||||
{
|
{
|
||||||
var selection = SelectedApplication;
|
if (SelectedApplication != null)
|
||||||
if (selection != null)
|
|
||||||
{
|
{
|
||||||
await ApplicationHelper.ExtractSection(NcaSectionType.Data, selection.Path);
|
await ApplicationHelper.ExtractSection(NcaSectionType.Data, SelectedApplication.Path, SelectedApplication.TitleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ExtractExeFs()
|
private async void ExtractExeFs()
|
||||||
{
|
{
|
||||||
var selection = SelectedApplication;
|
if (SelectedApplication != null)
|
||||||
if (selection != null)
|
|
||||||
{
|
{
|
||||||
await ApplicationHelper.ExtractSection(NcaSectionType.Code, selection.Path);
|
await ApplicationHelper.ExtractSection(NcaSectionType.Code, SelectedApplication.Path, SelectedApplication.TitleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,7 +1137,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
private void InitializeGame()
|
private void InitializeGame()
|
||||||
{
|
{
|
||||||
RendererControl.RendererInitialized += GlRenderer_Created;
|
RendererHostControl.WindowCreated += RendererHost_Created;
|
||||||
|
|
||||||
AppHost.StatusUpdatedEvent += Update_StatusBar;
|
AppHost.StatusUpdatedEvent += Update_StatusBar;
|
||||||
AppHost.AppExit += AppHost_AppExit;
|
AppHost.AppExit += AppHost_AppExit;
|
||||||
@@ -1203,7 +1196,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GlRenderer_Created(object sender, EventArgs e)
|
private void RendererHost_Created(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
ShowLoading(false);
|
ShowLoading(false);
|
||||||
|
|
||||||
@@ -1333,10 +1326,15 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeLanguage(object obj)
|
public void ChangeLanguage(object languageCode)
|
||||||
{
|
{
|
||||||
LocaleManager.Instance.LoadDefaultLanguage();
|
LocaleManager.Instance.LoadLanguage((string)languageCode);
|
||||||
LocaleManager.Instance.LoadLanguage((string)obj);
|
|
||||||
|
if (Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
ConfigurationState.Instance.Ui.LanguageCode.Value = (string)languageCode;
|
||||||
|
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void ManageProfiles()
|
public async void ManageProfiles()
|
||||||
@@ -1374,7 +1372,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
// FIXME: Found a way to reproduce the bold effect on the title name (fork?).
|
// FIXME: Found a way to reproduce the bold effect on the title name (fork?).
|
||||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning],
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning],
|
||||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogPPTCDeletionMessage], selection.TitleName),
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, selection.TitleName),
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||||
@@ -1401,7 +1399,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogPPTCDeletionErrorMessage], file.Name, e));
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1438,7 +1436,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
// FIXME: Found a way to reproduce the bold effect on the title name (fork?).
|
// FIXME: Found a way to reproduce the bold effect on the title name (fork?).
|
||||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning],
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning],
|
||||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogShaderDeletionMessage], selection.TitleName),
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, selection.TitleName),
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||||
@@ -1463,7 +1461,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogPPTCDeletionErrorMessage], directory.Name, e));
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, directory.Name, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1476,62 +1474,12 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.ShaderCachePurgeError], file.Name, e));
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.ShaderCachePurgeError, file.Name, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenDeviceSaveDirectory()
|
|
||||||
{
|
|
||||||
ApplicationData selection = SelectedApplication;
|
|
||||||
if (selection != null)
|
|
||||||
{
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
|
|
||||||
{
|
|
||||||
async void Action()
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(Action);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, SaveDataType.Device, userId: default, saveDataId: default, index: default);
|
|
||||||
OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenBcatSaveDirectory()
|
|
||||||
{
|
|
||||||
ApplicationData selection = SelectedApplication;
|
|
||||||
if (selection != null)
|
|
||||||
{
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
|
|
||||||
{
|
|
||||||
async void Action()
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(Action);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
|
|
||||||
OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ToggleFavorite()
|
public void ToggleFavorite()
|
||||||
{
|
{
|
||||||
ApplicationData selection = SelectedApplication;
|
ApplicationData selection = SelectedApplication;
|
||||||
@@ -1550,37 +1498,45 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public void OpenUserSaveDirectory()
|
public void OpenUserSaveDirectory()
|
||||||
{
|
{
|
||||||
ApplicationData selection = SelectedApplication;
|
OpenSaveDirectory(SaveDataType.Account, userId: new UserId((ulong)AccountManager.LastOpenedUser.UserId.High, (ulong)AccountManager.LastOpenedUser.UserId.Low));
|
||||||
if (selection != null)
|
}
|
||||||
|
|
||||||
|
public void OpenDeviceSaveDirectory()
|
||||||
|
{
|
||||||
|
OpenSaveDirectory(SaveDataType.Device, userId: default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenBcatSaveDirectory()
|
||||||
|
{
|
||||||
|
OpenSaveDirectory(SaveDataType.Bcat, userId: default);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenSaveDirectory(SaveDataType saveDataType, UserId userId)
|
||||||
|
{
|
||||||
|
if (SelectedApplication != null)
|
||||||
{
|
{
|
||||||
Task.Run(() =>
|
if (!ulong.TryParse(SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
|
||||||
{
|
{
|
||||||
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
async void Action()
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
|
||||||
{
|
});
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(Action);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
|
||||||
}
|
|
||||||
|
|
||||||
UserId userId = new((ulong)AccountManager.LastOpenedUser.UserId.High, (ulong)AccountManager.LastOpenedUser.UserId.Low);
|
ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, SelectedApplication.ControlHolder, SelectedApplication.TitleName);
|
||||||
SaveDataFilter saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveType: default, userId, saveDataId: default, index: default);
|
|
||||||
OpenSaveDirectory(in saveDataFilter, selection, titleIdNumber);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenModsDirectory()
|
public void OpenModsDirectory()
|
||||||
{
|
{
|
||||||
ApplicationData selection = SelectedApplication;
|
if (SelectedApplication != null)
|
||||||
if (selection != null)
|
|
||||||
{
|
{
|
||||||
string modsBasePath = VirtualFileSystem.ModLoader.GetModsBasePath();
|
string modsBasePath = VirtualFileSystem.ModLoader.GetModsBasePath();
|
||||||
string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, selection.TitleId);
|
string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, SelectedApplication.TitleId);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
@@ -1588,12 +1544,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public void OpenSdModsDirectory()
|
public void OpenSdModsDirectory()
|
||||||
{
|
{
|
||||||
ApplicationData selection = SelectedApplication;
|
if (SelectedApplication != null)
|
||||||
|
|
||||||
if (selection != null)
|
|
||||||
{
|
{
|
||||||
string sdModsBasePath = VirtualFileSystem.ModLoader.GetSdModsBasePath();
|
string sdModsBasePath = VirtualFileSystem.ModLoader.GetSdModsBasePath();
|
||||||
string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, selection.TitleId);
|
string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, SelectedApplication.TitleId);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
@@ -1601,37 +1555,25 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public async void OpenTitleUpdateManager()
|
public async void OpenTitleUpdateManager()
|
||||||
{
|
{
|
||||||
ApplicationData selection = SelectedApplication;
|
if (SelectedApplication != null)
|
||||||
if (selection != null)
|
|
||||||
{
|
{
|
||||||
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
await TitleUpdateWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
|
||||||
{
|
|
||||||
await new TitleUpdateWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OpenDownloadableContentManager()
|
public async void OpenDownloadableContentManager()
|
||||||
{
|
{
|
||||||
ApplicationData selection = SelectedApplication;
|
if (SelectedApplication != null)
|
||||||
if (selection != null)
|
|
||||||
{
|
{
|
||||||
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName).ShowDialog(TopLevel as Window);
|
||||||
{
|
|
||||||
await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName).ShowDialog(desktop.MainWindow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OpenCheatManager()
|
public async void OpenCheatManager()
|
||||||
{
|
{
|
||||||
ApplicationData selection = SelectedApplication;
|
if (SelectedApplication != null)
|
||||||
if (selection != null)
|
|
||||||
{
|
{
|
||||||
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
await new CheatWindow(VirtualFileSystem, SelectedApplication.TitleId, SelectedApplication.TitleName).ShowDialog(TopLevel as Window);
|
||||||
{
|
|
||||||
await new CheatWindow(VirtualFileSystem, selection.TitleId, selection.TitleName).ShowDialog(desktop.MainWindow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1645,7 +1587,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
StatusBarProgressMaximum = 0;
|
StatusBarProgressMaximum = 0;
|
||||||
StatusBarProgressValue = 0;
|
StatusBarProgressValue = 0;
|
||||||
|
|
||||||
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0);
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
ReloadGameList?.Invoke();
|
ReloadGameList?.Invoke();
|
||||||
@@ -1735,18 +1677,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
PrepareLoadScreen();
|
PrepareLoadScreen();
|
||||||
|
|
||||||
RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel);
|
RendererHostControl = new RendererHost();
|
||||||
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl)
|
|
||||||
{
|
|
||||||
RendererControl.CreateOpenGL();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RendererControl.CreateVulkan();
|
|
||||||
}
|
|
||||||
|
|
||||||
AppHost = new AppHost(
|
AppHost = new AppHost(
|
||||||
RendererControl,
|
RendererHostControl,
|
||||||
InputManager,
|
InputManager,
|
||||||
path,
|
path,
|
||||||
VirtualFileSystem,
|
VirtualFileSystem,
|
||||||
@@ -1767,8 +1701,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
CanUpdate = false;
|
CanUpdate = false;
|
||||||
LoadHeading = string.IsNullOrWhiteSpace(titleName) ? string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], AppHost.Device.Application.TitleName) : titleName;
|
|
||||||
TitleName = string.IsNullOrWhiteSpace(titleName) ? AppHost.Device.Application.TitleName : titleName;
|
LoadHeading = TitleName = titleName;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(titleName))
|
||||||
|
{
|
||||||
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Application.TitleName);
|
||||||
|
TitleName = AppHost.Device.Application.TitleName;
|
||||||
|
}
|
||||||
|
|
||||||
SwitchToRenderer(startFullscreen);
|
SwitchToRenderer(startFullscreen);
|
||||||
|
|
||||||
@@ -1787,9 +1727,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
SwitchToGameControl(startFullscreen);
|
SwitchToGameControl(startFullscreen);
|
||||||
|
|
||||||
SetMainContent(RendererControl);
|
SetMainContent(RendererHostControl);
|
||||||
|
|
||||||
RendererControl.Focus();
|
RendererHostControl.Focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1819,14 +1759,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
if (version != null)
|
if (version != null)
|
||||||
{
|
{
|
||||||
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion,
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarSystemVersion, version.VersionString);
|
||||||
version.VersionString);
|
|
||||||
|
|
||||||
hasApplet = version.Major > 3;
|
hasApplet = version.Major > 3;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion, "0.0");
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarSystemVersion, "0.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
IsAppletMenuActive = hasApplet;
|
IsAppletMenuActive = hasApplet;
|
||||||
@@ -1857,8 +1796,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
HandleRelaunch();
|
HandleRelaunch();
|
||||||
});
|
});
|
||||||
|
|
||||||
RendererControl.RendererInitialized -= GlRenderer_Created;
|
RendererHostControl.WindowCreated -= RendererHost_Created;
|
||||||
RendererControl = null;
|
RendererHostControl = null;
|
||||||
|
|
||||||
SelectedIcon = null;
|
SelectedIcon = null;
|
||||||
|
|
||||||
@@ -1918,7 +1857,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||||
|
|
||||||
if (result != UserResult.Yes)
|
if (result == UserResult.Yes)
|
||||||
{
|
{
|
||||||
ConfigurationState.Instance.Logger.EnableTrace.Value = false;
|
ConfigurationState.Instance.Logger.EnableTrace.Value = false;
|
||||||
|
|
||||||
@@ -1938,7 +1877,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||||
|
|
||||||
if (result != UserResult.Yes)
|
if (result == UserResult.Yes)
|
||||||
{
|
{
|
||||||
ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = "";
|
ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = "";
|
||||||
|
|
||||||
|
@@ -151,7 +151,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
public bool IsSoundIoEnabled { get; set; }
|
public bool IsSoundIoEnabled { get; set; }
|
||||||
public bool IsSDL2Enabled { get; set; }
|
public bool IsSDL2Enabled { get; set; }
|
||||||
public bool EnableCustomTheme { get; set; }
|
public bool EnableCustomTheme { get; set; }
|
||||||
public bool IsCustomResolutionScaleActive => _resolutionScale == 0;
|
public bool IsCustomResolutionScaleActive => _resolutionScale == 4;
|
||||||
public bool IsVulkanSelected => GraphicsBackendIndex == 0;
|
public bool IsVulkanSelected => GraphicsBackendIndex == 0;
|
||||||
|
|
||||||
public string TimeZone { get; set; }
|
public string TimeZone { get; set; }
|
||||||
@@ -311,25 +311,66 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
ConfigurationState config = ConfigurationState.Instance;
|
ConfigurationState config = ConfigurationState.Instance;
|
||||||
|
|
||||||
GameDirectories.Clear();
|
// User Interface
|
||||||
GameDirectories.AddRange(config.Ui.GameDirs.Value);
|
|
||||||
|
|
||||||
EnableDiscordIntegration = config.EnableDiscordIntegration;
|
EnableDiscordIntegration = config.EnableDiscordIntegration;
|
||||||
CheckUpdatesOnStart = config.CheckUpdatesOnStart;
|
CheckUpdatesOnStart = config.CheckUpdatesOnStart;
|
||||||
ShowConfirmExit = config.ShowConfirmExit;
|
ShowConfirmExit = config.ShowConfirmExit;
|
||||||
HideCursorOnIdle = config.HideCursorOnIdle;
|
HideCursorOnIdle = config.HideCursorOnIdle;
|
||||||
|
|
||||||
|
GameDirectories.Clear();
|
||||||
|
GameDirectories.AddRange(config.Ui.GameDirs.Value);
|
||||||
|
|
||||||
|
EnableCustomTheme = config.Ui.EnableCustomTheme;
|
||||||
|
CustomThemePath = config.Ui.CustomThemePath;
|
||||||
|
BaseStyleIndex = config.Ui.BaseStyle == "Light" ? 0 : 1;
|
||||||
|
|
||||||
|
// Input
|
||||||
EnableDockedMode = config.System.EnableDockedMode;
|
EnableDockedMode = config.System.EnableDockedMode;
|
||||||
EnableKeyboard = config.Hid.EnableKeyboard;
|
EnableKeyboard = config.Hid.EnableKeyboard;
|
||||||
EnableMouse = config.Hid.EnableMouse;
|
EnableMouse = config.Hid.EnableMouse;
|
||||||
|
|
||||||
|
// Keyboard Hotkeys
|
||||||
|
KeyboardHotkeys = config.Hid.Hotkeys.Value;
|
||||||
|
|
||||||
|
// System
|
||||||
|
Region = (int)config.System.Region.Value;
|
||||||
|
Language = (int)config.System.Language.Value;
|
||||||
|
TimeZone = config.System.TimeZone;
|
||||||
|
|
||||||
|
DateTime dateTimeOffset = DateTime.Now.AddSeconds(config.System.SystemTimeOffset);
|
||||||
|
|
||||||
|
DateOffset = dateTimeOffset.Date;
|
||||||
|
TimeOffset = dateTimeOffset.TimeOfDay;
|
||||||
EnableVsync = config.Graphics.EnableVsync;
|
EnableVsync = config.Graphics.EnableVsync;
|
||||||
EnablePptc = config.System.EnablePtc;
|
|
||||||
EnableInternetAccess = config.System.EnableInternetAccess;
|
|
||||||
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
|
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
|
||||||
IgnoreMissingServices = config.System.IgnoreMissingServices;
|
|
||||||
ExpandDramSize = config.System.ExpandRam;
|
ExpandDramSize = config.System.ExpandRam;
|
||||||
|
IgnoreMissingServices = config.System.IgnoreMissingServices;
|
||||||
|
|
||||||
|
// CPU
|
||||||
|
EnablePptc = config.System.EnablePtc;
|
||||||
|
MemoryMode = (int)config.System.MemoryManagerMode.Value;
|
||||||
|
|
||||||
|
// Graphics
|
||||||
|
GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value;
|
||||||
|
PreferredGpuIndex = _gpuIds.Contains(config.Graphics.PreferredGpu) ? _gpuIds.IndexOf(config.Graphics.PreferredGpu) : 0;
|
||||||
EnableShaderCache = config.Graphics.EnableShaderCache;
|
EnableShaderCache = config.Graphics.EnableShaderCache;
|
||||||
EnableTextureRecompression = config.Graphics.EnableTextureRecompression;
|
EnableTextureRecompression = config.Graphics.EnableTextureRecompression;
|
||||||
EnableMacroHLE = config.Graphics.EnableMacroHLE;
|
EnableMacroHLE = config.Graphics.EnableMacroHLE;
|
||||||
|
ResolutionScale = config.Graphics.ResScale == -1 ? 4 : config.Graphics.ResScale - 1;
|
||||||
|
CustomResolutionScale = config.Graphics.ResScaleCustom;
|
||||||
|
MaxAnisotropy = config.Graphics.MaxAnisotropy == -1 ? 0 : (int)(MathF.Log2(config.Graphics.MaxAnisotropy));
|
||||||
|
AspectRatio = (int)config.Graphics.AspectRatio.Value;
|
||||||
|
GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value;
|
||||||
|
ShaderDumpPath = config.Graphics.ShadersDumpPath;
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
AudioBackend = (int)config.System.AudioBackend.Value;
|
||||||
|
Volume = config.System.AudioVolume * 100;
|
||||||
|
|
||||||
|
// Network
|
||||||
|
EnableInternetAccess = config.System.EnableInternetAccess;
|
||||||
|
|
||||||
|
// Logging
|
||||||
EnableFileLog = config.Logger.EnableFileLog;
|
EnableFileLog = config.Logger.EnableFileLog;
|
||||||
EnableStub = config.Logger.EnableStub;
|
EnableStub = config.Logger.EnableStub;
|
||||||
EnableInfo = config.Logger.EnableInfo;
|
EnableInfo = config.Logger.EnableInfo;
|
||||||
@@ -339,94 +380,69 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
EnableGuest = config.Logger.EnableGuest;
|
EnableGuest = config.Logger.EnableGuest;
|
||||||
EnableDebug = config.Logger.EnableDebug;
|
EnableDebug = config.Logger.EnableDebug;
|
||||||
EnableFsAccessLog = config.Logger.EnableFsAccessLog;
|
EnableFsAccessLog = config.Logger.EnableFsAccessLog;
|
||||||
EnableCustomTheme = config.Ui.EnableCustomTheme;
|
|
||||||
Volume = config.System.AudioVolume * 100;
|
|
||||||
|
|
||||||
GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value;
|
|
||||||
|
|
||||||
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
|
|
||||||
|
|
||||||
TimeZone = config.System.TimeZone;
|
|
||||||
ShaderDumpPath = config.Graphics.ShadersDumpPath;
|
|
||||||
CustomThemePath = config.Ui.CustomThemePath;
|
|
||||||
BaseStyleIndex = config.Ui.BaseStyle == "Light" ? 0 : 1;
|
|
||||||
GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value;
|
|
||||||
|
|
||||||
PreferredGpuIndex = _gpuIds.Contains(config.Graphics.PreferredGpu) ? _gpuIds.IndexOf(config.Graphics.PreferredGpu) : 0;
|
|
||||||
|
|
||||||
Language = (int)config.System.Language.Value;
|
|
||||||
Region = (int)config.System.Region.Value;
|
|
||||||
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
|
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
|
||||||
AudioBackend = (int)config.System.AudioBackend.Value;
|
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
|
||||||
MemoryMode = (int)config.System.MemoryManagerMode.Value;
|
|
||||||
|
|
||||||
float anisotropy = config.Graphics.MaxAnisotropy;
|
|
||||||
|
|
||||||
MaxAnisotropy = anisotropy == -1 ? 0 : (int)(MathF.Log2(anisotropy));
|
|
||||||
AspectRatio = (int)config.Graphics.AspectRatio.Value;
|
|
||||||
|
|
||||||
int resolution = config.Graphics.ResScale;
|
|
||||||
|
|
||||||
ResolutionScale = resolution == -1 ? 0 : resolution;
|
|
||||||
CustomResolutionScale = config.Graphics.ResScaleCustom;
|
|
||||||
|
|
||||||
DateTime dateTimeOffset = DateTime.Now.AddSeconds(config.System.SystemTimeOffset);
|
|
||||||
|
|
||||||
DateOffset = dateTimeOffset.Date;
|
|
||||||
TimeOffset = dateTimeOffset.TimeOfDay;
|
|
||||||
|
|
||||||
KeyboardHotkeys = config.Hid.Hotkeys.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveSettings()
|
public void SaveSettings()
|
||||||
{
|
{
|
||||||
ConfigurationState config = ConfigurationState.Instance;
|
ConfigurationState config = ConfigurationState.Instance;
|
||||||
|
|
||||||
|
// User Interface
|
||||||
|
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
|
||||||
|
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
|
||||||
|
config.ShowConfirmExit.Value = ShowConfirmExit;
|
||||||
|
config.HideCursorOnIdle.Value = HideCursorOnIdle;
|
||||||
|
|
||||||
if (_directoryChanged)
|
if (_directoryChanged)
|
||||||
{
|
{
|
||||||
List<string> gameDirs = new List<string>(GameDirectories);
|
List<string> gameDirs = new(GameDirectories);
|
||||||
config.Ui.GameDirs.Value = gameDirs;
|
config.Ui.GameDirs.Value = gameDirs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Ui.EnableCustomTheme.Value = EnableCustomTheme;
|
||||||
|
config.Ui.CustomThemePath.Value = CustomThemePath;
|
||||||
|
config.Ui.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark";
|
||||||
|
|
||||||
|
// Input
|
||||||
|
config.System.EnableDockedMode.Value = EnableDockedMode;
|
||||||
|
config.Hid.EnableKeyboard.Value = EnableKeyboard;
|
||||||
|
config.Hid.EnableMouse.Value = EnableMouse;
|
||||||
|
|
||||||
|
// Keyboard Hotkeys
|
||||||
|
config.Hid.Hotkeys.Value = KeyboardHotkeys;
|
||||||
|
|
||||||
|
// System
|
||||||
|
config.System.Region.Value = (Region)Region;
|
||||||
|
config.System.Language.Value = (Language)Language;
|
||||||
|
|
||||||
if (_validTzRegions.Contains(TimeZone))
|
if (_validTzRegions.Contains(TimeZone))
|
||||||
{
|
{
|
||||||
config.System.TimeZone.Value = TimeZone;
|
config.System.TimeZone.Value = TimeZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Logger.EnableError.Value = EnableError;
|
TimeSpan systemTimeOffset = DateOffset - DateTime.Now;
|
||||||
config.Logger.EnableTrace.Value = EnableTrace;
|
|
||||||
config.Logger.EnableWarn.Value = EnableWarn;
|
config.System.SystemTimeOffset.Value = systemTimeOffset.Seconds;
|
||||||
config.Logger.EnableInfo.Value = EnableInfo;
|
|
||||||
config.Logger.EnableStub.Value = EnableStub;
|
|
||||||
config.Logger.EnableDebug.Value = EnableDebug;
|
|
||||||
config.Logger.EnableGuest.Value = EnableGuest;
|
|
||||||
config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog;
|
|
||||||
config.Logger.EnableFileLog.Value = EnableFileLog;
|
|
||||||
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
|
|
||||||
config.System.EnableDockedMode.Value = EnableDockedMode;
|
|
||||||
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
|
|
||||||
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
|
|
||||||
config.ShowConfirmExit.Value = ShowConfirmExit;
|
|
||||||
config.HideCursorOnIdle.Value = HideCursorOnIdle;
|
|
||||||
config.Graphics.EnableVsync.Value = EnableVsync;
|
config.Graphics.EnableVsync.Value = EnableVsync;
|
||||||
|
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
|
||||||
|
config.System.ExpandRam.Value = ExpandDramSize;
|
||||||
|
config.System.IgnoreMissingServices.Value = IgnoreMissingServices;
|
||||||
|
|
||||||
|
// CPU
|
||||||
|
config.System.EnablePtc.Value = EnablePptc;
|
||||||
|
config.System.MemoryManagerMode.Value = (MemoryManagerMode)MemoryMode;
|
||||||
|
|
||||||
|
// Graphics
|
||||||
|
config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex;
|
||||||
|
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
|
||||||
config.Graphics.EnableShaderCache.Value = EnableShaderCache;
|
config.Graphics.EnableShaderCache.Value = EnableShaderCache;
|
||||||
config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression;
|
config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression;
|
||||||
config.Graphics.EnableMacroHLE.Value = EnableMacroHLE;
|
config.Graphics.EnableMacroHLE.Value = EnableMacroHLE;
|
||||||
config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex;
|
config.Graphics.ResScale.Value = ResolutionScale == 4 ? -1 : ResolutionScale + 1;
|
||||||
config.System.EnablePtc.Value = EnablePptc;
|
config.Graphics.ResScaleCustom.Value = CustomResolutionScale;
|
||||||
config.System.EnableInternetAccess.Value = EnableInternetAccess;
|
config.Graphics.MaxAnisotropy.Value = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy);
|
||||||
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
|
config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio;
|
||||||
config.System.IgnoreMissingServices.Value = IgnoreMissingServices;
|
|
||||||
config.System.ExpandRam.Value = ExpandDramSize;
|
|
||||||
config.Hid.EnableKeyboard.Value = EnableKeyboard;
|
|
||||||
config.Hid.EnableMouse.Value = EnableMouse;
|
|
||||||
config.Ui.CustomThemePath.Value = CustomThemePath;
|
|
||||||
config.Ui.EnableCustomTheme.Value = EnableCustomTheme;
|
|
||||||
config.Ui.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark";
|
|
||||||
config.System.Language.Value = (Language)Language;
|
|
||||||
config.System.Region.Value = (Region)Region;
|
|
||||||
|
|
||||||
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
|
|
||||||
|
|
||||||
if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex)
|
if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex)
|
||||||
{
|
{
|
||||||
@@ -434,22 +450,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex;
|
config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex;
|
||||||
|
|
||||||
TimeSpan systemTimeOffset = DateOffset - DateTime.Now;
|
|
||||||
|
|
||||||
config.System.SystemTimeOffset.Value = systemTimeOffset.Seconds;
|
|
||||||
config.Graphics.ShadersDumpPath.Value = ShaderDumpPath;
|
config.Graphics.ShadersDumpPath.Value = ShaderDumpPath;
|
||||||
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
|
|
||||||
config.System.MemoryManagerMode.Value = (MemoryManagerMode)MemoryMode;
|
|
||||||
|
|
||||||
float anisotropy = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy);
|
|
||||||
|
|
||||||
config.Graphics.MaxAnisotropy.Value = anisotropy;
|
|
||||||
config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio;
|
|
||||||
config.Graphics.ResScale.Value = ResolutionScale == 0 ? -1 : ResolutionScale;
|
|
||||||
config.Graphics.ResScaleCustom.Value = CustomResolutionScale;
|
|
||||||
config.System.AudioVolume.Value = Volume / 100;
|
|
||||||
|
|
||||||
|
// Audio
|
||||||
AudioBackend audioBackend = (AudioBackend)AudioBackend;
|
AudioBackend audioBackend = (AudioBackend)AudioBackend;
|
||||||
if (audioBackend != config.System.AudioBackend.Value)
|
if (audioBackend != config.System.AudioBackend.Value)
|
||||||
{
|
{
|
||||||
@@ -458,7 +461,23 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
Logger.Info?.Print(LogClass.Application, $"AudioBackend toggled to: {audioBackend}");
|
Logger.Info?.Print(LogClass.Application, $"AudioBackend toggled to: {audioBackend}");
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Hid.Hotkeys.Value = KeyboardHotkeys;
|
config.System.AudioVolume.Value = Volume / 100;
|
||||||
|
|
||||||
|
// Network
|
||||||
|
config.System.EnableInternetAccess.Value = EnableInternetAccess;
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
config.Logger.EnableFileLog.Value = EnableFileLog;
|
||||||
|
config.Logger.EnableStub.Value = EnableStub;
|
||||||
|
config.Logger.EnableInfo.Value = EnableInfo;
|
||||||
|
config.Logger.EnableWarn.Value = EnableWarn;
|
||||||
|
config.Logger.EnableError.Value = EnableError;
|
||||||
|
config.Logger.EnableTrace.Value = EnableTrace;
|
||||||
|
config.Logger.EnableGuest.Value = EnableGuest;
|
||||||
|
config.Logger.EnableDebug.Value = EnableDebug;
|
||||||
|
config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog;
|
||||||
|
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
|
||||||
|
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
|
||||||
|
|
||||||
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||||
|
|
||||||
|
250
Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
Normal file
250
Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Ns;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.HOS;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.ViewModels;
|
||||||
|
|
||||||
|
public class TitleUpdateViewModel : BaseModel
|
||||||
|
{
|
||||||
|
public TitleUpdateMetadata _titleUpdateWindowData;
|
||||||
|
public readonly string _titleUpdateJsonPath;
|
||||||
|
private VirtualFileSystem _virtualFileSystem { get; }
|
||||||
|
private ulong _titleId { get; }
|
||||||
|
private string _titleName { get; }
|
||||||
|
|
||||||
|
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
||||||
|
private AvaloniaList<object> _views = new();
|
||||||
|
private object _selectedUpdate;
|
||||||
|
|
||||||
|
public AvaloniaList<TitleUpdateModel> TitleUpdates
|
||||||
|
{
|
||||||
|
get => _titleUpdates;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_titleUpdates = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvaloniaList<object> Views
|
||||||
|
{
|
||||||
|
get => _views;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_views = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object SelectedUpdate
|
||||||
|
{
|
||||||
|
get => _selectedUpdate;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_selectedUpdate = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||||
|
{
|
||||||
|
_virtualFileSystem = virtualFileSystem;
|
||||||
|
|
||||||
|
_titleId = titleId;
|
||||||
|
_titleName = titleName;
|
||||||
|
|
||||||
|
_titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {_titleId} at {_titleUpdateJsonPath}");
|
||||||
|
|
||||||
|
_titleUpdateWindowData = new TitleUpdateMetadata
|
||||||
|
{
|
||||||
|
Selected = "",
|
||||||
|
Paths = new List<string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadUpdates()
|
||||||
|
{
|
||||||
|
foreach (string path in _titleUpdateWindowData.Paths)
|
||||||
|
{
|
||||||
|
AddUpdate(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Save the list again to remove leftovers.
|
||||||
|
Save();
|
||||||
|
|
||||||
|
TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected, null);
|
||||||
|
|
||||||
|
SelectedUpdate = selected;
|
||||||
|
|
||||||
|
SortUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SortUpdates()
|
||||||
|
{
|
||||||
|
var list = TitleUpdates.ToList();
|
||||||
|
|
||||||
|
list.Sort((first, second) =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
Views.Clear();
|
||||||
|
Views.Add(new BaseModel());
|
||||||
|
Views.AddRange(list);
|
||||||
|
|
||||||
|
if (SelectedUpdate == null)
|
||||||
|
{
|
||||||
|
SelectedUpdate = Views[0];
|
||||||
|
}
|
||||||
|
else if (!TitleUpdates.Contains(SelectedUpdate))
|
||||||
|
{
|
||||||
|
if (Views.Count > 1)
|
||||||
|
{
|
||||||
|
SelectedUpdate = Views[1];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedUpdate = Views[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddUpdate(string path)
|
||||||
|
{
|
||||||
|
if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
|
||||||
|
{
|
||||||
|
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
|
||||||
|
|
||||||
|
if (controlNca != null && patchNca != null)
|
||||||
|
{
|
||||||
|
ApplicationControlProperty controlData = new();
|
||||||
|
|
||||||
|
using UniqueRef<IFile> nacpFile = new();
|
||||||
|
|
||||||
|
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
||||||
|
|
||||||
|
TitleUpdates.Add(new TitleUpdateModel(controlData, path));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(async () =>
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(async () =>
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveUpdate(TitleUpdateModel update)
|
||||||
|
{
|
||||||
|
TitleUpdates.Remove(update);
|
||||||
|
|
||||||
|
SortUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Add()
|
||||||
|
{
|
||||||
|
OpenFileDialog dialog = new()
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle],
|
||||||
|
AllowMultiple = true
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.Filters.Add(new FileDialogFilter
|
||||||
|
{
|
||||||
|
Name = "NSP",
|
||||||
|
Extensions = { "nsp" }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
string[] files = await dialog.ShowAsync(desktop.MainWindow);
|
||||||
|
|
||||||
|
if (files != null)
|
||||||
|
{
|
||||||
|
foreach (string file in files)
|
||||||
|
{
|
||||||
|
AddUpdate(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SortUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
_titleUpdateWindowData.Paths.Clear();
|
||||||
|
_titleUpdateWindowData.Selected = "";
|
||||||
|
|
||||||
|
foreach (TitleUpdateModel update in TitleUpdates)
|
||||||
|
{
|
||||||
|
_titleUpdateWindowData.Paths.Add(update.Path);
|
||||||
|
|
||||||
|
if (update == SelectedUpdate)
|
||||||
|
{
|
||||||
|
_titleUpdateWindowData.Selected = update.Path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllBytes(_titleUpdateJsonPath, Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
|
||||||
|
}
|
||||||
|
}
|
230
Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
Normal file
230
Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
using Avalonia.Media;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Ncm;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using Color = Avalonia.Media.Color;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
{
|
||||||
|
internal class UserFirmwareAvatarSelectorViewModel : BaseModel
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, byte[]> _avatarStore = new();
|
||||||
|
|
||||||
|
private ObservableCollection<ProfileImageModel> _images;
|
||||||
|
private Color _backgroundColor = Colors.White;
|
||||||
|
|
||||||
|
private int _selectedIndex;
|
||||||
|
private byte[] _selectedImage;
|
||||||
|
|
||||||
|
public UserFirmwareAvatarSelectorViewModel()
|
||||||
|
{
|
||||||
|
_images = new ObservableCollection<ProfileImageModel>();
|
||||||
|
|
||||||
|
LoadImagesFromStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color BackgroundColor
|
||||||
|
{
|
||||||
|
get => _backgroundColor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_backgroundColor = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
ChangeImageBackground();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<ProfileImageModel> Images
|
||||||
|
{
|
||||||
|
get => _images;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_images = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SelectedIndex
|
||||||
|
{
|
||||||
|
get => _selectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_selectedIndex = value;
|
||||||
|
|
||||||
|
if (_selectedIndex == -1)
|
||||||
|
{
|
||||||
|
SelectedImage = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedImage = _images[_selectedIndex].Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] SelectedImage
|
||||||
|
{
|
||||||
|
get => _selectedImage;
|
||||||
|
private set => _selectedImage = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadImagesFromStore()
|
||||||
|
{
|
||||||
|
Images.Clear();
|
||||||
|
|
||||||
|
foreach (var image in _avatarStore)
|
||||||
|
{
|
||||||
|
Images.Add(new ProfileImageModel(image.Key, image.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChangeImageBackground()
|
||||||
|
{
|
||||||
|
foreach (var image in Images)
|
||||||
|
{
|
||||||
|
image.BackgroundColor = new SolidColorBrush(BackgroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
|
||||||
|
{
|
||||||
|
if (_avatarStore.Count > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data);
|
||||||
|
string avatarPath = virtualFileSystem.SwitchPathToSystemPath(contentPath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(avatarPath))
|
||||||
|
{
|
||||||
|
using (IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open))
|
||||||
|
{
|
||||||
|
Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
|
||||||
|
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
|
||||||
|
{
|
||||||
|
// TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
|
||||||
|
if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs"))
|
||||||
|
{
|
||||||
|
using var file = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
romfs.OpenFile(ref file.Ref(), ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
using (MemoryStream stream = new())
|
||||||
|
using (MemoryStream streamPng = new())
|
||||||
|
{
|
||||||
|
file.Get.AsStream().CopyTo(stream);
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
|
||||||
|
Image avatarImage = Image.LoadPixelData<Rgba32>(DecompressYaz0(stream), 256, 256);
|
||||||
|
|
||||||
|
avatarImage.SaveAsPng(streamPng);
|
||||||
|
|
||||||
|
_avatarStore.Add(item.FullPath, streamPng.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] DecompressYaz0(Stream stream)
|
||||||
|
{
|
||||||
|
using (BinaryReader reader = new(stream))
|
||||||
|
{
|
||||||
|
reader.ReadInt32(); // Magic
|
||||||
|
|
||||||
|
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
|
||||||
|
|
||||||
|
reader.ReadInt64(); // Padding
|
||||||
|
|
||||||
|
byte[] input = new byte[stream.Length - stream.Position];
|
||||||
|
stream.Read(input, 0, input.Length);
|
||||||
|
|
||||||
|
uint inputOffset = 0;
|
||||||
|
|
||||||
|
byte[] output = new byte[decodedLength];
|
||||||
|
uint outputOffset = 0;
|
||||||
|
|
||||||
|
ushort mask = 0;
|
||||||
|
byte header = 0;
|
||||||
|
|
||||||
|
while (outputOffset < decodedLength)
|
||||||
|
{
|
||||||
|
if ((mask >>= 1) == 0)
|
||||||
|
{
|
||||||
|
header = input[inputOffset++];
|
||||||
|
mask = 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((header & mask) != 0)
|
||||||
|
{
|
||||||
|
if (outputOffset == output.Length)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
output[outputOffset++] = input[inputOffset++];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
byte byte1 = input[inputOffset++];
|
||||||
|
byte byte2 = input[inputOffset++];
|
||||||
|
|
||||||
|
uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
|
||||||
|
uint position = outputOffset - (dist + 1);
|
||||||
|
|
||||||
|
uint length = (uint)byte1 >> 4;
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
length = (uint)input[inputOffset++] + 0x12;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
length += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint gap = outputOffset - position;
|
||||||
|
uint nonOverlappingLength = length;
|
||||||
|
|
||||||
|
if (nonOverlappingLength > gap)
|
||||||
|
{
|
||||||
|
nonOverlappingLength = gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
|
||||||
|
outputOffset += nonOverlappingLength;
|
||||||
|
position += nonOverlappingLength;
|
||||||
|
length -= nonOverlappingLength;
|
||||||
|
|
||||||
|
while (length-- > 0)
|
||||||
|
{
|
||||||
|
output[outputOffset++] = output[position++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
{
|
||||||
|
internal class UserProfileImageSelectorViewModel : BaseModel
|
||||||
|
{
|
||||||
|
private bool _firmwareFound;
|
||||||
|
|
||||||
|
public bool FirmwareFound
|
||||||
|
{
|
||||||
|
get => _firmwareFound;
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_firmwareFound = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,215 +1,25 @@
|
|||||||
using Avalonia;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Avalonia.Threading;
|
|
||||||
using FluentAvalonia.UI.Controls;
|
|
||||||
using LibHac.Common;
|
|
||||||
using LibHac.Fs;
|
|
||||||
using LibHac.Fs.Shim;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
|
||||||
using Ryujinx.Ava.UI.Controls;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
|
||||||
using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId;
|
|
||||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.ViewModels
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
public class UserProfileViewModel : BaseModel, IDisposable
|
public class UserProfileViewModel : BaseModel, IDisposable
|
||||||
{
|
{
|
||||||
private readonly NavigationDialogHost _owner;
|
|
||||||
|
|
||||||
private UserProfile _selectedProfile;
|
|
||||||
private UserProfile _highlightedProfile;
|
|
||||||
|
|
||||||
public UserProfileViewModel()
|
public UserProfileViewModel()
|
||||||
{
|
{
|
||||||
Profiles = new ObservableCollection<UserProfile>();
|
Profiles = new ObservableCollection<BaseModel>();
|
||||||
LostProfiles = new ObservableCollection<UserProfile>();
|
LostProfiles = new ObservableCollection<UserProfile>();
|
||||||
|
IsEmpty = LostProfiles.IsNullOrEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserProfileViewModel(NavigationDialogHost owner) : this()
|
public ObservableCollection<BaseModel> Profiles { get; set; }
|
||||||
{
|
|
||||||
_owner = owner;
|
|
||||||
|
|
||||||
LoadProfiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObservableCollection<UserProfile> Profiles { get; set; }
|
|
||||||
|
|
||||||
public ObservableCollection<UserProfile> LostProfiles { get; set; }
|
public ObservableCollection<UserProfile> LostProfiles { get; set; }
|
||||||
|
|
||||||
public UserProfile SelectedProfile
|
public bool IsEmpty { get; set; }
|
||||||
{
|
|
||||||
get => _selectedProfile;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_selectedProfile = value;
|
|
||||||
|
|
||||||
OnPropertyChanged();
|
|
||||||
OnPropertyChanged(nameof(IsHighlightedProfileDeletable));
|
|
||||||
OnPropertyChanged(nameof(IsHighlightedProfileEditable));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsHighlightedProfileEditable => _highlightedProfile != null;
|
|
||||||
|
|
||||||
public bool IsHighlightedProfileDeletable => _highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId;
|
|
||||||
|
|
||||||
public UserProfile HighlightedProfile
|
|
||||||
{
|
|
||||||
get => _highlightedProfile;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_highlightedProfile = value;
|
|
||||||
|
|
||||||
OnPropertyChanged();
|
|
||||||
OnPropertyChanged(nameof(IsHighlightedProfileDeletable));
|
|
||||||
OnPropertyChanged(nameof(IsHighlightedProfileEditable));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
|
|
||||||
public void LoadProfiles()
|
|
||||||
{
|
|
||||||
Profiles.Clear();
|
|
||||||
LostProfiles.Clear();
|
|
||||||
|
|
||||||
var profiles = _owner.AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open);
|
|
||||||
|
|
||||||
foreach (var profile in profiles)
|
|
||||||
{
|
|
||||||
Profiles.Add(new UserProfile(profile, _owner));
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId);
|
|
||||||
|
|
||||||
if (SelectedProfile == null)
|
|
||||||
{
|
|
||||||
SelectedProfile = Profiles.First();
|
|
||||||
|
|
||||||
if (SelectedProfile != null)
|
|
||||||
{
|
|
||||||
_owner.AccountManager.OpenUser(_selectedProfile.UserId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account,
|
|
||||||
default, saveDataId: default, index: default);
|
|
||||||
|
|
||||||
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
|
|
||||||
|
|
||||||
_owner.HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
|
|
||||||
|
|
||||||
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
|
|
||||||
|
|
||||||
HashSet<UserId> lostAccounts = new HashSet<UserId>();
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
|
|
||||||
|
|
||||||
if (readCount == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < readCount; i++)
|
|
||||||
{
|
|
||||||
var save = saveDataInfo[i];
|
|
||||||
var id = new UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High);
|
|
||||||
if (Profiles.FirstOrDefault( x=> x.UserId == id) == null)
|
|
||||||
{
|
|
||||||
lostAccounts.Add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach(var account in lostAccounts)
|
|
||||||
{
|
|
||||||
LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), _owner));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddUser()
|
|
||||||
{
|
|
||||||
UserProfile userProfile = null;
|
|
||||||
|
|
||||||
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void ManageSaves()
|
|
||||||
{
|
|
||||||
UserProfile userProfile = _highlightedProfile ?? SelectedProfile;
|
|
||||||
|
|
||||||
SaveManager manager = new SaveManager(userProfile, _owner.HorizonClient, _owner.VirtualFileSystem);
|
|
||||||
|
|
||||||
ContentDialog contentDialog = new ContentDialog
|
|
||||||
{
|
|
||||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.SaveManagerHeading], userProfile.Name),
|
|
||||||
PrimaryButtonText = "",
|
|
||||||
SecondaryButtonText = "",
|
|
||||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
|
|
||||||
Content = manager,
|
|
||||||
Padding = new Thickness(0)
|
|
||||||
};
|
|
||||||
|
|
||||||
await contentDialog.ShowAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EditUser()
|
|
||||||
{
|
|
||||||
_owner.Navigate(typeof(UserEditor), (this._owner, _highlightedProfile ?? SelectedProfile, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void DeleteUser()
|
|
||||||
{
|
|
||||||
if (_highlightedProfile != null)
|
|
||||||
{
|
|
||||||
var lastUserId = _owner.AccountManager.LastOpenedUser.UserId;
|
|
||||||
|
|
||||||
if (_highlightedProfile.UserId == lastUserId)
|
|
||||||
{
|
|
||||||
// If we are deleting the currently open profile, then we must open something else before deleting.
|
|
||||||
var profile = Profiles.FirstOrDefault(x => x.UserId != lastUserId);
|
|
||||||
|
|
||||||
if (profile == null)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(async () =>
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_owner.AccountManager.OpenUser(profile.UserId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result =
|
|
||||||
await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage], "",
|
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], "");
|
|
||||||
|
|
||||||
if (result == UserResult.Yes)
|
|
||||||
{
|
|
||||||
_owner.AccountManager.DeleteUser(_highlightedProfile.UserId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadProfiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void GoBack()
|
|
||||||
{
|
|
||||||
_owner.GoBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RecoverLostAccounts()
|
|
||||||
{
|
|
||||||
_owner.Navigate(typeof(UserRecoverer), (this._owner, this));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
120
Ryujinx.Ava/UI/ViewModels/UserSaveManagerViewModel.cs
Normal file
120
Ryujinx.Ava/UI/ViewModels/UserSaveManagerViewModel.cs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
using DynamicData;
|
||||||
|
using DynamicData.Binding;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
{
|
||||||
|
public class UserSaveManagerViewModel : BaseModel
|
||||||
|
{
|
||||||
|
private int _sortIndex;
|
||||||
|
private int _orderIndex;
|
||||||
|
private string _search;
|
||||||
|
private ObservableCollection<SaveModel> _saves = new();
|
||||||
|
private ObservableCollection<SaveModel> _views = new();
|
||||||
|
private AccountManager _accountManager;
|
||||||
|
|
||||||
|
public string SaveManagerHeading => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SaveManagerHeading, _accountManager.LastOpenedUser.Name, _accountManager.LastOpenedUser.UserId);
|
||||||
|
|
||||||
|
public int SortIndex
|
||||||
|
{
|
||||||
|
get => _sortIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_sortIndex = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int OrderIndex
|
||||||
|
{
|
||||||
|
get => _orderIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_orderIndex = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Search
|
||||||
|
{
|
||||||
|
get => _search;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_search = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<SaveModel> Saves
|
||||||
|
{
|
||||||
|
get => _saves;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_saves = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<SaveModel> Views
|
||||||
|
{
|
||||||
|
get => _views;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_views = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserSaveManagerViewModel(AccountManager accountManager)
|
||||||
|
{
|
||||||
|
_accountManager = accountManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Sort()
|
||||||
|
{
|
||||||
|
Saves.AsObservableChangeSet()
|
||||||
|
.Filter(Filter)
|
||||||
|
.Sort(GetComparer())
|
||||||
|
.Bind(out var view).AsObservableList();
|
||||||
|
|
||||||
|
_views.Clear();
|
||||||
|
_views.AddRange(view);
|
||||||
|
OnPropertyChanged(nameof(Views));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Filter(object arg)
|
||||||
|
{
|
||||||
|
if (arg is SaveModel save)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(_search) || save.Title.ToLower().Contains(_search.ToLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IComparer<SaveModel> GetComparer()
|
||||||
|
{
|
||||||
|
switch (SortIndex)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return OrderIndex == 0
|
||||||
|
? SortExpressionComparer<SaveModel>.Ascending(save => save.Title)
|
||||||
|
: SortExpressionComparer<SaveModel>.Descending(save => save.Title);
|
||||||
|
case 1:
|
||||||
|
return OrderIndex == 0
|
||||||
|
? SortExpressionComparer<SaveModel>.Ascending(save => save.Size)
|
||||||
|
: SortExpressionComparer<SaveModel>.Descending(save => save.Size);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user