diff --git a/.github/workflows/build_and_draft.yml b/.github/workflows/build_and_draft.yml
index e9c92924..89efeebe 100644
--- a/.github/workflows/build_and_draft.yml
+++ b/.github/workflows/build_and_draft.yml
@@ -14,7 +14,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: ${{ startsWith(matrix.os, 'macos') && '9.0.312' || '9.0.x' }}
+ dotnet-version: ${{ startsWith(matrix.os, 'macos') && '10.0.x' || '10.0.x' }}
- name: Restore Core Tests
run: dotnet restore ./VoiceCraft.Core.Tests/VoiceCraft.Core.Tests.csproj
@@ -38,7 +38,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
- arch: [ x64, x86, arm64 ]
+ arch: [x64, x86, arm64]
fail-fast: false
defaults:
run:
@@ -48,8 +48,8 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.x"
-
+ dotnet-version: "10.0.x"
+
- name: Install WorkLoads
run: dotnet workload restore
@@ -60,20 +60,20 @@ jobs:
shell: pwsh
run: |
$uri = "https://aka.ms/vc14/vc_redist.${{ matrix.arch }}.exe"
- $outFile = "./bin/Release/net9.0-windows/win-${{ matrix.arch }}/publish/vc_redist.${{ matrix.arch }}.exe"
+ $outFile = "./bin/Release/net10.0-windows/win-${{ matrix.arch }}/publish/vc_redist.${{ matrix.arch }}.exe"
Invoke-WebRequest -Uri $uri -OutFile $outFile
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.Windows.${{ matrix.arch }}
- path: ./VoiceCraft.Client/VoiceCraft.Client.Windows/bin/Release/net9.0-windows/win-${{ matrix.arch }}/publish/
-
+ path: ./VoiceCraft.Client/VoiceCraft.Client.Windows/bin/Release/net10.0-windows/win-${{ matrix.arch }}/publish/
+
VoiceCraft-Client-Linux:
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [ x64, arm, arm64 ]
+ arch: [x64, arm, arm64]
fail-fast: false
defaults:
run:
@@ -83,7 +83,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -95,13 +95,13 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.Linux.${{ matrix.arch }}
- path: ./VoiceCraft.Client/VoiceCraft.Client.Linux/bin/Release/net9.0/linux-${{ matrix.arch }}/publish/
-
+ path: ./VoiceCraft.Client/VoiceCraft.Client.Linux/bin/Release/net10.0/linux-${{ matrix.arch }}/publish/
+
VoiceCraft-Client-Android:
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [ arm, arm64 ]
+ arch: [arm64]
fail-fast: false
defaults:
run:
@@ -111,11 +111,11 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
-
+
- name: Setup Keystore File
run: echo -n "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode >> android.keystore
@@ -126,7 +126,7 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.Android.${{ matrix.arch }}
- path: ./VoiceCraft.Client/VoiceCraft.Client.Android/bin/Release/net9.0-android/android-${{ matrix.arch }}/publish/com.AvionBlock.VoiceCraft.Client-Signed.apk
+ path: ./VoiceCraft.Client/VoiceCraft.Client.Android/bin/Release/net10.0-android/android-${{ matrix.arch }}/publish/*.apk
VoiceCraft-Client-iOS:
runs-on: macos-26
@@ -138,7 +138,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.312"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -150,13 +150,13 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.iOS.arm64
- path: ./VoiceCraft.Client/VoiceCraft.Client.iOS/bin/Release/net9.0-ios/ios-arm64/publish/*.ipa
-
+ path: ./VoiceCraft.Client/VoiceCraft.Client.iOS/bin/Release/net10.0-ios/ios-arm64/publish/*.ipa
+
VoiceCraft-Client-MacOS:
runs-on: macos-26
strategy:
matrix:
- arch: [ x64, arm64 ]
+ arch: [x64, arm64]
fail-fast: false
defaults:
run:
@@ -166,20 +166,20 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.312"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
- name: Restore ${{ matrix.arch }} Workloads
run: dotnet restore -r osx-${{ matrix.arch }}
-
+
- name: Publish Build
run: dotnet publish -c Release -r osx-${{ matrix.arch }} --no-restore -p:ValidateXcodeVersion=false
-
+
- name: Build VoiceCraft.Client.MacOS.${{ matrix.arch }}.dmg
run: |
- cd ./bin/Release/net9.0-macos/osx-${{ matrix.arch }}
+ cd ./bin/Release/net10.0-macos/osx-${{ matrix.arch }}
APP_BUNDLE=$(find . -maxdepth 1 -name "*.app" -print -quit)
if [ -z "$APP_BUNDLE" ]; then
echo "No .app bundle found"
@@ -198,27 +198,27 @@ jobs:
- name: Normalize VoiceCraft.Client.MacOS.${{ matrix.arch }}.pkg name
run: |
- cd ./bin/Release/net9.0-macos/osx-${{ matrix.arch }}
+ cd ./bin/Release/net10.0-macos/osx-${{ matrix.arch }}
PKG_PATH=$(ls ./publish/*.pkg | head -n 1)
cp "$PKG_PATH" "VoiceCraft.Client.MacOS.${{ matrix.arch }}.pkg"
-
+
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.MacOS.${{ matrix.arch }}.dmg
- path: ./VoiceCraft.Client/VoiceCraft.Client.MacOS/bin/Release/net9.0-macos/osx-${{ matrix.arch }}/VoiceCraft.Client.MacOS.${{ matrix.arch }}.dmg
+ path: ./VoiceCraft.Client/VoiceCraft.Client.MacOS/bin/Release/net10.0-macos/osx-${{ matrix.arch }}/VoiceCraft.Client.MacOS.${{ matrix.arch }}.dmg
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.MacOS.${{ matrix.arch }}.pkg
- path: ./VoiceCraft.Client/VoiceCraft.Client.MacOS/bin/Release/net9.0-macos/osx-${{ matrix.arch }}/VoiceCraft.Client.MacOS.${{ matrix.arch }}.pkg
+ path: ./VoiceCraft.Client/VoiceCraft.Client.MacOS/bin/Release/net10.0-macos/osx-${{ matrix.arch }}/VoiceCraft.Client.MacOS.${{ matrix.arch }}.pkg
VoiceCraft-Server-Windows:
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [ x64, x86, arm64 ]
+ arch: [x64, x86, arm64]
fail-fast: false
defaults:
run:
@@ -228,7 +228,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -240,13 +240,13 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Server.Windows.${{ matrix.arch }}
- path: ./VoiceCraft.Server/bin/Release/net9.0/win-${{ matrix.arch }}/publish
-
+ path: ./VoiceCraft.Server/bin/Release/net10.0/win-${{ matrix.arch }}/publish
+
VoiceCraft-Server-Linux:
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [ x64, arm, arm64 ]
+ arch: [x64, arm, arm64]
fail-fast: false
defaults:
run:
@@ -256,7 +256,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -268,27 +268,30 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Server.Linux.${{ matrix.arch }}
- path: ./VoiceCraft.Server/bin/Release/net9.0/linux-${{ matrix.arch }}/publish
-
+ path: ./VoiceCraft.Server/bin/Release/net10.0/linux-${{ matrix.arch }}/publish
+
Publish-Draft:
runs-on: ubuntu-latest
- needs: ["VoiceCraft-Client-Windows",
- "VoiceCraft-Tests",
- "VoiceCraft-Client-Linux",
- "VoiceCraft-Client-Android",
- "VoiceCraft-Client-iOS",
- "VoiceCraft-Client-MacOS",
- "VoiceCraft-Server-Windows",
- "VoiceCraft-Server-Linux"]
+ needs:
+ [
+ "VoiceCraft-Client-Windows",
+ "VoiceCraft-Tests",
+ "VoiceCraft-Client-Linux",
+ "VoiceCraft-Client-Android",
+ "VoiceCraft-Client-iOS",
+ "VoiceCraft-Client-MacOS",
+ "VoiceCraft-Server-Windows",
+ "VoiceCraft-Server-Linux",
+ ]
steps:
- uses: actions/checkout@v5
-
+
- name: Download Artifacts
uses: actions/download-artifact@v8
with:
skip-decompress: true
path: ./artifacts
-
+
- name: Create Draft Release
uses: softprops/action-gh-release@v2
with:
diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml
index b94c0574..da854215 100644
--- a/.github/workflows/build_test.yml
+++ b/.github/workflows/build_test.yml
@@ -16,7 +16,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: ${{ startsWith(matrix.os, 'macos') && '9.0.312' || '9.0.x' }}
+ dotnet-version: ${{ startsWith(matrix.os, 'macos') && '10.0.x' || '10.0.x' }}
- name: Restore Core Tests
run: dotnet restore ./VoiceCraft.Core.Tests/VoiceCraft.Core.Tests.csproj
@@ -40,7 +40,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
- arch: [ x64, x86, arm64 ]
+ arch: [x64, x86, arm64]
fail-fast: false
defaults:
run:
@@ -50,7 +50,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -62,20 +62,20 @@ jobs:
shell: pwsh
run: |
$uri = "https://aka.ms/vc14/vc_redist.${{ matrix.arch }}.exe"
- $outFile = "./bin/Release/net9.0-windows/win-${{ matrix.arch }}/publish/vc_redist.${{ matrix.arch }}.exe"
+ $outFile = "./bin/Release/net10.0-windows/win-${{ matrix.arch }}/publish/vc_redist.${{ matrix.arch }}.exe"
Invoke-WebRequest -Uri $uri -OutFile $outFile
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.Windows.${{ matrix.arch }}
- path: ./VoiceCraft.Client/VoiceCraft.Client.Windows/bin/Release/net9.0-windows/win-${{ matrix.arch }}/publish/
-
+ path: ./VoiceCraft.Client/VoiceCraft.Client.Windows/bin/Release/net10.0-windows/win-${{ matrix.arch }}/publish/
+
VoiceCraft-Client-Linux:
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [ x64, arm, arm64 ]
+ arch: [x64, arm, arm64]
fail-fast: false
defaults:
run:
@@ -85,7 +85,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -97,13 +97,13 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.Linux.${{ matrix.arch }}
- path: ./VoiceCraft.Client/VoiceCraft.Client.Linux/bin/Release/net9.0/linux-${{ matrix.arch }}/publish/
-
+ path: ./VoiceCraft.Client/VoiceCraft.Client.Linux/bin/Release/net10.0/linux-${{ matrix.arch }}/publish/
+
VoiceCraft-Client-Android:
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [arm, arm64]
+ arch: [arm64]
fail-fast: false
defaults:
run:
@@ -118,7 +118,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -130,7 +130,8 @@ jobs:
- name: Publish Build (Signed)
if: ${{ github.event_name != 'pull_request' && env.ANDROID_KEYSTORE != '' }}
run: >
- dotnet publish -c Release -r android-${{ matrix.arch }}
+ dotnet publish -c Release
+ -r android-${{ matrix.arch }}
-p:AndroidKeyStore=true
-p:AndroidSigningKeyStore=./android.keystore
-p:AndroidSigningKeyAlias=$ALIAS
@@ -145,7 +146,7 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.Android.${{ matrix.arch }}
- path: ./VoiceCraft.Client/VoiceCraft.Client.Android/bin/Release/net9.0-android/android-${{ matrix.arch }}/publish/*.apk
+ path: ./VoiceCraft.Client/VoiceCraft.Client.Android/bin/Release/net10.0-android/android-${{ matrix.arch }}/publish/*.apk
VoiceCraft-Client-iOS:
runs-on: macos-26
@@ -157,7 +158,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.312"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -169,13 +170,13 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.iOS.arm64
- path: ./VoiceCraft.Client/VoiceCraft.Client.iOS/bin/Release/net9.0-ios/ios-arm64/publish/*.ipa
-
+ path: ./VoiceCraft.Client/VoiceCraft.Client.iOS/bin/Release/net10.0-ios/ios-arm64/publish/*.ipa
+
VoiceCraft-Client-MacOS:
runs-on: macos-26
strategy:
matrix:
- arch: [ x64, arm64 ]
+ arch: [x64, arm64]
fail-fast: false
defaults:
run:
@@ -185,7 +186,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.312"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -198,7 +199,7 @@ jobs:
- name: Build VoiceCraft.Client.MacOS.${{ matrix.arch }}.dmg
run: |
- cd ./bin/Release/net9.0-macos/osx-${{ matrix.arch }}
+ cd ./bin/Release/net10.0-macos/osx-${{ matrix.arch }}
APP_BUNDLE=$(find . -maxdepth 1 -name "*.app" -print -quit)
if [ -z "$APP_BUNDLE" ]; then
echo "No .app bundle found"
@@ -217,7 +218,7 @@ jobs:
- name: Normalize VoiceCraft.Client.MacOS.${{ matrix.arch }}.pkg name
run: |
- cd ./bin/Release/net9.0-macos/osx-${{ matrix.arch }}
+ cd ./bin/Release/net10.0-macos/osx-${{ matrix.arch }}
PKG_PATH=$(ls ./publish/*.pkg | head -n 1)
cp "$PKG_PATH" "VoiceCraft.Client.MacOS.${{ matrix.arch }}.pkg"
@@ -225,19 +226,19 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.MacOS.${{ matrix.arch }}.dmg
- path: ./VoiceCraft.Client/VoiceCraft.Client.MacOS/bin/Release/net9.0-macos/osx-${{ matrix.arch }}/VoiceCraft.Client.MacOS.${{ matrix.arch }}.dmg
+ path: ./VoiceCraft.Client/VoiceCraft.Client.MacOS/bin/Release/net10.0-macos/osx-${{ matrix.arch }}/VoiceCraft.Client.MacOS.${{ matrix.arch }}.dmg
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Client.MacOS.${{ matrix.arch }}.pkg
- path: ./VoiceCraft.Client/VoiceCraft.Client.MacOS/bin/Release/net9.0-macos/osx-${{ matrix.arch }}/VoiceCraft.Client.MacOS.${{ matrix.arch }}.pkg
+ path: ./VoiceCraft.Client/VoiceCraft.Client.MacOS/bin/Release/net10.0-macos/osx-${{ matrix.arch }}/VoiceCraft.Client.MacOS.${{ matrix.arch }}.pkg
VoiceCraft-Server-Windows:
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [ x64, x86, arm64 ]
+ arch: [x64, x86, arm64]
fail-fast: false
defaults:
run:
@@ -247,7 +248,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -259,13 +260,13 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Server.Windows.${{ matrix.arch }}
- path: ./VoiceCraft.Server/bin/Release/net9.0/win-${{ matrix.arch }}/publish
-
+ path: ./VoiceCraft.Server/bin/Release/net10.0/win-${{ matrix.arch }}/publish
+
VoiceCraft-Server-Linux:
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [ x64, arm, arm64 ]
+ arch: [x64, arm, arm64]
fail-fast: false
defaults:
run:
@@ -275,7 +276,7 @@ jobs:
- name: Setup Dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Install WorkLoads
run: dotnet workload restore
@@ -287,4 +288,4 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: VoiceCraft.Server.Linux.${{ matrix.arch }}
- path: ./VoiceCraft.Server/bin/Release/net9.0/linux-${{ matrix.arch }}/publish
+ path: ./VoiceCraft.Server/bin/Release/net10.0/linux-${{ matrix.arch }}/publish
diff --git a/.gitignore b/.gitignore
index 86bb20a0..0549b186 100644
--- a/.gitignore
+++ b/.gitignore
@@ -819,3 +819,5 @@ $RECYCLE.BIN/
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
+.temp/
+config/CrashLogs.json
diff --git a/Directory.Build.props b/Directory.Build.props
index 34a40042..f75158c6 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- 1.5.1
+ 1.6.0
15
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 85cfb02e..1e129f6b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,7 +2,7 @@
true
- 11.3.14
+ 12.0.1
@@ -10,31 +10,29 @@
-
- all
- none
-
+
+
-
+
-
+
-
+
-
+
-
-
+
+
-
\ No newline at end of file
+
diff --git a/NuGet.config b/NuGet.config
new file mode 100644
index 00000000..765346e5
--- /dev/null
+++ b/NuGet.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/VoiceCraft.Client.Tests/Audio/CombinedAudioPreprocessorTests.cs b/VoiceCraft.Client.Tests/Audio/CombinedAudioPreprocessorTests.cs
index 5d3f1c90..b11b0ee7 100644
--- a/VoiceCraft.Client.Tests/Audio/CombinedAudioPreprocessorTests.cs
+++ b/VoiceCraft.Client.Tests/Audio/CombinedAudioPreprocessorTests.cs
@@ -46,17 +46,16 @@ public void Dispose_DisposesInstantiatedProcessorsAndBlocksFurtherUse()
{
FakeAudioPreprocessor? gain = null;
FakeAudioPreprocessor? denoiser = null;
- using var preprocessor = new CombinedAudioPreprocessor(
+ var preprocessor = new CombinedAudioPreprocessor(
new RegisteredAudioPreprocessor(Guid.NewGuid(), "gain", () => gain = new FakeAudioPreprocessor(), true, true, true),
new RegisteredAudioPreprocessor(Guid.NewGuid(), "denoiser", () => denoiser = new FakeAudioPreprocessor(), true, true, true),
null);
-
+
preprocessor.Dispose();
-
Assert.NotNull(gain);
Assert.NotNull(denoiser);
- Assert.Equal(1, gain!.DisposeCalls);
- Assert.Equal(1, denoiser!.DisposeCalls);
+ Assert.Equal(1, gain.DisposeCalls);
+ Assert.Equal(1, denoiser.DisposeCalls);
Assert.Throws(() => preprocessor.Process([0]));
}
diff --git a/VoiceCraft.Client.Tests/Services/LogServiceTests.cs b/VoiceCraft.Client.Tests/Services/LogServiceTests.cs
new file mode 100644
index 00000000..ee3a35dc
--- /dev/null
+++ b/VoiceCraft.Client.Tests/Services/LogServiceTests.cs
@@ -0,0 +1,17 @@
+using VoiceCraft.Client.Services;
+
+namespace VoiceCraft.Client.Tests.Services;
+
+public class LogServiceTests
+{
+ [Fact]
+ public void Log_TrimsExceptionLogsToLimit()
+ {
+ LogService.ClearExceptionLogs();
+
+ for (var i = 0; i < 55; i++)
+ LogService.Log(new InvalidOperationException($"test-{i}"));
+
+ Assert.InRange(LogService.ExceptionLogs.Count(), 1, 50);
+ }
+}
diff --git a/VoiceCraft.Client.Tests/Services/SettingsServiceTests.cs b/VoiceCraft.Client.Tests/Services/SettingsServiceTests.cs
index 9e778514..a0d22c9a 100644
--- a/VoiceCraft.Client.Tests/Services/SettingsServiceTests.cs
+++ b/VoiceCraft.Client.Tests/Services/SettingsServiceTests.cs
@@ -53,11 +53,27 @@ public async Task SaveImmediate_WritesSerializedSettings()
var saved = JsonSerializer.Deserialize(storage.StoredBytes, SettingsStructureGenerationContext.Default.SettingsStructure);
Assert.NotNull(saved);
- Assert.Equal("Mic-2", saved!.InputSettings.InputDevice);
+ Assert.Equal("Mic-2", saved.InputSettings.InputDevice);
Assert.Equal("Speaker-2", saved.OutputSettings.OutputDevice);
Assert.Equal(Constants.LightThemeGuid, saved.ThemeSettings.SelectedTheme);
}
+ [Fact]
+ public void Constructor_WhenPersistedSettingsAreInvalid_FallsBackToDefaults()
+ {
+ InitializeLocalizer();
+ var storage = new FakeStorageService
+ {
+ ExistsResult = true,
+ StoredBytes = Encoding.UTF8.GetBytes("{")
+ };
+
+ var service = new SettingsService(storage);
+
+ Assert.Equal("Default", service.InputSettings.InputDevice);
+ Assert.NotEqual(Guid.Empty, service.UserGuid);
+ }
+
private static void InitializeLocalizer()
{
Localizer.BaseLocalizer = new EmbeddedJsonLocalizer("VoiceCraft.Client.Locales");
diff --git a/VoiceCraft.Client.Tests/VoiceCraft.Client.Tests.csproj b/VoiceCraft.Client.Tests/VoiceCraft.Client.Tests.csproj
index d52f31f6..eea21d1e 100644
--- a/VoiceCraft.Client.Tests/VoiceCraft.Client.Tests.csproj
+++ b/VoiceCraft.Client.Tests/VoiceCraft.Client.Tests.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net10.0
enable
enable
false
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Android/AndroidApp.cs b/VoiceCraft.Client/VoiceCraft.Client.Android/AndroidApp.cs
new file mode 100644
index 00000000..eb281696
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client.Android/AndroidApp.cs
@@ -0,0 +1,101 @@
+using System;
+using Android.Media;
+using Android.Runtime;
+using Avalonia;
+using Avalonia.Android;
+using Microsoft.Extensions.DependencyInjection;
+using SoundFlow.Abstracts;
+using VoiceCraft.Client.Android.Audio;
+using VoiceCraft.Client.Services;
+using VoiceCraft.Core;
+using VoiceCraft.Core.Interfaces;
+using VoiceCraft.Network.Clients;
+using Debug = System.Diagnostics.Debug;
+using Exception = System.Exception;
+
+namespace VoiceCraft.Client.Android;
+
+[global::Android.App.Application]
+public class AndroidApp : AvaloniaAndroidApplication
+{
+ protected AndroidApp(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
+ {
+
+ }
+
+ public override void OnCreate()
+ {
+ BootstrapServices();
+ base.OnCreate();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ try
+ {
+ if (App.ServiceProvider == null) return;
+ var serviceProvider = App.ServiceProvider;
+ serviceProvider.Dispose();
+ }
+ finally
+ {
+ base.Dispose(disposing);
+ }
+ }
+
+ protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
+ {
+ return base.CustomizeAppBuilder(builder)
+ .WithInterFont();
+ }
+
+ private static void BootstrapServices()
+ {
+ var nativeStorage = new NativeStorageService();
+ LogService.NativeStorageService = nativeStorage;
+ LogService.Load();
+ AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException;
+
+ var audioManager = (AudioManager?)Context.GetSystemService(AudioService);
+ if (audioManager == null)
+ throw new Exception($"Could not find {AudioService}. Cannot initialize audio service.");
+
+ App.ServiceCollection.AddSingleton(_ =>
+ new AndroidMiniAudioEngine(audioManager));
+ App.ServiceCollection.AddSingleton(nativeStorage);
+ App.ServiceCollection.AddSingleton();
+ App.ServiceCollection.AddSingleton(x =>
+ new NativeBackgroundService(x.GetRequiredService(), x.GetRequiredService));
+ App.ServiceCollection.AddSingleton(_ =>
+ new RegisteredAudioPreprocessor(
+ Constants.SpeexDspPreprocessorGuid,
+ "AudioService.Preprocessors.Speex",
+ () => new SpeexDspPreprocessor(
+ Constants.SampleRate,
+ Constants.FrameSize,
+ Constants.RecordingChannels,
+ Constants.PlaybackChannels),
+ true,
+ true,
+ true));
+ App.ServiceCollection.AddTransient(x =>
+ new LiteNetVoiceCraftClient(
+ x.GetRequiredService(),
+ x.GetRequiredService));
+ App.ServiceCollection.AddTransient();
+ App.ServiceCollection.AddTransient();
+ }
+
+ private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ try
+ {
+ if (e.ExceptionObject is Exception ex)
+ LogService.LogCrash(ex);
+ }
+ catch (Exception writeEx)
+ {
+ Debug.WriteLine(writeEx);
+ }
+ }
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Android/Audio/AndroidMiniAudioEngine.cs b/VoiceCraft.Client/VoiceCraft.Client.Android/Audio/AndroidMiniAudioEngine.cs
index 629aba94..6eed55bc 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Android/Audio/AndroidMiniAudioEngine.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.Android/Audio/AndroidMiniAudioEngine.cs
@@ -1,19 +1,24 @@
using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Android.Media;
+using Android.OS;
using SoundFlow.Abstracts;
using SoundFlow.Abstracts.Devices;
using SoundFlow.Backends.MiniAudio.Devices;
using SoundFlow.Backends.MiniAudio.Enums;
using SoundFlow.Enums;
+using VoiceCraft.Core.Audio;
namespace VoiceCraft.Client.Android.Audio;
public class AndroidMiniAudioEngine : AudioEngine
{
private readonly AudioManager _audioManager;
+ private readonly List _activeDevices = [];
public AndroidMiniAudioEngine(AudioManager audioManager)
{
@@ -23,22 +28,42 @@ public AndroidMiniAudioEngine(AudioManager audioManager)
protected override void CleanupBackend()
{
+ foreach (var device in _activeDevices.ToList())
+ {
+ device.Dispose();
+ }
+
+ _activeDevices.Clear();
}
public override AudioPlaybackDevice InitializePlaybackDevice(
- SoundFlow.Structs.DeviceInfo? deviceInfo,
+ SoundFlow.Structs.DeviceInfo? deviceInfo,
SoundFlow.Structs.AudioFormat format,
DeviceConfig? config = null)
{
- throw new System.NotImplementedException();
+ if (config != null && config is not MiniAudioDeviceConfig)
+ throw new ArgumentException($"config must be of type {typeof(MiniAudioDeviceConfig)}");
+ config ??= new MiniAudioDeviceConfig();
+
+ var device = new AndroidAudioPlaybackDevice(_audioManager, this, deviceInfo, format, config);
+ _activeDevices.Add(device);
+ device.OnDisposed += OnDeviceDisposing;
+ return device;
}
public override AudioCaptureDevice InitializeCaptureDevice(
- SoundFlow.Structs.DeviceInfo? deviceInfo,
+ SoundFlow.Structs.DeviceInfo? deviceInfo,
SoundFlow.Structs.AudioFormat format,
DeviceConfig? config = null)
{
- throw new System.NotImplementedException();
+ if (config != null && config is not MiniAudioDeviceConfig)
+ throw new ArgumentException($"config must be of type {typeof(MiniAudioDeviceConfig)}");
+ config ??= new MiniAudioDeviceConfig();
+
+ var device = new AndroidAudioCaptureDevice(_audioManager, this, deviceInfo, format, config);
+ _activeDevices.Add(device);
+ device.OnDisposed += OnDeviceDisposing;
+ return device;
}
public override FullDuplexDevice InitializeFullDuplexDevice(
@@ -72,7 +97,7 @@ public override AudioPlaybackDevice SwitchDevice(
}
public override AudioCaptureDevice SwitchDevice(
- AudioCaptureDevice oldDevice,
+ AudioCaptureDevice oldDevice,
SoundFlow.Structs.DeviceInfo newDeviceInfo,
DeviceConfig? config = null)
{
@@ -89,7 +114,7 @@ public override AudioCaptureDevice SwitchDevice(
public override FullDuplexDevice SwitchDevice(
FullDuplexDevice oldDevice,
SoundFlow.Structs.DeviceInfo? newPlaybackInfo,
- SoundFlow.Structs.DeviceInfo? newCaptureInfo,
+ SoundFlow.Structs.DeviceInfo? newCaptureInfo,
DeviceConfig? config = null)
{
throw new NotSupportedException("Full duplex mode is not supported by the Android audio engine.");
@@ -128,6 +153,14 @@ private void UpdateCaptureDevices()
CaptureDevices = [];
}
}
+
+ private void OnDeviceDisposing(object? sender, EventArgs e)
+ {
+ if (sender is AudioDevice device)
+ {
+ _activeDevices.Remove(device);
+ }
+ }
}
internal sealed class AndroidAudioCaptureDevice : AudioCaptureDevice
@@ -135,6 +168,8 @@ internal sealed class AndroidAudioCaptureDevice : AudioCaptureDevice
private readonly Lock _lock = new();
private readonly AudioRecord _nativeRecorder;
private readonly float[] _buffer;
+ private readonly short[] _pcm16Buffer;
+ private readonly bool _readPcm16;
public AndroidAudioCaptureDevice(
AudioManager audioManager,
@@ -168,13 +203,15 @@ public AndroidAudioCaptureDevice(
var periods = deviceConfig?.Periods > 0 ? deviceConfig.Periods : 3;
var bufferSize = periodFrames * format.Channels;
_buffer = new float[bufferSize];
+ _readPcm16 = IsGoogleDevice();
+ _pcm16Buffer = _readPcm16 ? new short[bufferSize] : [];
_nativeRecorder = new AudioRecord(
source,
format.SampleRate,
channelMask,
- Encoding.PcmFloat,
- (int)(bufferSize * sizeof(float) * periods));
+ _readPcm16 ? Encoding.Pcm16bit : Encoding.PcmFloat,
+ (int)(bufferSize * (_readPcm16 ? sizeof(short) : sizeof(float)) * periods));
var device = audioManager.GetDevices(GetDevicesTargets.Inputs)
?.FirstOrDefault(device => device.Id == deviceInfo?.Id);
_nativeRecorder.SetPreferredDevice(device);
@@ -200,7 +237,9 @@ public override void Stop()
if (IsDisposed || !IsRunning)
return;
- _nativeRecorder.Stop();
+ if (_nativeRecorder is { RecordingState: RecordState.Recording, State: State.Initialized })
+ _nativeRecorder.Stop();
+
IsRunning = false;
}
}
@@ -220,23 +259,58 @@ private void RecordingLogic()
{
while (_nativeRecorder is { RecordingState: RecordState.Recording, State: State.Initialized })
{
- lock (_lock)
+ Array.Clear(_buffer, 0, _buffer.Length);
+ var read = 0;
+ try
{
- Array.Clear(_buffer, 0, _buffer.Length);
- var read = _nativeRecorder.Read(_buffer, 0, _buffer.Length, 0);
- if (read <= 0) return;
- InvokeOnAudioProcessed(_buffer);
+ lock (_lock)
+ {
+ if (_readPcm16)
+ {
+ read = _nativeRecorder.Read(_pcm16Buffer, 0, _pcm16Buffer.Length, 0);
+ if (read > 0)
+ Sample16ToFloat.Read(_pcm16Buffer.AsSpan(0, read), _buffer.AsSpan(0, read));
+ }
+ else
+ {
+ read = _nativeRecorder.Read(_buffer, 0, _buffer.Length, 0);
+ }
+ }
}
+ catch (Exception)
+ {
+ //Do Nothing
+ }
+
+ if (read <= 0)
+ continue;
+
+ InvokeOnAudioProcessed(_buffer);
}
+
+ Stop();
+ }
+
+ private static bool IsGoogleDevice()
+ {
+ var manufacturer = Build.Manufacturer ?? string.Empty;
+ var brand = Build.Brand ?? string.Empty;
+ return manufacturer.Equals("Google", StringComparison.OrdinalIgnoreCase) ||
+ brand.Equals("google", StringComparison.OrdinalIgnoreCase);
}
}
internal sealed class AndroidAudioPlaybackDevice : AudioPlaybackDevice
{
+ private delegate void SoundComponentProcessDelegate(SoundComponent component, Span outputBuffer,
+ int channels);
+
+ private static readonly SoundComponentProcessDelegate ProcessComponent = BuildProcessDelegate();
+
private readonly Lock _lock = new();
private readonly AudioTrack _nativePlayer;
private readonly float[] _buffer;
-
+
public AndroidAudioPlaybackDevice(
AudioManager audioManager,
AudioEngine engine,
@@ -246,7 +320,7 @@ public AndroidAudioPlaybackDevice(
{
Capability = Capability.Playback;
Info = deviceInfo;
-
+
var deviceConfig = config as MiniAudioDeviceConfig;
var periodFrames = deviceConfig?.PeriodSizeInFrames switch
{
@@ -262,9 +336,9 @@ public AndroidAudioPlaybackDevice(
AAudioUsage.Notification => AudioUsageKind.Notification,
AAudioUsage.NotificationRingtone => AudioUsageKind.NotificationRingtone,
AAudioUsage.NotificationEvent => AudioUsageKind.NotificationEvent,
- AAudioUsage.AssistanceAccessibility => AudioUsageKind.AssistanceAccessibility,
- AAudioUsage.AssistanceNavigationGuidance => AudioUsageKind.AssistanceNavigationGuidance,
- AAudioUsage.AssistanceSonification => AudioUsageKind.AssistanceSonification,
+ AAudioUsage.AssistanceAccessibility => AudioUsageKind.AssistanceAccessibility,
+ AAudioUsage.AssistanceNavigationGuidance => AudioUsageKind.AssistanceNavigationGuidance,
+ AAudioUsage.AssistanceSonification => AudioUsageKind.AssistanceSonification,
AAudioUsage.Game => AudioUsageKind.Game,
_ => AudioUsageKind.Unknown
};
@@ -303,12 +377,12 @@ public AndroidAudioPlaybackDevice(
.SetTransferMode(AudioTrackMode.Stream)
.Build();
_nativePlayer.SetVolume(1.0f);
-
+
var device = audioManager.GetDevices(GetDevicesTargets.Outputs)
?.FirstOrDefault(device => device.Id == deviceInfo?.Id);
_nativePlayer.SetPreferredDevice(device);
}
-
+
public override void Start()
{
lock (_lock)
@@ -329,7 +403,13 @@ public override void Stop()
if (IsDisposed || !IsRunning)
return;
- _nativePlayer.Stop();
+ if (_nativePlayer is { PlayState: PlayState.Playing, State: AudioTrackState.Initialized })
+ {
+ _nativePlayer.Pause();
+ _nativePlayer.Flush();
+ _nativePlayer.Stop();
+ }
+
IsRunning = false;
}
}
@@ -344,21 +424,38 @@ public override void Dispose()
IsDisposed = true;
}
}
-
+
private void PlaybackLogic()
{
while (_nativePlayer is { PlayState: PlayState.Playing, State: AudioTrackState.Initialized })
{
- lock (_lock)
+ Array.Clear(_buffer, 0, _buffer.Length);
+ // Process the audio graph
+ var soloed = Engine.GetSoloedComponent();
+ if (soloed != null)
+ ProcessComponent(soloed, _buffer, Format.Channels);
+ else
+ ProcessComponent(MasterMixer, _buffer, Format.Channels);
+
+ try
+ {
+ lock (_lock)
+ _nativePlayer.Write(_buffer, 0, _buffer.Length, WriteMode.Blocking);
+ }
+ catch (Exception)
{
- Array.Clear(_buffer, 0, _buffer.Length);
- // Process the audio graph
- var soloed = Engine.GetSoloedComponent();
- if (soloed != null)
- soloed.Process(_buffer, Format.Channels);
- else
- MasterMixer.Process(_buffer, Format.Channels);
+ //Do Nothing
}
}
+
+ Stop();
}
-}
\ No newline at end of file
+
+ private static SoundComponentProcessDelegate BuildProcessDelegate()
+ {
+ var method = typeof(SoundComponent).GetMethod("Process", BindingFlags.Instance | BindingFlags.NonPublic);
+ return method == null
+ ? throw new InvalidOperationException("SoundFlow process method not found.")
+ : method.CreateDelegate();
+ }
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Android/Audio/SpeexDspPreprocessor.cs b/VoiceCraft.Client/VoiceCraft.Client.Android/Audio/SpeexDspPreprocessor.cs
index aca101da..2e24fb54 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Android/Audio/SpeexDspPreprocessor.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.Android/Audio/SpeexDspPreprocessor.cs
@@ -84,7 +84,7 @@ public SpeexDspPreprocessor(int sampleRate, int frameSize, int nbMicrophones, in
var filterLength = 100 * sampleRate / 1000;
_preprocessor = new SpeexDSPPreprocessor(frameSize, sampleRate);
- _echoCanceler = new SpeexDSPEchoCanceler(frameSize, filterLength, 1, nbSpeakers);
+ _echoCanceler = new SpeexDSPEchoCanceler(frameSize, filterLength, 1, nbSpeakers, use_static: false);
_captureBuffer = new SampleBufferProvider(filterLength * nbSpeakers);
_captureBufferFrame = new short[frameSize * nbSpeakers];
DenoiserEnabled = true;
@@ -178,4 +178,4 @@ private void Dispose(bool disposing)
_disposed = true;
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Android/MainActivity.cs b/VoiceCraft.Client/VoiceCraft.Client.Android/MainActivity.cs
index 48078106..df23c09a 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Android/MainActivity.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.Android/MainActivity.cs
@@ -1,22 +1,10 @@
-using System;
using Android.App;
using Android.Content.PM;
-using Android.Media;
using Android.OS;
-using AndroidX.Activity;
-using Avalonia;
using Avalonia.Android;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.ApplicationModel;
-using SoundFlow.Abstracts;
-using SoundFlow.Backends.MiniAudio;
-using VoiceCraft.Client.Android.Audio;
using VoiceCraft.Client.Services;
-using VoiceCraft.Core;
-using VoiceCraft.Core.Interfaces;
-using VoiceCraft.Network.Clients;
-using Debug = System.Diagnostics.Debug;
-using Exception = System.Exception;
namespace VoiceCraft.Client.Android;
@@ -26,86 +14,27 @@ namespace VoiceCraft.Client.Android;
Icon = "@drawable/icon",
MainLauncher = true,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
-public class MainActivity : AvaloniaMainActivity
+public class MainActivity : AvaloniaMainActivity
{
- public override void OnRequestPermissionsResult(int requestCode, string[] permissions,
- Permission[] grantResults)
- {
- Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
-
- base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
- }
-
- protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
- {
- return base.CustomizeAppBuilder(builder)
- .WithInterFont();
- }
-
protected override void OnCreate(Bundle? app)
{
- var nativeStorage = new NativeStorageService();
- LogService.NativeStorageService = nativeStorage;
- LogService.Load();
- AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException;
- OnBackPressedDispatcher.AddCallback(this, new BackPressedCallback(this));
-
- var audioManager = (AudioManager?)GetSystemService(AudioService);
- if (audioManager == null)
- throw new Exception($"Could not find {AudioService}. Cannot initialize audio service.");
-
- App.ServiceCollection.AddSingleton();
- App.ServiceCollection.AddSingleton(nativeStorage);
- App.ServiceCollection.AddSingleton();
- App.ServiceCollection.AddSingleton(x =>
- new NativeBackgroundService(x.GetRequiredService(), x.GetRequiredService));
- App.ServiceCollection.AddSingleton(_ =>
- new RegisteredAudioPreprocessor(
- Constants.SpeexDspPreprocessorGuid,
- "AudioService.Preprocessors.Speex",
- () => new SpeexDspPreprocessor(
- Constants.SampleRate,
- Constants.FrameSize,
- Constants.RecordingChannels,
- Constants.PlaybackChannels),
- true,
- true,
- true));
- App.ServiceCollection.AddTransient(x =>
- new LiteNetVoiceCraftClient(x.GetRequiredService(),
- x.GetRequiredService));
- App.ServiceCollection.AddTransient();
- App.ServiceCollection.AddTransient();
-
- Platform.Init(this, app);
base.OnCreate(app);
+ Platform.Init(this, app);
}
-
- protected override void OnDestroy()
+
+ public override void OnRequestPermissionsResult(int requestCode, string[] permissions,
+ Permission[] grantResults)
{
- try
- {
- if (App.ServiceProvider == null) return;
- var serviceProvider = App.ServiceProvider;
- serviceProvider.Dispose();
- }
- finally
- {
- base.OnDestroy();
- }
+ Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
+ base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
- private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs e)
+ public override void OnBackPressed()
{
- try
- {
- if (e.ExceptionObject is Exception ex)
- LogService.LogCrash(ex); //Log it
- }
- catch (Exception writeEx)
- {
- Debug.WriteLine(writeEx); //We don't want to crash if the log failed.
- }
+ if (BackButtonBehavior()) return;
+ #pragma warning disable
+ base.OnBackPressed();
+ #pragma warning restore
}
private static bool BackButtonBehavior()
@@ -114,14 +43,4 @@ private static bool BackButtonBehavior()
var navigationService = App.ServiceProvider.GetService();
return navigationService?.Back(true) != null;
}
-
- private class BackPressedCallback(MainActivity activity, bool enabled = true) : OnBackPressedCallback(enabled)
- {
- public override void HandleOnBackPressed()
- {
- if (BackButtonBehavior()) return;
- activity.FinishAndRemoveTask();
- Process.KillProcess(Process.MyPid());
- }
- }
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Android/NativeBackgroundService.cs b/VoiceCraft.Client/VoiceCraft.Client.Android/NativeBackgroundService.cs
index d6bd0fdf..6d00975a 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Android/NativeBackgroundService.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.Android/NativeBackgroundService.cs
@@ -58,7 +58,7 @@ public async Task StartServiceAsync(Action, Action>
public void Dispose()
{
var context = Application.Context;
- var intent = new Intent(context, typeof(NativeBackgroundService));
+ var intent = new Intent(context, typeof(AndroidBackgroundService));
context.StopService(intent);
GC.SuppressFinalize(this);
}
@@ -146,4 +146,4 @@ public void Dispose()
GC.SuppressFinalize(this);
}
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Android/NativeHotKeyService.cs b/VoiceCraft.Client/VoiceCraft.Client.Android/NativeHotKeyService.cs
index 32797870..42a2ca0b 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Android/NativeHotKeyService.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.Android/NativeHotKeyService.cs
@@ -4,13 +4,9 @@
namespace VoiceCraft.Client.Android;
-public class NativeHotKeyService : HotKeyService
+public class NativeHotKeyService(IEnumerable registeredHotKeyActions, SettingsService settingsService)
+ : HotKeyService(registeredHotKeyActions, settingsService)
{
- public NativeHotKeyService(IEnumerable registeredHotKeyActions, SettingsService settingsService) :
- base(registeredHotKeyActions, settingsService)
- {
- }
-
public override void Initialize()
{
}
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Android/Resources/values-v31/styles.xml b/VoiceCraft.Client/VoiceCraft.Client.Android/Resources/values-v31/styles.xml
index a93e7c47..a4601725 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Android/Resources/values-v31/styles.xml
+++ b/VoiceCraft.Client/VoiceCraft.Client.Android/Resources/values-v31/styles.xml
@@ -11,11 +11,6 @@
- @color/splash_background
- @drawable/avalonia_anim
- 1000
- - @style/MyTheme.Main
-
-
-
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Android/VoiceCraft.Client.Android.csproj b/VoiceCraft.Client/VoiceCraft.Client.Android/VoiceCraft.Client.Android.csproj
index 948e0b52..17e06191 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Android/VoiceCraft.Client.Android.csproj
+++ b/VoiceCraft.Client/VoiceCraft.Client.Android/VoiceCraft.Client.Android.csproj
@@ -1,10 +1,10 @@
Exe
- net9.0-android
+ net10.0-android
21
23
- 35
+ 36
enable
com.AvionBlock.VoiceCraft.Client
$(AndroidVersion)
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Browser/VoiceCraft.Client.Browser.csproj b/VoiceCraft.Client/VoiceCraft.Client.Browser/VoiceCraft.Client.Browser.csproj
index f0844958..30eeb8b0 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Browser/VoiceCraft.Client.Browser.csproj
+++ b/VoiceCraft.Client/VoiceCraft.Client.Browser/VoiceCraft.Client.Browser.csproj
@@ -1,6 +1,6 @@
-
+
- net9.0-browser
+ net10.0-browser
Exe
true
enable
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Linux/Audio/SpeexDspPreprocessor.cs b/VoiceCraft.Client/VoiceCraft.Client.Linux/Audio/SpeexDspPreprocessor.cs
index dcf00c12..502000bc 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Linux/Audio/SpeexDspPreprocessor.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.Linux/Audio/SpeexDspPreprocessor.cs
@@ -84,7 +84,7 @@ public SpeexDspPreprocessor(int sampleRate, int frameSize, int nbMicrophones, in
var filterLength = 100 * sampleRate / 1000;
_preprocessor = new SpeexDSPPreprocessor(frameSize, sampleRate);
- _echoCanceler = new SpeexDSPEchoCanceler(frameSize, filterLength, 1, nbSpeakers);
+ _echoCanceler = new SpeexDSPEchoCanceler(frameSize, filterLength, 1, nbSpeakers, use_static: false);
_captureBuffer = new SampleBufferProvider(filterLength * nbSpeakers);
_captureBufferFrame = new short[frameSize * nbSpeakers];
DenoiserEnabled = true;
@@ -178,4 +178,4 @@ private void Dispose(bool disposing)
_disposed = true;
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Linux/Program.cs b/VoiceCraft.Client/VoiceCraft.Client.Linux/Program.cs
index beae28d2..977ac893 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Linux/Program.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.Linux/Program.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Diagnostics;
using Avalonia;
using Microsoft.Extensions.DependencyInjection;
@@ -45,7 +45,8 @@ public static void Main(string[] args)
true,
true));
App.ServiceCollection.AddTransient(x =>
- new LiteNetVoiceCraftClient(x.GetRequiredService(),
+ new LiteNetVoiceCraftClient(
+ x.GetRequiredService(),
x.GetRequiredService));
App.ServiceCollection.AddTransient();
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
@@ -81,4 +82,4 @@ private static void CurrentDomainOnUnhandledException(object sender, UnhandledEx
Debug.WriteLine(writeEx); //We don't want to crash if the log failed.
}
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Linux/VoiceCraft.Client.Linux.csproj b/VoiceCraft.Client/VoiceCraft.Client.Linux/VoiceCraft.Client.Linux.csproj
index be3a2a6b..7a5de2dd 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Linux/VoiceCraft.Client.Linux.csproj
+++ b/VoiceCraft.Client/VoiceCraft.Client.Linux/VoiceCraft.Client.Linux.csproj
@@ -1,7 +1,9 @@
Exe
- net9.0
+
+ net10.0
enable
true
link
diff --git a/VoiceCraft.Client/VoiceCraft.Client.MacOS/Info.plist b/VoiceCraft.Client/VoiceCraft.Client.MacOS/Info.plist
index a6e52985..c62049a2 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.MacOS/Info.plist
+++ b/VoiceCraft.Client/VoiceCraft.Client.MacOS/Info.plist
@@ -11,9 +11,9 @@
CFBundleIdentifier
com.avionblock.voicecraft.client.macos
CFBundleShortVersionString
- 1.5.1
+ 1.6.0
CFBundleVersion
- 1.5.1
+ 1.6.0
CFBundleIconFile
AppIcon.icns
LSApplicationCategoryType
diff --git a/VoiceCraft.Client/VoiceCraft.Client.MacOS/Program.cs b/VoiceCraft.Client/VoiceCraft.Client.MacOS/Program.cs
index 6ba94332..d8940921 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.MacOS/Program.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.MacOS/Program.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Diagnostics;
using Avalonia;
using Microsoft.Extensions.DependencyInjection;
diff --git a/VoiceCraft.Client/VoiceCraft.Client.MacOS/VoiceCraft.Client.MacOS.csproj b/VoiceCraft.Client/VoiceCraft.Client.MacOS/VoiceCraft.Client.MacOS.csproj
index 75d09850..67f29b83 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.MacOS/VoiceCraft.Client.MacOS.csproj
+++ b/VoiceCraft.Client/VoiceCraft.Client.MacOS/VoiceCraft.Client.MacOS.csproj
@@ -1,7 +1,9 @@
Exe
- net9.0-macos
+
+ net10.0-macos
13.0
VoiceCraft
VoiceCraft
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Windows/Audio/SpeexDspPreprocessor.cs b/VoiceCraft.Client/VoiceCraft.Client.Windows/Audio/SpeexDspPreprocessor.cs
index 360770a6..a1ccca10 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Windows/Audio/SpeexDspPreprocessor.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.Windows/Audio/SpeexDspPreprocessor.cs
@@ -84,7 +84,7 @@ public SpeexDspPreprocessor(int sampleRate, int frameSize, int nbMicrophones, in
var filterLength = 100 * sampleRate / 1000;
_preprocessor = new SpeexDSPPreprocessor(frameSize, sampleRate);
- _echoCanceler = new SpeexDSPEchoCanceler(frameSize, filterLength, 1, nbSpeakers);
+ _echoCanceler = new SpeexDSPEchoCanceler(frameSize, filterLength, 1, nbSpeakers, use_static: false);
_captureBuffer = new SampleBufferProvider(filterLength * nbSpeakers);
_captureBufferFrame = new short[frameSize * nbSpeakers];
DenoiserEnabled = true;
@@ -178,4 +178,4 @@ private void Dispose(bool disposing)
_disposed = true;
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Windows/Program.cs b/VoiceCraft.Client/VoiceCraft.Client.Windows/Program.cs
index 558879a2..bd337ec3 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Windows/Program.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.Windows/Program.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Diagnostics;
using Avalonia;
using Microsoft.Extensions.DependencyInjection;
@@ -37,7 +37,7 @@ public static void Main(string[] args)
new NativeBackgroundService(x.GetRequiredService));
App.ServiceCollection.AddSingleton(_ =>
new RegisteredAudioPreprocessor(
- Constants.SpeexDspPreprocessorGuid,
+ Constants.SpeexDspPreprocessorGuid,
"AudioService.Preprocessors.Speex",
() => new SpeexDspPreprocessor(
Constants.SampleRate,
@@ -48,7 +48,8 @@ public static void Main(string[] args)
true,
true));
App.ServiceCollection.AddTransient(x =>
- new LiteNetVoiceCraftClient(x.GetRequiredService(),
+ new LiteNetVoiceCraftClient(
+ x.GetRequiredService(),
x.GetRequiredService));
App.ServiceCollection.AddTransient();
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
diff --git a/VoiceCraft.Client/VoiceCraft.Client.Windows/VoiceCraft.Client.Windows.csproj b/VoiceCraft.Client/VoiceCraft.Client.Windows/VoiceCraft.Client.Windows.csproj
index 4083664f..bb57261f 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.Windows/VoiceCraft.Client.Windows.csproj
+++ b/VoiceCraft.Client/VoiceCraft.Client.Windows/VoiceCraft.Client.Windows.csproj
@@ -1,7 +1,9 @@
WinExe
- net9.0-windows
+
+ net10.0-windows
enable
link
true
@@ -14,6 +16,8 @@
+
+
diff --git a/VoiceCraft.Client/VoiceCraft.Client.iOS/Audio/IosMiniAudioEngine.cs b/VoiceCraft.Client/VoiceCraft.Client.iOS/Audio/IosMiniAudioEngine.cs
index e6c76a5e..9618d062 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.iOS/Audio/IosMiniAudioEngine.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client.iOS/Audio/IosMiniAudioEngine.cs
@@ -124,7 +124,7 @@ public IosAudioCaptureDevice(AudioEngine engine, DeviceInfo deviceInfo, AudioFor
_periodFrames = config is MiniAudioDeviceConfig deviceConfig && deviceConfig.PeriodSizeInFrames > 0
? deviceConfig.PeriodSizeInFrames
: 960u;
- _chunkSamples = Constants.FrameSize * Math.Max(1, format.Channels);
+ _chunkSamples = (int)_periodFrames * Math.Max(1, format.Channels);
_stagingBuffer = new float[_chunkSamples * 8];
_stagingCount = 0;
}
diff --git a/VoiceCraft.Client/VoiceCraft.Client.iOS/Info.plist b/VoiceCraft.Client/VoiceCraft.Client.iOS/Info.plist
index 3cfd94a8..bec982bb 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.iOS/Info.plist
+++ b/VoiceCraft.Client/VoiceCraft.Client.iOS/Info.plist
@@ -9,9 +9,9 @@
CFBundleIdentifier
com.avionblock.voicecraft.client
CFBundleShortVersionString
- 1.5.1
+ 1.6.0
CFBundleVersion
- 1.5.1
+ 1.6.0
NSMicrophoneUsageDescription
VoiceCraft needs microphone access for voice chat.
LSRequiresIPhoneOS
diff --git a/VoiceCraft.Client/VoiceCraft.Client.iOS/VoiceCraft.Client.iOS.csproj b/VoiceCraft.Client/VoiceCraft.Client.iOS/VoiceCraft.Client.iOS.csproj
index 75c84d2a..22d4cbc8 100644
--- a/VoiceCraft.Client/VoiceCraft.Client.iOS/VoiceCraft.Client.iOS.csproj
+++ b/VoiceCraft.Client/VoiceCraft.Client.iOS/VoiceCraft.Client.iOS.csproj
@@ -1,7 +1,7 @@
-
+
Exe
- net9.0-ios
+ net10.0-ios
13.0
ios-arm64
VoiceCraft
diff --git a/VoiceCraft.Client/VoiceCraft.Client/App.axaml.cs b/VoiceCraft.Client/VoiceCraft.Client/App.axaml.cs
index 56268eff..6acbd9e8 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/App.axaml.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/App.axaml.cs
@@ -1,9 +1,7 @@
using System;
-using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
using Microsoft.Extensions.DependencyInjection;
@@ -15,10 +13,12 @@
using VoiceCraft.Client.Themes.Dark;
using VoiceCraft.Client.ViewModels;
using VoiceCraft.Client.ViewModels.Home;
+using VoiceCraft.Client.ViewModels.Modals;
using VoiceCraft.Client.ViewModels.Settings;
using VoiceCraft.Client.Views;
using VoiceCraft.Client.Views.Error;
using VoiceCraft.Client.Views.Home;
+using VoiceCraft.Client.Views.Modals;
using VoiceCraft.Client.Views.Settings;
using VoiceCraft.Core;
using VoiceCraft.Core.Audio;
@@ -42,10 +42,6 @@ public override void OnFrameworkInitializationCompleted()
{
try
{
- // Avoid duplicate validations from both Avalonia and the CommunityToolkit.
- // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
- DisableAvaloniaDataAnnotationValidation();
-
var serviceProvider = BuildServiceProvider();
SetupServices(serviceProvider);
@@ -56,17 +52,31 @@ public override void OnFrameworkInitializationCompleted()
{
DataContext = serviceProvider.GetRequiredService()
};
+ serviceProvider.GetRequiredService().RegisterTopLevel(desktop.MainWindow);
desktop.MainWindow.Closing += (__, ___) =>
{
_ = serviceProvider.GetRequiredService().SaveImmediate();
};
break;
+ case IActivityApplicationLifetime activityLifetime:
+ activityLifetime.MainViewFactory = () =>
+ {
+ var mainView = new MainView
+ {
+ DataContext = serviceProvider.GetRequiredService()
+ };
+ RegisterClipboardWhenAttached(serviceProvider, mainView);
+ return mainView;
+ };
+ break;
case ISingleViewApplicationLifetime singleViewPlatform:
- singleViewPlatform.MainView = new MainView
+ var singleView = new MainView
{
DataContext = serviceProvider.GetRequiredService()
};
+ RegisterClipboardWhenAttached(serviceProvider, singleView);
+ singleViewPlatform.MainView = singleView;
break;
}
@@ -83,6 +93,12 @@ public override void OnFrameworkInitializationCompleted()
DataContext = new ErrorViewModel { ErrorMessage = ex.ToString() }
};
break;
+ case IActivityApplicationLifetime activityLifetime:
+ activityLifetime.MainViewFactory = () => new MainView()
+ {
+ DataContext = new ErrorViewModel { ErrorMessage = ex.ToString() }
+ };
+ break;
case ISingleViewApplicationLifetime singleViewPlatform:
singleViewPlatform.MainView = new ErrorView
{
@@ -97,18 +113,6 @@ public override void OnFrameworkInitializationCompleted()
base.OnFrameworkInitializationCompleted();
}
- private static void DisableAvaloniaDataAnnotationValidation()
- {
-#pragma warning disable IL2026
- // Get an array of plugins to remove
- var dataValidationPluginsToRemove =
- BindingPlugins.DataValidators.OfType().ToArray();
-
- // remove each entry found
- foreach (var plugin in dataValidationPluginsToRemove) BindingPlugins.DataValidators.Remove(plugin);
-#pragma warning restore IL2026
- }
-
private static ServiceProvider BuildServiceProvider()
{
//Service Registry
@@ -117,6 +121,8 @@ private static ServiceProvider BuildServiceProvider()
ServiceCollection.AddSingleton(x =>
new NavigationService(y => (ViewModelBase)x.GetRequiredService(y)));
ServiceCollection.AddSingleton();
+ ServiceCollection.AddSingleton();
+ ServiceCollection.AddSingleton();
ServiceCollection.AddSingleton(x => new PermissionsService(
x.GetRequiredService(),
y => (Permissions.BasePermission)x.GetRequiredService(y)));
@@ -127,6 +133,11 @@ private static ServiceProvider BuildServiceProvider()
//Pages Registry
ServiceCollection.AddSingleton();
+ //Modals
+ ServiceCollection.AddTransient();
+ ServiceCollection.AddTransient();
+ ServiceCollection.AddTransient();
+
//Main Pages
ServiceCollection.AddSingleton();
ServiceCollection.AddTransient();
@@ -149,6 +160,9 @@ private static ServiceProvider BuildServiceProvider()
//Views
ServiceCollection.AddKeyedTransient(typeof(MainView).FullName);
+ ServiceCollection.AddKeyedTransient(typeof(TelemetryConsentView).FullName);
+ ServiceCollection.AddKeyedTransient(typeof(EntityDataSettingsView).FullName);
+ ServiceCollection.AddKeyedTransient(typeof(HotKeyCaptureView).FullName);
ServiceCollection.AddKeyedTransient(typeof(HomeView).FullName);
ServiceCollection.AddKeyedTransient(typeof(EditServerView).FullName);
ServiceCollection.AddKeyedTransient(typeof(AddServerView).FullName);
@@ -247,5 +261,15 @@ private void SetupServices(IServiceProvider serviceProvider)
{
Localizer.BaseLocalizer = new EmbeddedJsonLocalizer("VoiceCraft.Client.Locales");
DataTemplates.Add(serviceProvider.GetRequiredService());
+ _ = serviceProvider.GetRequiredService().ReportStartupAsync();
+ }
+
+ private static void RegisterClipboardWhenAttached(IServiceProvider serviceProvider, Control control)
+ {
+ control.AttachedToVisualTree += (_, _) =>
+ {
+ if (TopLevel.GetTopLevel(control) is { } topLevel)
+ serviceProvider.GetRequiredService().RegisterTopLevel(topLevel);
+ };
}
-}
+}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Locales/LocalizeExtension.cs b/VoiceCraft.Client/VoiceCraft.Client/Locales/LocalizeExtension.cs
index 1560b14b..c0496b80 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Locales/LocalizeExtension.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Locales/LocalizeExtension.cs
@@ -33,8 +33,8 @@ public override object ProvideValue(IServiceProvider serviceProvider)
};
}
- if (arg is not IBinding binding)
- throw new Exception("Argument must be of type IBinding or string!");
+ if (arg is not BindingBase binding)
+ throw new Exception("Argument must be of type BindingBase or string!");
var mb = new MultiBinding
{
@@ -48,4 +48,4 @@ public override object ProvideValue(IServiceProvider serviceProvider)
return mb;
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Locales/de-DE.json b/VoiceCraft.Client/VoiceCraft.Client/Locales/de-DE.json
index 2dec2de6..3eccbed1 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Locales/de-DE.json
+++ b/VoiceCraft.Client/VoiceCraft.Client/Locales/de-DE.json
@@ -13,7 +13,8 @@
"Language": "Sprache",
"NotificationDismiss": "Benachrichtigung ausblenden (ms)",
"DisableNotifications": "Benachrichtigungen deaktivieren",
- "HideServerAddresses": "Serveradressen ausblenden"
+ "HideServerAddresses": "Serveradressen ausblenden",
+ "EnableTelemetry": "Anonyme Telemetrie aktivieren"
},
"Appearance": {
"Title": "Darstellung",
@@ -28,6 +29,9 @@
"Denoisers": "Rauschunterdrückung",
"AutomaticGainControllers": "Automatische Verstärkungsregelung",
"EchoCancelers": "Echo-Unterdrückung",
+ "PushToTalk": "Zum Sprechen gedrückt halten",
+ "HardwarePreprocessors": null,
+ "PlayPushToTalkCue": "Push-to-Talk-Sounds abspielen",
"MicrophoneTest": {
"MicrophoneTest": "Mikrofontest",
"Test": "Test"
@@ -58,7 +62,11 @@
},
"Network": {
"Title": "Netzwerkeinstellungen",
- "PositioningType": "Positionierungstyp",
+ "PositioningType": {
+ "Title": "Positionierungstyp",
+ "Server": null,
+ "Client": null
+ },
"McWssListenIp": "MCWSS Listening-IP-Adresse",
"McWssHostPort": "MCWSS-Host-Port"
},
@@ -68,7 +76,6 @@
"CapturePrompt": "Taste oder Maustaste drücken",
"SaveBinding": "Belegung speichern",
"CancelRebind": "Abbrechen",
- "PlayPushToTalkCues": "Push-to-Talk-Sounds abspielen",
"Actions": {
"Mute": "Stummschalten",
"Deafen": "Taub schalten",
@@ -79,10 +86,15 @@
"Title": "Erweiterte Einstellungen",
"TriggerGc": "Garbage Collector auslösen",
"Crash": "Testabsturz",
+ "ResetAllSettings": "Alle Einstellungen zurücksetzen",
"Notification": {
"GC": {
"Badge": "GC",
"Triggered": "Garbage Collection ausgelöst. Freigegebener Speicher: {0}mb."
+ },
+ "Reset": {
+ "Badge": "Einstellungen",
+ "Done": "Alle Einstellungen wurden auf Standardwerte zurückgesetzt. Starte den Client neu, damit Sprache und Design vollständig übernommen werden."
}
}
},
@@ -110,14 +122,31 @@
},
"CrashLogs": {
"Title": "Absturzprotokolle",
+ "Action": {
+ "DumpLink": "Dump-Link"
+ },
"Notification": {
"Badge": "Absturzprotokolle",
"Cleared": "Alle Protokolle wurden erfolgreich gelöscht.",
"Copied": "Absturzprotokolle wurden in die Zwischenablage kopiert.",
+ "DumpCopied": "Link zum Absturz-Dump wurde in die Zwischenablage kopiert.",
+ "DumpUploadFailed": "Der Absturz-Dump konnte nicht hochgeladen werden.",
+ "DumpUnavailable": "Für dieses Absturzprotokoll ist noch kein Dump-Link verfügbar.",
"Empty": "Keine Absturzprotokolle zum Kopieren vorhanden."
}
},
- "AddServer": {
+ "TelemetryConsent": {
+ "Title": "VoiceCraft verbessern",
+ "Description": "VoiceCraft kann anonyme Telemetrie senden, damit wir Abstürze besser verstehen und die Stabilität auf allen Plattformen verbessern können.",
+ "CollectsTitle": "Was wir erfassen",
+ "CollectsBody": "App-Version, Plattform, Betriebssystem- und Runtime-Informationen, Architektur, Gebietsschema, CPU- und Speicherinformationen sowie anonyme Start- und Heartbeat-Diagnosen.",
+ "NotCollectedTitle": "Was wir nicht automatisch erfassen",
+ "NotCollectedBody": "Wir laden deine Serverliste, Tokens, Konfigurationswerte oder Chat- und Inhaltsdaten nicht automatisch hoch.",
+ "DumpBody": "Absturz-Dumps werden nur hochgeladen, wenn du im Absturzprotokoll ausdrücklich auf die Dump-Link-Schaltfläche klickst.",
+ "Accept": "Akzeptieren",
+ "Decline": "Ablehnen"
+ },
+ "AddServer": {
"Title": "Server hinzufügen",
"Name": "Name",
"IP": "IP",
@@ -214,7 +243,8 @@
"TanhSoft": "Weicher Tanh-Clipper"
},
"AudioDeviceInfo": {
- "Default": "{0} - Standard"
+ "Default": "Standard",
+ "DefaultDevice": "{0} - Standard"
}
},
"ThemesService": {
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Locales/en-US.json b/VoiceCraft.Client/VoiceCraft.Client/Locales/en-US.json
index 4de55c51..a17efce2 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Locales/en-US.json
+++ b/VoiceCraft.Client/VoiceCraft.Client/Locales/en-US.json
@@ -13,7 +13,8 @@
"Language": "Language",
"NotificationDismiss": "Notification Dismiss(ms)",
"DisableNotifications": "Disable Notifications",
- "HideServerAddresses": "Hide Server Addresses"
+ "HideServerAddresses": "Hide Server Addresses",
+ "EnableTelemetry": "Enable Anonymous Telemetry"
},
"Appearance": {
"Title": "Appearance",
@@ -28,6 +29,9 @@
"Denoisers": "Denoisers",
"AutomaticGainControllers": "Automatic Gain Controllers",
"EchoCancelers": "Echo Cancelers",
+ "HardwarePreprocessors": "Hardware Preprocessors",
+ "PushToTalk": "Push To Talk",
+ "PlayPushToTalkCue": "Play Push To Talk sounds",
"MicrophoneTest": {
"MicrophoneTest": "Microphone Test",
"Test": "Test"
@@ -58,7 +62,11 @@
},
"Network": {
"Title": "Network Settings",
- "PositioningType": "Positioning Type",
+ "PositioningType": {
+ "Title": "Positioning Type",
+ "Server": "Server",
+ "Client": "Client"
+ },
"McWssListenIp": "MCWSS Listening IP Address",
"McWssHostPort": "MCWSS Host Port"
},
@@ -68,7 +76,6 @@
"CapturePrompt": "Press a key or mouse button",
"SaveBinding": "Save Binding",
"CancelRebind": "Cancel",
- "PlayPushToTalkCues": "Play Push To Talk sounds",
"Actions": {
"Mute": "Mute",
"Deafen": "Deafen",
@@ -79,10 +86,15 @@
"Title": "Advanced Settings",
"TriggerGc": "Trigger Garbage Collector",
"Crash": "Test Crash",
+ "ResetAllSettings": "Reset All Settings",
"Notification": {
"GC": {
"Badge": "GC",
"Triggered": "Garbage Collection Triggered. Memory Cleared: {0}mb."
+ },
+ "Reset": {
+ "Badge": "Settings",
+ "Done": "All settings were reset to defaults. Restart the client to fully apply theme and language changes."
}
}
},
@@ -110,14 +122,31 @@
},
"CrashLogs": {
"Title": "Crash Logs",
+ "Action": {
+ "DumpLink": "Dump Link"
+ },
"Notification": {
"Badge": "CrashLogs",
"Cleared": "Successfully cleared all logs.",
"Copied": "Crash logs copied to clipboard.",
+ "DumpCopied": "Crash dump link copied to clipboard.",
+ "DumpUploadFailed": "Failed to upload the crash dump.",
+ "DumpUnavailable": "No uploaded dump link is available for this crash log.",
"Empty": "No crash logs to copy."
}
},
-
+ "TelemetryConsent": {
+ "Title": "Help Improve VoiceCraft",
+ "Description": "VoiceCraft can send anonymous telemetry so we can understand crashes and improve stability across platforms.",
+ "CollectsTitle": "What we collect",
+ "CollectsBody": "App version, platform, OS and runtime info, architecture, locale, CPU and memory info, plus anonymous startup and heartbeat diagnostics.",
+ "NotCollectedTitle": "What we do not collect automatically",
+ "NotCollectedBody": "We do not automatically upload your server list, tokens, config values, or chat/content data.",
+ "DumpBody": "Crash dumps are only uploaded when you explicitly press the dump link button in crash logs.",
+ "Accept": "Accept",
+ "Decline": "Decline"
+ },
+
"AddServer": {
"Title": "Add Server",
"Name": "Name",
@@ -217,7 +246,8 @@
"TanhSoft": "Tanh Soft Clipper"
},
"AudioDeviceInfo": {
- "Default": "{0} - Default"
+ "Default": "Default",
+ "DefaultDevice": "{0} - Default"
}
},
"ThemesService": {
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Locales/nl-NL.json b/VoiceCraft.Client/VoiceCraft.Client/Locales/nl-NL.json
index a1ab1e86..33a82354 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Locales/nl-NL.json
+++ b/VoiceCraft.Client/VoiceCraft.Client/Locales/nl-NL.json
@@ -13,7 +13,8 @@
"Language": "Taal",
"NotificationDismiss": "Melding sluiten (ms)",
"DisableNotifications": "Meldingen uitschakelen",
- "HideServerAddresses": "Serveradressen verbergen"
+ "HideServerAddresses": "Serveradressen verbergen",
+ "EnableTelemetry": "Anonieme telemetrie inschakelen"
},
"Appearance": {
"Title": "Uiterlijk",
@@ -28,6 +29,9 @@
"Denoisers": "Ruisonderdrukkers",
"AutomaticGainControllers": "Automatische versterkingsregelaars",
"EchoCancelers": "Echo-onderdrukkers",
+ "HardwarePreprocessors": null,
+ "PushToTalk": "Ingedrukt houden om te praten",
+ "PlayPushToTalkCue": "Push-to-talk-geluiden afspelen",
"MicrophoneTest": {
"MicrophoneTest": "Microfoontest",
"Test": "Test"
@@ -58,7 +62,11 @@
},
"Network": {
"Title": "Netwerkinstellingen",
- "PositioningType": "Positioneringstype",
+ "PositioningType": {
+ "Title": "Positioneringstype",
+ "Server": null,
+ "Client": null
+ },
"McWssListenIp": "MCWSS-luister-IP-adres",
"McWssHostPort": "MCWSS-hostpoort"
},
@@ -68,7 +76,6 @@
"CapturePrompt": "Druk op een toets of muisknop",
"SaveBinding": "Binding opslaan",
"CancelRebind": "Annuleren",
- "PlayPushToTalkCues": "Push-to-talk-geluiden afspelen",
"Actions": {
"Mute": "Dempen",
"Deafen": "Doof maken",
@@ -79,10 +86,15 @@
"Title": "Geavanceerde instellingen",
"TriggerGc": "Garbagecollector uitvoeren",
"Crash": "Testcrash",
+ "ResetAllSettings": "Alle instellingen resetten",
"Notification": {
"GC": {
"Badge": "GC",
"Triggered": "Garbage Collection uitgevoerd. Vrijgemaakt geheugen: {0}mb."
+ },
+ "Reset": {
+ "Badge": "Instellingen",
+ "Done": "Alle instellingen zijn teruggezet naar de standaardwaarden. Start de client opnieuw om taal en thema volledig toe te passen."
}
}
},
@@ -110,14 +122,31 @@
},
"CrashLogs": {
"Title": "Crashlogboeken",
+ "Action": {
+ "DumpLink": "Dumplink"
+ },
"Notification": {
"Badge": "Crashlogboeken",
"Cleared": "Alle logboeken zijn succesvol gewist.",
"Copied": "Crashlogboeken zijn naar het klembord gekopieerd.",
+ "DumpCopied": "Crashdumplink is naar het klembord gekopieerd.",
+ "DumpUploadFailed": "Het uploaden van de crashdump is mislukt.",
+ "DumpUnavailable": "Er is nog geen geuploade dumplink beschikbaar voor dit crashlogboek.",
"Empty": "Geen crashlogboeken om te kopieren."
}
},
- "AddServer": {
+ "TelemetryConsent": {
+ "Title": "Help VoiceCraft verbeteren",
+ "Description": "VoiceCraft kan anonieme telemetrie verzenden zodat we crashes beter begrijpen en de stabiliteit op alle platforms kunnen verbeteren.",
+ "CollectsTitle": "Wat we verzamelen",
+ "CollectsBody": "App-versie, platform, OS- en runtime-informatie, architectuur, landinstelling, CPU- en geheugengegevens, plus anonieme opstart- en heartbeat-diagnostiek.",
+ "NotCollectedTitle": "Wat we niet automatisch verzamelen",
+ "NotCollectedBody": "We uploaden niet automatisch je serverlijst, tokens, configuratiewaarden of chat-/inhoudsgegevens.",
+ "DumpBody": "Crashdumps worden alleen geüpload als je zelf expliciet op de dump-linkknop in de crashlogs drukt.",
+ "Accept": "Accepteren",
+ "Decline": "Weigeren"
+ },
+ "AddServer": {
"Title": "Server toevoegen",
"Name": "Naam",
"IP": "IP",
@@ -214,7 +243,8 @@
"TanhSoft": "Zachte Tanh-clipper"
},
"AudioDeviceInfo": {
- "Default": "{0} - Standaard"
+ "Default": "Standaard",
+ "DefaultDevice": "{0} - Standaard"
}
},
"ThemesService": {
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Locales/pl-PL.json b/VoiceCraft.Client/VoiceCraft.Client/Locales/pl-PL.json
index 6dfc3fe0..178bd189 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Locales/pl-PL.json
+++ b/VoiceCraft.Client/VoiceCraft.Client/Locales/pl-PL.json
@@ -13,7 +13,8 @@
"Language": "Język",
"NotificationDismiss": "Zamykanie powiadomienia (ms)",
"DisableNotifications": "Wyłącz powiadomienia",
- "HideServerAddresses": "Ukryj adresy serwerów"
+ "HideServerAddresses": "Ukryj adresy serwerów",
+ "EnableTelemetry": "Włącz anonimową telemetrię"
},
"Appearance": {
"Title": "Wygląd",
@@ -28,6 +29,9 @@
"Denoisers": "Redukcja szumów",
"AutomaticGainControllers": "Automatyczna regulacja wzmocnienia",
"EchoCancelers": "Redukcja echa",
+ "HardwarePreprocessors": null,
+ "PushToTalk": "Przytrzymaj, aby mówić",
+ "PlayPushToTalkCue": "Odtwarzaj dźwięki Push To Talk",
"MicrophoneTest": {
"MicrophoneTest": "Test mikrofonu",
"Test": "Test"
@@ -58,7 +62,11 @@
},
"Network": {
"Title": "Ustawienia sieci",
- "PositioningType": "Typ pozycjonowania",
+ "PositioningType": {
+ "Title": "Typ pozycjonowania",
+ "Server": null,
+ "Client": null
+ },
"McWssListenIp": "Adres IP nasłuchiwania MCWSS",
"McWssHostPort": "Port hosta MCWSS"
},
@@ -68,7 +76,6 @@
"CapturePrompt": "Naciśnij klawisz lub przycisk myszy",
"SaveBinding": "Zapisz przypisanie",
"CancelRebind": "Anuluj",
- "PlayPushToTalkCues": "Odtwarzaj dźwięki Push To Talk",
"Actions": {
"Mute": "Wycisz",
"Deafen": "Zagłusz",
@@ -79,10 +86,15 @@
"Title": "Ustawienia zaawansowane",
"TriggerGc": "Uruchom garbage collector",
"Crash": "Test awarii",
+ "ResetAllSettings": "Zresetuj wszystkie ustawienia",
"Notification": {
"GC": {
"Badge": "GC",
"Triggered": "Uruchomiono garbage collection. Zwolniona pamięć: {0}mb."
+ },
+ "Reset": {
+ "Badge": "Ustawienia",
+ "Done": "Wszystkie ustawienia zostały przywrócone do domyślnych. Uruchom klienta ponownie, aby w pełni zastosować język i motyw."
}
}
},
@@ -110,14 +122,31 @@
},
"CrashLogs": {
"Title": "Logi awarii",
+ "Action": {
+ "DumpLink": "Link do zrzutu"
+ },
"Notification": {
"Badge": "Logi awarii",
"Cleared": "Pomyślnie wyczyszczono wszystkie logi.",
"Copied": "Logi awarii skopiowano do schowka.",
+ "DumpCopied": "Link do zrzutu awarii został skopiowany do schowka.",
+ "DumpUploadFailed": "Nie udało się przesłać zrzutu awarii.",
+ "DumpUnavailable": "Dla tego logu awarii nie ma jeszcze linku do przesłanego zrzutu.",
"Empty": "Brak logów awarii do skopiowania."
}
},
- "AddServer": {
+ "TelemetryConsent": {
+ "Title": "Pomóż ulepszyć VoiceCraft",
+ "Description": "VoiceCraft może wysyłać anonimową telemetrię, abyśmy mogli lepiej rozumieć awarie i poprawiać stabilność na wszystkich platformach.",
+ "CollectsTitle": "Co zbieramy",
+ "CollectsBody": "Wersję aplikacji, platformę, informacje o systemie operacyjnym i runtime, architekturę, ustawienia regionalne, dane o CPU i pamięci oraz anonimowe dane diagnostyczne dotyczące uruchomienia i heartbeat.",
+ "NotCollectedTitle": "Czego nie zbieramy automatycznie",
+ "NotCollectedBody": "Nie przesyłamy automatycznie listy serwerów, tokenów, wartości konfiguracji ani danych czatu lub treści.",
+ "DumpBody": "Zrzuty awarii są wysyłane tylko wtedy, gdy samodzielnie naciśniesz przycisk linku do zrzutu w logach awarii.",
+ "Accept": "Akceptuj",
+ "Decline": "Odrzuć"
+ },
+ "AddServer": {
"Title": "Dodaj serwer",
"Name": "Nazwa",
"IP": "IP",
@@ -214,7 +243,8 @@
"TanhSoft": "Miękki clipper Tanh"
},
"AudioDeviceInfo": {
- "Default": "{0} - Domyślne"
+ "Default": "Domyślne",
+ "DefaultDevice": "{0} - Domyślne"
}
},
"ThemesService": {
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Locales/ru-RU.json b/VoiceCraft.Client/VoiceCraft.Client/Locales/ru-RU.json
index fb693789..2b85d743 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Locales/ru-RU.json
+++ b/VoiceCraft.Client/VoiceCraft.Client/Locales/ru-RU.json
@@ -13,7 +13,8 @@
"Language": "Язык",
"NotificationDismiss": "Скрытие уведомления (мс)",
"DisableNotifications": "Отключить уведомления",
- "HideServerAddresses": "Скрывать адреса серверов"
+ "HideServerAddresses": "Скрывать адреса серверов",
+ "EnableTelemetry": "Включить анонимную телеметрию"
},
"Appearance": {
"Title": "Внешний вид",
@@ -28,6 +29,9 @@
"Denoisers": "Шумоподавители",
"AutomaticGainControllers": "Автоматическая регулировка усиления",
"EchoCancelers": "Подавление эха",
+ "HardwarePreprocessors": null,
+ "PushToTalk": "Удерживай, чтобы говорить",
+ "PlayPushToTalkCue": "Проигрывать звуки Push To Talk",
"MicrophoneTest": {
"MicrophoneTest": "Проверка микрофона",
"Test": "Тест"
@@ -58,7 +62,11 @@
},
"Network": {
"Title": "Сетевые настройки",
- "PositioningType": "Тип позиционирования",
+ "PositioningType": {
+ "Title": "Тип позиционирования",
+ "Server": null,
+ "Client": null
+ },
"McWssListenIp": "IP-адрес прослушивания MCWSS",
"McWssHostPort": "Порт хоста MCWSS"
},
@@ -68,7 +76,6 @@
"CapturePrompt": "Нажми клавишу или кнопку мыши",
"SaveBinding": "Сохранить",
"CancelRebind": "Отмена",
- "PlayPushToTalkCues": "Проигрывать звуки Push To Talk",
"Actions": {
"Mute": "Выключить микрофон",
"Deafen": "Оглушить",
@@ -79,10 +86,15 @@
"Title": "Расширенные настройки",
"TriggerGc": "Запустить сборщик мусора",
"Crash": "Тестовый сбой",
+ "ResetAllSettings": "Сбросить все настройки",
"Notification": {
"GC": {
"Badge": "GC",
"Triggered": "Сборка мусора запущена. Очищено памяти: {0}mb."
+ },
+ "Reset": {
+ "Badge": "Настройки",
+ "Done": "Все настройки сброшены к значениям по умолчанию. Перезапустите клиент, чтобы тема и язык применились полностью."
}
}
},
@@ -110,13 +122,30 @@
},
"CrashLogs": {
"Title": "Журналы сбоев",
+ "Action": {
+ "DumpLink": "Ссылка на дамп"
+ },
"Notification": {
"Badge": "Журналы сбоев",
"Cleared": "Все журналы успешно очищены.",
"Copied": "Журналы сбоев скопированы в буфер обмена.",
+ "DumpCopied": "Ссылка на дамп сбоя скопирована в буфер обмена.",
+ "DumpUploadFailed": "Не удалось загрузить дамп сбоя.",
+ "DumpUnavailable": "Для этого журнала сбоя ещё нет ссылки на загруженный дамп.",
"Empty": "Нет журналов сбоев для копирования."
}
},
+ "TelemetryConsent": {
+ "Title": "Помогите улучшить VoiceCraft",
+ "Description": "VoiceCraft может отправлять анонимную телеметрию, чтобы мы лучше понимали падения и улучшали стабильность на всех платформах.",
+ "CollectsTitle": "Что мы собираем",
+ "CollectsBody": "Версию приложения, платформу, информацию об ОС и рантайме, архитектуру, локаль, данные о CPU и памяти, а также анонимные события запуска и heartbeat.",
+ "NotCollectedTitle": "Что мы не собираем автоматически",
+ "NotCollectedBody": "Мы не загружаем автоматически ваш список серверов, токены, значения конфигов или содержимое чатов и данных.",
+ "DumpBody": "Краш-дампы загружаются только когда вы сами нажимаете кнопку ссылки на дамп в журнале сбоев.",
+ "Accept": "Принять",
+ "Decline": "Отклонить"
+ },
"AddServer": {
"Title": "Добавить сервер",
"Name": "Название",
@@ -214,7 +243,8 @@
"TanhSoft": "Мягкий клиппер Tanh"
},
"AudioDeviceInfo": {
- "Default": "{0} - По умолчанию"
+ "Default": "По умолчанию",
+ "DefaultDevice": "{0} - По умолчанию"
}
},
"ThemesService": {
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Locales/zh-CN.json b/VoiceCraft.Client/VoiceCraft.Client/Locales/zh-CN.json
index 5837467b..958bb3f4 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Locales/zh-CN.json
+++ b/VoiceCraft.Client/VoiceCraft.Client/Locales/zh-CN.json
@@ -13,7 +13,8 @@
"Language": "语言",
"NotificationDismiss": "通知消失时间(毫秒)",
"DisableNotifications": "禁用通知",
- "HideServerAddresses": "隐藏服务器地址"
+ "HideServerAddresses": "隐藏服务器地址",
+ "EnableTelemetry": "启用匿名遥测"
},
"Appearance": {
"Title": "外观",
@@ -28,6 +29,9 @@
"Denoisers": "降噪器",
"AutomaticGainControllers": "自动增益控制器",
"EchoCancelers": "回声消除器",
+ "HardwarePreprocessors": null,
+ "PushToTalk": "按住说话",
+ "PlayPushToTalkCue": "播放按住说话提示音",
"MicrophoneTest": {
"MicrophoneTest": "麦克风测试",
"Test": "测试"
@@ -58,7 +62,11 @@
},
"Network": {
"Title": "网络设置",
- "PositioningType": "定位类型",
+ "PositioningType": {
+ "Title": "定位类型",
+ "Server": null,
+ "Client": null
+ },
"McWssListenIp": "MCWSS 监听 IP 地址",
"McWssHostPort": "MCWSS 主机端口"
},
@@ -68,7 +76,6 @@
"CapturePrompt": "按下按键或鼠标按钮",
"SaveBinding": "保存绑定",
"CancelRebind": "取消",
- "PlayPushToTalkCues": "播放按住说话提示音",
"Actions": {
"Mute": "静音",
"Deafen": "拒听",
@@ -79,10 +86,15 @@
"Title": "高级设置",
"TriggerGc": "触发垃圾回收",
"Crash": "测试崩溃",
+ "ResetAllSettings": "重置所有设置",
"Notification": {
"GC": {
"Badge": "GC",
"Triggered": "已触发垃圾回收。清理的内存: {0}mb。"
+ },
+ "Reset": {
+ "Badge": "设置",
+ "Done": "所有设置已重置为默认值。请重新启动客户端以完整应用语言和主题更改。"
}
}
},
@@ -110,14 +122,31 @@
},
"CrashLogs": {
"Title": "崩溃日志",
+ "Action": {
+ "DumpLink": "转储链接"
+ },
"Notification": {
"Badge": "崩溃日志",
"Cleared": "已成功清除所有日志。",
"Copied": "崩溃日志已复制到剪贴板。",
+ "DumpCopied": "崩溃转储链接已复制到剪贴板。",
+ "DumpUploadFailed": "上传崩溃转储失败。",
+ "DumpUnavailable": "此崩溃日志暂时还没有可用的已上传转储链接。",
"Empty": "没有可复制的崩溃日志。"
}
},
- "AddServer": {
+ "TelemetryConsent": {
+ "Title": "帮助改进 VoiceCraft",
+ "Description": "VoiceCraft 可以发送匿名遥测,以便我们了解崩溃情况并提升所有平台上的稳定性。",
+ "CollectsTitle": "我们收集什么",
+ "CollectsBody": "应用版本、平台、操作系统和运行时信息、架构、区域设置、CPU 和内存信息,以及匿名的启动和心跳诊断数据。",
+ "NotCollectedTitle": "我们不会自动收集什么",
+ "NotCollectedBody": "我们不会自动上传你的服务器列表、令牌、配置值或聊天与内容数据。",
+ "DumpBody": "只有当你在崩溃日志中明确点击转储链接按钮时,崩溃转储才会被上传。",
+ "Accept": "接受",
+ "Decline": "拒绝"
+ },
+ "AddServer": {
"Title": "添加服务器",
"Name": "名称",
"IP": "IP",
@@ -214,7 +243,8 @@
"TanhSoft": "Tanh 软限幅器"
},
"AudioDeviceInfo": {
- "Default": "{0} - 默认"
+ "Default": "默认",
+ "DefaultDevice": "{0} - 默认"
}
},
"ThemesService": {
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Locales/zh-TW.json b/VoiceCraft.Client/VoiceCraft.Client/Locales/zh-TW.json
index 32d1e607..8d465317 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Locales/zh-TW.json
+++ b/VoiceCraft.Client/VoiceCraft.Client/Locales/zh-TW.json
@@ -13,7 +13,8 @@
"Language": "語言",
"NotificationDismiss": "通知消失時間(毫秒)",
"DisableNotifications": "停用通知",
- "HideServerAddresses": "隱藏伺服器地址"
+ "HideServerAddresses": "隱藏伺服器地址",
+ "EnableTelemetry": "啟用匿名遙測"
},
"Appearance": {
"Title": "外觀",
@@ -28,6 +29,9 @@
"Denoisers": "降噪器",
"AutomaticGainControllers": "自動增益控制器",
"EchoCancelers": "回聲消除器",
+ "HardwarePreprocessors": null,
+ "PushToTalk": "按住說話",
+ "PlayPushToTalkCue": "播放按住說話提示音",
"MicrophoneTest": {
"MicrophoneTest": "麥克風測試",
"Test": "測試"
@@ -58,7 +62,11 @@
},
"Network": {
"Title": "網路設定",
- "PositioningType": "定位類型",
+ "PositioningType": {
+ "Title": "定位類型",
+ "Server": null,
+ "Client": null
+ },
"McWssListenIp": "MCWSS 監聽 IP 位址",
"McWssHostPort": "MCWSS 主機連接埠"
},
@@ -68,7 +76,6 @@
"CapturePrompt": "按下按鍵或滑鼠按鈕",
"SaveBinding": "儲存綁定",
"CancelRebind": "取消",
- "PlayPushToTalkCues": "播放按住說話提示音",
"Actions": {
"Mute": "靜音",
"Deafen": "拒聽",
@@ -79,10 +86,15 @@
"Title": "進階設定",
"TriggerGc": "觸發垃圾回收",
"Crash": "測試當機",
+ "ResetAllSettings": "重設所有設定",
"Notification": {
"GC": {
"Badge": "GC",
"Triggered": "已觸發垃圾回收。清理的記憶體: {0}mb。"
+ },
+ "Reset": {
+ "Badge": "設定",
+ "Done": "所有設定已重設為預設值。請重新啟動客戶端,以完整套用語言與主題變更。"
}
}
},
@@ -110,13 +122,30 @@
},
"CrashLogs": {
"Title": "當機記錄",
+ "Action": {
+ "DumpLink": "傾印連結"
+ },
"Notification": {
"Badge": "當機記錄",
"Cleared": "已成功清除所有記錄。",
"Copied": "當機記錄已複製到剪貼簿。",
+ "DumpCopied": "當機傾印連結已複製到剪貼簿。",
+ "DumpUploadFailed": "上傳當機傾印失敗。",
+ "DumpUnavailable": "這筆當機記錄目前還沒有可用的已上傳傾印連結。",
"Empty": "沒有可複製的當機記錄。"
}
},
+ "TelemetryConsent": {
+ "Title": "協助改進 VoiceCraft",
+ "Description": "VoiceCraft 可以傳送匿名遙測,讓我們更了解當機情況並提升所有平台上的穩定性。",
+ "CollectsTitle": "我們會收集什麼",
+ "CollectsBody": "應用程式版本、平台、作業系統與執行階段資訊、架構、地區設定、CPU 與記憶體資訊,以及匿名的啟動與 heartbeat 診斷資料。",
+ "NotCollectedTitle": "我們不會自動收集什麼",
+ "NotCollectedBody": "我們不會自動上傳你的伺服器清單、權杖、設定值或聊天與內容資料。",
+ "DumpBody": "只有當你在當機記錄中明確按下傾印連結按鈕時,當機傾印才會上傳。",
+ "Accept": "接受",
+ "Decline": "拒絕"
+ },
"AddServer": {
"Title": "新增伺服器",
"Name": "名稱",
@@ -214,7 +243,8 @@
"TanhSoft": "Tanh 軟限幅器"
},
"AudioDeviceInfo": {
- "Default": "{0} - 預設"
+ "Default": "預設",
+ "DefaultDevice": "{0} - 預設"
}
},
"ThemesService": {
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/EntityDataSettingsNavigationData.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/EntityDataSettingsNavigationData.cs
new file mode 100644
index 00000000..6ea62641
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/EntityDataSettingsNavigationData.cs
@@ -0,0 +1,5 @@
+using VoiceCraft.Client.ViewModels.Data;
+
+namespace VoiceCraft.Client.Models;
+
+public record EntityDataSettingsNavigationData(EntityDataViewModel Entity);
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/HotKeyCaptureNavigationData.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/HotKeyCaptureNavigationData.cs
new file mode 100644
index 00000000..5c7a5161
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/HotKeyCaptureNavigationData.cs
@@ -0,0 +1,5 @@
+using VoiceCraft.Client.ViewModels.Data;
+
+namespace VoiceCraft.Client.Models;
+
+public record HotKeyCaptureNavigationData(HotKeyActionDataViewModel HotKey);
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/HotKeySettings.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/HotKeySettings.cs
index dd7bf4c8..ae95c309 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/HotKeySettings.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/HotKeySettings.cs
@@ -6,24 +6,22 @@ namespace VoiceCraft.Client.Models.Settings;
public class HotKeySettings : Setting
{
- private Dictionary _bindings = new();
-
public Dictionary Bindings
{
- get => _bindings;
+ get;
set
{
- _bindings = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = new();
public override event Action? OnUpdated;
public override object Clone()
{
var clone = (HotKeySettings)MemberwiseClone();
- clone.Bindings = new Dictionary(_bindings, StringComparer.Ordinal);
+ clone.Bindings = new Dictionary(Bindings, StringComparer.Ordinal);
clone.OnUpdated = null;
return clone;
}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/InputSettings.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/InputSettings.cs
index 06974d08..fe67bb41 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/InputSettings.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/InputSettings.cs
@@ -5,100 +5,99 @@ namespace VoiceCraft.Client.Models.Settings;
public class InputSettings : Setting
{
- private string _inputDevice = "Default";
- private float _inputVolume = 1.0f;
- private float _microphoneSensitivity = 0.04f;
-
- private Guid _automaticGainController = Guid.Empty;
- private Guid _denoiser = Guid.Empty;
- private Guid _echoCanceler = Guid.Empty;
-
- private bool _pushToTalkEnabled;
- private bool _pushToTalkCue = true;
-
public string InputDevice
{
- get => _inputDevice;
+ get;
set
{
- _inputDevice = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = "Default";
public float InputVolume
{
- get => _inputVolume;
+ get;
set
{
- if(value is > 2 or < 0)
+ if (value is > 2 or < 0)
throw new ArgumentException("Settings.Input.Validation.InputVolume");
- _inputVolume = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = 1.0f;
public float MicrophoneSensitivity
{
- get => _microphoneSensitivity;
+ get;
set
{
if (value is > 1 or < 0)
throw new ArgumentException("Settings.Input.Validation.MicrophonesSensitivity");
- _microphoneSensitivity = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
-
+ } = 0.04f;
+
public Guid AutomaticGainController
{
- get => _automaticGainController;
+ get;
set
{
- _automaticGainController = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = Guid.Empty;
public Guid Denoiser
{
- get => _denoiser;
+ get;
set
{
- _denoiser = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = Guid.Empty;
public Guid EchoCanceler
{
- get => _echoCanceler;
+ get;
set
{
- _echoCanceler = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = Guid.Empty;
+
+ public bool HardwarePreprocessorsEnabled
+ {
+ get;
+ set
+ {
+ field = value;
+ OnUpdated?.Invoke(this);
+ }
+ } = true;
public bool PushToTalkEnabled
{
- get => _pushToTalkEnabled;
+ get;
set
{
- _pushToTalkEnabled = value;
+ field = value;
OnUpdated?.Invoke(this);
}
}
public bool PushToTalkCue
{
- get => _pushToTalkCue;
+ get;
set
{
- _pushToTalkCue = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = true;
public override event Action? OnUpdated;
@@ -108,4 +107,4 @@ public override object Clone()
clone.OnUpdated = null;
return clone;
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/LocaleSettings.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/LocaleSettings.cs
index 28271b61..cf55ad5e 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/LocaleSettings.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/LocaleSettings.cs
@@ -8,19 +8,17 @@ namespace VoiceCraft.Client.Models.Settings;
public class LocaleSettings : Setting
{
- private string _culture = Localizer.Languages.Contains(CultureInfo.CurrentCulture.Name)
- ? CultureInfo.CurrentCulture.Name
- : Constants.DefaultLanguage;
-
public string Culture
{
- get => _culture;
+ get;
set
{
- _culture = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = Localizer.Languages.Contains(CultureInfo.CurrentCulture.Name)
+ ? CultureInfo.CurrentCulture.Name
+ : Constants.DefaultLanguage;
public override event Action? OnUpdated;
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/NetworkSettings.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/NetworkSettings.cs
index 3ede4316..952f6604 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/NetworkSettings.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/NetworkSettings.cs
@@ -6,39 +6,35 @@ namespace VoiceCraft.Client.Models.Settings;
public class NetworkSettings : Setting
{
- private ushort _mcWssHostPort = 8080;
- private string _mcWssListenIp = "127.0.0.1";
- private PositioningType _positioningType = PositioningType.Server;
-
public PositioningType PositioningType
{
- get => _positioningType;
+ get;
set
{
- _positioningType = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = PositioningType.Server;
public string McWssListenIp
{
- get => _mcWssListenIp;
+ get;
set
{
- _mcWssListenIp = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = "127.0.0.1";
public ushort McWssHostPort
{
- get => _mcWssHostPort;
+ get;
set
{
- _mcWssHostPort = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = 8080;
public override event Action? OnUpdated;
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/NotificationSettings.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/NotificationSettings.cs
index d4adfc3a..22ae5493 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/NotificationSettings.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/NotificationSettings.cs
@@ -5,28 +5,25 @@ namespace VoiceCraft.Client.Models.Settings;
public class NotificationSettings : Setting
{
- private bool _disableNotifications;
- private ushort _dismissDelayMs = 2000;
-
public bool DisableNotifications
{
- get => _disableNotifications;
+ get;
set
{
- _disableNotifications = value;
+ field = value;
OnUpdated?.Invoke(this);
}
}
public ushort DismissDelayMs
{
- get => _dismissDelayMs;
+ get;
set
{
- _dismissDelayMs = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = 2000;
public override event Action? OnUpdated;
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/OutputSettings.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/OutputSettings.cs
index 72fa3c82..759b7237 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/OutputSettings.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/OutputSettings.cs
@@ -6,41 +6,37 @@ namespace VoiceCraft.Client.Models.Settings;
public class OutputSettings : Setting
{
- private string _outputDevice = "Default";
- private float _outputVolume = 1.0f;
- private Guid _audioClipper = Constants.TanhSoftAudioClipperGuid; //Set as default on initialize.
-
public string OutputDevice
{
- get => _outputDevice;
+ get;
set
{
- _outputDevice = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = "Default";
public float OutputVolume
{
- get => _outputVolume;
+ get;
set
{
if (value is > 2 or < 0)
throw new ArgumentException("Settings.Input.Validation.OutputVolume");
- _outputVolume = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = 1.0f;
public Guid AudioClipper
{
- get => _audioClipper;
+ get;
set
{
- _audioClipper = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = Constants.TanhSoftAudioClipperGuid;
public override event Action? OnUpdated;
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/ServersSettings.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/ServersSettings.cs
index 35e7f2c6..18feae88 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/ServersSettings.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/ServersSettings.cs
@@ -7,15 +7,14 @@ namespace VoiceCraft.Client.Models.Settings;
public class ServersSettings : Setting
{
- private bool _hideServerAddresses;
private List _servers = [];
public bool HideServerAddresses
{
- get => _hideServerAddresses;
+ get;
set
{
- _hideServerAddresses = value;
+ field = value;
OnUpdated?.Invoke(this);
}
}
@@ -73,46 +72,42 @@ public class Server : Setting
{
public const int NameLimit = 12;
public const int IpLimit = 30;
- private string _ip = string.Empty;
-
- private string _name = string.Empty;
- private ushort _port = 9050;
public string Name
{
- get => _name;
+ get;
set
{
if (value.Length > NameLimit)
throw new ArgumentException($"Settings.Servers.Validation.NameLimit:{NameLimit}");
- _name = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = string.Empty;
public string Ip
{
- get => _ip;
+ get;
set
{
if (value.Length > IpLimit)
throw new ArgumentException($"Settings.Servers.Validation.IpLimit:{IpLimit}");
- _ip = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = string.Empty;
public ushort Port
{
- get => _port;
+ get;
set
{
if (value < 1)
throw new ArgumentException("Settings.Servers.Validation.Port");
- _port = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = 9050;
public override event Action? OnUpdated;
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/TelemetrySettings.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/TelemetrySettings.cs
new file mode 100644
index 00000000..31b0be3f
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/TelemetrySettings.cs
@@ -0,0 +1,36 @@
+using System;
+using VoiceCraft.Client.Services;
+
+namespace VoiceCraft.Client.Models.Settings;
+
+public class TelemetrySettings : Setting
+{
+ public bool Enabled
+ {
+ get;
+ set
+ {
+ field = value;
+ OnUpdated?.Invoke(this);
+ }
+ } = true;
+
+ public bool ConsentShown
+ {
+ get;
+ set
+ {
+ field = value;
+ OnUpdated?.Invoke(this);
+ }
+ }
+
+ public override event Action? OnUpdated;
+
+ public override object Clone()
+ {
+ var clone = (TelemetrySettings)MemberwiseClone();
+ clone.OnUpdated = null;
+ return clone;
+ }
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/ThemeSettings.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/ThemeSettings.cs
index 8de192de..a89340fa 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/ThemeSettings.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/ThemeSettings.cs
@@ -6,28 +6,25 @@ namespace VoiceCraft.Client.Models.Settings;
public class ThemeSettings : Setting
{
- private Guid _selectedBackgroundImage = Constants.DockNightGuid;
- private Guid _selectedTheme = Constants.DarkThemeGuid;
-
public Guid SelectedBackgroundImage
{
- get => _selectedBackgroundImage;
+ get;
set
{
- _selectedBackgroundImage = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = Constants.DockNightGuid;
public Guid SelectedTheme
{
- get => _selectedTheme;
+ get;
set
{
- _selectedTheme = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = Constants.DarkThemeGuid;
public override event Action? OnUpdated;
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/UserSetting.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/UserSetting.cs
index b7ec0453..e7572b84 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/UserSetting.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/UserSetting.cs
@@ -5,25 +5,22 @@ namespace VoiceCraft.Client.Models.Settings;
public class UserSetting : Setting
{
- private bool _userMuted;
- private float _volume = 1f;
-
public float Volume
{
- get => _volume;
+ get;
set
{
- _volume = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = 1f;
public bool UserMuted
{
- get => _userMuted;
+ get;
set
{
- _userMuted = value;
+ field = value;
OnUpdated?.Invoke(this);
}
}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/UserSettings.cs b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/UserSettings.cs
index 4fdf8171..0c2a9788 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/UserSettings.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Models/Settings/UserSettings.cs
@@ -6,17 +6,15 @@ namespace VoiceCraft.Client.Models.Settings;
public class UserSettings : Setting
{
- private Dictionary _users = new();
-
public Dictionary Users
{
- get => _users;
+ get;
set
{
- _users = value;
+ field = value;
OnUpdated?.Invoke(this);
}
- }
+ } = new();
public override event Action? OnUpdated;
@@ -24,7 +22,7 @@ public override object Clone()
{
var clone = (UserSettings)MemberwiseClone();
clone.Users = new Dictionary();
- foreach (var user in _users)
+ foreach (var user in Users)
{
var clonedEntity = (UserSetting)user.Value.Clone();
clone.Users.TryAdd(user.Key, clonedEntity);
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Servers/McWssServer.cs b/VoiceCraft.Client/VoiceCraft.Client/Servers/McWssServer.cs
index c774d03f..f3af035f 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Servers/McWssServer.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Servers/McWssServer.cs
@@ -75,7 +75,10 @@ private static void SendEventSubscribe(IWebSocketConnection socket, string event
private void OnClientConnected(IWebSocketConnection socket)
{
if (_peerConnection != null)
+ {
socket.Close(); //Full.
+ return;
+ }
_customEventTriggered = false;
_peerConnection = socket;
@@ -225,4 +228,4 @@ private void HandleLocalPlayerUpdatedEvent(McWssLocalPlayerUpdatedEvent localPla
client.CaveFactor = caveFactor;
client.MuffleFactor = muffleFactor;
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Services/AudioService.cs b/VoiceCraft.Client/VoiceCraft.Client/Services/AudioService.cs
index 5f201a75..0144b329 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Services/AudioService.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Services/AudioService.cs
@@ -62,14 +62,15 @@ public IEnumerable GetInputDevices()
{
defaultDevice = new AudioDeviceInfo(
"Default",
- Localizer.Get($"AudioService.AudioDeviceInfo.Default:{device.Name}"),
+ Localizer.Get($"AudioService.AudioDeviceInfo.DefaultDevice:{device.Name}"),
true);
}
devices.Add(new AudioDeviceInfo(device.Name, device.Name, false));
}
- devices.Insert(0, defaultDevice ?? new AudioDeviceInfo("Default", "Default", true));
+ devices.Insert(0,
+ defaultDevice ?? new AudioDeviceInfo("Default", "AudioService.AudioDeviceInfo.Default", true));
return devices;
}
@@ -84,18 +85,24 @@ public IEnumerable GetOutputDevices()
{
defaultDevice = new AudioDeviceInfo(
"Default",
- Localizer.Get($"AudioService.AudioDeviceInfo.Default:{device.Name}"),
+ Localizer.Get($"AudioService.AudioDeviceInfo.DefaultDevice:{device.Name}"),
true);
}
devices.Add(new AudioDeviceInfo(device.Name, device.Name, false));
}
-
- devices.Insert(0, defaultDevice ?? new AudioDeviceInfo("Default", "Default", true));
+
+ devices.Insert(0,
+ defaultDevice ?? new AudioDeviceInfo("Default", "AudioService.AudioDeviceInfo.Default", true));
return devices;
}
- public AudioCaptureDevice InitializeCaptureDevice(int sampleRate, int channels, uint frameSize, string inputDevice)
+ public AudioCaptureDevice InitializeCaptureDevice(
+ int sampleRate,
+ int channels,
+ uint frameSize,
+ string inputDevice,
+ bool hardwarePreprocessorsEnabled)
{
_engine.UpdateAudioDevicesInfo();
var format = new AudioFormat()
@@ -110,11 +117,15 @@ public AudioCaptureDevice InitializeCaptureDevice(int sampleRate, int channels,
AAudio = new AAudioSettings()
{
Usage = AAudioUsage.VoiceCommunication,
- InputPreset = AAudioInputPreset.VoiceCommunication
+ InputPreset = hardwarePreprocessorsEnabled
+ ? AAudioInputPreset.VoiceCommunication
+ : AAudioInputPreset.Default
},
OpenSL = new OpenSlSettings()
{
- RecordingPreset = OpenSlRecordingPreset.VoiceCommunication
+ RecordingPreset = hardwarePreprocessorsEnabled
+ ? OpenSlRecordingPreset.VoiceCommunication
+ : OpenSlRecordingPreset.Default
},
CoreAudio = new CoreAudioSettings()
{
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Services/ClientTelemetryService.cs b/VoiceCraft.Client/VoiceCraft.Client/Services/ClientTelemetryService.cs
new file mode 100644
index 00000000..9c10fd4a
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Services/ClientTelemetryService.cs
@@ -0,0 +1,257 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.Maui.Devices;
+using VoiceCraft.Core;
+using VoiceCraft.Core.Telemetry;
+
+namespace VoiceCraft.Client.Services;
+
+public sealed class ClientTelemetryService(SettingsService settingsService)
+{
+ private const int MaxCrashLogLength = 250_000;
+ private readonly TelemetryTransport _transport = new();
+ private bool IsEnabled =>
+ settingsService.TelemetrySettings is { Enabled: true, ConsentShown: true };
+
+ public async Task ReportStartupAsync()
+ {
+ await ReportStartupAsync(3);
+ }
+
+ public async Task ReportCrashAsync(string crashText, string? title = null)
+ {
+ if (!IsEnabled)
+ return null;
+
+ var trimmedCrashText = TrimCrashText(crashText, out var wasTrimmed);
+ var payload = new TelemetryDumpRequest
+ {
+ Role = "client",
+ Category = "crash",
+ Title = string.IsNullOrWhiteSpace(title) ? "CrashLog" : title,
+ App = BuildAppInfo(),
+ Device = BuildDeviceInfo(),
+ Payload = new Dictionary
+ {
+ ["crash_log"] = trimmedCrashText,
+ ["truncated"] = wasTrimmed.ToString()
+ }
+ };
+
+ try
+ {
+ return await _transport.SendDumpAsync(payload);
+ }
+ catch(Exception ex)
+ {
+ LogService.Log(ex);
+ return null;
+ }
+ }
+
+ private async Task ReportStartupAsync(int attempts)
+ {
+ if (!IsEnabled)
+ return;
+
+ var payload = new TelemetryEventRequest
+ {
+ Fingerprint = settingsService.TelemetryToken,
+ Role = "client",
+ App = BuildAppInfo(),
+ Device = BuildDeviceInfo(),
+ Metrics = new Dictionary
+ {
+ ["positioning_type"] = settingsService.NetworkSettings.PositioningType.ToString(),
+ ["push_to_talk_enabled"] = settingsService.InputSettings.PushToTalkEnabled.ToString()
+ },
+ Tags = ["startup"],
+ Timestamp = DateTime.UtcNow.ToString("O")
+ };
+
+ var retries = Math.Max(1, attempts);
+ for (var attempt = 0; attempt < retries; attempt++)
+ {
+ try
+ {
+ await _transport.SendTelemetryAsync(payload);
+ }
+ catch(Exception ex)
+ {
+ LogService.Log(ex);
+ if (attempt < retries - 1)
+ await Task.Delay(1500);
+ }
+ }
+ }
+
+ private static TelemetryAppInfo BuildAppInfo()
+ {
+ return new TelemetryAppInfo
+ {
+ AppName = "VoiceCraft",
+ Version = ResolveVersion(),
+ Channel = ResolveChannel(),
+ Build = ResolveBuild()
+ };
+ }
+
+ private static TelemetryDeviceInfo BuildDeviceInfo()
+ {
+ var deviceInfo = TryGetDeviceInfo();
+ var totalAvailableBytes = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
+ long? memoryMb = totalAvailableBytes > 0 ? totalAvailableBytes / (1024 * 1024) : null;
+
+ return new TelemetryDeviceInfo
+ {
+ OsName = GetPlatformName(deviceInfo),
+ OsVersion = GetOsVersion(deviceInfo),
+ OsBuild = GetOsBuild(deviceInfo),
+ OsDescription = GetOsDescription(deviceInfo),
+ Vendor = NormalizeValue(deviceInfo?.Manufacturer),
+ Model = NormalizeValue(deviceInfo?.Model),
+ Architecture = RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(),
+ ProcessArchitecture = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(),
+ Runtime = RuntimeInformation.FrameworkDescription,
+ Locale = CultureInfo.CurrentUICulture.Name,
+ CpuCores = Environment.ProcessorCount,
+ MemoryMb = memoryMb
+ };
+ }
+
+ private static string ResolveVersion()
+ {
+ var version = Assembly.GetEntryAssembly()?.GetName().Version;
+ return version != null
+ ? $"{version.Major}.{version.Minor}.{version.Build}"
+ : $"{Constants.Major}.{Constants.Minor}.{Constants.Patch}";
+ }
+
+ private static string ResolveBuild()
+ {
+ var informationalVersion = Assembly.GetEntryAssembly()?
+ .GetCustomAttribute()?
+ .InformationalVersion;
+
+ return string.IsNullOrWhiteSpace(informationalVersion) ? string.Empty : informationalVersion;
+ }
+
+ private static string ResolveChannel()
+ {
+#if DEBUG
+ return "debug";
+#else
+ return "stable";
+#endif
+ }
+
+ private static IDeviceInfo? TryGetDeviceInfo()
+ {
+ if (!(OperatingSystem.IsAndroid() || OperatingSystem.IsIOS() || OperatingSystem.IsMacOS()))
+ return null;
+
+ try
+ {
+ return DeviceInfo.Current;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static string GetPlatformName(IDeviceInfo? deviceInfo)
+ {
+ if (deviceInfo?.Platform == DevicePlatform.WinUI)
+ return IsWindows11(deviceInfo.Version) ? "Windows 11" : "Windows";
+ if (deviceInfo?.Platform == DevicePlatform.Android)
+ return "Android";
+ if (deviceInfo?.Platform == DevicePlatform.iOS)
+ return "iOS";
+ if (deviceInfo?.Platform == DevicePlatform.macOS)
+ return "macOS";
+ if (OperatingSystem.IsWindows())
+ return IsWindows11(Environment.OSVersion.Version) ? "Windows 11" : "Windows";
+ if (OperatingSystem.IsLinux())
+ return "Linux";
+ return OperatingSystem.IsBrowser()
+ ? "Browser"
+ : RuntimeInformation.OSDescription;
+ }
+
+ private static string GetOsVersion(IDeviceInfo? deviceInfo)
+ {
+ if (deviceInfo?.Platform == DevicePlatform.WinUI)
+ return GetPlatformName(deviceInfo);
+ if (deviceInfo?.Platform == DevicePlatform.Android)
+ return $"Android {NormalizeVersionString(deviceInfo.VersionString, deviceInfo.Version)}";
+ if (deviceInfo?.Platform == DevicePlatform.iOS)
+ return $"iOS {NormalizeVersionString(deviceInfo.VersionString, deviceInfo.Version)}";
+ if (deviceInfo?.Platform == DevicePlatform.macOS)
+ return $"macOS {NormalizeVersionString(deviceInfo.VersionString, deviceInfo.Version)}";
+ if (OperatingSystem.IsWindows())
+ return GetPlatformName(deviceInfo);
+
+ if (deviceInfo == null) return Environment.OSVersion.VersionString;
+ var version = NormalizeVersionString(deviceInfo.VersionString, deviceInfo.Version);
+ return !string.IsNullOrWhiteSpace(version)
+ ? version
+ : Environment.OSVersion.VersionString;
+ }
+
+ private static string GetOsBuild(IDeviceInfo? deviceInfo)
+ {
+ return deviceInfo is not null
+ ? deviceInfo.Version.ToString()
+ : Environment.OSVersion.Version.ToString();
+ }
+
+ private static string GetOsDescription(IDeviceInfo? deviceInfo)
+ {
+ if (deviceInfo?.Platform == DevicePlatform.WinUI)
+ return $"{GetPlatformName(deviceInfo)} build {GetOsBuild(deviceInfo)}";
+ if (deviceInfo?.Platform == DevicePlatform.Android)
+ return
+ $"Android {NormalizeVersionString(deviceInfo.VersionString, deviceInfo.Version)} (API level {deviceInfo.Version.Major})";
+ if (deviceInfo?.Platform == DevicePlatform.iOS)
+ return $"iOS {NormalizeVersionString(deviceInfo.VersionString, deviceInfo.Version)}";
+ if (deviceInfo?.Platform == DevicePlatform.macOS)
+ return $"macOS {NormalizeVersionString(deviceInfo.VersionString, deviceInfo.Version)}";
+ return OperatingSystem.IsWindows()
+ ? $"{GetPlatformName(deviceInfo)} build {Environment.OSVersion.Version}"
+ : RuntimeInformation.OSDescription;
+ }
+
+ private static string NormalizeVersionString(string? versionString, Version version)
+ {
+ return !string.IsNullOrWhiteSpace(versionString)
+ ? versionString
+ : version.ToString();
+ }
+
+ private static string? NormalizeValue(string? value)
+ {
+ return string.IsNullOrWhiteSpace(value) ? null : value;
+ }
+
+ private static bool IsWindows11(Version version)
+ {
+ return version is { Major: >= 10, Build: >= 22000 };
+ }
+
+ private static string TrimCrashText(string crashText, out bool wasTrimmed)
+ {
+ if (crashText.Length <= MaxCrashLogLength)
+ {
+ wasTrimmed = false;
+ return crashText;
+ }
+
+ wasTrimmed = true;
+ return crashText[..MaxCrashLogLength];
+ }
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Services/ClipboardService.cs b/VoiceCraft.Client/VoiceCraft.Client/Services/ClipboardService.cs
new file mode 100644
index 00000000..11b39efb
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Services/ClipboardService.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Input.Platform;
+
+namespace VoiceCraft.Client.Services;
+
+public class ClipboardService
+{
+ private IClipboard? _clipboard;
+
+ public void RegisterTopLevel(TopLevel topLevel)
+ {
+ _clipboard = topLevel.Clipboard;
+ }
+
+ public async Task SetTextAsync(string text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ throw new ArgumentException("Clipboard text cannot be empty.", nameof(text));
+
+ if (_clipboard is null)
+ throw new InvalidOperationException("Clipboard is not available yet.");
+
+ await _clipboard.SetTextAsync(text);
+ }
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Services/HotKeyService.cs b/VoiceCraft.Client/VoiceCraft.Client/Services/HotKeyService.cs
index 4d9653cb..3d445d9d 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Services/HotKeyService.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Services/HotKeyService.cs
@@ -182,48 +182,44 @@ public sealed class HotKeyBinding(HotKeyAction action, string keyCombo)
public class MuteAction(IBackgroundService backgroundService) : HotKeyAction
{
public override string Id => "Mute";
- public override string Title => "Mute";
+ public override string Title => "Settings.HotKey.Actions.Mute";
public override string DefaultKeyCombo => "LeftControl\0LeftShift\0M";
public override void Press()
{
var service = backgroundService.GetService();
- if(service == null) return;
- service.Muted = !service.Muted;
+ service?.Muted = !service.Muted;
}
}
public class DeafenAction(IBackgroundService backgroundService) : HotKeyAction
{
public override string Id => "Deafen";
- public override string Title => "Deafen";
+ public override string Title => "Settings.HotKey.Actions.Deafen";
public override string DefaultKeyCombo => "LeftControl\0LeftShift\0D";
public override void Press()
{
var service = backgroundService.GetService();
- if(service == null) return;
- service.Deafened = !service.Deafened;
+ service?.Deafened = !service.Deafened;
}
}
public class PushToTalkAction(IBackgroundService backgroundService) : HotKeyAction
{
public override string Id => "PushToTalk";
- public override string Title => "PushToTalk";
+ public override string Title => "Settings.HotKey.Actions.PushToTalk";
public override string DefaultKeyCombo => "LeftControl";
public override void Press()
{
var service = backgroundService.GetService();
- if (service == null) return;
- service.PushToTalk = true;
+ service?.PushToTalk = true;
}
public override void Release()
{
var service = backgroundService.GetService();
- if (service == null) return;
- service.PushToTalk = false;
+ service?.PushToTalk = false;
}
}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Services/LogService.cs b/VoiceCraft.Client/VoiceCraft.Client/Services/LogService.cs
index fa168eff..63085be4 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Services/LogService.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Services/LogService.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using VoiceCraft.Core;
+using VoiceCraft.Core.Diagnostics;
namespace VoiceCraft.Client.Services;
@@ -20,26 +22,9 @@ public static class LogService
public static IEnumerable> ExceptionLogs =>
_exceptionLogs.ExceptionLogs.OrderByDescending(d => d.Key);
- public static IEnumerable> CrashLogs =>
+ public static IEnumerable> CrashLogs =>
_exceptionLogs.CrashLogs.OrderByDescending(d => d.Key);
- public static void Log(Exception exception)
- {
- Console.WriteLine(exception);
- _exceptionLogs.ExceptionLogs.TryAdd(DateTime.UtcNow, exception.ToString());
- TrimExceptionLogs();
- _ = SaveAsync();
- }
-
- public static void LogCrash(Exception exception)
- {
- if (exception is TaskCanceledException) return; //Ignore this shit.
- Console.WriteLine(exception);
- _exceptionLogs.CrashLogs.TryAdd(DateTime.UtcNow, exception.ToString());
- TrimCrashLogs();
- SaveLogs();
- }
-
public static void Load()
{
try
@@ -48,11 +33,10 @@ public static void Load()
return;
var result = NativeStorageService?.Load(Constants.ExceptionLogsFile);
- var loadedLogs =
- JsonSerializer.Deserialize(result,
- CrashLogGenerationContext.Default.ExceptionLogsStructure);
- if (loadedLogs == null) return;
- _exceptionLogs = loadedLogs;
+ if (result == null)
+ return;
+
+ if (!TryLoadCurrent(result) && !TryLoadLegacy(result)) return;
TrimExceptionLogs();
TrimCrashLogs();
}
@@ -61,11 +45,37 @@ public static void Load()
Log(ex); //Log it, Don't care what we log.
}
}
+
+ public static void LogCrash(Exception exception)
+ {
+ if (exception is TaskCanceledException) return; //Ignore this shit.
+ Console.WriteLine(exception);
+ _exceptionLogs.CrashLogs.TryAdd(DateTime.UtcNow, new CrashLogRecord
+ {
+ Message = exception.ToString()
+ });
+ TrimCrashLogs();
+ SaveLogs();
+ }
+
+ public static void Log(Exception exception)
+ {
+ Console.WriteLine(exception);
+ _exceptionLogs.ExceptionLogs.TryAdd(DateTime.UtcNow, exception.ToString());
+ TrimExceptionLogs();
+ _ = SaveAsync();
+ }
+
+ public static bool TryGetLog(DateTime timeStamp, [NotNullWhen(true)] out string? log)
+ {
+ return _exceptionLogs.ExceptionLogs.TryGetValue(timeStamp, out log);
+ }
- public static void ClearCrashLogs()
+ public static void UpdateLog(DateTime timeStamp, string log)
{
- _exceptionLogs.CrashLogs.Clear();
- _ = SaveAsync(); //Since we don't to a save immediate, we need to call the save.
+ if (!_exceptionLogs.ExceptionLogs.ContainsKey(timeStamp)) return;
+ _exceptionLogs.ExceptionLogs[timeStamp] = log;
+ _ = SaveAsync();
}
public static void ClearExceptionLogs()
@@ -74,15 +84,24 @@ public static void ClearExceptionLogs()
_ = SaveAsync();
}
- private static void TrimExceptionLogs()
+ public static bool TryGetCrashLog(DateTime timeStamp, [NotNullWhen(true)] out CrashLogRecord? crashLog)
{
- foreach (var log in _exceptionLogs.ExceptionLogs.OrderBy(d => d.Key))
- {
- if (_exceptionLogs.CrashLogs.Count <= Limit) return;
- _exceptionLogs.ExceptionLogs.TryRemove(log.Key, out _);
- }
+ return _exceptionLogs.CrashLogs.TryGetValue(timeStamp, out crashLog);
+ }
+
+ public static void UpdateCrashLog(DateTime timeStamp, CrashLogRecord crashLog)
+ {
+ if (!_exceptionLogs.CrashLogs.ContainsKey(timeStamp)) return;
+ _exceptionLogs.CrashLogs[timeStamp] = crashLog;
+ _ = SaveAsync();
}
+ public static void ClearCrashLogs()
+ {
+ _exceptionLogs.CrashLogs.Clear();
+ _ = SaveAsync(); //Since we don't to a save immediate, we need to call the save.
+ }
+
private static void TrimCrashLogs()
{
foreach (var log in _exceptionLogs.CrashLogs.OrderBy(d => d.Key))
@@ -92,6 +111,46 @@ private static void TrimCrashLogs()
}
}
+ private static void TrimExceptionLogs()
+ {
+ foreach (var log in _exceptionLogs.ExceptionLogs.OrderBy(d => d.Key))
+ {
+ if (_exceptionLogs.ExceptionLogs.Count <= Limit) return;
+ _exceptionLogs.ExceptionLogs.TryRemove(log.Key, out _);
+ }
+ }
+
+ private static bool TryLoadCurrent(byte[] result)
+ {
+ var loadedLogs = JsonSerializer.Deserialize(
+ result,
+ CrashLogGenerationContext.Default.ExceptionLogsStructure);
+ if (loadedLogs == null)
+ return false;
+
+ _exceptionLogs = loadedLogs;
+ return true;
+ }
+
+ private static bool TryLoadLegacy(byte[] result)
+ {
+ var loadedLogs = JsonSerializer.Deserialize(
+ result,
+ CrashLogGenerationContext.Default.LegacyExceptionLogsStructure);
+ if (loadedLogs == null)
+ return false;
+
+ _exceptionLogs = new ExceptionLogsStructure
+ {
+ ExceptionLogs = loadedLogs.ExceptionLogs,
+ CrashLogs = new ConcurrentDictionary(
+ loadedLogs.CrashLogs.ToDictionary(
+ x => x.Key,
+ x => new CrashLogRecord { Message = x.Value }))
+ };
+ return true;
+ }
+
private static async Task SaveAsync()
{
_queueWrite = true;
@@ -118,6 +177,12 @@ private static void SaveLogs()
}
public class ExceptionLogsStructure
+{
+ public ConcurrentDictionary CrashLogs { get; set; } = new();
+ public ConcurrentDictionary ExceptionLogs { get; set; } = new();
+}
+
+public class LegacyExceptionLogsStructure
{
public ConcurrentDictionary CrashLogs { get; set; } = new();
public ConcurrentDictionary ExceptionLogs { get; set; } = new();
@@ -125,4 +190,5 @@ public class ExceptionLogsStructure
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(ExceptionLogsStructure), GenerationMode = JsonSourceGenerationMode.Metadata)]
-public partial class CrashLogGenerationContext : JsonSerializerContext;
\ No newline at end of file
+[JsonSerializable(typeof(LegacyExceptionLogsStructure), GenerationMode = JsonSourceGenerationMode.Metadata)]
+public partial class CrashLogGenerationContext : JsonSerializerContext;
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Services/NavigationService.cs b/VoiceCraft.Client/VoiceCraft.Client/Services/NavigationService.cs
index e1f6ef4d..4b2352a9 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Services/NavigationService.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Services/NavigationService.cs
@@ -10,7 +10,9 @@ public sealed class NavigationService(Func createViewModel,
{
private readonly Lock _lock = new();
private ViewModelBase? _currentViewModel;
+ private ViewModelBase? _currentModalViewModel;
private List _history = [];
+ private List _modalStack = [];
private int _historyIndex = -1;
// ReSharper disable once MemberCanBePrivate.Global
@@ -38,6 +40,7 @@ public bool HasPrev
}
public event Action? OnViewModelChanged;
+ public event Action? OnModalViewModelChanged;
private void Push(ViewModelBase item)
{
@@ -61,6 +64,11 @@ private void Push(ViewModelBase item)
_historyIndex = _history.Count - 1;
}
+ private void PushModal(ViewModelBase item)
+ {
+ _modalStack.Add(item);
+ }
+
private void SetCurrentViewModel(ViewModelBase viewModel, object? data = null)
{
if (viewModel == _currentViewModel) return;
@@ -70,6 +78,27 @@ private void SetCurrentViewModel(ViewModelBase viewModel, object? data = null)
OnViewModelChanged?.Invoke(viewModel);
}
+ private void SetCurrentModalViewModel(ViewModelBase? viewModel, object? data = null)
+ {
+ if (viewModel == _currentModalViewModel) return;
+ _currentModalViewModel?.OnDisappearing();
+ _currentModalViewModel = viewModel;
+ _currentModalViewModel?.OnAppearing(data);
+ OnModalViewModelChanged?.Invoke(viewModel);
+ }
+
+ private void ClearModalStack()
+ {
+ if (_currentModalViewModel != null)
+ SetCurrentModalViewModel(null);
+
+ foreach (var modal in _modalStack)
+ if (modal is IDisposable disposable)
+ disposable.Dispose();
+
+ _modalStack.Clear();
+ }
+
// ReSharper disable once MemberCanBePrivate.Global
public ViewModelBase? Go(int offset = 0, bool checkBackButton = false)
{
@@ -94,6 +123,12 @@ private void SetCurrentViewModel(ViewModelBase viewModel, object? data = null)
public ViewModelBase? Back(bool checkBackButton = false)
{
+ lock (_lock)
+ {
+ if (_currentModalViewModel != null)
+ return PopModal(checkBackButton);
+ }
+
return HasPrev ? Go(-1, checkBackButton) : null;
}
@@ -106,12 +141,45 @@ public void NavigateTo(object? data = null) where T : ViewModelBase
{
lock (_lock)
{
+ ClearModalStack();
var viewModel = InstantiateViewModel();
SetCurrentViewModel(viewModel, data);
Push(viewModel);
}
}
+ public void PushModal(object? data = null) where T : ViewModelBase
+ {
+ lock (_lock)
+ {
+ var viewModel = InstantiateViewModel();
+ PushModal(viewModel);
+ SetCurrentModalViewModel(viewModel, data);
+ }
+ }
+
+ public ViewModelBase? PopModal(bool checkBackButton = false)
+ {
+ lock (_lock)
+ {
+ if (_currentModalViewModel == null)
+ return null;
+
+ if (checkBackButton && _currentModalViewModel.DisableBackButton)
+ return _currentModalViewModel;
+
+ var currentModal = _currentModalViewModel;
+ _modalStack.Remove(currentModal);
+ var nextModal = _modalStack.LastOrDefault();
+ SetCurrentModalViewModel(nextModal);
+
+ if (currentModal is IDisposable disposable)
+ disposable.Dispose();
+
+ return nextModal;
+ }
+ }
+
private T InstantiateViewModel() where T : ViewModelBase
{
return (T)Convert.ChangeType(createViewModel(typeof(T)), typeof(T));
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Services/SettingsService.cs b/VoiceCraft.Client/VoiceCraft.Client/Services/SettingsService.cs
index 5aad82b2..1bd61a3d 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Services/SettingsService.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Services/SettingsService.cs
@@ -24,6 +24,8 @@ public SettingsService(StorageService storageService)
// ReSharper disable once InconsistentNaming
public Guid UserGuid => _settings.UserGuid;
public Guid ServerUserGuid => _settings.ServerUserGuid;
+ public string TelemetryToken => _settings.TelemetryToken;
+ public TelemetrySettings TelemetrySettings => _settings.TelemetrySettings;
public InputSettings InputSettings => _settings.InputSettings;
public OutputSettings OutputSettings => _settings.OutputSettings;
public LocaleSettings LocaleSettings => _settings.LocaleSettings;
@@ -40,6 +42,12 @@ public async Task SaveImmediate()
await SaveSettingsAsync();
}
+ public async Task ResetToDefaultsAsync()
+ {
+ _settings = new SettingsStructure();
+ await SaveSettingsAsync();
+ }
+
public async Task SaveAsync()
{
_queueWrite = true;
@@ -59,25 +67,33 @@ public async Task SaveAsync()
private void Load()
{
- if (!_storageService.Exists(Constants.SettingsFile)) return;
-
- var result = _storageService.Load(Constants.SettingsFile);
- var loadedSettings =
- JsonSerializer.Deserialize(result,
- SettingsStructureGenerationContext.Default.SettingsStructure);
- if (loadedSettings == null) return;
-
- loadedSettings.InputSettings.OnLoading();
- loadedSettings.OutputSettings.OnLoading();
- loadedSettings.LocaleSettings.OnLoading();
- loadedSettings.NotificationSettings.OnLoading();
- loadedSettings.ServersSettings.OnLoading();
- loadedSettings.ThemeSettings.OnLoading();
- loadedSettings.NetworkSettings.OnLoading();
- loadedSettings.UserSettings.OnLoading();
- loadedSettings.HotKeySettings.OnLoading();
-
- _settings = loadedSettings;
+ try
+ {
+ if (!_storageService.Exists(Constants.SettingsFile)) return;
+
+ var result = _storageService.Load(Constants.SettingsFile);
+ var loadedSettings =
+ JsonSerializer.Deserialize(result,
+ SettingsStructureGenerationContext.Default.SettingsStructure);
+ if (loadedSettings == null) return;
+
+ loadedSettings.InputSettings.OnLoading();
+ loadedSettings.OutputSettings.OnLoading();
+ loadedSettings.LocaleSettings.OnLoading();
+ loadedSettings.TelemetrySettings.OnLoading();
+ loadedSettings.NotificationSettings.OnLoading();
+ loadedSettings.ServersSettings.OnLoading();
+ loadedSettings.ThemeSettings.OnLoading();
+ loadedSettings.NetworkSettings.OnLoading();
+ loadedSettings.UserSettings.OnLoading();
+ loadedSettings.HotKeySettings.OnLoading();
+
+ _settings = loadedSettings;
+ }
+ catch (Exception ex)
+ {
+ LogService.Log(ex);
+ }
}
private async Task SaveSettingsAsync()
@@ -85,6 +101,7 @@ private async Task SaveSettingsAsync()
InputSettings.OnSaving();
OutputSettings.OnSaving();
LocaleSettings.OnSaving();
+ TelemetrySettings.OnSaving();
NotificationSettings.OnSaving();
ServersSettings.OnSaving();
ThemeSettings.OnSaving();
@@ -124,6 +141,8 @@ public class SettingsStructure
{
public Guid UserGuid { get; set; } = Guid.NewGuid();
public Guid ServerUserGuid { get; set; } = Guid.NewGuid();
+ public string TelemetryToken { get; set; } = Guid.NewGuid().ToString("N");
+ public TelemetrySettings TelemetrySettings { get; set; } = new();
public InputSettings InputSettings { get; set; } = new();
public OutputSettings OutputSettings { get; set; } = new();
public LocaleSettings LocaleSettings { get; set; } = new();
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Services/ThemesService.cs b/VoiceCraft.Client/VoiceCraft.Client/Services/ThemesService.cs
index 9f163e56..c5b121d4 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Services/ThemesService.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Services/ThemesService.cs
@@ -5,7 +5,6 @@
using System.Linq;
using Avalonia;
using Avalonia.Controls;
-using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Styling;
@@ -23,8 +22,10 @@ public class ThemesService
public ThemesService(IEnumerable registeredThemes,
IEnumerable registeredBackgroundImages)
{
- _registeredThemes.TryAdd(Guid.Empty, new RegisteredTheme(Guid.Empty, "Default", ThemeVariant.Default, [], []));
- _registeredBackgroundImages.TryAdd(Guid.Empty, new RegisteredBackgroundImage(Guid.Empty, "None", string.Empty));
+ _registeredThemes.TryAdd(Guid.Empty,
+ new RegisteredTheme(Guid.Empty, "ThemesService.Themes.Default", ThemeVariant.Default, [], []));
+ _registeredBackgroundImages.TryAdd(Guid.Empty,
+ new RegisteredBackgroundImage(Guid.Empty, "ThemesService.BackgroundImages.None", string.Empty));
foreach (var registeredTheme in registeredThemes) _registeredThemes.TryAdd(registeredTheme.Id, registeredTheme);
@@ -84,22 +85,6 @@ public void SwitchBackgroundImage(Guid id)
_currentBackgroundImage = backgroundImage;
OnBackgroundImageChanged?.Invoke(_currentBackgroundImage);
}
-
- ///
- /// Get Brush from resource . Returns if key has not been found OR
- /// returns a default color if has not been defined.
- ///
- /// Key for TryGetResource
- /// Fallback for when the resource cannot be found. Can be null
- /// An IBrush with the value of or or the default color.
- public static IBrush GetBrushResource(string key, IBrush? fallback = null)
- {
- return Application.Current is not null &&
- Application.Current.TryGetResource(key, Application.Current.ActualThemeVariant, out var val) &&
- val is not null
- ? (IBrush)val
- : fallback ?? new SolidColorBrush(new Color());
- }
}
public class RegisteredTheme(
@@ -123,23 +108,18 @@ public class RegisteredBackgroundImage(Guid id, string name, string path)
public string Path { get; } = path;
public Bitmap? BackgroundImageBitmap { get; private set; }
- public Bitmap LoadBitmap()
+ public void LoadBitmap()
{
UnloadBitmap();
if (File.Exists(Path))
{
- using (var fileStream = File.OpenRead(Path))
- {
- BackgroundImageBitmap = new Bitmap(fileStream);
- }
-
- return BackgroundImageBitmap;
+ using var fileStream = File.OpenRead(Path);
+ BackgroundImageBitmap = new Bitmap(fileStream);
}
- if (AssetLoader.Exists(new Uri(Path)))
- return BackgroundImageBitmap = new Bitmap(AssetLoader.Open(new Uri(Path)));
+ if (!AssetLoader.Exists(new Uri(Path))) throw new FileNotFoundException("Could not find image file.", Path);
+ BackgroundImageBitmap = new Bitmap(AssetLoader.Open(new Uri(Path)));
- throw new FileNotFoundException("Could not find image file.", Path);
}
public void UnloadBitmap()
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Services/VoiceCraftService.cs b/VoiceCraft.Client/VoiceCraft.Client/Services/VoiceCraftService.cs
index 13c8e0d1..cc59054f 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Services/VoiceCraftService.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Services/VoiceCraftService.cs
@@ -23,11 +23,9 @@ public class VoiceCraftService(
NotificationService notificationService)
{
private McWssServer? _mcWssServer;
+ private int _disconnecting;
private bool _pttEnabled;
private bool _pttCue;
- private bool _pushToTalk;
- private string _title = string.Empty;
- private string _description = string.Empty;
//Audio
private AudioPlaybackDevice? _audioPlayer;
@@ -56,37 +54,37 @@ public bool Deafened
public bool PushToTalk
{
- get => _pushToTalk;
+ get;
set
{
- if (!_pttEnabled || _pushToTalk == value) return;
- _pushToTalk = value;
+ if (!_pttEnabled || field == value) return;
+ field = value;
client.Muted = !value;
-
- if(_pttCue)
+
+ if (_pttCue)
_pttToneProvider?.Play(TimeSpan.FromMilliseconds(80), value ? 880f : 620f);
}
}
public string Title
{
- get => _title;
+ get;
private set
{
- _title = value;
+ field = value;
OnUpdateTitle?.Invoke(value);
}
- }
+ } = string.Empty;
public string Description
{
- get => _description;
+ get;
private set
{
- _description = value;
+ field = value;
OnUpdateDescription?.Invoke(value);
}
- }
+ } = string.Empty;
//Events
public event Action? OnConnected;
@@ -103,6 +101,9 @@ private set
public async Task ConnectAsync(string ip, int port)
{
+ if (client.ConnectionState != VcConnectionState.Disconnected) return;
+ _disconnecting = 0;
+
try
{
client.OnConnected += ClientOnConnected;
@@ -141,7 +142,7 @@ public async Task ConnectAsync(string ip, int port)
client.MicrophoneSensitivity = inputSettings.MicrophoneSensitivity;
client.Muted = _pttEnabled;
- _audioRecorder = InitializeAudioRecorder(inputSettings.InputDevice);
+ _audioRecorder = InitializeAudioRecorder(inputSettings.InputDevice, inputSettings.HardwarePreprocessorsEnabled);
_audioPlayer = InitializeAudioPlayer(outputSettings.OutputDevice);
_audioPreprocessor = InitializeAudioPreprocessor(inputSettings);
_pttToneProvider = InitializeToneProvider(_audioPlayer);
@@ -152,7 +153,7 @@ public async Task ConnectAsync(string ip, int port)
if (!_audioRecorder.IsRunning)
throw new Exception("VoiceCraft.DisconnectReason.Error");
_audioPlayer.Start();
- if (!_audioRecorder.IsRunning)
+ if (!_audioPlayer.IsRunning)
throw new Exception("VoiceCraft.DisconnectReason.Error");
_mcWssServer?.Start(networkSettings.McWssListenIp, networkSettings.McWssHostPort);
@@ -162,7 +163,7 @@ public async Task ConnectAsync(string ip, int port)
settingsService.ServerUserGuid,
localeSettings.Culture,
networkSettings.PositioningType);
- _ = Task.Run(() => UpdateLogic(client));
+ _ = Task.Run(UpdateLogicAsync);
await result;
}
catch (Exception ex)
@@ -174,6 +175,8 @@ public async Task ConnectAsync(string ip, int port)
public async Task DisconnectAsync(string? reason = null)
{
+ if (Interlocked.Exchange(ref _disconnecting, 1) == 1) return;
+
await StopClientAsync(client, reason);
if (_audioRecorder != null)
@@ -222,17 +225,29 @@ private int Read(Span buffer)
return read;
}
- private static void UpdateLogic(VoiceCraftClient client)
+ private async Task UpdateLogicAsync()
{
- var startTime = DateTime.UtcNow;
- while (client.ConnectionState != VcConnectionState.Disconnected)
+ try
{
- client.Update(); //Update all networking processes.
- var dist = DateTime.UtcNow - startTime;
- var delay = Constants.TickRate - dist.TotalMilliseconds;
- if (delay > 0)
- Thread.Sleep((int)delay);
- startTime = DateTime.UtcNow;
+ var startTime = DateTime.UtcNow;
+ while (client.ConnectionState != VcConnectionState.Disconnected)
+ {
+ if (_audioRecorder is { IsRunning: false } ||
+ _audioPlayer is { IsRunning: false })
+ throw new Exception("VoiceCraft.DisconnectReason.Error");
+
+ client.Update(); //Update all networking processes.
+ var dist = DateTime.UtcNow - startTime;
+ var delay = Constants.TickRate - dist.TotalMilliseconds;
+ if (delay > 0)
+ await Task.Delay((int)delay);
+ startTime = DateTime.UtcNow;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogService.Log(ex);
+ await DisconnectAsync(ex.Message);
}
}
@@ -325,10 +340,10 @@ private static ToneProvider InitializeToneProvider(AudioPlaybackDevice playbackD
return toneProvider;
}
- private AudioCaptureDevice InitializeAudioRecorder(string inputDevice)
+ private AudioCaptureDevice InitializeAudioRecorder(string inputDevice, bool hardwarePreprocessorsEnabled)
{
var recorder = audioService.InitializeCaptureDevice(Constants.SampleRate, Constants.RecordingChannels,
- Constants.FrameSize, inputDevice);
+ Constants.FrameSize, inputDevice, hardwarePreprocessorsEnabled);
recorder.OnAudioProcessed += Write;
return recorder;
}
@@ -422,4 +437,4 @@ private void StopMcWssServer(McWssServer server)
LogService.Log(ex);
}
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/AddServerViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/AddServerViewModel.cs
index a6efec9f..eb334253 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/AddServerViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/AddServerViewModel.cs
@@ -13,10 +13,9 @@ public partial class AddServerViewModel(
{
private bool _updatingPort;
- [ObservableProperty] private Server _server = new();
- [ObservableProperty] private decimal? _serverPort = 9050;
-
- [ObservableProperty] private ServersSettings _servers = settings.ServersSettings;
+ [ObservableProperty] public partial Server Server { get; set; } = new();
+ [ObservableProperty] public partial decimal? ServerPort { get; set; } = 9050;
+ [ObservableProperty] public partial ServersSettings Servers { get; set; } = settings.ServersSettings;
partial void OnServerChanged(Server value)
{
@@ -61,4 +60,4 @@ private void AddServer()
notificationService.SendErrorNotification("AddServer.Notification.Badge", ex.Message);
}
}
-}
+}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ContributorDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ContributorDataViewModel.cs
index 746fa184..4aed2857 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ContributorDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ContributorDataViewModel.cs
@@ -6,7 +6,7 @@ namespace VoiceCraft.Client.ViewModels.Data;
public partial class ContributorDataViewModel(string name, string[] roles, Bitmap? imageIcon = null) : ObservableObject
{
- [ObservableProperty] private Bitmap? _imageIcon = imageIcon;
- [ObservableProperty] private string _name = name;
- [ObservableProperty] private ObservableCollection _roles = new(roles);
+ [ObservableProperty] public partial Bitmap? ImageIcon { get; set; } = imageIcon;
+ [ObservableProperty] public partial string Name { get; set; } = name;
+ [ObservableProperty] public partial ObservableCollection Roles { get; set; } = new(roles);
}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/EntityDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/EntityDataViewModel.cs
index e92acbc6..80d17860 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/EntityDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/EntityDataViewModel.cs
@@ -14,20 +14,21 @@ public partial class EntityDataViewModel : ObservableObject
private readonly UserSettings _userSettings;
//Entity Display.
- [ObservableProperty] private string _displayName;
- [ObservableProperty] private bool _isDeafened;
- [ObservableProperty] private bool _isMuted;
- [ObservableProperty] private bool _isServerDeafened;
- [ObservableProperty] private bool _isServerMuted;
- [ObservableProperty] private bool _isSpeaking;
- [ObservableProperty] private bool _isVisible;
- [ObservableProperty] private bool _userMuted;
+ [ObservableProperty] public partial string DisplayName { get; set; }
+ [ObservableProperty] public partial bool IsDeafened { get; set; }
+ [ObservableProperty] public partial bool IsMuted { get; set; }
+ [ObservableProperty] public partial bool IsServerDeafened { get; set; }
+ [ObservableProperty] public partial bool IsServerMuted { get; set; }
+ [ObservableProperty] public partial bool IsSpeaking { get; set; }
+ [ObservableProperty] public partial bool IsVisible { get; set; }
+ [ObservableProperty] public partial bool UserMuted { get; set; }
+
private bool _userMutedUpdating;
private bool _userVolumeUpdating;
//User Settings
[ObservableProperty] private float _volume;
-
+
public VoiceCraftClientEntity Entity { get; }
public EntityDataViewModel(VoiceCraftClientEntity entity, SettingsService settingsService)
@@ -39,8 +40,8 @@ public EntityDataViewModel(VoiceCraftClientEntity entity, SettingsService settin
if (entity is VoiceCraftClientNetworkEntity networkEntity)
{
_entityUserId = networkEntity.UserGuid;
- _isServerMuted = networkEntity.ServerMuted;
- _isServerDeafened = networkEntity.ServerDeafened;
+ IsServerMuted = networkEntity.ServerMuted;
+ IsServerDeafened = networkEntity.ServerDeafened;
if (_userSettings.Users.TryGetValue((Guid)_entityUserId, out var entitySetting))
{
entity.Volume = entitySetting.Volume;
@@ -51,13 +52,13 @@ public EntityDataViewModel(VoiceCraftClientEntity entity, SettingsService settin
networkEntity.OnServerDeafenUpdated += (value, _) => IsServerDeafened = value;
}
- _displayName = entity.Name;
- _isMuted = entity.Muted;
- _isDeafened = entity.Deafened;
- _isVisible = entity.IsVisible;
- _isSpeaking = entity.IsSpeaking;
+ DisplayName = entity.Name;
+ IsMuted = entity.Muted;
+ IsDeafened = entity.Deafened;
+ IsVisible = entity.IsVisible;
+ IsSpeaking = entity.IsSpeaking;
_volume = entity.Volume;
- _userMuted = entity.UserMuted;
+ UserMuted = entity.UserMuted;
entity.OnNameUpdated += (value, _) => DisplayName = value;
entity.OnMuteUpdated += (value, _) => IsMuted = value;
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/HotKeyActionDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/HotKeyActionDataViewModel.cs
index 4ceb2187..b64fcda4 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/HotKeyActionDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/HotKeyActionDataViewModel.cs
@@ -6,6 +6,6 @@ namespace VoiceCraft.Client.ViewModels.Data;
public partial class HotKeyActionDataViewModel(HotKeyAction hotKeyAction, string keybind) : ObservableObject
{
public HotKeyAction Action { get; } = hotKeyAction;
- [ObservableProperty] private string _keybind = keybind.Replace("\0", " + ");
- [ObservableProperty] private string _title = $"Settings.HotKey.Actions.{hotKeyAction.Title}";
-}
+ [ObservableProperty] public partial string Keybind { get; set; } = keybind.Replace("\0", " + ");
+ [ObservableProperty] public partial string Title { get; set; } = hotKeyAction.Title;
+}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/HotKeySettingsDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/HotKeySettingsDataViewModel.cs
index a20948a8..689404c4 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/HotKeySettingsDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/HotKeySettingsDataViewModel.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
-using VoiceCraft.Client.Models.Settings;
using VoiceCraft.Client.Services;
namespace VoiceCraft.Client.ViewModels.Data;
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/InputSettingsDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/InputSettingsDataViewModel.cs
index 47d24ed4..e8b20bdb 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/InputSettingsDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/InputSettingsDataViewModel.cs
@@ -13,20 +13,26 @@ public partial class InputSettingsDataViewModel : ObservableObject, IDisposable
private readonly InputSettings _inputSettings;
private readonly SettingsService _settingsService;
- [ObservableProperty] private string _inputDevice;
- [ObservableProperty] private float _inputVolume;
- [ObservableProperty] private float _microphoneSensitivity;
- [ObservableProperty] private Guid _denoiser;
- [ObservableProperty] private Guid _automaticGainController;
- [ObservableProperty] private Guid _echoCanceler;
- [ObservableProperty] private bool _pushToTalkEnabled;
- [ObservableProperty] private bool _pushToTalkCue;
+ [ObservableProperty] public partial string InputDevice { get; set; }
+ [ObservableProperty] public partial float InputVolume { get; set; }
+ [ObservableProperty] public partial float MicrophoneSensitivity { get; set; }
+ [ObservableProperty] public partial Guid Denoiser { get; set; }
+ [ObservableProperty] public partial Guid AutomaticGainController { get; set; }
+ [ObservableProperty] public partial Guid EchoCanceler { get; set; }
+ [ObservableProperty] public partial bool HardwarePreprocessorsEnabled { get; set; }
+ [ObservableProperty] public partial bool PushToTalkEnabled { get; set; }
+ [ObservableProperty] public partial bool PushToTalkCue { get; set; }
//Lists
- [ObservableProperty] private ObservableCollection _inputDevices = [];
- [ObservableProperty] private ObservableCollection _denoisers = [];
- [ObservableProperty] private ObservableCollection _automaticGainControllers = [];
- [ObservableProperty] private ObservableCollection _echoCancelers = [];
+ [ObservableProperty] public partial ObservableCollection InputDevices { get; set; } = [];
+ [ObservableProperty] public partial ObservableCollection Denoisers { get; set; } = [];
+
+ [ObservableProperty]
+ public partial ObservableCollection AutomaticGainControllers { get; set; } = [];
+
+ [ObservableProperty]
+ public partial ObservableCollection EchoCancelers { get; set; } = [];
+
private bool _disposed;
private bool _updating;
@@ -37,14 +43,15 @@ public InputSettingsDataViewModel(SettingsService settingsService, AudioService
_settingsService = settingsService;
_inputSettings.OnUpdated += Update;
- _inputDevice = _inputSettings.InputDevice;
- _inputVolume = _inputSettings.InputVolume;
- _microphoneSensitivity = _inputSettings.MicrophoneSensitivity;
- _denoiser = _inputSettings.Denoiser;
- _automaticGainController = _inputSettings.AutomaticGainController;
- _echoCanceler = _inputSettings.EchoCanceler;
- _pushToTalkEnabled = _inputSettings.PushToTalkEnabled;
- _pushToTalkCue = _inputSettings.PushToTalkCue;
+ InputDevice = _inputSettings.InputDevice;
+ InputVolume = _inputSettings.InputVolume;
+ MicrophoneSensitivity = _inputSettings.MicrophoneSensitivity;
+ Denoiser = _inputSettings.Denoiser;
+ AutomaticGainController = _inputSettings.AutomaticGainController;
+ EchoCanceler = _inputSettings.EchoCanceler;
+ HardwarePreprocessorsEnabled = _inputSettings.HardwarePreprocessorsEnabled;
+ PushToTalkEnabled = _inputSettings.PushToTalkEnabled;
+ PushToTalkCue = _inputSettings.PushToTalkCue;
}
public void Dispose()
@@ -62,7 +69,7 @@ public void ReloadDevices()
Denoisers = [.._audioService.RegisteredAudioPreprocessors.Where(x => x.DenoiserSupported)];
AutomaticGainControllers = [.._audioService.RegisteredAudioPreprocessors.Where(x => x.GainControllerSupported)];
EchoCancelers = [.._audioService.RegisteredAudioPreprocessors.Where(x => x.EchoCancelerSupported)];
-
+
if (InputDevices.All(outputDevice => outputDevice.Name != InputDevice))
InputDevice = InputDevices.First(x => x.IsDefault).Name;
if (Denoisers.FirstOrDefault(x => x.Id == Denoiser) == null)
@@ -139,6 +146,17 @@ partial void OnEchoCancelerChanging(Guid value)
_updating = false;
}
+ partial void OnHardwarePreprocessorsEnabledChanging(bool value)
+ {
+ ThrowIfDisposed();
+
+ if (_updating) return;
+ _updating = true;
+ _inputSettings.HardwarePreprocessorsEnabled = value;
+ _ = _settingsService.SaveAsync();
+ _updating = false;
+ }
+
partial void OnPushToTalkEnabledChanging(bool value)
{
ThrowIfDisposed();
@@ -149,7 +167,7 @@ partial void OnPushToTalkEnabledChanging(bool value)
_ = _settingsService.SaveAsync();
_updating = false;
}
-
+
partial void OnPushToTalkCueChanging(bool value)
{
ThrowIfDisposed();
@@ -172,6 +190,7 @@ private void Update(InputSettings inputSettings)
Denoiser = inputSettings.Denoiser;
AutomaticGainController = inputSettings.AutomaticGainController;
EchoCanceler = inputSettings.EchoCanceler;
+ HardwarePreprocessorsEnabled = inputSettings.HardwarePreprocessorsEnabled;
PushToTalkEnabled = inputSettings.PushToTalkEnabled;
PushToTalkCue = inputSettings.PushToTalkCue;
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/LocaleSettingsDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/LocaleSettingsDataViewModel.cs
index d3f3b37e..42899c72 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/LocaleSettingsDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/LocaleSettingsDataViewModel.cs
@@ -11,7 +11,8 @@ public partial class LocaleSettingsDataViewModel : ObservableObject, IDisposable
private readonly LocaleSettings _localeSettings;
private readonly SettingsService _settingsService;
- [ObservableProperty] private string _culture;
+ [ObservableProperty] public partial string Culture { get; set; }
+
private bool _disposed;
private bool _updating;
@@ -20,7 +21,7 @@ public LocaleSettingsDataViewModel(SettingsService settingsService)
_localeSettings = settingsService.LocaleSettings;
_settingsService = settingsService;
_localeSettings.OnUpdated += Update;
- _culture = _localeSettings.Culture;
+ Culture = _localeSettings.Culture;
}
public void Dispose()
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/NetworkSettingsDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/NetworkSettingsDataViewModel.cs
index 69cee3b3..e9b1b842 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/NetworkSettingsDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/NetworkSettingsDataViewModel.cs
@@ -11,10 +11,10 @@ public partial class NetworkSettingsDataViewModel : ObservableObject, IDisposabl
private readonly NetworkSettings _networkSettings;
private readonly SettingsService _settingsService;
private bool _disposed;
- [ObservableProperty] private ushort _mcWssHostPort;
- [ObservableProperty] private string _mcWssListenIp;
+ [ObservableProperty] public partial ushort McWssHostPort { get; set; }
+ [ObservableProperty] public partial string McWssListenIp { get; set; }
+ [ObservableProperty] public partial PositioningType PositioningType { get; set; }
- [ObservableProperty] private PositioningType _positioningType;
private bool _updating;
public NetworkSettingsDataViewModel(SettingsService settingsService)
@@ -22,9 +22,9 @@ public NetworkSettingsDataViewModel(SettingsService settingsService)
_networkSettings = settingsService.NetworkSettings;
_settingsService = settingsService;
_networkSettings.OnUpdated += Update;
- _positioningType = _networkSettings.PositioningType;
- _mcWssListenIp = _networkSettings.McWssListenIp;
- _mcWssHostPort = _networkSettings.McWssHostPort;
+ PositioningType = _networkSettings.PositioningType;
+ McWssListenIp = _networkSettings.McWssListenIp;
+ McWssHostPort = _networkSettings.McWssHostPort;
}
public void Dispose()
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/NotificationSettingsDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/NotificationSettingsDataViewModel.cs
index 93a40242..3ab5c7a2 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/NotificationSettingsDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/NotificationSettingsDataViewModel.cs
@@ -9,9 +9,9 @@ public partial class NotificationSettingsDataViewModel : ObservableObject, IDisp
{
private readonly NotificationSettings _notificationSettings;
private readonly SettingsService _settingsService;
- [ObservableProperty] private bool _disableNotifications;
+ [ObservableProperty] public partial bool DisableNotifications { get; set; }
+ [ObservableProperty] public partial ushort DismissDelayMs { get; set; }
- [ObservableProperty] private ushort _dismissDelayMs;
private bool _disposed;
private bool _updating;
@@ -20,8 +20,8 @@ public NotificationSettingsDataViewModel(SettingsService settingsService)
_notificationSettings = settingsService.NotificationSettings;
_settingsService = settingsService;
_notificationSettings.OnUpdated += Update;
- _dismissDelayMs = _notificationSettings.DismissDelayMs;
- _disableNotifications = _notificationSettings.DisableNotifications;
+ DismissDelayMs = _notificationSettings.DismissDelayMs;
+ DisableNotifications = _notificationSettings.DisableNotifications;
}
public void Dispose()
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/OutputSettingsDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/OutputSettingsDataViewModel.cs
index 7e344cfe..9ff73437 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/OutputSettingsDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/OutputSettingsDataViewModel.cs
@@ -13,15 +13,16 @@ public partial class OutputSettingsDataViewModel : ObservableObject, IDisposable
private readonly OutputSettings _outputSettings;
private readonly SettingsService _settingsService;
- [ObservableProperty] private string _outputDevice;
- [ObservableProperty] private float _outputVolume;
- [ObservableProperty] private Guid _audioClipper;
+ [ObservableProperty] public partial string OutputDevice { get; set; }
+ [ObservableProperty] public partial float OutputVolume { get; set; }
+ [ObservableProperty] public partial Guid AudioClipper { get; set; }
+
private bool _disposed;
private bool _updating;
//Lists
- [ObservableProperty] private ObservableCollection _outputDevices = [];
- [ObservableProperty] private ObservableCollection _audioClippers = [];
+ [ObservableProperty] public partial ObservableCollection OutputDevices { get; set; } = [];
+ [ObservableProperty] public partial ObservableCollection AudioClippers { get; set; } = [];
public OutputSettingsDataViewModel(SettingsService settingsService, AudioService audioService)
{
@@ -30,9 +31,9 @@ public OutputSettingsDataViewModel(SettingsService settingsService, AudioService
_settingsService = settingsService;
_outputSettings.OnUpdated += Update;
- _outputDevice = _outputSettings.OutputDevice;
- _outputVolume = _outputSettings.OutputVolume;
- _audioClipper = _outputSettings.AudioClipper;
+ OutputDevice = _outputSettings.OutputDevice;
+ OutputVolume = _outputSettings.OutputVolume;
+ AudioClipper = _outputSettings.AudioClipper;
}
public void Dispose()
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ServerDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ServerDataViewModel.cs
index 76bf5dfe..93f59ddb 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ServerDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ServerDataViewModel.cs
@@ -11,9 +11,10 @@ public partial class ServerDataViewModel : ObservableObject, IDisposable
public readonly Server Server;
private bool _disposed;
- [ObservableProperty] private string _ip;
- [ObservableProperty] private string _name;
- [ObservableProperty] private ushort _port;
+ [ObservableProperty] public partial string Ip { get; set; }
+ [ObservableProperty] public partial string Name { get; set; }
+ [ObservableProperty] public partial ushort Port { get; set; }
+
private bool _updating;
public ServerDataViewModel(Server server, SettingsService settingsService)
@@ -21,9 +22,9 @@ public ServerDataViewModel(Server server, SettingsService settingsService)
Server = server;
_settingsService = settingsService;
Server.OnUpdated += Update;
- _name = Server.Name;
- _ip = Server.Ip;
- _port = Server.Port;
+ Name = Server.Name;
+ Ip = Server.Ip;
+ Port = Server.Port;
}
public void Dispose()
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ServersSettingsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ServersSettingsViewModel.cs
index 44876977..b51bf851 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ServersSettingsViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ServersSettingsViewModel.cs
@@ -13,8 +13,9 @@ public partial class ServersSettingsViewModel : ObservableObject, IDisposable
public readonly ServersSettings ServersSettings;
private bool _disposed;
- [ObservableProperty] private bool _hideServerAddresses;
- [ObservableProperty] private ObservableCollection _servers;
+ [ObservableProperty] public partial bool HideServerAddresses { get; set; }
+ [ObservableProperty] public partial ObservableCollection Servers { get; set; }
+
private bool _updating;
public ServersSettingsViewModel(SettingsService settingsService)
@@ -22,8 +23,8 @@ public ServersSettingsViewModel(SettingsService settingsService)
ServersSettings = settingsService.ServersSettings;
_settingsService = settingsService;
ServersSettings.OnUpdated += Update;
- _hideServerAddresses = ServersSettings.HideServerAddresses;
- _servers = new ObservableCollection(
+ HideServerAddresses = ServersSettings.HideServerAddresses;
+ Servers = new ObservableCollection(
ServersSettings.Servers.Select(s => new ServerDataViewModel(s, _settingsService)));
}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/TelemetrySettingsDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/TelemetrySettingsDataViewModel.cs
new file mode 100644
index 00000000..c27be054
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/TelemetrySettingsDataViewModel.cs
@@ -0,0 +1,58 @@
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+using VoiceCraft.Client.Models.Settings;
+using VoiceCraft.Client.Services;
+
+namespace VoiceCraft.Client.ViewModels.Data;
+
+public partial class TelemetrySettingsDataViewModel : ObservableObject, IDisposable
+{
+ private readonly SettingsService _settingsService;
+ private readonly TelemetrySettings _telemetrySettings;
+ private bool _disposed;
+ private bool _updating;
+
+ [ObservableProperty] public partial bool Enabled { get; set; }
+
+ public TelemetrySettingsDataViewModel(SettingsService settingsService)
+ {
+ _settingsService = settingsService;
+ _telemetrySettings = settingsService.TelemetrySettings;
+ _telemetrySettings.OnUpdated += Update;
+ Enabled = _telemetrySettings.Enabled;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _telemetrySettings.OnUpdated -= Update;
+ _disposed = true;
+ GC.SuppressFinalize(this);
+ }
+
+ partial void OnEnabledChanging(bool value)
+ {
+ ThrowIfDisposed();
+
+ if (_updating) return;
+ _updating = true;
+ _telemetrySettings.Enabled = value;
+ _telemetrySettings.ConsentShown = true;
+ _ = _settingsService.SaveAsync();
+ _updating = false;
+ }
+
+ private void Update(TelemetrySettings telemetrySettings)
+ {
+ if (_updating) return;
+ _updating = true;
+ Enabled = telemetrySettings.Enabled;
+ _updating = false;
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (!_disposed) return;
+ throw new ObjectDisposedException(typeof(TelemetrySettingsDataViewModel).ToString());
+ }
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ThemeSettingsDataViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ThemeSettingsDataViewModel.cs
index fd02848f..241479d8 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ThemeSettingsDataViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Data/ThemeSettingsDataViewModel.cs
@@ -11,9 +11,9 @@ public partial class ThemeSettingsDataViewModel : ObservableObject, IDisposable
private readonly ThemeSettings _themeSettings;
private readonly ThemesService _themesService;
private bool _disposed;
- [ObservableProperty] private Guid _selectedBackgroundImage;
+ [ObservableProperty] public partial Guid SelectedBackgroundImage { get; set; }
+ [ObservableProperty] public partial Guid SelectedTheme { get; set; }
- [ObservableProperty] private Guid _selectedTheme;
private bool _updating;
public ThemeSettingsDataViewModel(SettingsService settingsService, ThemesService themesService)
@@ -22,8 +22,8 @@ public ThemeSettingsDataViewModel(SettingsService settingsService, ThemesService
_settingsService = settingsService;
_themesService = themesService;
_themeSettings.OnUpdated += Update;
- _selectedTheme = _themeSettings.SelectedTheme;
- _selectedBackgroundImage = _themeSettings.SelectedBackgroundImage;
+ SelectedTheme = _themeSettings.SelectedTheme;
+ SelectedBackgroundImage = _themeSettings.SelectedBackgroundImage;
}
public void Dispose()
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/EditServerViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/EditServerViewModel.cs
index 26560d94..add19482 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/EditServerViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/EditServerViewModel.cs
@@ -15,9 +15,9 @@ public partial class EditServerViewModel(
{
private bool _updatingPort;
- [ObservableProperty] private Server _editableServer = new();
- [ObservableProperty] private decimal? _editableServerPort = 9050;
- [ObservableProperty] private Server _server = new();
+ [ObservableProperty] public partial Server EditableServer { get; set; } = new();
+ [ObservableProperty] public partial decimal? EditableServerPort { get; set; } = 9050;
+ [ObservableProperty] public partial Server Server { get; set; } = new();
public override void OnAppearing(object? data = null)
{
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/ErrorViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/ErrorViewModel.cs
index cee74359..d3db89ab 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/ErrorViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/ErrorViewModel.cs
@@ -4,5 +4,5 @@ namespace VoiceCraft.Client.ViewModels;
public partial class ErrorViewModel : ViewModelBase
{
- [ObservableProperty] private string _errorMessage = string.Empty;
+ [ObservableProperty] public partial string ErrorMessage { get; set; } = string.Empty;
}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/CrashLogViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/CrashLogViewModel.cs
index 597bd674..98a3918c 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/CrashLogViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/CrashLogViewModel.cs
@@ -6,14 +6,33 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using Microsoft.Maui.ApplicationModel.DataTransfer;
using VoiceCraft.Client.Services;
+using VoiceCraft.Core.Diagnostics;
namespace VoiceCraft.Client.ViewModels.Home;
-public partial class CrashLogViewModel(NotificationService notificationService) : ViewModelBase
+public partial class CrashLogViewModel(
+ NotificationService notificationService,
+ ClientTelemetryService clientTelemetryService,
+ ClipboardService clipboardService) : ViewModelBase
{
- [ObservableProperty] private ObservableCollection> _crashLogs = [];
+ [ObservableProperty] public partial ObservableCollection> CrashLogs { get; set; } = [];
+
+ private async Task UploadCrashLog(DateTime timeStamp)
+ {
+ if (!LogService.TryGetCrashLog(timeStamp, out var crashLog)) return null;
+ if (!string.IsNullOrWhiteSpace(crashLog.DumpUrl))
+ return crashLog.DumpUrl;
+
+ var dumpResponse = await clientTelemetryService.ReportCrashAsync(crashLog.Message);
+ var dumpUrl = dumpResponse?.ViewUrl ?? dumpResponse?.Url;
+ if (string.IsNullOrWhiteSpace(dumpUrl))
+ return null;
+
+ crashLog.DumpUrl = dumpUrl;
+ LogService.UpdateCrashLog(timeStamp, crashLog);
+ return dumpUrl;
+ }
[RelayCommand]
private async Task CopyLogs()
@@ -32,11 +51,13 @@ private async Task CopyLogs()
foreach (var crashLog in CrashLogs.OrderByDescending(x => x.Key))
{
_ = text.AppendLine($"[{crashLog.Key:O}]");
- _ = text.AppendLine(crashLog.Value);
+ _ = text.AppendLine(crashLog.Value.Message);
+ if (!string.IsNullOrWhiteSpace(crashLog.Value.DumpUrl))
+ _ = text.AppendLine($"Dump: {crashLog.Value.DumpUrl}");
_ = text.AppendLine();
}
- await Clipboard.Default.SetTextAsync(text.ToString().Trim());
+ await clipboardService.SetTextAsync(text.ToString().Trim());
notificationService.SendSuccessNotification(
"CrashLogs.Notification.Badge",
"CrashLogs.Notification.Copied");
@@ -49,6 +70,41 @@ private async Task CopyLogs()
}
}
+ [RelayCommand]
+ private async Task CopyDumpLink(KeyValuePair? crashLog)
+ {
+ try
+ {
+ if (crashLog is null)
+ {
+ notificationService.SendNotification(
+ "CrashLogs.Notification.Badge",
+ "CrashLogs.Notification.DumpUnavailable");
+ return;
+ }
+
+ var dumpUrl = await UploadCrashLog(crashLog.Value.Key);
+ if (string.IsNullOrWhiteSpace(dumpUrl))
+ {
+ notificationService.SendNotification(
+ "CrashLogs.Notification.Badge",
+ "CrashLogs.Notification.DumpUploadFailed");
+ return;
+ }
+
+ await clipboardService.SetTextAsync(dumpUrl);
+ notificationService.SendSuccessNotification(
+ "CrashLogs.Notification.Badge",
+ "CrashLogs.Notification.DumpCopied");
+ }
+ catch (Exception ex)
+ {
+ notificationService.SendErrorNotification(
+ "CrashLogs.Notification.Badge",
+ ex.Message);
+ }
+ }
+
[RelayCommand]
private void ClearLogs()
{
@@ -70,6 +126,6 @@ private void ClearLogs()
public override void OnAppearing(object? data = null)
{
- CrashLogs = new ObservableCollection>(LogService.CrashLogs);
+ CrashLogs = new ObservableCollection>(LogService.CrashLogs);
}
}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/CreditsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/CreditsViewModel.cs
index 988499bb..88806687 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/CreditsViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/CreditsViewModel.cs
@@ -15,17 +15,14 @@ public partial class CreditsViewModel : ViewModelBase
{
//private readonly Bitmap? _defaultIcon = LoadImage("avares://VoiceCraft.Client/Assets/Contributors/vc.png");
- [ObservableProperty] private string _appVersion = string.Empty;
-
- [ObservableProperty] private string _codec = string.Empty;
-
- [ObservableProperty] private ObservableCollection _contributors;
-
- [ObservableProperty] private string _version = string.Empty;
+ [ObservableProperty] public partial string AppVersion { get; set; } = string.Empty;
+ [ObservableProperty] public partial string Codec { get; set; } = string.Empty;
+ [ObservableProperty] public partial ObservableCollection Contributors { get; set; }
+ [ObservableProperty] public partial string Version { get; set; } = string.Empty;
public CreditsViewModel()
{
- _contributors =
+ Contributors =
[
new ContributorDataViewModel(
"SineVector241",
@@ -82,4 +79,4 @@ private static string GetCodecVersion()
return "N.A.";
}
}
-}
+}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/ServersViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/ServersViewModel.cs
index 98051bab..7029f7af 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/ServersViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/ServersViewModel.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using VoiceCraft.Client.Models;
@@ -13,26 +14,23 @@ public partial class ServersViewModel(
SettingsService settings)
: ViewModelBase, IDisposable
{
- [ObservableProperty] private ServerDataViewModel? _selectedServer;
-
- [ObservableProperty] private ServersSettingsViewModel _serversSettings = new(settings);
+ [ObservableProperty] public partial ServerDataViewModel? SelectedServer { get; set; }
+ [ObservableProperty] public partial ServersSettingsViewModel ServersSettings { get; set; } = new(settings);
public void Dispose()
{
ServersSettings.Dispose();
GC.SuppressFinalize(this);
}
-
- private void OpenServer(ServerDataViewModel? server)
- {
- if (server == null) return;
- navigationService.NavigateTo(new SelectedServerNavigationData(server.Server));
- }
partial void OnSelectedServerChanged(ServerDataViewModel? value)
{
- OpenServer(value);
- SelectedServer = null;
+ if (value == null) return;
+ navigationService.NavigateTo(new SelectedServerNavigationData(value.Server));
+ Task.Run(() =>
+ {
+ SelectedServer = null; //This bug is annoying.
+ });
}
[RelayCommand]
@@ -57,4 +55,4 @@ private void EditServer(ServerDataViewModel? server)
if (server == null) return;
navigationService.NavigateTo(new EditServerNavigationData(server.Server));
}
-}
+}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/SettingsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/SettingsViewModel.cs
index 3b25ea20..8218e54a 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/SettingsViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Home/SettingsViewModel.cs
@@ -8,32 +8,35 @@ namespace VoiceCraft.Client.ViewModels.Home;
public partial class SettingsViewModel : ViewModelBase
{
- [ObservableProperty] private ObservableCollection _items = [];
- [ObservableProperty] private ListItemTemplate? _selectedListItem;
+ [ObservableProperty] public partial ObservableCollection Items { get; set; }
+ [ObservableProperty] public partial ListItemTemplate? SelectedListItem { get; set; }
public SettingsViewModel(NavigationService navigationService)
{
- _items.Add(new ListItemTemplate("Settings.General.Title",
- () => { navigationService.NavigateTo(); }));
- _items.Add(new ListItemTemplate("Settings.Appearance.Title",
- () => { navigationService.NavigateTo(); }));
- _items.Add(new ListItemTemplate("Settings.Input.Title",
- () => { navigationService.NavigateTo(); }));
- _items.Add(new ListItemTemplate("Settings.Output.Title",
- () => { navigationService.NavigateTo(); }));
- _items.Add(new ListItemTemplate("Settings.Network.Title",
- () => { navigationService.NavigateTo(); }));
- _items.Add(new ListItemTemplate("Settings.HotKey.Title",
- () => { navigationService.NavigateTo(); }));
- _items.Add(new ListItemTemplate("Settings.Advanced.Title",
- () => { navigationService.NavigateTo(); }));
+ Items =
+ [
+ new ListItemTemplate("Settings.General.Title",
+ () => { navigationService.NavigateTo(); }),
+ new ListItemTemplate("Settings.Appearance.Title",
+ () => { navigationService.NavigateTo(); }),
+ new ListItemTemplate("Settings.Input.Title",
+ () => { navigationService.NavigateTo(); }),
+ new ListItemTemplate("Settings.Output.Title",
+ () => { navigationService.NavigateTo(); }),
+ new ListItemTemplate("Settings.Network.Title",
+ () => { navigationService.NavigateTo(); }),
+ new ListItemTemplate("Settings.HotKey.Title",
+ () => { navigationService.NavigateTo(); }),
+ new ListItemTemplate("Settings.Advanced.Title",
+ () => { navigationService.NavigateTo(); })
+ ];
}
partial void OnSelectedListItemChanged(ListItemTemplate? value)
{
if (value == null) return;
value.OnClicked.Invoke();
- _selectedListItem = null;
+ SelectedListItem = null;
}
}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/HomeViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/HomeViewModel.cs
index 2e984cde..98509f8e 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/HomeViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/HomeViewModel.cs
@@ -9,24 +9,25 @@ namespace VoiceCraft.Client.ViewModels;
public partial class HomeViewModel : ViewModelBase
{
- [ObservableProperty] private ViewModelBase _content;
-
- [ObservableProperty] private ObservableCollection _items = [];
-
- [ObservableProperty] private ListItemTemplate? _selectedListItem;
- [ObservableProperty] private string _title;
+ [ObservableProperty] public partial ViewModelBase Content { get; set; }
+ [ObservableProperty] public partial ObservableCollection Items { get; set; }
+ [ObservableProperty] public partial ListItemTemplate? SelectedListItem { get; set; }
+ [ObservableProperty] public partial string Title { get; set; }
public HomeViewModel(ServersViewModel servers, SettingsViewModel settings, CreditsViewModel credits,
CrashLogViewModel crashLog)
{
- _items.Add(new ListItemTemplate("Servers.Title", servers, "HomeRegular"));
- _items.Add(new ListItemTemplate("Settings.Title", settings, "SettingsRegular"));
- _items.Add(new ListItemTemplate("Credits.Title", credits, "InformationRegular"));
- _items.Add(new ListItemTemplate("CrashLogs.Title", crashLog, "NotebookErrorRegular"));
-
- SelectedListItem = _items[0];
- _content = _items[0].Content;
- _title = _items[0].Title;
+ Items =
+ [
+ new ListItemTemplate("Servers.Title", servers, "HomeRegular"),
+ new ListItemTemplate("Settings.Title", settings, "SettingsRegular"),
+ new ListItemTemplate("Credits.Title", credits, "InformationRegular"),
+ new ListItemTemplate("CrashLogs.Title", crashLog, "NotebookErrorRegular")
+ ];
+
+ SelectedListItem = Items[0];
+ Content = Items[0].Content;
+ Title = Items[0].Title;
}
partial void OnSelectedListItemChanged(ListItemTemplate? value)
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/MainViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/MainViewModel.cs
index 021873b5..f4f0100f 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/MainViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/MainViewModel.cs
@@ -1,15 +1,19 @@
-using Avalonia.Media.Imaging;
+using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using VoiceCraft.Client.Models;
using VoiceCraft.Client.Services;
+using VoiceCraft.Client.ViewModels.Modals;
using VoiceCraft.Core.Locales;
namespace VoiceCraft.Client.ViewModels;
public partial class MainViewModel : ObservableObject
{
- [ObservableProperty] private Bitmap? _backgroundImage;
- [ObservableProperty] private object? _content;
+ private readonly NavigationService _navigationService;
+ [ObservableProperty] public partial Bitmap? BackgroundImage { get; set; }
+ [ObservableProperty] public partial object? Content { get; set; }
+ [ObservableProperty] public partial object? ModalContent { get; set; }
+ [ObservableProperty] public partial bool HasModal { get; set; }
public MainViewModel(NavigationService navigationService,
ThemesService themesService,
@@ -18,6 +22,8 @@ public MainViewModel(NavigationService navigationService,
HotKeyService hotKeyService,
IBackgroundService backgroundService)
{
+ _navigationService = navigationService;
+
themesService.OnBackgroundImageChanged += backgroundImage =>
{
BackgroundImage = backgroundImage?.BackgroundImageBitmap;
@@ -29,6 +35,11 @@ public MainViewModel(NavigationService navigationService,
Content = viewModel;
discordRpcService.SetState($"In page {viewModel.GetType().Name.Replace("ViewModel", "")}");
};
+ navigationService.OnModalViewModelChanged += viewModel =>
+ {
+ ModalContent = viewModel;
+ HasModal = viewModel != null;
+ };
//Initialize Themes
var themeSettings = settingsService.ThemeSettings;
themesService.SwitchTheme(themeSettings.SelectedTheme);
@@ -40,9 +51,17 @@ public MainViewModel(NavigationService navigationService,
// change to HomeView
navigationService.NavigateTo();
-
+
var voiceCraftService = backgroundService.GetService();
- if (voiceCraftService == null) return;
- navigationService.NavigateTo(new VoiceNavigationData(voiceCraftService));
+ if (voiceCraftService != null)
+ navigationService.NavigateTo(new VoiceNavigationData(voiceCraftService));
+
+ if (!settingsService.TelemetrySettings.ConsentShown)
+ navigationService.PushModal();
+ }
+
+ public void PopModal()
+ {
+ _navigationService.PopModal(true);
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Modals/EntityDataSettingsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Modals/EntityDataSettingsViewModel.cs
new file mode 100644
index 00000000..6483d19f
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Modals/EntityDataSettingsViewModel.cs
@@ -0,0 +1,18 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using VoiceCraft.Client.Models;
+using VoiceCraft.Client.ViewModels.Data;
+
+namespace VoiceCraft.Client.ViewModels.Modals;
+
+public partial class EntityDataSettingsViewModel : ViewModelBase
+{
+ [ObservableProperty] public partial EntityDataViewModel? Entity { get; private set; }
+
+ public override void OnAppearing(object? data = null)
+ {
+ if (data is EntityDataSettingsNavigationData navigationData)
+ {
+ Entity = navigationData.Entity;
+ }
+ }
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Modals/HotKeyCaptureViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Modals/HotKeyCaptureViewModel.cs
new file mode 100644
index 00000000..34f7274d
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Modals/HotKeyCaptureViewModel.cs
@@ -0,0 +1,60 @@
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using VoiceCraft.Client.Models;
+using VoiceCraft.Client.Services;
+using VoiceCraft.Client.ViewModels.Data;
+
+namespace VoiceCraft.Client.ViewModels.Modals;
+
+public partial class HotKeyCaptureViewModel(NavigationService navigationService, HotKeyService hotKeyService)
+ : ViewModelBase
+{
+ public override bool DisableBackButton => true;
+ private readonly HotKeySettingsDataViewModel _hotKeySettingsData = new(hotKeyService);
+ private HotKeyActionDataViewModel? _hotKeyAction;
+ [ObservableProperty] public partial string RebindingTitle { get; set; } = string.Empty;
+ [ObservableProperty] public partial string RebindingPreview { get; set; } = string.Empty;
+
+ public override void OnAppearing(object? data = null)
+ {
+ if (data is not HotKeyCaptureNavigationData navigationData) return;
+ _hotKeyAction = navigationData.HotKey;
+ RebindingTitle = _hotKeyAction.Title;
+ RebindingPreview = _hotKeyAction.Keybind;
+ }
+
+ [RelayCommand]
+ private void ConfirmRebind()
+ {
+ if (string.IsNullOrWhiteSpace(RebindingPreview) ||
+ string.IsNullOrWhiteSpace(_hotKeyAction?.Action.Id)) return;
+ var action = _hotKeyAction;
+ if (action == null) return;
+
+ _hotKeySettingsData.SetBinding(action.Action,
+ HotKeyService.NormalizeKeyCombo(RebindingPreview.Replace(" + ", "\0")));
+ _hotKeyAction.Keybind = RebindingPreview;
+ CancelRebind();
+ }
+
+ [RelayCommand]
+ private void UpdateBindingPreview(string? text)
+ {
+ if (string.IsNullOrWhiteSpace(text)) return;
+ var keys = text.Split(" + ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ RebindingPreview = HotKeyService.NormalizeKeyCombo(keys).Replace("\0", " + ");
+ }
+
+ [RelayCommand]
+ private void CancelRebind()
+ {
+ navigationService.PopModal();
+ }
+
+ [RelayCommand]
+ private void ClearBindingPreview()
+ {
+ RebindingPreview = string.Empty;
+ }
+}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Modals/TelemetryConsentViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Modals/TelemetryConsentViewModel.cs
new file mode 100644
index 00000000..a8dfc4a8
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Modals/TelemetryConsentViewModel.cs
@@ -0,0 +1,32 @@
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.Input;
+using VoiceCraft.Client.Services;
+
+namespace VoiceCraft.Client.ViewModels.Modals;
+
+public partial class TelemetryConsentViewModel(
+ NavigationService navigationService,
+ SettingsService settingsService,
+ ClientTelemetryService clientTelemetryService) : ViewModelBase
+{
+ public override bool DisableBackButton { get; protected set; } = true;
+
+ [RelayCommand]
+ private async Task Accept()
+ {
+ settingsService.TelemetrySettings.Enabled = true;
+ settingsService.TelemetrySettings.ConsentShown = true;
+ await settingsService.SaveImmediate();
+ _ = clientTelemetryService.ReportStartupAsync();
+ navigationService.PopModal();
+ }
+
+ [RelayCommand]
+ private async Task Decline()
+ {
+ settingsService.TelemetrySettings.Enabled = false;
+ settingsService.TelemetrySettings.ConsentShown = true;
+ await settingsService.SaveImmediate();
+ navigationService.PopModal();
+ }
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/SelectedServerViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/SelectedServerViewModel.cs
index b011f680..e271c8d9 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/SelectedServerViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/SelectedServerViewModel.cs
@@ -20,13 +20,13 @@ public partial class SelectedServerViewModel(
: ViewModelBase, IDisposable
{
private CancellationTokenSource? _cts;
- [ObservableProperty] private string _connectedClients = string.Empty;
- [ObservableProperty] private string _latency = string.Empty;
- [ObservableProperty] private string _motd = string.Empty;
- [ObservableProperty] private string _positioningType = string.Empty;
- [ObservableProperty] private ServerDataViewModel? _selectedServer;
- [ObservableProperty] private ServersSettingsViewModel _serversSettings = new(settingsService);
- [ObservableProperty] private string _version = string.Empty;
+ [ObservableProperty] public partial string ConnectedClients { get; set; } = string.Empty;
+ [ObservableProperty] public partial string Latency { get; set; } = string.Empty;
+ [ObservableProperty] public partial string Motd { get; set; } = string.Empty;
+ [ObservableProperty] public partial string PositioningType { get; set; } = string.Empty;
+ [ObservableProperty] public partial ServerDataViewModel? SelectedServer { get; set; }
+ [ObservableProperty] public partial ServersSettingsViewModel ServersSettings { get; set; } = new(settingsService);
+ [ObservableProperty] public partial string Version { get; set; } = string.Empty;
public void Dispose()
{
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/AdvancedSettingsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/AdvancedSettingsViewModel.cs
index 3d406db5..8faccab9 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/AdvancedSettingsViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/AdvancedSettingsViewModel.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using VoiceCraft.Client.Services;
@@ -6,7 +7,8 @@ namespace VoiceCraft.Client.ViewModels.Settings;
public partial class AdvancedSettingsViewModel(
NavigationService navigationService,
- NotificationService notificationService) : ViewModelBase
+ NotificationService notificationService,
+ SettingsService settingsService) : ViewModelBase
{
[RelayCommand]
private void TriggerGc()
@@ -33,10 +35,28 @@ private static void Crash()
throw new Exception("Task failed successfully.");
}
+ [RelayCommand]
+ private async Task ResetAllSettings()
+ {
+ try
+ {
+ await settingsService.ResetToDefaultsAsync();
+ notificationService.SendSuccessNotification(
+ "Settings.Advanced.Notification.Reset.Badge",
+ "Settings.Advanced.Notification.Reset.Done");
+ }
+ catch (Exception ex)
+ {
+ notificationService.SendErrorNotification(
+ "Settings.Advanced.Notification.Reset.Badge",
+ ex.Message);
+ }
+ }
+
[RelayCommand]
private void Cancel()
{
if (DisableBackButton) return;
navigationService.Back();
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/AppearanceSettingsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/AppearanceSettingsViewModel.cs
index c0c4d333..7e2e6055 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/AppearanceSettingsViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/AppearanceSettingsViewModel.cs
@@ -12,11 +12,15 @@ public partial class AppearanceSettingsViewModel(
ThemesService themesService,
SettingsService settingsService) : ViewModelBase, IDisposable
{
- [ObservableProperty] private ObservableCollection _backgroundImages =
+ [ObservableProperty]
+ public partial ObservableCollection BackgroundImages { get; set; } =
new(themesService.RegisteredBackgroundImages);
- [ObservableProperty] private ObservableCollection _themes = new(themesService.RegisteredThemes);
- [ObservableProperty] private ThemeSettingsDataViewModel _themeSettingsData = new(settingsService, themesService);
+ [ObservableProperty]
+ public partial ObservableCollection Themes { get; set; } = new(themesService.RegisteredThemes);
+
+ [ObservableProperty]
+ public partial ThemeSettingsDataViewModel ThemeSettingsData { get; set; } = new(settingsService, themesService);
public void Dispose()
{
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/GeneralSettingsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/GeneralSettingsViewModel.cs
index feb5da4c..b8ba0f23 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/GeneralSettingsViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/GeneralSettingsViewModel.cs
@@ -15,30 +15,36 @@ public partial class GeneralSettingsViewModel
{
private readonly NavigationService _navigationService;
- [ObservableProperty] private ObservableCollection> _locales = [];
+ [ObservableProperty] public partial ObservableCollection> Locales { get; set; } = [];
//Language Settings
- [ObservableProperty] private LocaleSettingsDataViewModel _localeSettingsData;
+ [ObservableProperty] public partial LocaleSettingsDataViewModel LocaleSettingsData { get; set; }
//Notification Settings
- [ObservableProperty] private NotificationSettingsDataViewModel _notificationSettingsData;
+ [ObservableProperty] public partial NotificationSettingsDataViewModel NotificationSettingsData { get; set; }
+
+ //Telemetry Settings
+ [ObservableProperty] public partial TelemetrySettingsDataViewModel TelemetrySettingsData { get; set; }
//Server Settings
- [ObservableProperty] private ServersSettingsViewModel _serversSettings;
+ [ObservableProperty] public partial ServersSettingsViewModel ServersSettings { get; set; }
public GeneralSettingsViewModel(NavigationService navigationService, SettingsService settingsService)
{
foreach (var locale in Localizer.Languages)
- _locales.Add(new KeyValuePair(CultureInfo.GetCultureInfo(locale).NativeName, locale));
+ Locales.Add(new KeyValuePair(CultureInfo.GetCultureInfo(locale).NativeName, locale));
_navigationService = navigationService;
- _localeSettingsData = new LocaleSettingsDataViewModel(settingsService);
- _notificationSettingsData = new NotificationSettingsDataViewModel(settingsService);
- _serversSettings = new ServersSettingsViewModel(settingsService);
+ LocaleSettingsData = new LocaleSettingsDataViewModel(settingsService);
+ TelemetrySettingsData = new TelemetrySettingsDataViewModel(settingsService);
+ NotificationSettingsData = new NotificationSettingsDataViewModel(settingsService);
+ ServersSettings = new ServersSettingsViewModel(settingsService);
}
public void Dispose()
{
NotificationSettingsData.Dispose();
+ TelemetrySettingsData.Dispose();
+ LocaleSettingsData.Dispose();
ServersSettings.Dispose();
GC.SuppressFinalize(this);
}
@@ -49,4 +55,4 @@ private void Cancel()
if (DisableBackButton) return;
_navigationService.Back();
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/HotKeySettingsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/HotKeySettingsViewModel.cs
index 169e90ea..a4f8b9b8 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/HotKeySettingsViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/HotKeySettingsViewModel.cs
@@ -1,10 +1,10 @@
using System;
-using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+using VoiceCraft.Client.Models;
using VoiceCraft.Client.Services;
using VoiceCraft.Client.ViewModels.Data;
-using VoiceCraft.Core.Locales;
+using VoiceCraft.Client.ViewModels.Modals;
namespace VoiceCraft.Client.ViewModels.Settings;
@@ -12,18 +12,15 @@ public partial class HotKeySettingsViewModel : ViewModelBase, IDisposable
{
private readonly NavigationService _navigationService;
private readonly HotKeySettingsDataViewModel _hotKeySettingsData;
- private string? _rebindActionId;
- [ObservableProperty] private System.Collections.ObjectModel.ObservableCollection _hotKeys;
- [ObservableProperty] private bool _isRebinding;
- [ObservableProperty] private string _rebindingTitle = string.Empty;
- [ObservableProperty] private string _rebindingPreview = string.Empty;
+ [ObservableProperty]
+ public partial System.Collections.ObjectModel.ObservableCollection HotKeys { get; set; }
public HotKeySettingsViewModel(NavigationService navigationService, HotKeyService hotKeyService)
{
_navigationService = navigationService;
_hotKeySettingsData = new HotKeySettingsDataViewModel(hotKeyService);
- _hotKeys = _hotKeySettingsData.HotKeys;
+ HotKeys = _hotKeySettingsData.HotKeys;
}
public void Dispose()
@@ -42,47 +39,6 @@ private void Cancel()
[RelayCommand]
private void StartRebind(HotKeyActionDataViewModel hotKey)
{
- if (IsRebinding) return;
- _rebindActionId = hotKey.Action.Id;
- IsRebinding = true;
- DisableBackButton = true;
- RebindingTitle = Localizer.Get(hotKey.Title);
- RebindingPreview = hotKey.Keybind;
- }
-
- [RelayCommand]
- private void UpdateBindingPreview(string? text)
- {
- if (!IsRebinding || string.IsNullOrWhiteSpace(text)) return;
- var keys = text.Split(" + ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
- RebindingPreview = HotKeyService.NormalizeKeyCombo(keys).Replace("\0", " + ");
- }
-
- [RelayCommand]
- private void ClearBindingPreview()
- {
- if (!IsRebinding) return;
- RebindingPreview = string.Empty;
- }
-
- [RelayCommand]
- private void ConfirmRebind()
- {
- if (!IsRebinding || string.IsNullOrWhiteSpace(RebindingPreview) || string.IsNullOrWhiteSpace(_rebindActionId)) return;
- var action = HotKeys.FirstOrDefault(x => x.Action.Id == _rebindActionId)?.Action;
- if (action == null) return;
- _hotKeySettingsData.SetBinding(action, HotKeyService.NormalizeKeyCombo(RebindingPreview.Replace(" + ", "\0")));
- HotKeys = _hotKeySettingsData.HotKeys;
- CancelRebind();
- }
-
- [RelayCommand]
- private void CancelRebind()
- {
- _rebindActionId = null;
- IsRebinding = false;
- DisableBackButton = false;
- RebindingTitle = string.Empty;
- RebindingPreview = string.Empty;
+ _navigationService.PushModal(new HotKeyCaptureNavigationData(hotKey));
}
-}
+}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/InputSettingsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/InputSettingsViewModel.cs
index 84a7adbb..6ded3b90 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/InputSettingsViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/InputSettingsViewModel.cs
@@ -24,10 +24,13 @@ public partial class InputSettingsViewModel(
{
private readonly Lock _lock = new();
- [ObservableProperty] private InputSettingsDataViewModel _inputSettingsData = new(settingsService, audioService);
- [ObservableProperty] private bool _isRecording;
- [ObservableProperty] private float _microphoneValue;
- [ObservableProperty] private bool _detectingVoiceActivity;
+ [ObservableProperty]
+ public partial InputSettingsDataViewModel InputSettingsData { get; set; } = new(settingsService, audioService);
+
+ [ObservableProperty] public partial bool IsRecording { get; set; }
+ [ObservableProperty] public partial float MicrophoneValue { get; set; }
+ [ObservableProperty] public partial bool DetectingVoiceActivity { get; set; }
+
private AudioCaptureDevice? _captureDevice;
private CombinedAudioPreprocessor? _audioPreprocessor;
@@ -69,7 +72,8 @@ private async Task OpenRecorder()
{
try
{
- if (await permissionsService.CheckAndRequestPermission() != PermissionStatus.Granted)
+ if (await permissionsService.CheckAndRequestPermission() !=
+ PermissionStatus.Granted)
throw new PermissionException("Settings.Input.Permissions.MicrophoneNotGranted");
lock (_lock)
@@ -81,7 +85,8 @@ private async Task OpenRecorder()
Constants.SampleRate,
Constants.RecordingChannels,
Constants.FrameSize,
- InputSettingsData.InputDevice);
+ InputSettingsData.InputDevice,
+ InputSettingsData.HardwarePreprocessorsEnabled);
_captureDevice.Start();
_captureDevice.OnAudioProcessed += Write;
IsRecording = true;
@@ -92,7 +97,7 @@ private async Task OpenRecorder()
CloseRecorder();
// ReSharper disable once InconsistentlySynchronizedField
notificationService.SendErrorNotification(
- "Settings.Input.Notification.Badge",
+ "Settings.Input.Notification.Badge",
ex.Message);
}
}
@@ -100,6 +105,7 @@ private async Task OpenRecorder()
private void Write(Span buffer, Capability _)
{
+ if (!IsRecording) return;
_audioPreprocessor?.Process(buffer);
var floatCount = SampleVolume.Read(buffer, InputSettingsData.InputVolume);
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/NetworkSettingsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/NetworkSettingsViewModel.cs
index 6c3a674b..2b377bdd 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/NetworkSettingsViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/NetworkSettingsViewModel.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using VoiceCraft.Client.Services;
@@ -13,8 +14,15 @@ public partial class NetworkSettingsViewModel(
: ViewModelBase, IDisposable
{
//Network Settings
- [ObservableProperty] private NetworkSettingsDataViewModel _networkSettingsData = new(settingsService);
- [ObservableProperty] private PositioningType[] _positioningTypes = Enum.GetValues();
+ [ObservableProperty]
+ public partial NetworkSettingsDataViewModel NetworkSettingsData { get; set; } = new(settingsService);
+
+ [ObservableProperty]
+ public partial ObservableCollection PositioningTypes { get; set; } =
+ [
+ new("Settings.Network.PositioningType.Server", PositioningType.Server),
+ new("Settings.Network.PositioningType.Client", PositioningType.Client)
+ ];
public void Dispose()
{
@@ -28,4 +36,6 @@ private void Cancel()
if (DisableBackButton) return;
navigationService.Back();
}
+
+ public record PositioningTypeValue(string Title, PositioningType Value);
}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/OutputSettingsViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/OutputSettingsViewModel.cs
index 5f273c28..909e80d2 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/OutputSettingsViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/Settings/OutputSettingsViewModel.cs
@@ -20,8 +20,9 @@ public partial class OutputSettingsViewModel : ViewModelBase, IDisposable
private readonly NotificationService _notificationService;
private readonly Lock _lock = new();
- [ObservableProperty] private OutputSettingsDataViewModel _outputSettingsData;
- [ObservableProperty] private bool _isPlaying;
+ [ObservableProperty] public partial OutputSettingsDataViewModel OutputSettingsData { get; set; }
+ [ObservableProperty] public partial bool IsPlaying { get; set; }
+
private AudioPlaybackDevice? _playbackDevice;
private IAudioClipper? _audioClipper;
@@ -34,7 +35,7 @@ public OutputSettingsViewModel(
_navigationService = navigationService;
_audioService = audioService;
_notificationService = notificationService;
- _outputSettingsData = new OutputSettingsDataViewModel(settingsService, _audioService);
+ OutputSettingsData = new OutputSettingsDataViewModel(settingsService, _audioService);
}
public void Dispose()
@@ -99,7 +100,7 @@ private void OpenPlayer()
var callbackComponent = new CallbackProvider(_playbackDevice.Engine, _playbackDevice.Format, Read);
callbackComponent.ConnectInput(new Oscillator(_playbackDevice.Engine, _playbackDevice.Format));
_playbackDevice.MasterMixer.AddComponent(callbackComponent);
-
+
_playbackDevice.Start();
IsPlaying = true;
}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/VoiceViewModel.cs b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/VoiceViewModel.cs
index fb5ab343..dbae3c4b 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/ViewModels/VoiceViewModel.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/ViewModels/VoiceViewModel.cs
@@ -9,6 +9,7 @@
using VoiceCraft.Client.Models;
using VoiceCraft.Client.Services;
using VoiceCraft.Client.ViewModels.Data;
+using VoiceCraft.Client.ViewModels.Modals;
using VoiceCraft.Network;
using VoiceCraft.Network.World;
@@ -22,17 +23,15 @@ public partial class VoiceViewModel(
: ViewModelBase, IDisposable
{
private VoiceCraftService? _service;
- [ObservableProperty] private ObservableCollection _entityViewModels = [];
- [ObservableProperty] private bool _isDeafened;
- [ObservableProperty] private bool _isMuted;
- [ObservableProperty] private bool _isServerDeafened;
- [ObservableProperty] private bool _isServerMuted;
- [ObservableProperty] private bool _isSpeaking;
- [ObservableProperty] private EntityDataViewModel? _selectedEntity;
- [ObservableProperty] private bool _showModal;
- [ObservableProperty] private string _statusDescriptionText = string.Empty;
-
- [ObservableProperty] private string _statusTitleText = string.Empty;
+ [ObservableProperty] public partial ObservableCollection EntityViewModels { get; set; } = [];
+ [ObservableProperty] public partial bool IsDeafened { get; set; }
+ [ObservableProperty] public partial bool IsMuted { get; set; }
+ [ObservableProperty] public partial bool IsServerDeafened { get; set; }
+ [ObservableProperty] public partial bool IsServerMuted { get; set; }
+ [ObservableProperty] public partial bool IsSpeaking { get; set; }
+ [ObservableProperty] public partial EntityDataViewModel? SelectedEntity { get; set; }
+ [ObservableProperty] public partial string StatusDescriptionText { get; set; } = string.Empty;
+ [ObservableProperty] public partial string StatusTitleText { get; set; } = string.Empty;
public override bool DisableBackButton { get; protected set; } = true;
public void Dispose()
@@ -55,13 +54,12 @@ public void Dispose()
partial void OnSelectedEntityChanged(EntityDataViewModel? value)
{
- if (value == null)
+ if (value == null) return;
+ navigationService.PushModal(new EntityDataSettingsNavigationData(value));
+ Task.Run(() =>
{
- ShowModal = false;
- return;
- }
-
- ShowModal = true;
+ SelectedEntity = null; //This bug is annoying.
+ });
}
[RelayCommand]
@@ -71,7 +69,7 @@ private void ToggleMute()
_service.Muted = !_service.Muted;
IsMuted = _service.Muted;
}
-
+
[RelayCommand]
private void ToggleDeafen()
{
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Error/ErrorMainWindow.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/Error/ErrorMainWindow.axaml
index fe6399aa..e5aa2ed1 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/Error/ErrorMainWindow.axaml
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Error/ErrorMainWindow.axaml
@@ -7,7 +7,8 @@
xmlns:views="clr-namespace:VoiceCraft.Client.Views.Error"
MinHeight="200" MinWidth="200"
Height="850" Width="1600"
+ WindowStartupLocation="CenterScreen"
Icon="/Assets/vc.png"
Title="Error">
-
\ No newline at end of file
+
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Home/CrashLogView.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/Home/CrashLogView.axaml
index 4e4b1458..548a7116 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/Home/CrashLogView.axaml
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Home/CrashLogView.axaml
@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="VoiceCraft.Client.Views.Home.CrashLogView"
+ xmlns:locale="clr-namespace:VoiceCraft.Client.Locales"
xmlns:vm="clr-namespace:VoiceCraft.Client.ViewModels.Home"
x:DataType="vm:CrashLogViewModel">
@@ -17,8 +18,11 @@
-
-
+
+
+
+
+
@@ -54,4 +78,4 @@
-
+
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/MainView.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/MainView.axaml
index 0a08bbd1..b5af931a 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/MainView.axaml
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/MainView.axaml
@@ -7,10 +7,38 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="VoiceCraft.Client.Views.MainView"
x:DataType="vm:MainViewModel">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/MainView.axaml.cs b/VoiceCraft.Client/VoiceCraft.Client/Views/MainView.axaml.cs
index 4ec07a0d..625b1ea1 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/MainView.axaml.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/MainView.axaml.cs
@@ -1,4 +1,7 @@
using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using VoiceCraft.Client.ViewModels;
namespace VoiceCraft.Client.Views;
@@ -8,4 +11,22 @@ public MainView()
{
InitializeComponent();
}
+
+ protected override void OnLoaded(RoutedEventArgs e)
+ {
+ base.OnLoaded(e);
+ var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager;
+
+ if (insetsManager == null) return;
+ insetsManager.DisplayEdgeToEdgePreference = true;
+ insetsManager.IsSystemBarVisible = true;
+ }
+
+ private void ModalBackgroundOnPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ var viewModel = (MainViewModel?)DataContext;
+ if (viewModel == null) return;
+ if (sender is not Border border || (border.Child?.IsPointerOver ?? false)) return;
+ viewModel.PopModal();
+ }
}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/MainWindow.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/MainWindow.axaml
index d5bf44e2..84bd2e32 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/MainWindow.axaml
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/MainWindow.axaml
@@ -6,9 +6,10 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
MinHeight="200" MinWidth="200"
Height="850" Width="1600"
+ WindowStartupLocation="CenterScreen"
x:Class="VoiceCraft.Client.Views.MainWindow"
Icon="/Assets/vc.png"
Title="VoiceCraft">
-
\ No newline at end of file
+
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/EntityDataSettingsView.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/EntityDataSettingsView.axaml
new file mode 100644
index 00000000..e46193a1
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/EntityDataSettingsView.axaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/EntityDataSettingsView.axaml.cs b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/EntityDataSettingsView.axaml.cs
new file mode 100644
index 00000000..b5954465
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/EntityDataSettingsView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace VoiceCraft.Client.Views.Modals;
+
+public partial class EntityDataSettingsView : UserControl
+{
+ public EntityDataSettingsView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/HotKeyCaptureView.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/HotKeyCaptureView.axaml
new file mode 100644
index 00000000..f0c2cc27
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/HotKeyCaptureView.axaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/HotKeyCaptureView.axaml.cs b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/HotKeyCaptureView.axaml.cs
new file mode 100644
index 00000000..6217ebea
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/HotKeyCaptureView.axaml.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using VoiceCraft.Client.Services;
+using VoiceCraft.Client.ViewModels.Modals;
+
+namespace VoiceCraft.Client.Views.Modals;
+
+public partial class HotKeyCaptureView : UserControl
+{
+ private readonly HashSet _pressedKeys = [];
+ private readonly HashSet _pressedMouseButtons = [];
+
+ public HotKeyCaptureView()
+ {
+ InitializeComponent();
+ }
+
+ private void HotKeyCapture_OnKeyDown(object? sender, KeyEventArgs e)
+ {
+ if (e.Key is Key.None or Key.System) return;
+
+ _pressedKeys.Add(e.Key);
+ UpdatePreview();
+ e.Handled = true;
+ }
+
+ private void HotKeyCapture_OnKeyUp(object? sender, KeyEventArgs e)
+ {
+ if (e.Key is Key.None or Key.System) return;
+
+ _pressedKeys.Remove(e.Key);
+ e.Handled = true;
+ }
+
+ private void HotKeyCapture_OnPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ var mouseButton = GetMouseButton(e.GetCurrentPoint(this).Properties.PointerUpdateKind);
+
+ if (mouseButton == null)
+ return;
+
+ _pressedMouseButtons.Add(mouseButton);
+ UpdatePreview();
+ e.Handled = true;
+ }
+
+ private void HotKeyCapture_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ var mouseButton = e.InitialPressMouseButton switch
+ {
+ MouseButton.Left => HotKeyService.NormalizeMouseButton("Left"),
+ MouseButton.Right => HotKeyService.NormalizeMouseButton("Right"),
+ MouseButton.Middle => HotKeyService.NormalizeMouseButton("Middle"),
+ MouseButton.XButton1 => HotKeyService.NormalizeMouseButton("XButton1"),
+ MouseButton.XButton2 => HotKeyService.NormalizeMouseButton("XButton2"),
+ _ => null
+ };
+
+ if (mouseButton == null)
+ return;
+
+ _pressedMouseButtons.Remove(mouseButton);
+ e.Handled = true;
+ }
+
+ private void Visual_OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
+ {
+ if(sender is Control control)
+ control.Focus();
+ }
+
+ private void UpdatePreview()
+ {
+ if (DataContext is not HotKeyCaptureViewModel viewModel) return;
+ var keys = _pressedKeys.Select(key => key.ToString()).ToList();
+ keys.AddRange(_pressedMouseButtons);
+ viewModel.UpdateBindingPreviewCommand.Execute(string.Join(" + ", keys));
+ }
+
+ private static string? GetMouseButton(PointerUpdateKind updateKind)
+ {
+ return updateKind switch
+ {
+ PointerUpdateKind.LeftButtonPressed => HotKeyService.NormalizeMouseButton("Left"),
+ PointerUpdateKind.RightButtonPressed => HotKeyService.NormalizeMouseButton("Right"),
+ PointerUpdateKind.MiddleButtonPressed => HotKeyService.NormalizeMouseButton("Middle"),
+ PointerUpdateKind.XButton1Pressed => HotKeyService.NormalizeMouseButton("XButton1"),
+ PointerUpdateKind.XButton2Pressed => HotKeyService.NormalizeMouseButton("XButton2"),
+ _ => null
+ };
+ }
+}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/TelemetryConsentView.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/TelemetryConsentView.axaml
new file mode 100644
index 00000000..c05c3e62
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/TelemetryConsentView.axaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/TelemetryConsentView.axaml.cs b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/TelemetryConsentView.axaml.cs
new file mode 100644
index 00000000..58e9fc7d
--- /dev/null
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Modals/TelemetryConsentView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace VoiceCraft.Client.Views.Modals;
+
+public partial class TelemetryConsentView : UserControl
+{
+ public TelemetryConsentView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/AdvancedSettingsView.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/AdvancedSettingsView.axaml
index a0cf9dd8..7f890238 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/AdvancedSettingsView.axaml
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/AdvancedSettingsView.axaml
@@ -42,8 +42,13 @@
Command="{Binding CrashCommand}">
+
+
-
\ No newline at end of file
+
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/GeneralSettingsView.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/GeneralSettingsView.axaml
index 70f8a6db..7f6dceda 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/GeneralSettingsView.axaml
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/GeneralSettingsView.axaml
@@ -63,8 +63,13 @@
IsChecked="{Binding NotificationSettingsData.DisableNotifications}">
+
+
+
+
-
\ No newline at end of file
+
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/HotKeySettingsView.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/HotKeySettingsView.axaml
index 12d202d3..7d4a0fbd 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/HotKeySettingsView.axaml
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/HotKeySettingsView.axaml
@@ -1,20 +1,10 @@
+ x:Class="VoiceCraft.Client.Views.Settings.HotKeySettingsView">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/HotKeySettingsView.axaml.cs b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/HotKeySettingsView.axaml.cs
index 15753c37..752e6b4f 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/HotKeySettingsView.axaml.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/HotKeySettingsView.axaml.cs
@@ -1,125 +1,11 @@
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
using Avalonia.Controls;
-using Avalonia.Input;
-using VoiceCraft.Client.Services;
-using VoiceCraft.Client.ViewModels.Settings;
namespace VoiceCraft.Client.Views.Settings;
public partial class HotKeySettingsView : UserControl
{
- private readonly HashSet _pressedKeys = [];
- private readonly HashSet _pressedMouseButtons = [];
- private HotKeySettingsViewModel? _observedViewModel;
-
public HotKeySettingsView()
{
InitializeComponent();
- DataContextChanged += OnDataContextChanged;
- }
-
- private void HotKeyCapture_OnKeyDown(object? sender, KeyEventArgs e)
- {
- if (DataContext is not HotKeySettingsViewModel viewModel) return;
- if (!viewModel.IsRebinding || e.Key is Key.None or Key.System) return;
-
- _pressedKeys.Add(e.Key);
- UpdatePreview(viewModel);
- e.Handled = true;
- }
-
- private void HotKeyCapture_OnKeyUp(object? sender, KeyEventArgs e)
- {
- if (DataContext is not HotKeySettingsViewModel viewModel) return;
- if (!viewModel.IsRebinding ||e.Key is Key.None or Key.System) return;
-
- _pressedKeys.Remove(e.Key);
- e.Handled = true;
- }
-
- private void HotKeyCapture_OnPointerPressed(object? sender, PointerPressedEventArgs e)
- {
- if (DataContext is not HotKeySettingsViewModel viewModel) return;
- if (!viewModel.IsRebinding) return;
-
- var mouseButton = GetMouseButton(e.GetCurrentPoint(this).Properties.PointerUpdateKind);
-
- if (mouseButton == null)
- return;
-
- _pressedMouseButtons.Add(mouseButton);
- UpdatePreview(viewModel);
- e.Handled = true;
- }
-
- private void HotKeyCapture_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
- {
- if (DataContext is not HotKeySettingsViewModel viewModel) return;
- if (!viewModel.IsRebinding) return;
-
- var mouseButton = e.InitialPressMouseButton switch
- {
- MouseButton.Left => HotKeyService.NormalizeMouseButton("Left"),
- MouseButton.Right => HotKeyService.NormalizeMouseButton("Right"),
- MouseButton.Middle => HotKeyService.NormalizeMouseButton("Middle"),
- MouseButton.XButton1 => HotKeyService.NormalizeMouseButton("XButton1"),
- MouseButton.XButton2 => HotKeyService.NormalizeMouseButton("XButton2"),
- _ => null
- };
-
- if (mouseButton == null)
- return;
-
- _pressedMouseButtons.Remove(mouseButton);
- e.Handled = true;
- }
-
- private void OnDataContextChanged(object? sender, System.EventArgs e)
- {
- if (_observedViewModel != null)
- _observedViewModel.PropertyChanged -= OnViewModelPropertyChanged;
-
- _observedViewModel = DataContext as HotKeySettingsViewModel;
-
- if (_observedViewModel != null)
- _observedViewModel.PropertyChanged += OnViewModelPropertyChanged;
- }
-
- private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
- {
- if (sender is not HotKeySettingsViewModel viewModel || e.PropertyName != nameof(HotKeySettingsViewModel.IsRebinding))
- return;
-
- ResetCapture(clearPreview: !viewModel.IsRebinding);
- }
-
- private void UpdatePreview(HotKeySettingsViewModel viewModel)
- {
- var keys = _pressedKeys.Select(key => key.ToString()).ToList();
- keys.AddRange(_pressedMouseButtons);
- viewModel.UpdateBindingPreviewCommand.Execute(string.Join(" + ", keys));
- }
-
- private static string? GetMouseButton(PointerUpdateKind updateKind)
- {
- return updateKind switch
- {
- PointerUpdateKind.LeftButtonPressed => HotKeyService.NormalizeMouseButton("Left"),
- PointerUpdateKind.RightButtonPressed => HotKeyService.NormalizeMouseButton("Right"),
- PointerUpdateKind.MiddleButtonPressed => HotKeyService.NormalizeMouseButton("Middle"),
- PointerUpdateKind.XButton1Pressed => HotKeyService.NormalizeMouseButton("XButton1"),
- PointerUpdateKind.XButton2Pressed => HotKeyService.NormalizeMouseButton("XButton2"),
- _ => null
- };
- }
-
- private void ResetCapture(bool clearPreview)
- {
- _pressedKeys.Clear();
- _pressedMouseButtons.Clear();
- if (clearPreview && DataContext is HotKeySettingsViewModel viewModel)
- viewModel.ClearBindingPreviewCommand.Execute(null);
}
}
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/InputSettingsView.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/InputSettingsView.axaml
index d7092677..eb085cd6 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/InputSettingsView.axaml
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/InputSettingsView.axaml
@@ -103,14 +103,19 @@
SelectedValue="{Binding InputSettingsData.EchoCanceler}"
SelectedValueBinding="{Binding Id}" />
+
+
+
+
-
+
-
+
@@ -150,4 +155,4 @@
-
\ No newline at end of file
+
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/NetworkSettingsView.axaml b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/NetworkSettingsView.axaml
index 452f3264..397d0306 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/NetworkSettingsView.axaml
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/Settings/NetworkSettingsView.axaml
@@ -36,11 +36,13 @@
+ Text="{locale:Localize Settings.Network.PositioningType.Title}" />
+ DisplayMemberBinding="{locale:Localize {Binding Title}}"
+ SelectedValue="{Binding NetworkSettingsData.PositioningType}"
+ SelectedValueBinding="{Binding Value}" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/Views/VoiceView.axaml.cs b/VoiceCraft.Client/VoiceCraft.Client/Views/VoiceView.axaml.cs
index 694dfc98..91a81897 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/Views/VoiceView.axaml.cs
+++ b/VoiceCraft.Client/VoiceCraft.Client/Views/VoiceView.axaml.cs
@@ -1,6 +1,4 @@
using Avalonia.Controls;
-using Avalonia.Input;
-using VoiceCraft.Client.ViewModels;
namespace VoiceCraft.Client.Views;
@@ -10,12 +8,4 @@ public VoiceView()
{
InitializeComponent();
}
-
- private void ModalBackgroundOnPointerPressed(object? sender, PointerPressedEventArgs e)
- {
- var viewModel = (VoiceViewModel?)DataContext;
- if (viewModel == null) return;
- if (sender is not Border border || (border.Child?.IsPointerOver ?? false)) return;
- viewModel.SelectedEntity = null;
- }
}
\ No newline at end of file
diff --git a/VoiceCraft.Client/VoiceCraft.Client/VoiceCraft.Client.csproj b/VoiceCraft.Client/VoiceCraft.Client/VoiceCraft.Client.csproj
index 3bd7df9c..e8d434d8 100644
--- a/VoiceCraft.Client/VoiceCraft.Client/VoiceCraft.Client.csproj
+++ b/VoiceCraft.Client/VoiceCraft.Client/VoiceCraft.Client.csproj
@@ -1,6 +1,6 @@
-
+
- net9.0
+ net10.0
enable
latest
true
@@ -19,15 +19,10 @@
-
-
- None
- All
-
-
+
diff --git a/VoiceCraft.Core.Tests/VoiceCraft.Core.Tests.csproj b/VoiceCraft.Core.Tests/VoiceCraft.Core.Tests.csproj
index dc23f17b..707bdd1a 100644
--- a/VoiceCraft.Core.Tests/VoiceCraft.Core.Tests.csproj
+++ b/VoiceCraft.Core.Tests/VoiceCraft.Core.Tests.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net10.0
enable
enable
false
diff --git a/VoiceCraft.Core.Tests/World/VoiceCraftEntityTests.cs b/VoiceCraft.Core.Tests/World/VoiceCraftEntityTests.cs
index 02655473..57ae0c73 100644
--- a/VoiceCraft.Core.Tests/World/VoiceCraftEntityTests.cs
+++ b/VoiceCraft.Core.Tests/World/VoiceCraftEntityTests.cs
@@ -10,7 +10,7 @@ public class VoiceCraftEntityTests
public void Name_TooLong_Throws()
{
var entity = new VoiceCraftEntity(1);
- var tooLong = new string('a', VoiceCraft.Core.Constants.MaxStringLength + 1);
+ var tooLong = new string('a', Constants.MaxStringLength + 1);
Assert.Throws(() => entity.Name = tooLong);
}
@@ -19,7 +19,7 @@ public void Name_TooLong_Throws()
public void WorldId_TooLong_Throws()
{
var entity = new VoiceCraftEntity(1);
- var tooLong = new string('a', VoiceCraft.Core.Constants.MaxStringLength + 1);
+ var tooLong = new string('a', Constants.MaxStringLength + 1);
Assert.Throws(() => entity.WorldId = tooLong);
}
@@ -36,6 +36,22 @@ public void CaveFactor_And_MuffleFactor_AreClamped()
Assert.Equal(0.0f, entity.MuffleFactor, 3);
}
+ [Fact]
+ public void NonFiniteSpatialValues_AreSanitized()
+ {
+ var entity = new VoiceCraftEntity(1);
+
+ entity.Position = new Vector3(float.NaN, float.PositiveInfinity, 3);
+ entity.Rotation = new Vector2(float.NegativeInfinity, 5);
+ entity.CaveFactor = float.NaN;
+ entity.MuffleFactor = float.PositiveInfinity;
+
+ Assert.Equal(new Vector3(0, 0, 3), entity.Position);
+ Assert.Equal(new Vector2(0, 5), entity.Rotation);
+ Assert.Equal(0.0f, entity.CaveFactor, 3);
+ Assert.Equal(0.0f, entity.MuffleFactor, 3);
+ }
+
[Fact]
public void AddVisibleEntity_IgnoresSelfAndDuplicates()
{
diff --git a/VoiceCraft.Core/Audio/SampleLerpVolume.cs b/VoiceCraft.Core/Audio/SampleLerpVolume.cs
index 08add839..a870e888 100644
--- a/VoiceCraft.Core/Audio/SampleLerpVolume.cs
+++ b/VoiceCraft.Core/Audio/SampleLerpVolume.cs
@@ -6,23 +6,23 @@ public class SampleLerpVolume
{
public float TargetVolume
{
- get => _targetVolume;
+ get;
set
{
- if (Math.Abs(_targetVolume - value) < Constants.FloatingPointTolerance)
+ if (Math.Abs(field - value) < Constants.FloatingPointTolerance)
return; //Return since it's the same target volume, and we don't want to reset the counter.
- _previousVolume = _targetVolume;
- _targetVolume = value;
+ _previousVolume = field;
+ field = value;
_fadeSamplesPosition = 0; //Reset position since we have a new target.
}
- }
+ } = 1;
public TimeSpan FadeDuration
{
- get => _fadeDuration;
+ get;
set
{
- _fadeDuration = value;
+ field = value;
var newSamples = (int)(value.TotalMilliseconds * _sampleRate / 1000);
if (newSamples < _fadeSamplesPosition) //Make sure we don't overshoot the target when lerping.
{
@@ -33,9 +33,7 @@ public TimeSpan FadeDuration
}
}
- private int _sampleRate;
- private TimeSpan _fadeDuration;
- private float _targetVolume = 1;
+ private readonly int _sampleRate;
private float _previousVolume;
private float _fadeSamplesDuration;
private float _fadeSamplesPosition;
diff --git a/VoiceCraft.Core/Constants.cs b/VoiceCraft.Core/Constants.cs
index 99e58b86..876fcba1 100644
--- a/VoiceCraft.Core/Constants.cs
+++ b/VoiceCraft.Core/Constants.cs
@@ -5,8 +5,8 @@ namespace VoiceCraft.Core
public static class Constants
{
public const int Major = 1; //These need to be the same on both client and server!
- public const int Minor = 5; //These need to be the same on both client and server!
- public const int Patch = 1; //This does not need to be the same on client and server.
+ public const int Minor = 6; //These need to be the same on both client and server!
+ public const int Patch = 0; //This does not need to be the same on client and server.
//Tick
public const int TickRate = 50;
@@ -40,6 +40,7 @@ public static class Constants
public const string ApplicationDirectory = "voicecraft";
public const string SettingsFile = "Settings.json";
public const string ExceptionLogsFile = "ExceptionLogs.json";
+ public const string TelemetryBaseUrl = "https://vc-api.avion.team";
//RPC
public const string ApplicationId = "1364434932968984669";
diff --git a/VoiceCraft.Core/Diagnostics/CrashLogRecord.cs b/VoiceCraft.Core/Diagnostics/CrashLogRecord.cs
new file mode 100644
index 00000000..1375ce9a
--- /dev/null
+++ b/VoiceCraft.Core/Diagnostics/CrashLogRecord.cs
@@ -0,0 +1,43 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace VoiceCraft.Core.Diagnostics;
+
+public class CrashLogRecord : INotifyPropertyChanged
+{
+ public string Message
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = string.Empty;
+
+ public string? DumpUrl
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasDumpUrl));
+ }
+ }
+
+ public bool HasDumpUrl => !string.IsNullOrWhiteSpace(DumpUrl);
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
diff --git a/VoiceCraft.Core/Locales/Localizer.cs b/VoiceCraft.Core/Locales/Localizer.cs
index 98168aef..90ba5841 100644
--- a/VoiceCraft.Core/Locales/Localizer.cs
+++ b/VoiceCraft.Core/Locales/Localizer.cs
@@ -3,40 +3,62 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
+using System.Threading;
using VoiceCraft.Core.Interfaces;
namespace VoiceCraft.Core.Locales
{
public sealed class Localizer : INotifyPropertyChanged, INotifyPropertyChanging
{
+ private static readonly Lock Lock = new();
private static IBaseLocalizer _baseLocalizer = new EmptyBaseLocalizer();
//Private set language
- private string _language = "";
public static Localizer Instance { get; } = new();
public static IBaseLocalizer BaseLocalizer
{
- get => _baseLocalizer;
+ get
+ {
+ lock (Lock)
+ {
+ return _baseLocalizer;
+ }
+ }
set
{
- if (value == _baseLocalizer) return;
- _baseLocalizer = value;
- Instance.Language = Instance.Language;
+ lock (Lock)
+ {
+ if (value == _baseLocalizer) return;
+ _baseLocalizer = value;
+ Instance.Language = Instance.Language;
+ }
}
}
public string Language
{
- get => _language;
+ get;
set
{
- value = _baseLocalizer.Reload(value);
- Instance.SetField(ref _language, value);
+ lock (Lock)
+ {
+ value = _baseLocalizer.Reload(value);
+ Instance.SetField(ref field, value);
+ }
}
- }
+ } = "";
- public static ObservableCollection Languages => _baseLocalizer.Languages;
+ public static ObservableCollection Languages
+ {
+ get
+ {
+ lock (Lock)
+ {
+ return _baseLocalizer.Languages;
+ }
+ }
+ }
public event PropertyChangedEventHandler? PropertyChanged;
//Property Changed Events
@@ -45,7 +67,10 @@ public string Language
public static string Get(string key)
{
- return _baseLocalizer.Get(key);
+ lock (Lock)
+ {
+ return _baseLocalizer.Get(key);
+ }
}
private void SetField(ref T field, T value, [CallerMemberName] string? propertyName = null)
@@ -88,4 +113,4 @@ public string Reload(string language)
throw new NotSupportedException();
}
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Core/Telemetry/TelemetryContracts.cs b/VoiceCraft.Core/Telemetry/TelemetryContracts.cs
new file mode 100644
index 00000000..415401bd
--- /dev/null
+++ b/VoiceCraft.Core/Telemetry/TelemetryContracts.cs
@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+
+namespace VoiceCraft.Core.Telemetry;
+
+public class TelemetryAppInfo
+{
+ public string AppName { get; set; } = string.Empty;
+ public string Version { get; set; } = string.Empty;
+ public string? Channel { get; set; }
+ public string? Build { get; set; }
+ public Dictionary? Config { get; set; }
+}
+
+public class TelemetryDeviceInfo
+{
+ public string? OsVersion { get; set; }
+ public string? OsName { get; set; }
+ public string? OsBuild { get; set; }
+ public string? OsDescription { get; set; }
+ public string? Vendor { get; set; }
+ public string? Model { get; set; }
+ public string? Architecture { get; set; }
+ public string? ProcessArchitecture { get; set; }
+ public string? Runtime { get; set; }
+ public string? Locale { get; set; }
+ public int? CpuCores { get; set; }
+ public long? MemoryMb { get; set; }
+}
+
+public class TelemetryServerInfo
+{
+ public int? CpuCores { get; set; }
+ public long? MemoryMb { get; set; }
+ public long? UptimeSec { get; set; }
+ public string? Platform { get; set; }
+ public string? Architecture { get; set; }
+ public string? Locale { get; set; }
+ public int? ConnectedClients { get; set; }
+}
+
+public class TelemetryEventRequest
+{
+ public string? Fingerprint { get; set; }
+ public string Role { get; set; } = string.Empty;
+ public TelemetryAppInfo App { get; set; } = new();
+ public TelemetryDeviceInfo? Device { get; set; }
+ public TelemetryServerInfo? Server { get; set; }
+ public Dictionary? Metrics { get; set; }
+ public string[]? Tags { get; set; }
+ public string? Timestamp { get; set; }
+}
+
+public class TelemetryDumpRequest
+{
+ public string? Fingerprint { get; set; }
+ public string Role { get; set; } = string.Empty;
+ public string Category { get; set; } = string.Empty;
+ public string? Title { get; set; }
+ public TelemetryAppInfo? App { get; set; }
+ public TelemetryDeviceInfo? Device { get; set; }
+ public TelemetryServerInfo? Server { get; set; }
+ public Dictionary Payload { get; set; } = new();
+}
+
+public class TelemetryDumpResponse
+{
+ public string Id { get; set; } = string.Empty;
+ public string Url { get; set; } = string.Empty;
+ public string? ViewUrl { get; set; }
+ public string? JsonUrl { get; set; }
+ public string CreatedAt { get; set; } = string.Empty;
+}
diff --git a/VoiceCraft.Core/Telemetry/TelemetryTransport.cs b/VoiceCraft.Core/Telemetry/TelemetryTransport.cs
new file mode 100644
index 00000000..c03fd045
--- /dev/null
+++ b/VoiceCraft.Core/Telemetry/TelemetryTransport.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace VoiceCraft.Core.Telemetry;
+
+public class TelemetryTransport
+{
+ private readonly HttpClient _httpClient = new()
+ {
+ Timeout = TimeSpan.FromSeconds(15)
+ };
+
+ public async Task SendTelemetryAsync(TelemetryEventRequest payload,
+ CancellationToken cancellationToken = default)
+ {
+ var uri = BuildUri("/v1/telemetry");
+ Exception exception;
+ try
+ {
+ using var content = JsonContent.Create(payload, TelemetryJsonContext.Default.TelemetryEventRequest);
+ using var response = await _httpClient.PostAsync(uri, content, cancellationToken);
+ if (response.IsSuccessStatusCode) return;
+ var body = await response.Content.ReadAsStringAsync(cancellationToken);
+ exception = new Exception(
+ $"Telemetry POST {uri} failed with {(int)response.StatusCode} {response.ReasonPhrase}. Body: {body}");
+ }
+ catch (Exception ex)
+ {
+ exception = new Exception($"Telemetry POST {uri} failed with exception {ex.GetType().Name}: {ex.Message}");
+ }
+
+ throw exception;
+ }
+
+ public async Task SendDumpAsync(TelemetryDumpRequest payload,
+ CancellationToken cancellationToken = default)
+ {
+ var uri = BuildUri("/v1/dumps");
+ Exception exception;
+ try
+ {
+ using var content = JsonContent.Create(payload, TelemetryJsonContext.Default.TelemetryDumpRequest);
+ using var response = await _httpClient.PostAsync(uri, content, cancellationToken);
+ if (response.IsSuccessStatusCode)
+ return await response.Content.ReadFromJsonAsync(
+ TelemetryJsonContext.Default.TelemetryDumpResponse,
+ cancellationToken);
+ var body = await response.Content.ReadAsStringAsync(cancellationToken);
+ exception = new Exception(
+ $"Telemetry dump POST {uri} failed with {(int)response.StatusCode} {response.ReasonPhrase}. Body: {body}");
+ }
+ catch (Exception ex)
+ {
+ exception = new Exception(
+ $"Telemetry dump POST {uri} failed with exception {ex.GetType().Name}: {ex.Message}");
+ }
+
+ throw exception;
+ }
+
+ private static Uri BuildUri(string relativePath)
+ {
+ return new Uri(new Uri(Constants.TelemetryBaseUrl, UriKind.Absolute), relativePath);
+ }
+}
+
+[JsonSourceGenerationOptions(
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true)]
+[JsonSerializable(typeof(TelemetryEventRequest))]
+[JsonSerializable(typeof(TelemetryDumpRequest))]
+[JsonSerializable(typeof(TelemetryDumpResponse))]
+public partial class TelemetryJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/VoiceCraft.Core/VoiceCraft.Core.csproj b/VoiceCraft.Core/VoiceCraft.Core.csproj
index 8eabd440..93a84f5a 100644
--- a/VoiceCraft.Core/VoiceCraft.Core.csproj
+++ b/VoiceCraft.Core/VoiceCraft.Core.csproj
@@ -1,6 +1,6 @@
- net9.0
+ net10.0
enable
true
diff --git a/VoiceCraft.Core/World/VoiceCraftEntity.cs b/VoiceCraft.Core/World/VoiceCraftEntity.cs
index d042b517..9859e7c9 100644
--- a/VoiceCraft.Core/World/VoiceCraftEntity.cs
+++ b/VoiceCraft.Core/World/VoiceCraftEntity.cs
@@ -9,18 +9,7 @@ namespace VoiceCraft.Core.World
public class VoiceCraftEntity(int id)
{
private readonly ConcurrentDictionary _visibleEntities = new();
- private float _caveFactor;
- private bool _deafened;
- private ushort _effectBitmask = ushort.MaxValue;
- private ushort _listenBitmask = ushort.MaxValue;
private float _loudness;
- private float _muffleFactor;
- private bool _muted;
- private string _name = "New Entity";
- private Vector3 _position;
- private Vector2 _rotation;
- private ushort _talkBitmask = ushort.MaxValue;
- private string _worldId = string.Empty;
//Properties
public int Id { get; } = id;
@@ -116,127 +105,156 @@ public virtual void Destroy()
public string WorldId
{
- get => _worldId;
+ get;
set
{
- if (_worldId == value) return;
+ if (field == value) return;
if (value.Length > Constants.MaxStringLength) throw new ArgumentOutOfRangeException();
- _worldId = value;
- OnWorldIdUpdated?.Invoke(_worldId, this);
+ field = value;
+ OnWorldIdUpdated?.Invoke(field, this);
}
- }
+ } = string.Empty;
public string Name
{
- get => _name;
+ get;
set
{
- if (_name == value) return;
+ if (field == value) return;
if (value.Length > Constants.MaxStringLength) throw new ArgumentOutOfRangeException();
- _name = value;
- OnNameUpdated?.Invoke(_name, this);
+ field = value;
+ OnNameUpdated?.Invoke(field, this);
}
- }
+ } = "New Entity";
public bool Muted
{
- get => _muted;
+ get;
set
{
- if (_muted == value) return;
- _muted = value;
- OnMuteUpdated?.Invoke(_muted, this);
+ if (field == value) return;
+ field = value;
+ OnMuteUpdated?.Invoke(field, this);
}
}
public bool Deafened
{
- get => _deafened;
+ get;
set
{
- if (_deafened == value) return;
- _deafened = value;
- OnDeafenUpdated?.Invoke(_deafened, this);
+ if (field == value) return;
+ field = value;
+ OnDeafenUpdated?.Invoke(field, this);
}
}
public ushort TalkBitmask
{
- get => _talkBitmask;
+ get;
set
{
- if (_talkBitmask == value) return;
- _talkBitmask = value;
- OnTalkBitmaskUpdated?.Invoke(_talkBitmask, this);
+ if (field == value) return;
+ field = value;
+ OnTalkBitmaskUpdated?.Invoke(field, this);
}
- }
+ } = ushort.MaxValue;
public ushort ListenBitmask
{
- get => _listenBitmask;
+ get;
set
{
- if (_listenBitmask == value) return;
- _listenBitmask = value;
- OnListenBitmaskUpdated?.Invoke(_listenBitmask, this);
+ if (field == value) return;
+ field = value;
+ OnListenBitmaskUpdated?.Invoke(field, this);
}
- }
+ } = ushort.MaxValue;
public ushort EffectBitmask
{
- get => _effectBitmask;
+ get;
set
{
- if (_effectBitmask == value) return;
- _effectBitmask = value;
- OnEffectBitmaskUpdated?.Invoke(_effectBitmask, this);
+ if (field == value) return;
+ field = value;
+ OnEffectBitmaskUpdated?.Invoke(field, this);
}
- }
+ } = ushort.MaxValue;
public Vector3 Position
{
- get => _position;
+ get;
set
{
- if (_position == value) return;
- _position = value;
- OnPositionUpdated?.Invoke(_position, this);
+ value = Sanitize(value);
+ if (field == value) return;
+ field = value;
+ OnPositionUpdated?.Invoke(field, this);
}
}
public Vector2 Rotation
{
- get => _rotation;
+ get;
set
{
- if (_rotation == value) return;
- _rotation = value;
- OnRotationUpdated?.Invoke(_rotation, this);
+ value = Sanitize(value);
+ if (field == value) return;
+ field = value;
+ OnRotationUpdated?.Invoke(field, this);
}
}
public float CaveFactor
{
- get => _caveFactor;
+ get;
set
{
- if (Math.Abs(_caveFactor - value) < Constants.FloatingPointTolerance) return;
- _caveFactor = Math.Clamp(value, 0f, 1f);
- OnCaveFactorUpdated?.Invoke(_caveFactor, this);
+ value = ClampFinite(value, 0f, 1f);
+ if (Math.Abs(field - value) < Constants.FloatingPointTolerance) return;
+ field = value;
+ OnCaveFactorUpdated?.Invoke(field, this);
}
}
public float MuffleFactor
{
- get => _muffleFactor;
+ get;
set
{
- if (Math.Abs(_muffleFactor - value) < Constants.FloatingPointTolerance) return;
- _muffleFactor = Math.Clamp(value, 0f, 1f);
- OnMuffleFactorUpdated?.Invoke(_muffleFactor, this);
+ value = ClampFinite(value, 0f, 1f);
+ if (Math.Abs(field - value) < Constants.FloatingPointTolerance) return;
+ field = value;
+ OnMuffleFactorUpdated?.Invoke(field, this);
}
}
#endregion
+
+ private static Vector3 Sanitize(Vector3 value)
+ {
+ return new Vector3(
+ Sanitize(value.X),
+ Sanitize(value.Y),
+ Sanitize(value.Z));
+ }
+
+ private static Vector2 Sanitize(Vector2 value)
+ {
+ return new Vector2(
+ Sanitize(value.X),
+ Sanitize(value.Y));
+ }
+
+ private static float Sanitize(float value)
+ {
+ return float.IsFinite(value) ? value : 0f;
+ }
+
+ private static float ClampFinite(float value, float min, float max)
+ {
+ return float.IsFinite(value) ? Math.Clamp(value, min, max) : min;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/VoiceCraft.Core/World/VoiceCraftWorld.cs b/VoiceCraft.Core/World/VoiceCraftWorld.cs
index 5b3e6d8f..dddccd9c 100644
--- a/VoiceCraft.Core/World/VoiceCraftWorld.cs
+++ b/VoiceCraft.Core/World/VoiceCraftWorld.cs
@@ -44,6 +44,11 @@ public void AddEntity(VoiceCraftEntity entity)
return entity;
}
+ public bool ContainsEntity(int id)
+ {
+ return _entities.ContainsKey(id);
+ }
+
public int GetNextId()
{
while (_entities.ContainsKey(_nextEntityId))
diff --git a/VoiceCraft.Network.Tests/Audio/JitterBufferTests.cs b/VoiceCraft.Network.Tests/Audio/JitterBufferTests.cs
new file mode 100644
index 00000000..f60e5ac3
--- /dev/null
+++ b/VoiceCraft.Network.Tests/Audio/JitterBufferTests.cs
@@ -0,0 +1,19 @@
+using Xunit;
+using VoiceCraft.Network.Audio;
+
+namespace VoiceCraft.Network.Tests.Audio;
+
+public class JitterBufferTests
+{
+ [Fact]
+ public void Get_WhenSequenceWraps_DoesNotThrow()
+ {
+ var buffer = new JitterBuffer(TimeSpan.Zero);
+ var packet = new JitterPacket(ushort.MaxValue, [1, 2, 3]);
+
+ buffer.Add(packet);
+
+ Assert.True(buffer.Get(out var readPacket));
+ Assert.Same(packet, readPacket);
+ }
+}
diff --git a/VoiceCraft.Network.Tests/Clients/VoiceCraftClientTests.cs b/VoiceCraft.Network.Tests/Clients/VoiceCraftClientTests.cs
new file mode 100644
index 00000000..00bfff2b
--- /dev/null
+++ b/VoiceCraft.Network.Tests/Clients/VoiceCraftClientTests.cs
@@ -0,0 +1,105 @@
+using VoiceCraft.Core.Interfaces;
+using VoiceCraft.Network.Clients;
+using VoiceCraft.Network.Packets.VcPackets;
+using VoiceCraft.Network.Packets.VcPackets.Event;
+using Xunit;
+
+namespace VoiceCraft.Network.Tests.Clients;
+
+public class VoiceCraftClientTests
+{
+ [Fact]
+ public void EntityCreated_WhenEntityAlreadyExists_IsIgnored()
+ {
+ using var client = new TestVoiceCraftClient();
+ var packet = new VcOnEntityCreatedPacket();
+ packet.Set(42, "First");
+
+ client.Dispatch(packet);
+ packet.Set(42, "Duplicate", true, true);
+ client.Dispatch(packet);
+
+ var entity = Assert.Single(client.World.Entities);
+ Assert.Equal("First", entity.Name);
+ Assert.False(entity.Muted);
+ Assert.False(entity.Deafened);
+ }
+
+ [Fact]
+ public void EntityDestroyed_WhenEntityDoesNotExist_IsIgnored()
+ {
+ using var client = new TestVoiceCraftClient();
+
+ client.Dispatch(new VcOnEntityDestroyedPacket().Set(404));
+
+ Assert.Empty(client.World.Entities);
+ }
+
+ private sealed class TestVoiceCraftClient() : VoiceCraftClient(new FakeAudioEncoder(), () => new FakeAudioDecoder())
+ {
+ public override PositioningType PositioningType => PositioningType.Client;
+ public override event Action? OnConnected;
+ public override event Action? OnDisconnected;
+
+ public void Dispatch(IVoiceCraftPacket packet)
+ {
+ ExecutePacket(packet);
+ }
+
+ public override Task PingAsync(string ip, int port, CancellationToken token = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override Task ConnectAsync(string ip, int port, Guid userGuid, Guid serverUserGuid, string locale,
+ PositioningType positioningType)
+ {
+ OnConnected?.Invoke();
+ return Task.CompletedTask;
+ }
+
+ public override void Update()
+ {
+ }
+
+ public override Task DisconnectAsync(string? reason = null)
+ {
+ OnDisconnected?.Invoke(reason);
+ return Task.CompletedTask;
+ }
+
+ public override void SendUnconnectedPacket(string ip, int port, T packet)
+ {
+ PacketPool.Return(packet);
+ }
+
+ public override void SendPacket(T packet, VcDeliveryMethod deliveryMethod = VcDeliveryMethod.Reliable)
+ {
+ PacketPool.Return(packet);
+ }
+ }
+
+ private sealed class FakeAudioEncoder : IAudioEncoder
+ {
+ public int Encode(Span