diff --git a/Directory.Packages.props b/Directory.Packages.props
index b279b97..386b744 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -108,6 +108,11 @@
-->
+
+
diff --git a/DodoSSH.slnx b/DodoSSH.slnx
index 5d27ab4..bddd47c 100644
--- a/DodoSSH.slnx
+++ b/DodoSSH.slnx
@@ -21,6 +21,7 @@
+
diff --git a/src/DodoSSH.Client.Android/App.axaml b/src/DodoSSH.Client.Android/App.axaml
new file mode 100644
index 0000000..a0d22f3
--- /dev/null
+++ b/src/DodoSSH.Client.Android/App.axaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DodoSSH.Client.Android/App.axaml.cs b/src/DodoSSH.Client.Android/App.axaml.cs
new file mode 100644
index 0000000..16bca11
--- /dev/null
+++ b/src/DodoSSH.Client.Android/App.axaml.cs
@@ -0,0 +1,134 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+using DodoSSH.Client.Android.Platform;
+using DodoSSH.Client.Android.Views;
+using DodoSSH.Client.Session;
+using DodoSSH.Client.Shell.Terminal;
+using DodoSSH.Client.Shell.ViewModels;
+using DodoSSH.Client.Ssh;
+using DodoSSH.Client.Storage;
+using DodoSSH.Client.Terminal;
+
+namespace DodoSSH.Client.Android;
+
+///
+/// The Avalonia application, phone side.
+///
+///
+/// Named DodoSshApp for the same reason the desktop head's is, and then for a second reason on top
+/// of it: a type called App in a namespace ending .Android is what makes every
+/// Android.App in this assembly ambiguous. See the note at the top of MainActivity.
+///
+public sealed partial class DodoSshApp : Avalonia.Application
+{
+ ///
+ public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+ ///
+ public override void OnFrameworkInitializationCompleted()
+ {
+ // ISingleViewApplicationLifetime, not IClassicDesktopStyleApplicationLifetime: a phone has one
+ // surface and no window to own. That difference is the whole reason the two heads cannot share a
+ // composition root, and very nearly the only one — everything either of them composes is the same.
+ if (ApplicationLifetime is ISingleViewApplicationLifetime single)
+ {
+ single.MainView = Compose();
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ ///
+ ///
+ /// Composed by hand rather than through a container, matching the desktop head: the graph is a handful
+ /// of objects deep and an indirection to read through would buy nothing at this size. Read it beside
+ /// DodoSSH.Client.App/App.axaml.cs — the shape is deliberately identical, and the four
+ /// differences are the four things docs/android-port.md said would differ.
+ ///
+ ///
+ /// Nothing here is disposed on a lifecycle hook, and that is not an oversight either. Android
+ /// does not promise to call anything before killing a process, so a teardown path would be a comfort
+ /// rather than a guarantee. What actually protects the sessions is the foreground service; what
+ /// protects the vault keys is that they never leave memory this process owns.
+ ///
+ ///
+ private static PhoneShell Compose()
+ {
+ // Difference 1: the profile directory comes from the head. filesDir is per-app and non-roaming,
+ // which is what ClientPaths asks for and what no desktop platform guarantees.
+ var paths = PhoneEnvironment.Paths;
+ paths.EnsureCreated();
+
+ var caches = ClientCacheFactory.ForFile(paths.CacheFile);
+
+ var knownHosts = new VaultKnownHostStore();
+ var connections = new SshNetConnectionFactory(knownHosts);
+
+ var workspace = new TerminalWorkspace(
+ new AvaloniaTerminalAssetProvider(),
+ connections,
+ TimeProvider.System);
+
+ workspace.Start();
+
+ // Difference 2: the foreground service, which is what makes TerminalWorkspace's promise — that a
+ // shell outlives a vault lock — true on a platform that stops backgrounded processes.
+ // Zero transfers for now: file transfer is out of this head's first scope by decision, and this is
+ // the seam it arrives through rather than a dependency taken before there is anything behind it.
+ // A local rather than a field, matching the desktop head: an Avalonia Application has no disposal
+ // hook, so a field holding a disposable would have nowhere honest to release it. It stays alive
+ // because it is subscribed to the workspace, which lives as long as the process.
+ //
+ // Refresh() is called once here. Calling it again when a shell opens is what the terminal screen
+ // will wire, and there is nothing to wire it to yet — the workspace announces sessions ending on
+ // its own, which is the half that would otherwise leave a notification up over nothing.
+ var keepAlive = new SessionKeepAlive(workspace, activeTransfers: () => 0);
+
+ // Difference 3: the Android keystore, with a fingerprint or the device credential releasing the
+ // key. A straight implementation of the interface the session layer has always taken.
+ var deviceKeys = new AndroidDeviceKeyStore(paths);
+
+ var viewModel = new MainWindowViewModel(
+ paths,
+ caches,
+ workspace,
+ knownHosts,
+ deviceKeys,
+ SignInIsNotBuiltHere,
+ TimeProvider.System,
+ connections,
+ passphraseProfile: null);
+
+ // Started rather than awaited: framework initialisation must not block on a schema migration. The
+ // view model shows its own progress and handles its own failures.
+ _ = viewModel.StartAsync(CancellationToken.None);
+
+ keepAlive.Refresh();
+
+ return new PhoneShell { DataContext = viewModel };
+ }
+
+ ///
+ /// Difference 4, and the one that is a refusal rather than an implementation.
+ ///
+ ///
+ ///
+ /// Signing in needs a redirect this head has not got. The desktop client receives the authorization
+ /// response on a loopback TcpListener (RFC 8252 §7.3), and reusing that here would be a
+ /// security regression rather than a shortcut: on a shared device any other application can bind a
+ /// loopback port and race for the response, which is the attack §8.3 names and the reason app links
+ /// exist. Process.Start does not exist on this platform either.
+ ///
+ ///
+ /// So this throws rather than half-working, and the shell never reaches it: NeedsServer draws a
+ /// screen that says the same thing in the user's words. A phone enrolled from the desktop client
+ /// unlocks here perfectly well, because unlocking needs no network at all.
+ ///
+ ///
+ private static Task SignInIsNotBuiltHere(Uri serverUrl, CancellationToken cancellationToken) =>
+ throw new NotSupportedException(
+ "Signing in is not built on the Android head yet: it needs an app-link redirect rather than "
+ + "the desktop client's loopback listener. See docs/android-port.md §5.");
+}
diff --git a/src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj b/src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj
new file mode 100644
index 0000000..597c308
--- /dev/null
+++ b/src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj
@@ -0,0 +1,54 @@
+
+
+
+ net10.0-android
+ Exe
+ enable
+ true
+
+
+ 28
+ 36
+
+ dev.dodotech.dodossh
+ 1
+ 0.1.0
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DodoSSH.Client.Android/DodoSshAndroidApplication.cs b/src/DodoSSH.Client.Android/DodoSshAndroidApplication.cs
new file mode 100644
index 0000000..456174f
--- /dev/null
+++ b/src/DodoSSH.Client.Android/DodoSshAndroidApplication.cs
@@ -0,0 +1,53 @@
+// See the note at the top of MainActivity for why the platform namespaces are reached through `global::`.
+using global::Android.App;
+using global::Android.Runtime;
+
+using Avalonia;
+using Avalonia.Android;
+
+using DodoSSH.Client.Android.Platform;
+
+namespace DodoSSH.Client.Android;
+
+///
+/// The Android Application object, and where Avalonia is configured.
+///
+///
+///
+/// Avalonia 12 moved the hooks here from the activity, which is a better fit than
+/// it first looks: an activity is destroyed and recreated, and the application object is not. The desktop
+/// head's equivalent is Program.BuildAvaloniaApp.
+///
+///
+/// Any Avalonia 11 sample will show this on AvaloniaMainActivity<App> instead. That type is
+/// still here and is still what the launcher activity derives from — it simply no longer takes the
+/// application as a type argument.
+///
+///
+[Application(Label = "DodoSSH")]
+public sealed class DodoSshAndroidApplication : AvaloniaAndroidApplication
+{
+ ///
+ /// The JNI constructor, and the only one Android calls. It exists to hand the managed object its Java
+ /// peer; there is nothing to do in it and nothing may be done in it, because the application context is
+ /// not usable until .
+ ///
+ public DodoSshAndroidApplication(nint javaReference, JniHandleOwnership transfer)
+ : base(javaReference, transfer)
+ {
+ }
+
+ ///
+ public override void OnCreate()
+ {
+ // Before Avalonia, and from the application rather than the activity: this is the context whose
+ // lifetime matches the composition root's, and reading filesDir is the first thing startup does.
+ PhoneEnvironment.Attach(this);
+
+ base.OnCreate();
+ }
+
+ ///
+ protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) =>
+ base.CustomizeAppBuilder(builder).WithInterFont();
+}
diff --git a/src/DodoSSH.Client.Android/MainActivity.cs b/src/DodoSSH.Client.Android/MainActivity.cs
new file mode 100644
index 0000000..0f36537
--- /dev/null
+++ b/src/DodoSSH.Client.Android/MainActivity.cs
@@ -0,0 +1,50 @@
+// The Android SDK's own namespaces are reached through `global::` throughout this project, and it is not
+// a style choice. This assembly's root namespace ends in `Android`, so inside it a bare `Android.App`
+// binds to `DodoSSH.Client.Android.App` — this head's own Avalonia application type — rather than to the
+// platform. The desktop head hit the same class of collision and answered it by renaming its type; here
+// the collision is in the namespace itself, so the qualification is the honest fix.
+using global::Android.App;
+using global::Android.Content.PM;
+
+using Avalonia.Android;
+
+namespace DodoSSH.Client.Android;
+
+///
+/// The launcher activity.
+///
+///
+///
+/// Deliberately empty. Avalonia is configured on the application object — see
+/// — and the desktop head's Program.Main has no counterpart
+/// at all here: Android constructs the activity, and its [STAThread] is a WebView2 requirement that
+/// means nothing on this platform.
+///
+///
+/// The ConfigurationChanges list is load-bearing. Without it Android destroys and recreates the
+/// activity on every rotation and every time the software keyboard appears — and this head holds live SSH
+/// sessions and a terminal data plane behind a process-wide composition root. Letting the activity restart
+/// would tear the Avalonia application down under them. Declaring the changes handled is what keeps a
+/// shell alive across turning the phone sideways, which is the same promise the foreground service makes
+/// about backgrounding, arrived at from a different direction.
+///
+///
+/// SingleTask for the sign-in redirect: the authorization response comes back as an intent, and any
+/// other launch mode answers it with a second copy of this activity on top of the first — which on this
+/// head would mean a second Avalonia application over a live one.
+///
+///
+[Activity(
+ Label = "DodoSSH",
+ Theme = "@style/DodoTheme",
+ MainLauncher = true,
+ LaunchMode = LaunchMode.SingleTask,
+ ConfigurationChanges = ConfigChanges.Orientation
+ | ConfigChanges.ScreenSize
+ | ConfigChanges.ScreenLayout
+ | ConfigChanges.SmallestScreenSize
+ | ConfigChanges.KeyboardHidden
+ | ConfigChanges.UiMode)]
+public sealed class MainActivity : AvaloniaMainActivity
+{
+}
diff --git a/src/DodoSSH.Client.Android/Platform/AndroidDeviceKeyStore.cs b/src/DodoSSH.Client.Android/Platform/AndroidDeviceKeyStore.cs
new file mode 100644
index 0000000..579f3a0
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Platform/AndroidDeviceKeyStore.cs
@@ -0,0 +1,223 @@
+using global::Android.App;
+using global::Android.Security.Keystore;
+using global::Java.Security;
+using global::Javax.Crypto;
+using global::Javax.Crypto.Spec;
+
+using DodoSSH.Client.Session;
+
+namespace DodoSSH.Client.Android.Platform;
+
+///
+/// This phone's device key, wrapped by a hardware-held key that a fingerprint releases.
+///
+///
+///
+/// The Android counterpart of the desktop head's WindowsDeviceKeyStore, and a closer match to what
+/// the unlock screen wants than that one is: the Windows store leans on DPAPI plus a TPM-held key and a
+/// Hello gesture, whereas here the gesture is a property of the key itself and the platform will not
+/// release it without one. See ADR 0007 and docs/android-port.md §4.
+///
+///
+/// The X25519 scalar is not stored in the keystore, and cannot be: Android's keystore holds keys it
+/// generates and refuses to export them, while what the vault needs back is the raw 32 bytes. So the
+/// keystore holds an AES-GCM key that never leaves the secure hardware, and that key encrypts the scalar
+/// into an ordinary file beside the cache. The file is useless on its own — on this phone as much as on
+/// any other — which is the same shape the Windows store already has.
+///
+///
+/// StrongBox is asked for and not required. Where the phone has a separate security chip the key
+/// lives there; where it does not, generation throws
+/// and the key is made in the TEE instead. Refusing to fall
+/// back would mean a mid-range phone reporting no device key at all, which costs a real user a real
+/// feature to buy a distinction the threat model does not draw.
+///
+///
+/// Every failure here is answered by returning null rather than throwing, exactly as the interface
+/// asks. A user can cancel the prompt, a re-enrolled fingerprint invalidates the key permanently, and a
+/// phone can have no enrolled biometric at all — and the caller's answer to all three is the same one:
+/// ask for the passphrase.
+///
+///
+internal sealed class AndroidDeviceKeyStore(ClientPaths paths) : IDeviceKeyStore
+{
+ private const string KeystoreName = "AndroidKeyStore";
+ private const string KeyAlias = "dodossh.device";
+ private const string Transformation = "AES/GCM/NoPadding";
+
+ ///
+ /// The nonce is stored ahead of the ciphertext rather than derived.
+ ///
+ ///
+ /// The cipher picks it: a GCM key that is asked to encrypt twice under a caller-chosen nonce is one
+ /// misuse away from losing the key, and Android's keystore refuses a caller-supplied IV for exactly
+ /// that reason. Twelve bytes is what it generates.
+ ///
+ private const int NonceBytes = 12;
+
+ ///
+ ///
+ /// Three things have to be true, and the third is the one that is easy to forget: the platform has a
+ /// keystore, the phone has a screen lock, and something is actually enrolled to satisfy it. A phone
+ /// with no lock screen can still generate a key that requires authentication — and then no gesture can
+ /// ever release it.
+ ///
+ public ValueTask IsAvailableAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var keyguard = PhoneEnvironment.Require()
+ .GetSystemService(global::Android.Content.Context.KeyguardService) as KeyguardManager;
+
+ return ValueTask.FromResult(keyguard?.IsDeviceSecure == true);
+ }
+ catch (Exception exception) when (exception is not OutOfMemoryException)
+ {
+ return ValueTask.FromResult(false);
+ }
+ }
+
+ ///
+ public async ValueTask SaveAsync(ReadOnlyMemory devicePrivateKey, CancellationToken cancellationToken)
+ {
+ var key = GenerateWrappingKey();
+
+ var cipher = Cipher.GetInstance(Transformation)
+ ?? throw new InvalidOperationException("This phone has no AES/GCM/NoPadding cipher.");
+
+ cipher.Init(CipherMode.EncryptMode, key);
+
+ // Authenticated before the key is usable, exactly as loading is. Registering a device is itself a
+ // decision worth a gesture — it is the moment this phone gains the ability to open the vault
+ // without the passphrase.
+ await BiometricGate
+ .AuthenticateAsync(cipher, "Register this phone", cancellationToken)
+ .ConfigureAwait(false);
+
+ var sealed_ = cipher.DoFinal(devicePrivateKey.ToArray())
+ ?? throw new InvalidOperationException("The keystore cipher returned nothing.");
+
+ var nonce = cipher.GetIV() ?? throw new InvalidOperationException("The keystore cipher chose no IV.");
+
+ var blob = new byte[nonce.Length + sealed_.Length];
+ nonce.CopyTo(blob, 0);
+ sealed_.CopyTo(blob, nonce.Length);
+
+ paths.EnsureCreated();
+
+ await File.WriteAllBytesAsync(paths.DeviceKeyFile, blob, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async ValueTask TryLoadAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ if (!File.Exists(paths.DeviceKeyFile))
+ {
+ return null;
+ }
+
+ var blob = await File.ReadAllBytesAsync(paths.DeviceKeyFile, cancellationToken).ConfigureAwait(false);
+
+ if (blob.Length <= NonceBytes)
+ {
+ return null;
+ }
+
+ var store = KeyStore.GetInstance(KeystoreName)
+ ?? throw new InvalidOperationException("This phone has no AndroidKeyStore.");
+
+ store.Load(null);
+
+ // Null when the key was invalidated — a re-enrolled fingerprint or a reset screen lock does
+ // this, and it is permanent by design. The wrapped file is unopenable from here on, so the
+ // honest answer is the same as having no key: ask for the passphrase.
+ if (store.GetKey(KeyAlias, null) is not IKey key)
+ {
+ return null;
+ }
+
+ var cipher = Cipher.GetInstance(Transformation)
+ ?? throw new InvalidOperationException("This phone has no AES/GCM/NoPadding cipher.");
+
+ cipher.Init(CipherMode.DecryptMode, key, new GCMParameterSpec(128, blob, 0, NonceBytes));
+
+ await BiometricGate
+ .AuthenticateAsync(cipher, "Unlock DodoSSH", cancellationToken)
+ .ConfigureAwait(false);
+
+ return cipher.DoFinal(blob, NonceBytes, blob.Length - NonceBytes);
+ }
+ catch (Exception exception) when (exception is not OutOfMemoryException)
+ {
+ // Deliberately flat. KeyPermanentlyInvalidatedException, UserNotAuthenticatedException, a
+ // cancelled prompt and a truncated file are four different stories with one ending, and the
+ // interface says so: distinguishing them would describe the keystore rather than tell the user
+ // anything they can act on.
+ return null;
+ }
+ }
+
+ ///
+ public ValueTask ForgetAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var store = KeyStore.GetInstance(KeystoreName);
+ store?.Load(null);
+ store?.DeleteEntry(KeyAlias);
+ }
+ catch (Exception exception) when (exception is not OutOfMemoryException)
+ {
+ // Best effort: the file going is what actually withdraws this phone's ability to unlock, and
+ // an orphaned keystore entry opens nothing.
+ }
+
+ // After the keystore entry, not before. A file left behind with its key already deleted is merely
+ // unopenable; a key left behind with its file already gone is the same. Order costs nothing here,
+ // but the file is the one that matters, so it goes last and unconditionally.
+ if (File.Exists(paths.DeviceKeyFile))
+ {
+ File.Delete(paths.DeviceKeyFile);
+ }
+
+ return ValueTask.CompletedTask;
+ }
+
+ ///
+ /// SetInvalidatedByBiometricEnrollment is on, which is the setting that makes this worth having:
+ /// without it, somebody who can add their own fingerprint to an unlocked phone inherits the ability to
+ /// unlock the vault. With it, enrolling a new fingerprint destroys the key and the phone falls back to
+ /// the passphrase — which is the correct outcome and the reason the load path treats invalidation as
+ /// ordinary rather than exceptional.
+ ///
+ private static IKey GenerateWrappingKey()
+ {
+ static KeyGenParameterSpec.Builder Spec() =>
+ new KeyGenParameterSpec.Builder(KeyAlias, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
+ .SetBlockModes(KeyProperties.BlockModeGcm)!
+ .SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone)!
+ .SetKeySize(256)!
+ .SetUserAuthenticationRequired(true)!
+ .SetInvalidatedByBiometricEnrollment(true)!;
+
+ var generator = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, KeystoreName)
+ ?? throw new InvalidOperationException("This phone has no AES key generator in its keystore.");
+
+ try
+ {
+ generator.Init(Spec().SetIsStrongBoxBacked(true)!.Build());
+
+ return generator.GenerateKey()!;
+ }
+ catch (StrongBoxUnavailableException)
+ {
+ // No separate security chip. The TEE-backed key is still hardware-held and still gated by the
+ // same gesture; see the class remarks for why this is a fallback rather than a refusal.
+ generator.Init(Spec().Build());
+
+ return generator.GenerateKey()!;
+ }
+ }
+}
diff --git a/src/DodoSSH.Client.Android/Platform/BiometricGate.cs b/src/DodoSSH.Client.Android/Platform/BiometricGate.cs
new file mode 100644
index 0000000..d366147
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Platform/BiometricGate.cs
@@ -0,0 +1,108 @@
+using global::Android.Hardware.Biometrics;
+using global::Javax.Crypto;
+
+namespace DodoSSH.Client.Android.Platform;
+
+/// Raised when the gesture did not happen — cancelled, failed, or nothing enrolled.
+///
+/// One exception for every refusal, because answers all of them the
+/// same way. It carries the platform's own message only so it can reach a log; nothing shows it to a user,
+/// who has just watched the system's own dialogue say the same thing better.
+///
+internal sealed class BiometricRefusedException(string message) : Exception(message);
+
+///
+/// Puts the system's biometric prompt in front of a cipher, and waits for it.
+///
+///
+///
+/// The cipher is handed to the prompt rather than merely being used after it, and that is the whole point.
+/// A prompt that only returned "yes" would be a boolean this process could be tricked into skipping;
+/// binding the cipher to the prompt means the keystore itself will not perform the operation unless the
+/// gesture actually happened. The key is unusable to a caller that did not go through here.
+///
+///
+/// Separated from the store because it is the platform half and it is callback-shaped, and because it is
+/// the piece most likely to need a second implementation — androidx.biometric, if the floor ever drops
+/// below API 28.
+///
+///
+internal static class BiometricGate
+{
+ public static async Task AuthenticateAsync(Cipher cipher, string title, CancellationToken cancellationToken)
+ {
+ var context = PhoneEnvironment.Require();
+
+ var builder = new BiometricPrompt.Builder(context)
+ .SetTitle(title)!
+ .SetDescription("Releases this phone's device key. The vault itself never leaves it.")!;
+
+ if (OperatingSystem.IsAndroidVersionAtLeast(30))
+ {
+ // Device credential beside biometrics deliberately: a phone whose fingerprint reader is wet,
+ // or whose owner has none enrolled, still has a PIN, and the key is guarded either way. The
+ // alternative is an unlock screen that silently stops offering the fast path.
+ // BiometricManagerAuthenticators, not BiometricManager.Authenticators: .NET for Android
+ // flattens Java's nested classes, so the Java documentation's name is not the C# one.
+ // Cast because the binding types the flags as an enum and the setter as the raw int Java uses.
+ builder.SetAllowedAuthenticators(
+ (int)(BiometricManagerAuthenticators.BiometricStrong
+ | BiometricManagerAuthenticators.DeviceCredential));
+ }
+ else
+ {
+ // API 28 and 29 have no allowed-authenticators list, and a prompt with no negative button is
+ // rejected outright at build time. The button is the only way out of the dialogue on these two
+ // releases, which is why it says what it does.
+ builder.SetNegativeButton(
+ "Use passphrase",
+ context.MainExecutor!,
+ new RefusalListener());
+ }
+
+ var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var signal = new global::Android.OS.CancellationSignal();
+
+ // The token has to reach the dialogue, not merely the await. Without this a cancelled unlock leaves
+ // the system prompt on screen over an application that has stopped waiting for it.
+ using var registration = cancellationToken.Register(signal.Cancel);
+
+ builder.Build().Authenticate(
+ new BiometricPrompt.CryptoObject(cipher),
+ signal,
+ context.MainExecutor!,
+ new Callback(completion));
+
+ // Awaited here rather than returned, so the cancellation registration above outlives the prompt
+ // it is there to cancel.
+ await completion.Task.ConfigureAwait(false);
+ }
+
+ private sealed class Callback(TaskCompletionSource completion) : BiometricPrompt.AuthenticationCallback
+ {
+ public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult? result) =>
+ completion.TrySetResult();
+
+ ///
+ /// Terminal, unlike OnAuthenticationFailed. An error is the prompt giving up — cancelled,
+ /// locked out, nothing enrolled — whereas a failure is one finger not being recognised, and the
+ /// prompt stays up and keeps trying after it. Completing the task on a failure would abandon a
+ /// dialogue that is still on screen.
+ ///
+ public override void OnAuthenticationError(BiometricErrorCode errorCode, global::Java.Lang.ICharSequence? errString) =>
+ completion.TrySetException(
+ new BiometricRefusedException(errString?.ToString() ?? $"Biometric error {errorCode}."));
+ }
+
+ private sealed class RefusalListener : global::Java.Lang.Object, global::Android.Content.IDialogInterfaceOnClickListener
+ {
+ ///
+ /// Nothing to do: dismissing the prompt raises OnAuthenticationError as well, and that is
+ /// where the wait is completed. Answering here too would be a second completion on the same task.
+ ///
+ public void OnClick(global::Android.Content.IDialogInterface? dialog, int which)
+ {
+ }
+ }
+}
diff --git a/src/DodoSSH.Client.Android/Platform/PhoneEnvironment.cs b/src/DodoSSH.Client.Android/Platform/PhoneEnvironment.cs
new file mode 100644
index 0000000..656d3ac
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Platform/PhoneEnvironment.cs
@@ -0,0 +1,85 @@
+using global::Android.Content;
+using global::Android.OS;
+using global::Android.Provider;
+
+using DodoSSH.Client.Session;
+
+namespace DodoSSH.Client.Android.Platform;
+
+///
+/// The handful of facts about this phone that the platform-neutral layers need handed to them.
+///
+///
+///
+/// Named Phone rather than Android because Android.Runtime.AndroidEnvironment already
+/// exists and this type is not it.
+///
+///
+/// Both members below are things docs/android-port.md called out as needing to come from the head rather
+/// than be branched for inside the core: the profile directory, and a device name that is not
+/// Environment.MachineName.
+///
+///
+internal static class PhoneEnvironment
+{
+ private static Context? context;
+
+ /// Captured once, from the launcher activity, before Avalonia starts.
+ ///
+ /// The application context rather than the activity's. An activity is destroyed and recreated
+ /// on configuration changes this head does not declare as handled, and holding one in a static field is
+ /// the textbook Android leak; the application context lives as long as the process, which is exactly the
+ /// lifetime the composition root has.
+ ///
+ public static void Attach(Context activity) =>
+ context = activity.ApplicationContext ?? activity;
+
+ /// Where this phone keeps its profile.
+ ///
+ /// filesDir — per-app, non-roaming, not user-visible, and removed when the app is uninstalled.
+ /// asks for a local, non-roaming directory because two machines sharing one
+ /// cache file corrupts the outbox; on Android that is not merely satisfied but enforced by the platform,
+ /// since no other app can reach this path at all.
+ ///
+ public static ClientPaths Paths =>
+ new(Require().FilesDir?.AbsolutePath
+ ?? throw new InvalidOperationException("Android returned no filesDir for this application."));
+
+ ///
+ /// What this phone calls itself in connection and keychain log entries.
+ ///
+ ///
+ ///
+ /// Environment.MachineName returns localhost on Android, which would make every log entry
+ /// written from a phone indistinguishable from every other — the finding recorded in
+ /// docs/android-port.md §7.
+ ///
+ ///
+ /// Settings.Global.DeviceName is what the user themselves typed in Settings, so it is the name
+ /// they will recognise in a log written by a different device. It is null on phones that have never had
+ /// one set, and the fallback is the marketing model rather than the board name: a person reading a log
+ /// knows what a Pixel 8 is and does not know what shiba is.
+ ///
+ ///
+ public static string DeviceName
+ {
+ get
+ {
+ var chosen = Settings.Global.GetString(Require().ContentResolver, "device_name");
+
+ if (!string.IsNullOrWhiteSpace(chosen))
+ {
+ return chosen;
+ }
+
+ var model = Build.Model;
+
+ return string.IsNullOrWhiteSpace(model) ? "Android phone" : model;
+ }
+ }
+
+ /// The application context, once has run.
+ public static Context Require() =>
+ context ?? throw new InvalidOperationException(
+ "PhoneEnvironment was read before MainActivity attached it.");
+}
diff --git a/src/DodoSSH.Client.Android/Platform/SessionForegroundService.cs b/src/DodoSSH.Client.Android/Platform/SessionForegroundService.cs
new file mode 100644
index 0000000..61e5f05
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Platform/SessionForegroundService.cs
@@ -0,0 +1,134 @@
+using global::Android.App;
+using global::Android.Content;
+using global::Android.Content.PM;
+using global::Android.OS;
+
+namespace DodoSSH.Client.Android.Platform;
+
+///
+/// Keeps the process alive for as long as a shell or a transfer is live.
+///
+///
+///
+/// The decision recorded in docs/android-port.md: a persistent notification, for as long as there is
+/// something running that would be wrong to kill. It costs the user a notification and some battery, and it
+/// buys the behaviour the desktop client already promises and documents — that a shell outlives a vault
+/// lock, and that a transfer finishes.
+///
+///
+/// Why this exists at all is worth stating plainly. TerminalWorkspace's guarantee is that
+/// locking the vault does not close your shells, because the remote host never consulted the vault and the
+/// credential was already spent. On a desktop that guarantee is free — the process keeps running. On
+/// Android nothing keeps a backgrounded process running, so without this the guarantee would quietly become
+/// desktop-only, and a phone would drop a shell the moment the user checked a message.
+///
+///
+/// It holds no state and owns nothing. The sessions live in the composition root, exactly as they do
+/// on the desktop; this only asks Android not to stop the process they are in. That is why starting and
+/// stopping it is a count of live things rather than a lifecycle of its own — see
+/// .
+///
+///
+[Service(
+ Exported = false,
+
+ // Android 14 (API 34) refuses to start a foreground service whose type is not declared both here and
+ // in the manifest's permission list. dataSync is the type that matches: an SSH session and a file
+ // transfer are both the user's data moving to somewhere the user chose.
+ ForegroundServiceType = ForegroundService.TypeDataSync)]
+internal sealed class SessionForegroundService : Service
+{
+ private const string ChannelId = "dodossh.sessions";
+ private const int NotificationId = 1;
+
+ ///
+ /// A bound service would tie the sessions' lifetime to a binding, which is the opposite of what is
+ /// wanted here: the point is that they outlive whatever the user does with the interface.
+ ///
+ public override IBinder? OnBind(Intent? intent) => null;
+
+ public override StartCommandResult OnStartCommand(Intent? intent, StartCommandFlags flags, int startId)
+ {
+ StartForeground(NotificationId, BuildNotification(intent?.GetStringExtra("summary") ?? "Working"));
+
+ // NotSticky: if Android does kill this process, the SSH connections died with it and there is
+ // nothing to resume. Restarting the service would produce a notification claiming sessions that no
+ // longer exist, which is exactly the kind of dishonest state the unlock screen's shell count exists
+ // to prevent.
+ return StartCommandResult.NotSticky;
+ }
+
+ ///
+ /// Low importance on purpose. This notification is a receipt, not an alert — it exists because Android
+ /// requires one, and because the user is entitled to know the app is holding connections open. Making
+ /// it buzz would be a notification about nothing having happened.
+ ///
+ private Notification BuildNotification(string summary)
+ {
+ var manager = (NotificationManager)GetSystemService(NotificationService)!;
+
+ if (OperatingSystem.IsAndroidVersionAtLeast(26))
+ {
+ var channel = new NotificationChannel(ChannelId, "Live sessions", NotificationImportance.Low)
+ {
+ Description = "Shown while a shell or a transfer is open.",
+ };
+
+ channel.SetShowBadge(false);
+ manager.CreateNotificationChannel(channel);
+ }
+
+ var reopen = PendingIntent.GetActivity(
+ this,
+ 0,
+ new Intent(this, typeof(MainActivity)).SetFlags(ActivityFlags.SingleTop),
+ PendingIntentFlags.Immutable | PendingIntentFlags.UpdateCurrent);
+
+ return new Notification.Builder(this, ChannelId)
+ .SetContentTitle("DodoSSH")
+ .SetContentText(summary)
+ .SetSmallIcon(global::Android.Resource.Drawable.IcDialogInfo)
+ .SetContentIntent(reopen)
+ .SetOngoing(true)!
+ .Build();
+ }
+
+ /// Starts or stops the service to match what is actually running.
+ /// Shells with a live channel behind them.
+ /// Transfers still moving bytes.
+ public static void Reconcile(int liveSessions, int activeTransfers)
+ {
+ var context = PhoneEnvironment.Require();
+ var intent = new Intent(context, typeof(SessionForegroundService));
+
+ if (liveSessions == 0 && activeTransfers == 0)
+ {
+ context.StopService(intent);
+
+ return;
+ }
+
+ // The summary says what is actually held, counted rather than generic — the same principle the
+ // delete confirmations follow. "DodoSSH is running" would tell the user nothing they could act on.
+ intent.PutExtra("summary", Summarise(liveSessions, activeTransfers));
+
+ context.StartForegroundService(intent);
+ }
+
+ private static string Summarise(int liveSessions, int activeTransfers)
+ {
+ var parts = new List(2);
+
+ if (liveSessions > 0)
+ {
+ parts.Add(liveSessions == 1 ? "1 shell connected" : $"{liveSessions} shells connected");
+ }
+
+ if (activeTransfers > 0)
+ {
+ parts.Add(activeTransfers == 1 ? "1 transfer running" : $"{activeTransfers} transfers running");
+ }
+
+ return string.Join(" · ", parts);
+ }
+}
diff --git a/src/DodoSSH.Client.Android/Platform/SessionKeepAlive.cs b/src/DodoSSH.Client.Android/Platform/SessionKeepAlive.cs
new file mode 100644
index 0000000..c20a78a
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Platform/SessionKeepAlive.cs
@@ -0,0 +1,63 @@
+using DodoSSH.Client.Terminal;
+
+namespace DodoSSH.Client.Android.Platform;
+
+///
+/// Keeps in step with what is actually running.
+///
+///
+///
+/// The service is started and stopped from one place, and that place is a count rather than a lifecycle.
+/// Anything else drifts: a service started when a shell opens and stopped when a tab closes would leave
+/// the notification up after the last shell died on its own, and a phone showing "1 shell connected" over
+/// nothing is the same dishonesty the unlock screen's shell count exists to avoid.
+///
+///
+/// is deliberately the source of truth rather than a
+/// tally kept here. It already knows that a session whose shell exited half an hour ago is not live, which
+/// a counter incremented on open and decremented on close would not.
+///
+///
+internal sealed class SessionKeepAlive : IDisposable
+{
+ private readonly TerminalWorkspace workspace;
+ private readonly Func activeTransfers;
+
+ /// The live shells.
+ ///
+ /// How many transfers are moving bytes. A delegate rather than a queue, because file transfer is out
+ /// of this head's first scope — see the decision in docs/android-port.md — and this is the seam it
+ /// will arrive through rather than a dependency taken before there is anything to depend on.
+ ///
+ public SessionKeepAlive(TerminalWorkspace workspace, Func activeTransfers)
+ {
+ this.workspace = workspace;
+ this.activeTransfers = activeTransfers;
+
+ // Raised on whatever thread the pump unwound on, which is fine: starting and stopping a service is
+ // a binder call and needs no particular thread. Nothing here touches the interface.
+ workspace.SessionEnded += OnSessionEnded;
+ }
+
+ /// Re-reads the counts and starts or stops the service to match.
+ ///
+ /// Called after anything that could change either count — opening a shell, closing a tab, a transfer
+ /// finishing. Calling it when nothing changed is free: reconciling to the state it is already in is
+ /// either a redundant startForegroundService on a running service or a stopService on a
+ /// stopped one, and Android treats both as no-ops.
+ ///
+ public void Refresh() =>
+ SessionForegroundService.Reconcile(workspace.LiveSessionCount, activeTransfers());
+
+ ///
+ public void Dispose()
+ {
+ workspace.SessionEnded -= OnSessionEnded;
+
+ // The notification goes with the composition root. Leaving it up over a process that is shutting
+ // down is how an SSH client acquires a reputation for a notification you cannot get rid of.
+ SessionForegroundService.Reconcile(0, 0);
+ }
+
+ private void OnSessionEnded(object? sender, TerminalSessionEndedEventArgs e) => Refresh();
+}
diff --git a/src/DodoSSH.Client.Android/Properties/AndroidManifest.xml b/src/DodoSSH.Client.Android/Properties/AndroidManifest.xml
new file mode 100644
index 0000000..b740344
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Properties/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DodoSSH.Client.Android/Resources/values/colors.xml b/src/DodoSSH.Client.Android/Resources/values/colors.xml
new file mode 100644
index 0000000..9c62f67
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Resources/values/colors.xml
@@ -0,0 +1,13 @@
+
+
+
+ #0A0C0B
+
diff --git a/src/DodoSSH.Client.Android/Resources/values/styles.xml b/src/DodoSSH.Client.Android/Resources/values/styles.xml
new file mode 100644
index 0000000..551e59d
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Resources/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/src/DodoSSH.Client.Android/Resources/xml/network_security_config.xml b/src/DodoSSH.Client.Android/Resources/xml/network_security_config.xml
new file mode 100644
index 0000000..d73e69d
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Resources/xml/network_security_config.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ 127.0.0.1
+
+
diff --git a/src/DodoSSH.Client.Android/Views/LockedScreen.axaml b/src/DodoSSH.Client.Android/Views/LockedScreen.axaml
new file mode 100644
index 0000000..e63b333
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Views/LockedScreen.axaml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DodoSSH.Client.Android/Views/LockedScreen.axaml.cs b/src/DodoSSH.Client.Android/Views/LockedScreen.axaml.cs
new file mode 100644
index 0000000..3c3660b
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Views/LockedScreen.axaml.cs
@@ -0,0 +1,10 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace DodoSSH.Client.Android.Views;
+
+/// Design 01 — the unlock screen.
+internal sealed partial class LockedScreen : UserControl
+{
+ public LockedScreen() => AvaloniaXamlLoader.Load(this);
+}
diff --git a/src/DodoSSH.Client.Android/Views/PendingScreen.axaml b/src/DodoSSH.Client.Android/Views/PendingScreen.axaml
new file mode 100644
index 0000000..8c6904e
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Views/PendingScreen.axaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DodoSSH.Client.Android/Views/PendingScreen.axaml.cs b/src/DodoSSH.Client.Android/Views/PendingScreen.axaml.cs
new file mode 100644
index 0000000..7fe36ce
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Views/PendingScreen.axaml.cs
@@ -0,0 +1,43 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace DodoSSH.Client.Android.Views;
+
+///
+/// A state this head has not built, saying so in its own words.
+///
+///
+/// Styled properties rather than a view model: there is no state behind this control, and giving it one
+/// would make it look like a screen that might one day have data.
+///
+internal sealed partial class PendingScreen : UserControl
+{
+ public static readonly StyledProperty HeadingProperty =
+ AvaloniaProperty.Register(nameof(Heading), string.Empty);
+
+ public static readonly StyledProperty DetailProperty =
+ AvaloniaProperty.Register(nameof(Detail), string.Empty);
+
+ public PendingScreen()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Its own data context, so the two properties can be bound in XAML like anything else. Safe here
+ // and only here: this control deliberately shows nothing from the shell, so there is no inherited
+ // context worth keeping.
+ DataContext = this;
+ }
+
+ public string Heading
+ {
+ get => GetValue(HeadingProperty);
+ set => SetValue(HeadingProperty, value);
+ }
+
+ public string Detail
+ {
+ get => GetValue(DetailProperty);
+ set => SetValue(DetailProperty, value);
+ }
+}
diff --git a/src/DodoSSH.Client.Android/Views/PhoneShell.axaml b/src/DodoSSH.Client.Android/Views/PhoneShell.axaml
new file mode 100644
index 0000000..c16c1fb
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Views/PhoneShell.axaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DodoSSH.Client.Android/Views/PhoneShell.axaml.cs b/src/DodoSSH.Client.Android/Views/PhoneShell.axaml.cs
new file mode 100644
index 0000000..93dfc3b
--- /dev/null
+++ b/src/DodoSSH.Client.Android/Views/PhoneShell.axaml.cs
@@ -0,0 +1,10 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace DodoSSH.Client.Android.Views;
+
+/// The phone's single view. The desktop head's MainWindow, without the window.
+internal sealed partial class PhoneShell : UserControl
+{
+ public PhoneShell() => AvaloniaXamlLoader.Load(this);
+}
diff --git a/src/DodoSSH.Client.Android/packages.lock.json b/src/DodoSSH.Client.Android/packages.lock.json
new file mode 100644
index 0000000..5cd3587
--- /dev/null
+++ b/src/DodoSSH.Client.Android/packages.lock.json
@@ -0,0 +1,1169 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0-android36.0": {
+ "Avalonia": {
+ "type": "Direct",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "o8pZ1oE9AQ6gklpGM0lnOBp/JlVH0J/0mYszBf0GsSAcEnzHNCLM9NnrPZwKu4j2q9oNbVHYzzEPkszQeuqaKw==",
+ "dependencies": {
+ "Avalonia.BuildServices": "11.3.2",
+ "Avalonia.Remote.Protocol": "12.1.1",
+ "MicroCom.Runtime": "0.11.6"
+ }
+ },
+ "Avalonia.Android": {
+ "type": "Direct",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "r3ePTQtZoyNQsSlp4I2pZg7mca5QD3ijHWAF3BXOK5BarJx22m1lliixdqc0hwPDnJJPYC3A07c3f++qfZXUqA==",
+ "dependencies": {
+ "Avalonia": "12.1.1",
+ "Avalonia.HarfBuzz": "12.1.1",
+ "Avalonia.Skia": "12.1.1",
+ "Xamarin.AndroidX.AppCompat": "1.7.1.3",
+ "Xamarin.AndroidX.Window": "1.5.1.2"
+ }
+ },
+ "Avalonia.Controls.WebView": {
+ "type": "Direct",
+ "requested": "[12.0.1, )",
+ "resolved": "12.0.1",
+ "contentHash": "GrCIpIIBL7ueFDsNu3lyYc1mgO3QGGl1c1MCK8YAgjaNZwF9PV5PF2UB3lm1uuqj/MWOKNhemLwcSDLyYv0JjQ==",
+ "dependencies": {
+ "Avalonia": "12.0.0",
+ "Avalonia.Android": "12.0.0"
+ }
+ },
+ "Avalonia.Fonts.Inter": {
+ "type": "Direct",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "V2d7OM1jybcl31KThdhf8XSl5SjQ6Ll/tXIfwQ00kMeWuPegRcOx8DLVpECHOOEKO9JZelXW0enuUu1D7Svikg==",
+ "dependencies": {
+ "Avalonia": "12.1.1"
+ }
+ },
+ "Avalonia.Themes.Fluent": {
+ "type": "Direct",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "7vdtZsM9o8I8LpU9dyiB/Ah7WB8a5V3JHLsExe4T433ynn5x57F+DBskctZuNOxml8hbe1GmtNUNkBDsqSVKVg==",
+ "dependencies": {
+ "Avalonia": "12.1.1"
+ }
+ },
+ "CommunityToolkit.Mvvm": {
+ "type": "Direct",
+ "requested": "[8.4.2, )",
+ "resolved": "8.4.2",
+ "contentHash": "WadCzGEc2U+3e20avRLng4qNtt4zoOGWrdUISqJWrHe3/FSnrYjuM5Sb4yQb09LhkBXrrI4Zt3dLKgRMbItsrg=="
+ },
+ "Meziantou.Analyzer": {
+ "type": "Direct",
+ "requested": "[3.0.137, )",
+ "resolved": "3.0.137",
+ "contentHash": "48n8WsokyefLOZt2qGPpk+hjfF9rQRa7MGK+8hMS6sO22QuIDtBztfbXV7RtVfX7JKjUwDI9e06I8aeCc5XZzQ=="
+ },
+ "Microsoft.CodeAnalysis.BannedApiAnalyzers": {
+ "type": "Direct",
+ "requested": "[5.6.0, )",
+ "resolved": "5.6.0",
+ "contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
+ },
+ "Microsoft.NET.ILLink.Tasks": {
+ "type": "Direct",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
+ },
+ "Avalonia.BuildServices": {
+ "type": "Transitive",
+ "resolved": "11.3.2",
+ "contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
+ },
+ "Avalonia.HarfBuzz": {
+ "type": "Transitive",
+ "resolved": "12.1.1",
+ "contentHash": "vgd/Fl4bIXgv3QtYk6WCd6iGAag4i3OOAozo6/DW3xPr5dWjpi1MmJaeE9MYoZBEDJ4egD0gfHz6nYhkDADz9g==",
+ "dependencies": {
+ "Avalonia": "12.1.1",
+ "HarfBuzzSharp": "8.3.1.3",
+ "HarfBuzzSharp.NativeAssets.Linux": "8.3.1.3",
+ "HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.3"
+ }
+ },
+ "Avalonia.Remote.Protocol": {
+ "type": "Transitive",
+ "resolved": "12.1.1",
+ "contentHash": "0u77tnOwJnHtVLu+WBY7T56fN9W8n7++Uq9kHW6J+bfv5y13WZUMVS+PBzoBe32taYvz/oSomDGO1V41AHaFcQ=="
+ },
+ "Avalonia.Skia": {
+ "type": "Transitive",
+ "resolved": "12.1.1",
+ "contentHash": "Q0jRMXb212RYIZNV7p6L/zWx8lyPck9dFFW+l86moeN4dNxHjPBdJS3nLdLvAd20Eno7JC4gwRa5Fewg52td8g==",
+ "dependencies": {
+ "Avalonia": "12.1.1",
+ "HarfBuzzSharp": "8.3.1.3",
+ "HarfBuzzSharp.NativeAssets.Linux": "8.3.1.3",
+ "HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.3",
+ "SkiaSharp": "3.119.4",
+ "SkiaSharp.NativeAssets.Linux": "3.119.4",
+ "SkiaSharp.NativeAssets.WebAssembly": "3.119.4"
+ }
+ },
+ "HarfBuzzSharp": {
+ "type": "Transitive",
+ "resolved": "8.3.1.3",
+ "contentHash": "NGZ2+ZVNPM+NdHB/asW0/ykWngyHWwcqjrbN2nDeH1B/aptPGlCUl8wkQ2cSJxw5fdWgdmIPmNuTPWpLwNVXWg==",
+ "dependencies": {
+ "HarfBuzzSharp.NativeAssets.Android": "8.3.1.3"
+ }
+ },
+ "HarfBuzzSharp.NativeAssets.Android": {
+ "type": "Transitive",
+ "resolved": "8.3.1.3",
+ "contentHash": "Yte9/yYql8ngAjo7YgHlXSinLJcJXIRBM9gegVXpJ2SVYT1i2O/wMA+H3jmYiYiTQxHpHKi4exZUcMzry171MA=="
+ },
+ "HarfBuzzSharp.NativeAssets.Linux": {
+ "type": "Transitive",
+ "resolved": "8.3.1.3",
+ "contentHash": "RI6A1LgmooU30+4QIyFt5rmBCzP0VzTR+587IJSGvYIsHHWlahFufihYxtraLfsIhW7I8dn6+xX+DZGygOPKWQ=="
+ },
+ "HarfBuzzSharp.NativeAssets.WebAssembly": {
+ "type": "Transitive",
+ "resolved": "8.3.1.3",
+ "contentHash": "w2QfdNm9Uz/sUa0B5D+OnVQhyq3G/fBq6ibQMdWBlQqqwh0g0/5j3RFvYqZAmRZ5+RzvjVe8o8SFFnWYUSkuxA=="
+ },
+ "MicroCom.Runtime": {
+ "type": "Transitive",
+ "resolved": "0.11.6",
+ "contentHash": "NdNWGDiZ6eS/Mf/9+QHR91cj1K7Hy+PX9yrHI/zM7xFYuj9IWT2uxtB6sCHjrnxAeLV9fut1R6zHDUGKX6f9lQ=="
+ },
+ "Microsoft.Data.Sqlite.Core": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "TPCs0ldm7AWqcKmp6f/Xr+14sat7hx4rHfRlS4RgCURBH2thEWbAKEyX7cCWr63zVJVOJIJZTg2cBiUXa8ys6g==",
+ "dependencies": {
+ "SQLitePCLRaw.core": "2.1.11"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA=="
+ },
+ "Microsoft.EntityFrameworkCore.Analyzers": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q=="
+ },
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "YbVWMIouzwTKBiLms8boa7xeRT88wI14R1msv3XExFk9n0/sa8nU7MwDa1CKtfLGMJs7O7QWuS9/xhcQ72AD2A==",
+ "dependencies": {
+ "Microsoft.Data.Sqlite.Core": "10.0.10",
+ "Microsoft.EntityFrameworkCore.Relational": "10.0.10",
+ "Microsoft.Extensions.Caching.Memory": "10.0.10",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
+ "Microsoft.Extensions.DependencyModel": "10.0.10",
+ "Microsoft.Extensions.Logging": "10.0.10",
+ "SQLitePCLRaw.core": "2.1.11"
+ }
+ },
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Caching.Memory": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.10",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Options": "10.0.10",
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA=="
+ },
+ "Microsoft.Extensions.DependencyModel": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA=="
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.10",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Options": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ=="
+ },
+ "SkiaSharp": {
+ "type": "Transitive",
+ "resolved": "3.119.4",
+ "contentHash": "53NOSUZ1Us+91Sm0uCkIivh/k7jOowRErZT2sIWwPFN9mLUvdxnE6rS4sWo4255+Rd2MWUSF+j0NMZHD6Cke+Q==",
+ "dependencies": {
+ "SkiaSharp.NativeAssets.Android": "3.119.4"
+ }
+ },
+ "SkiaSharp.NativeAssets.Android": {
+ "type": "Transitive",
+ "resolved": "3.119.4",
+ "contentHash": "qfjNh5hZBZxpOIM1aeDByj2qNbcK2JZG5Y7YyGSeliaYnf1N/hVfsswIPUa+qzcMqS9Q0VCGk85zQLvwVXtrvQ=="
+ },
+ "SkiaSharp.NativeAssets.Linux": {
+ "type": "Transitive",
+ "resolved": "3.119.4",
+ "contentHash": "UAyVzbqNfZsZbKbzj68zXLyUyF/SbTKmzTfOO6qDu++dtIUMMTzPBe8oOuzU/DiewpfKoUUlOSsJmqWc6blxBw=="
+ },
+ "SkiaSharp.NativeAssets.WebAssembly": {
+ "type": "Transitive",
+ "resolved": "3.119.4",
+ "contentHash": "S1HOxtBbD4bYDtA2e9WH5TX+lxqRrTPvKjrjttRhxnHNNu7YY8VFo/LeCP7tNqoTA6PV+8vsvNbmRUEC2ip8RQ=="
+ },
+ "SQLitePCLRaw.lib.e_sqlite3.android": {
+ "type": "Transitive",
+ "resolved": "2.1.12",
+ "contentHash": "ZiUfCiq4kpazZc2fyyhezYvOpqE1XQfFmgP4AVoAF+eK4jVwHFeh7oYVrPPJGk2CSJBuX4APAS3n1dUiTSKWeg=="
+ },
+ "Xamarin.AndroidX.Activity": {
+ "type": "Transitive",
+ "resolved": "1.12.4.1",
+ "contentHash": "XvPeFfCBOL/l7GDuM9EwpoooB163TSiztwx/nKaH4ghDn4EBVL6zBQc2S7R4uTWEjOARCyOtsqIaN2bc9NjBBQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Core.Core.Ktx": "1.17.0.2",
+ "Xamarin.AndroidX.Core.ViewTree": "1.0.0.5",
+ "Xamarin.AndroidX.Lifecycle.Common": "2.10.0.2",
+ "Xamarin.AndroidX.Lifecycle.Runtime": "2.10.0.2",
+ "Xamarin.AndroidX.Lifecycle.ViewModel": "2.10.0.2",
+ "Xamarin.AndroidX.Lifecycle.ViewModelSavedState": "2.10.0.2",
+ "Xamarin.AndroidX.NavigationEvent": "1.0.2.1",
+ "Xamarin.AndroidX.ProfileInstaller.ProfileInstaller": "1.4.1.7",
+ "Xamarin.AndroidX.SavedState": "1.4.0.2",
+ "Xamarin.AndroidX.Tracing.Tracing": "1.3.0.3",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3"
+ }
+ },
+ "Xamarin.AndroidX.Annotation": {
+ "type": "Transitive",
+ "resolved": "1.9.1.7",
+ "contentHash": "o77RbvsTeE48+KMfg3Y5pLL1R0OgHb29aqphPLcnTusB7wCoOM0s/vsh6JsxusV+CAD5UPZB4qB45tTQ5jg8mQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation.Jvm": "1.9.1.7",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Annotation.Experimental": {
+ "type": "Transitive",
+ "resolved": "1.5.1.3",
+ "contentHash": "U1ewvEEGaB2s4L2E3ijnBx1I7JjZI1H9tiTIc99x5NCXtcxvZoRDoJRsOsffLAck8IpfiXVsNuMT3Dj3Yafe6Q==",
+ "dependencies": {
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Annotation.Jvm": {
+ "type": "Transitive",
+ "resolved": "1.9.1.7",
+ "contentHash": "mnpIIJq38C63y9Pg1LZxQgY7U5PiDHAepy8xldVJL8UPNigBSEXB3oOh0UDAsfqtDldfv0lH8ADcAq3plQRhhg==",
+ "dependencies": {
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.AppCompat": {
+ "type": "Transitive",
+ "resolved": "1.7.1.3",
+ "contentHash": "Sx7IB41pP1H6jJbuM8bxnUpppyjAGmpOh5SxuiL71+33lENESl7uWZ/aymmRMwgNMh6OSwhdTj5A0J1PLUvsAA==",
+ "dependencies": {
+ "Xamarin.AndroidX.Activity": "1.12.4.1",
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.AppCompat.AppCompatResources": "[1.7.1.3, 1.7.2)",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.AndroidX.Core.Core.Ktx": "1.17.0.2",
+ "Xamarin.AndroidX.CursorAdapter": "1.0.0.36",
+ "Xamarin.AndroidX.DrawerLayout": "1.2.0.20",
+ "Xamarin.AndroidX.Emoji2": "1.6.0.2",
+ "Xamarin.AndroidX.Emoji2.ViewsHelper": "1.6.0.2",
+ "Xamarin.AndroidX.Fragment": "1.8.9.2",
+ "Xamarin.AndroidX.Lifecycle.Runtime": "2.10.0.2",
+ "Xamarin.AndroidX.Lifecycle.ViewModel": "2.10.0.2",
+ "Xamarin.AndroidX.ProfileInstaller.ProfileInstaller": "1.4.1.7",
+ "Xamarin.AndroidX.ResourceInspection.Annotation": "1.0.1.24",
+ "Xamarin.AndroidX.SavedState": "1.4.0.2",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.AppCompat.AppCompatResources": {
+ "type": "Transitive",
+ "resolved": "1.7.1.3",
+ "contentHash": "2wPEdyIN/Xh1wcOowZ3jAMBX7RrRNtBA8mzwmec6eOB+MXj/ANTx+CISJQJBlXzsif8vvZz81dPJ80OAPmeV2Q==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.AndroidX.VectorDrawable": "1.2.0.10",
+ "Xamarin.AndroidX.VectorDrawable.Animated": "1.2.0.10"
+ }
+ },
+ "Xamarin.AndroidX.Arch.Core.Common": {
+ "type": "Transitive",
+ "resolved": "2.2.0.20",
+ "contentHash": "CJ4jsgchHOAXM1mSWEyc7WhRPxVLbSYQiV56K79VpKsTLmUGQrjXvZxjB5REyEG1yaUrlAlZ1QMNPtXLDJ+ijQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7"
+ }
+ },
+ "Xamarin.AndroidX.Arch.Core.Runtime": {
+ "type": "Transitive",
+ "resolved": "2.2.0.20",
+ "contentHash": "j+EaKYaCFOLZhknLWKVVDKOTz0v6HIk1BNu3FtGv5uECTD2NQv46/bNnVrxkf9ji9XMFhklq1aBTbXM+JAjRcA==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Arch.Core.Common": "[2.2.0.20, 2.2.1)"
+ }
+ },
+ "Xamarin.AndroidX.Collection": {
+ "type": "Transitive",
+ "resolved": "1.5.0.5",
+ "contentHash": "7WADsWiPLDB0FE2eaxlAS0q9rz/R9ONaktZvIIFSIwKDJNa6TD1zYBhBFYYlwOdPKhv8tSNcisw04Yf8At06pw==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Collection.Jvm": "1.5.0.5",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Collection.Jvm": {
+ "type": "Transitive",
+ "resolved": "1.5.0.5",
+ "contentHash": "r1V8niIXewad1V5f856rp993k0uM2kUTRhGEG2svVxfoEAVcQxsxkp/1yqVVKxKGvwX3LOm9zMhN4BMQzf57QA==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation.Jvm": "1.9.1.7",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Compose.Runtime.Annotation": {
+ "type": "Transitive",
+ "resolved": "1.10.4.1",
+ "contentHash": "GiMCWfs1EEKa8deibZ1YpGrP++eAnAS1QqX8Z1Bgjufxj8E3fbEs7XwcKnCdl/1g+nvpdunjXp+NjcnJeLOyAg==",
+ "dependencies": {
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Compose.Runtime.Annotation.Android": {
+ "type": "Transitive",
+ "resolved": "1.10.4.1",
+ "contentHash": "kSP1q3RaExDHuZ3W3pdWR4T3WAZYzGhRv8HK+wFpqeiU1ts/TJ3ZyFDI0AQRl587Af6dl5aki0pn10lD8l6XMg==",
+ "dependencies": {
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Concurrent.Futures": {
+ "type": "Transitive",
+ "resolved": "1.3.0.3",
+ "contentHash": "I2BZFcrd/c/zsCwv8BZt4j77jz7Bpb0/dkI537i/nb98uIk+Ka60WPtQpAcoiqQzhwIVijVBckzODh0ciTUJkg==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.Google.Guava.ListenableFuture": "1.0.0.31",
+ "Xamarin.JSpecify": "1.0.0.6"
+ }
+ },
+ "Xamarin.AndroidX.Core": {
+ "type": "Transitive",
+ "resolved": "1.17.0.2",
+ "contentHash": "W5QTwF99C4wP+OIpTXLJZYPunKdf0huPAhTdxpOSU0ps5mvJst9v2TVi5JZOCM5IXOl/mcGe8uFaG59ARPw1lA==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Annotation.Experimental": "1.5.1.3",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Concurrent.Futures": "1.3.0.3",
+ "Xamarin.AndroidX.Core.ViewTree": "1.0.0.5",
+ "Xamarin.AndroidX.Interpolator": "1.0.0.36",
+ "Xamarin.AndroidX.Lifecycle.Common": "2.10.0.2",
+ "Xamarin.AndroidX.Lifecycle.Runtime": "2.10.0.2",
+ "Xamarin.AndroidX.Tracing.Tracing": "1.3.0.3",
+ "Xamarin.AndroidX.VersionedParcelable": "1.2.1.5",
+ "Xamarin.Google.Guava.ListenableFuture": "1.0.0.31",
+ "Xamarin.JSpecify": "1.0.0.6",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3"
+ }
+ },
+ "Xamarin.AndroidX.Core.Core.Ktx": {
+ "type": "Transitive",
+ "resolved": "1.17.0.2",
+ "contentHash": "SY7p2ulFYcyjqdJENh3GAXrUhGE3N/KoIMrzlFi5/34QpMmCS2ejsCuo3cj1MRfR9iQkjj3XwGYk29oF83KTvw==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Core.ViewTree": {
+ "type": "Transitive",
+ "resolved": "1.0.0.5",
+ "contentHash": "7pEcGvCKXBa1AS+NPcvF4DjB4ZPtAzPIi9T3HEmisMKmY7jxfLLD0qPnpTQpznFn6Xt5axoKKF88/5H+ZZ+z2A==",
+ "dependencies": {
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.CursorAdapter": {
+ "type": "Transitive",
+ "resolved": "1.0.0.36",
+ "contentHash": "WKPYhD+48bJt3Xcr/0Hikv7xIjw3s0LOlkunz4n6kCtV3aLbNtwGemkqulKV/AkpNmYdFARWzz8ngV5RG4lP5Q==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7"
+ }
+ },
+ "Xamarin.AndroidX.CustomView": {
+ "type": "Transitive",
+ "resolved": "1.2.0.3",
+ "contentHash": "ekAHgOWMZdX6tP7TDIOveOq2W48oTVkUotOBokxZU739vwi+r3c9svx2K3bWKWszCBqCXWxtdxup18D55UlEXQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.JSpecify": "1.0.0.6"
+ }
+ },
+ "Xamarin.AndroidX.DrawerLayout": {
+ "type": "Transitive",
+ "resolved": "1.2.0.20",
+ "contentHash": "KelXwjsaO1fnJ2r6efBZAcYNvzvHtqt2M50Cu8+AnJvmnrp6avNxASHaYDk7Wnm0pqQuPQccX+UlVxBJPHW7oQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.AndroidX.CustomView": "1.2.0.3"
+ }
+ },
+ "Xamarin.AndroidX.Emoji2": {
+ "type": "Transitive",
+ "resolved": "1.6.0.2",
+ "contentHash": "4QKAApr9H9jUKnDacMkJnZUAfxf8WtVRCvZhlOApaLFNdlfJhV3GiBDQg9LyNKjY2+3fP4f6K0acnmyt7AlRhQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.AndroidX.Lifecycle.Process": "2.10.0.2",
+ "Xamarin.AndroidX.Startup.StartupRuntime": "1.2.0.7",
+ "Xamarin.JSpecify": "1.0.0.6"
+ }
+ },
+ "Xamarin.AndroidX.Emoji2.ViewsHelper": {
+ "type": "Transitive",
+ "resolved": "1.6.0.2",
+ "contentHash": "A2QPgZ+dlSid44LKiQgWD2Ev82w92dDQuopgDc6eZkGZitMD/Wm72gIwdasIiUckk4vmSJeFNTmK0q0ofYoqEw==",
+ "dependencies": {
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.AndroidX.Emoji2": "[1.6.0.2, 1.6.1)",
+ "Xamarin.JSpecify": "1.0.0.6"
+ }
+ },
+ "Xamarin.AndroidX.Fragment": {
+ "type": "Transitive",
+ "resolved": "1.8.9.2",
+ "contentHash": "i8fyLoxNhQ+J5chDFeIPpAPkRFevAqLqt/t+J+Wex67DsrPiJYTjeuJ+p5GX9c2qRWyYtDorb9WwpOL4/tMwxg==",
+ "dependencies": {
+ "Xamarin.AndroidX.Activity": "1.12.4.1",
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Annotation.Experimental": "1.5.1.3",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Core.Core.Ktx": "1.17.0.2",
+ "Xamarin.AndroidX.Lifecycle.LiveData.Core": "2.10.0.2",
+ "Xamarin.AndroidX.Lifecycle.Runtime": "2.10.0.2",
+ "Xamarin.AndroidX.Lifecycle.ViewModel": "2.10.0.2",
+ "Xamarin.AndroidX.Lifecycle.ViewModelSavedState": "2.10.0.2",
+ "Xamarin.AndroidX.Loader": "1.1.0.36",
+ "Xamarin.AndroidX.ProfileInstaller.ProfileInstaller": "1.4.1.7",
+ "Xamarin.AndroidX.SavedState": "1.4.0.2",
+ "Xamarin.AndroidX.ViewPager": "1.1.0.6",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Interpolator": {
+ "type": "Transitive",
+ "resolved": "1.0.0.36",
+ "contentHash": "eDykH8nqVeiFNk7P3epX7nGgl6DcWQo5XB5rLuEUSMybjo97SlGwdSElMA0Dn2H1us51yMQVcKmsqh4dXzUaCA==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7"
+ }
+ },
+ "Xamarin.AndroidX.Lifecycle.Common": {
+ "type": "Transitive",
+ "resolved": "2.10.0.2",
+ "contentHash": "ksfAj0Rp6vYUC4qEKKaskUQ2mqfiHgNddnnvz/F34a+mzpcKoSkdSzW6dqI/ZADIzLHsUKPi9lJ4Mt2QPTp82g==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Lifecycle.Common.Jvm": "2.10.0.2",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3"
+ }
+ },
+ "Xamarin.AndroidX.Lifecycle.Common.Jvm": {
+ "type": "Transitive",
+ "resolved": "2.10.0.2",
+ "contentHash": "Ut/Dzg3/T2yFWfSZLa4IkDh0RNOs8iqusAdvdRB5ZrSQgQfPF7BbkpfREd1n+JmFNE8/j1oO6s5N64vYIY9nEw==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation.Jvm": "1.9.1.7",
+ "Xamarin.JSpecify": "1.0.0.6",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3"
+ }
+ },
+ "Xamarin.AndroidX.Lifecycle.LiveData.Core": {
+ "type": "Transitive",
+ "resolved": "2.10.0.2",
+ "contentHash": "4V6+6FbIzIhSVjLSrWXnz7WkGqJvjgXYSqKlI2d8fkCGkAoxcjsBqCqFoxS3uTnNHdpmmRP37y2XxsjM44Vm7w==",
+ "dependencies": {
+ "Xamarin.AndroidX.Arch.Core.Common": "2.2.0.20",
+ "Xamarin.AndroidX.Arch.Core.Runtime": "2.2.0.20",
+ "Xamarin.AndroidX.Lifecycle.Common": "[2.10.0.2, 2.10.1)",
+ "Xamarin.JSpecify": "1.0.0.6",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Lifecycle.Process": {
+ "type": "Transitive",
+ "resolved": "2.10.0.2",
+ "contentHash": "hmlTAohFRnBhf86TeayiVeyE0CKUOoAELRP2hv3d36vPdxpAVrP+v9Lr104AKZUVoY7jsVuO9akAgUANY2aqRA==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Lifecycle.Runtime": "[2.10.0.2, 2.10.1)",
+ "Xamarin.AndroidX.Startup.StartupRuntime": "1.2.0.7",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Lifecycle.Runtime": {
+ "type": "Transitive",
+ "resolved": "2.10.0.2",
+ "contentHash": "V/Ab8a1jULOsIYTakUH1fxfznrr5BMa1wE1Bscq34hvq0Xd+pWdrf95bbO870H62lryRGMaS/Np6XQ/NnGgn2w==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Lifecycle.Common": "[2.10.0.2, 2.10.1)",
+ "Xamarin.AndroidX.Lifecycle.Runtime.Android": "2.10.0.2",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Lifecycle.Runtime.Android": {
+ "type": "Transitive",
+ "resolved": "2.10.0.2",
+ "contentHash": "bXducXU2t/Tp7NfZjyBFaPFMNY/tFYogxvq8Q9EqZsws9Eu/DvwWzsUs1WnrfOc8Dh505CaM+dRjtpGHD2/BcQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation.Jvm": "1.9.1.7",
+ "Xamarin.AndroidX.Arch.Core.Common": "2.2.0.20",
+ "Xamarin.AndroidX.Arch.Core.Runtime": "2.2.0.20",
+ "Xamarin.AndroidX.Core.ViewTree": "1.0.0.5",
+ "Xamarin.AndroidX.Lifecycle.Common.Jvm": "[2.10.0.2, 2.10.1)",
+ "Xamarin.AndroidX.ProfileInstaller.ProfileInstaller": "1.4.1.7",
+ "Xamarin.JSpecify": "1.0.0.6",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Android": "1.10.2.3"
+ }
+ },
+ "Xamarin.AndroidX.Lifecycle.ViewModel": {
+ "type": "Transitive",
+ "resolved": "2.10.0.2",
+ "contentHash": "kc9DGem8or9Olsa/xKDruV7a41GVPhao951Tq3zpdH5MxIqimdW6IRmi6Ieqq5TtEuSQJQQpkzf6tcdGQbVlNg==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Lifecycle.ViewModel.Android": "2.10.0.2",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3"
+ }
+ },
+ "Xamarin.AndroidX.Lifecycle.ViewModel.Android": {
+ "type": "Transitive",
+ "resolved": "2.10.0.2",
+ "contentHash": "cCu5OlCXfg4WnpCGSldOgV6eKfH+Aly+MpJKtJORMLeKpNGncEDsH5Ob9dxYJN2CcLIhpYVlEITKwZ2hU+UUyQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation.Jvm": "1.9.1.7",
+ "Xamarin.AndroidX.Core.ViewTree": "1.0.0.5",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Android": "1.10.2.3",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3"
+ }
+ },
+ "Xamarin.AndroidX.Lifecycle.ViewModelSavedState": {
+ "type": "Transitive",
+ "resolved": "2.10.0.2",
+ "contentHash": "FW3rckNEnS3Dx7Dig5+ONjBkwYAv69p+JfsGZG/mQhETcNdeZl60SBmBRrwiqhv8AoWObq53gsHt7P3O87Qhzw==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Lifecycle.Common": "[2.10.0.2, 2.10.1)",
+ "Xamarin.AndroidX.Lifecycle.ViewModel": "[2.10.0.2, 2.10.1)",
+ "Xamarin.AndroidX.Lifecycle.ViewModelSavedState.Android": "2.10.0.2",
+ "Xamarin.AndroidX.SavedState": "1.4.0.2",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3",
+ "Xamarin.KotlinX.Serialization.Core": "1.10.0.1"
+ }
+ },
+ "Xamarin.AndroidX.Lifecycle.ViewModelSavedState.Android": {
+ "type": "Transitive",
+ "resolved": "2.10.0.2",
+ "contentHash": "7lyx2t/ltqbC03jPZP3Ufsg0PqeD8yfmlqrq59lF5c/dJz3Y5JeICq8n+aY5T0c+WHzia4AdZ2gPJJ3Rk/l7EQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation.Jvm": "1.9.1.7",
+ "Xamarin.AndroidX.Core.Core.Ktx": "1.17.0.2",
+ "Xamarin.AndroidX.Lifecycle.Common.Jvm": "[2.10.0.2, 2.10.1)",
+ "Xamarin.AndroidX.Lifecycle.LiveData.Core": "[2.10.0.2, 2.10.1)",
+ "Xamarin.AndroidX.Lifecycle.ViewModel.Android": "[2.10.0.2, 2.10.1)",
+ "Xamarin.AndroidX.SavedState.SavedState.Android": "1.4.0.2",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Android": "1.10.2.3",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3",
+ "Xamarin.KotlinX.Serialization.Core.Jvm": "1.10.0.1"
+ }
+ },
+ "Xamarin.AndroidX.Loader": {
+ "type": "Transitive",
+ "resolved": "1.1.0.36",
+ "contentHash": "QwX56hQ4xC2XMU+Pba8DOnLvq+B/zDHkqlRReEf2qm62O97RX9lRxET64Rc/4ssJEEornDD9KMpn2aNftxEIMA==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.AndroidX.Lifecycle.LiveData.Core": "2.10.0.2",
+ "Xamarin.AndroidX.Lifecycle.ViewModel": "2.10.0.2"
+ }
+ },
+ "Xamarin.AndroidX.NavigationEvent": {
+ "type": "Transitive",
+ "resolved": "1.0.2.1",
+ "contentHash": "39yEKeQBzL3GiSHlewvmKFMwT5o5RdodoTTAKDWRz6pmKwXG+OOrUB31IhSjUTAs7n70SZ/9KXDzgmIaPVBsBQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Compose.Runtime.Annotation": "1.10.4.1",
+ "Xamarin.AndroidX.NavigationEvent.Android": "1.0.2.1",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3"
+ }
+ },
+ "Xamarin.AndroidX.NavigationEvent.Android": {
+ "type": "Transitive",
+ "resolved": "1.0.2.1",
+ "contentHash": "Zm4qWQY03CuvFWFQjGFkB6mCg2Ycuqmhd+vpitKyMm7bd/DJf9KK6ox3Kh9/MdI8pSiYcXr6fpjLoXM51jCGOw==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation.Jvm": "1.9.1.7",
+ "Xamarin.AndroidX.Compose.Runtime.Annotation.Android": "1.10.4.1",
+ "Xamarin.AndroidX.Core.ViewTree": "1.0.0.5",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Android": "1.10.2.3",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3"
+ }
+ },
+ "Xamarin.AndroidX.ProfileInstaller.ProfileInstaller": {
+ "type": "Transitive",
+ "resolved": "1.4.1.7",
+ "contentHash": "Xw4x6ZA4dSC5Fe49fbm5YJRblhAAANbcONidZHncznImuJZ0XJngQhbjvDFNI/UipP3OD41SOSpGHIweRJYagQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Concurrent.Futures": "1.3.0.3",
+ "Xamarin.AndroidX.Startup.StartupRuntime": "1.2.0.7",
+ "Xamarin.Google.Guava.ListenableFuture": "1.0.0.31"
+ }
+ },
+ "Xamarin.AndroidX.ResourceInspection.Annotation": {
+ "type": "Transitive",
+ "resolved": "1.0.1.24",
+ "contentHash": "jJKNvyHyMP7BbLw3eI4SatY7hbu1Qb+Y9xQ/p6AEExDOchTKRpg1mTEV9rp57OvgRek7QgPNLwgyMxT5O3mZIw==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7"
+ }
+ },
+ "Xamarin.AndroidX.SavedState": {
+ "type": "Transitive",
+ "resolved": "1.4.0.2",
+ "contentHash": "5auFgyvy2HRKz7w7w5W3zKZ49CoW30yhOjprqS2IGy5lPvLG4J2ZunO88cUjQWPu7xsXcEq74cpw2wzMnvUY/A==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Lifecycle.Common": "2.10.0.2",
+ "Xamarin.AndroidX.SavedState.SavedState.Android": "1.4.0.2",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3",
+ "Xamarin.KotlinX.Serialization.Core": "1.10.0.1"
+ }
+ },
+ "Xamarin.AndroidX.SavedState.SavedState.Android": {
+ "type": "Transitive",
+ "resolved": "1.4.0.2",
+ "contentHash": "tka3EwzK3asA5aTOt213TNNOVCJR3Q1qd1GWdSWYwNerhyEl31nKjW4lOWagjwyG8ai8dJtZVapXZ0+NMkoIwg==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation.Jvm": "1.9.1.7",
+ "Xamarin.AndroidX.Core.Core.Ktx": "1.17.0.2",
+ "Xamarin.AndroidX.Core.ViewTree": "1.0.0.5",
+ "Xamarin.AndroidX.Lifecycle.Common.Jvm": "2.10.0.2",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Core": "1.10.2.3",
+ "Xamarin.KotlinX.Serialization.Core.Jvm": "1.10.0.1"
+ }
+ },
+ "Xamarin.AndroidX.Startup.StartupRuntime": {
+ "type": "Transitive",
+ "resolved": "1.2.0.7",
+ "contentHash": "i3ZYBjKcaXahM64NK7+rHGImEK0dmAt8YjBw2mfdLOkmhMApC6TXVTEq58lLA2UdhuOCbAJTBdxdq7E6GQXbDw==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Tracing.Tracing": "1.3.0.3"
+ }
+ },
+ "Xamarin.AndroidX.Tracing.Tracing": {
+ "type": "Transitive",
+ "resolved": "1.3.0.3",
+ "contentHash": "RiUQWISFX42hjv4gS0LgFkDOJbVJOAOE1eITV0epUQk5hQJI6E0MUYAHtXEplg39lbp/bqHfOST/E8IbE1/n5w==",
+ "dependencies": {
+ "Xamarin.AndroidX.Tracing.Tracing.Android": "1.3.0.3",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Tracing.Tracing.Android": {
+ "type": "Transitive",
+ "resolved": "1.3.0.3",
+ "contentHash": "xxh4wt+VMEjO6PTEo0XLwyVaDyVTS4nPdM4qMVI2sxC8ozSAPtpZEmUzeaUgaHbX7zgZEmUO/Tv+ZCQWOjLMmQ==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation.Jvm": "1.9.1.7",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.VectorDrawable": {
+ "type": "Transitive",
+ "resolved": "1.2.0.10",
+ "contentHash": "ZRoUx2Qbu3pEBSvKmGlvHOXmIMqaCr0LQxhs24dJwmHVkwTi22l11T4ejtTbo2Gp75e5KI2tvHmh5FpO1bHnFA==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Core": "1.17.0.2"
+ }
+ },
+ "Xamarin.AndroidX.VectorDrawable.Animated": {
+ "type": "Transitive",
+ "resolved": "1.2.0.10",
+ "contentHash": "h9iGCtft2L2em5SRSSpOh1p49fhsaR43UvXt54qRhXeR5lXdnF2LBwH1rxv4E7TPdR9AfMKzki+B8mjrMZWh4w==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.AndroidX.Interpolator": "1.0.0.36",
+ "Xamarin.AndroidX.VectorDrawable": "1.2.0.10"
+ }
+ },
+ "Xamarin.AndroidX.VersionedParcelable": {
+ "type": "Transitive",
+ "resolved": "1.2.1.5",
+ "contentHash": "ToYJpFhA6l9wAGD53XnwNIxcQEeKC4vojlULGXuizzwkOYYgfpIvqT9YJoxGJqYJSg/WpFdXJhUey2HtXioMHg==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.JSpecify": "1.0.0.6"
+ }
+ },
+ "Xamarin.AndroidX.ViewPager": {
+ "type": "Transitive",
+ "resolved": "1.1.0.6",
+ "contentHash": "RCN1PymyCFY/7VSnZNOdOErU6OgEQg5n0dAOy8qxQKjoaA3m39JEo9wUATrXi3PwITBbCDMlxn4a/c13ZDa0Bw==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.AndroidX.CustomView": "1.2.0.3"
+ }
+ },
+ "Xamarin.AndroidX.Window": {
+ "type": "Transitive",
+ "resolved": "1.5.1.2",
+ "contentHash": "7f1EmaLM3xQFPbNKirCYRxTWW0EWDxWSORHvMVnmlGetBwt4iue8KjJ0mqw5Veq2tv1Q8SBrlT+jHqTXNxvvcA==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Collection": "1.5.0.5",
+ "Xamarin.AndroidX.Core": "1.17.0.2",
+ "Xamarin.AndroidX.Window.WindowCore": "[1.5.1.2, 1.5.2)",
+ "Xamarin.JSpecify": "1.0.0.6",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Android": "1.10.2.3"
+ }
+ },
+ "Xamarin.AndroidX.Window.WindowCore": {
+ "type": "Transitive",
+ "resolved": "1.5.1.2",
+ "contentHash": "mpO/kUUF/l3sipJM9yyNyHk7UpJ83wjNl2BMkMEuy29fVka1sb7avjAZPOuYICFIJE2gW9W/wvAoWclLLwVBXw==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation": "1.9.1.7",
+ "Xamarin.AndroidX.Window.WindowCore.Jvm": "1.5.1.2",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.AndroidX.Window.WindowCore.Jvm": {
+ "type": "Transitive",
+ "resolved": "1.5.1.2",
+ "contentHash": "ovPEz7SEdYUlA7wiwToLJLmJ7YkVEfoI9ZOAiKOO3HnqjaoC3PIzzCP9ybJGLF3FaHpfFwm6LTXEU+llaQSGbg==",
+ "dependencies": {
+ "Xamarin.AndroidX.Annotation.Jvm": "1.9.1.7",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.Google.Guava.ListenableFuture": {
+ "type": "Transitive",
+ "resolved": "1.0.0.31",
+ "contentHash": "V5w2z1+O1hGVexBvWOS+PemiVDU1wWx9EuK8J6+xqn7GgB1OPRe2GSeU3xexeNgrKF1+X64R0Vuv9bYoPDfIiA=="
+ },
+ "Xamarin.Jetbrains.Annotations": {
+ "type": "Transitive",
+ "resolved": "26.1.0.1",
+ "contentHash": "GQk+C9cG/ss7q3I73ewWEK6cSI7oXk5x6Z/LCe/m2Pf47SwLX5DYlsadIbSAJvxkRzeatm/SPd6SKU8kWQ1enA=="
+ },
+ "Xamarin.JSpecify": {
+ "type": "Transitive",
+ "resolved": "1.0.0.6",
+ "contentHash": "LilLB52Pijbp+5tSsLCT/POAjsTcALD95roNJvQAZWtp7qDem/pny2oZsgUVQ143kQIpMkZ2EETVeL4nI1Kwww=="
+ },
+ "Xamarin.Kotlin.StdLib": {
+ "type": "Transitive",
+ "resolved": "2.3.10.1",
+ "contentHash": "FJ2Yjvz/jHBXdQuF5KHDL27gacZ0WTBssABiopSBWE6ODa6jJiweld/3TqyhPUB9OEOgHiOAGUhlL98XLzdftw==",
+ "dependencies": {
+ "Xamarin.Jetbrains.Annotations": "26.1.0.1"
+ }
+ },
+ "Xamarin.KotlinX.Coroutines.Android": {
+ "type": "Transitive",
+ "resolved": "1.10.2.3",
+ "contentHash": "me/1VhRcYj46E/vn36vdgUZ11uGH69u/2JG6q+5i8kSL+3Tb10MN9HqTj4r6S4oloLP8QNhZ4yz/vIi6amk5qA==",
+ "dependencies": {
+ "Xamarin.Kotlin.StdLib": "2.3.10.1",
+ "Xamarin.KotlinX.Coroutines.Core.Jvm": "1.10.2.3"
+ }
+ },
+ "Xamarin.KotlinX.Coroutines.Core": {
+ "type": "Transitive",
+ "resolved": "1.10.2.3",
+ "contentHash": "nUFTp3liDGGgQHrPPoDxIOmgPE1qGqdbPLVapldo54Inn79fyhYQZ05z8cM7mPgC1a6AMqQcxca33zVeijD+tQ==",
+ "dependencies": {
+ "Xamarin.KotlinX.Coroutines.Core.Jvm": "1.10.2.3"
+ }
+ },
+ "Xamarin.KotlinX.Coroutines.Core.Jvm": {
+ "type": "Transitive",
+ "resolved": "1.10.2.3",
+ "contentHash": "7JxlouNHPQXVEHJpZcb6FXum/XYGepJuhujcs7dBO3sVaeQHSiDevDzZUOcKVnGyzOWfypyt4MLfAqteSzqdsg==",
+ "dependencies": {
+ "Xamarin.Jetbrains.Annotations": "26.1.0.1",
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "Xamarin.KotlinX.Serialization.Core": {
+ "type": "Transitive",
+ "resolved": "1.10.0.1",
+ "contentHash": "tw1jX3l3XgbK0sTU2KVCt/o4OOLtrZwF69A46W6DhVZyQgrW252bn7sC8+9RIb2/k0hGSIJvEA0kYXCFbfkj1g==",
+ "dependencies": {
+ "Xamarin.KotlinX.Serialization.Core.Jvm": "1.10.0.1"
+ }
+ },
+ "Xamarin.KotlinX.Serialization.Core.Jvm": {
+ "type": "Transitive",
+ "resolved": "1.10.0.1",
+ "contentHash": "kefEHOomdFnoJCE9WDKuJJLFKcqh1wLgmFWXAtGZ7+XiNOCJJDyqsj9J9gwmh/FSqsjm9e7dKWHnUJkwN50bUA==",
+ "dependencies": {
+ "Xamarin.Kotlin.StdLib": "2.3.10.1"
+ }
+ },
+ "dodossh.client.api": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Client.Auth": "[1.0.0, )",
+ "DodoSSH.Contracts": "[1.0.0, )",
+ "DodoSSH.Crypto": "[1.0.0, )"
+ }
+ },
+ "dodossh.client.auth": {
+ "type": "Project"
+ },
+ "dodossh.client.domain": {
+ "type": "Project"
+ },
+ "dodossh.client.session": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Client.Api": "[1.0.0, )",
+ "DodoSSH.Client.Auth": "[1.0.0, )",
+ "DodoSSH.Client.Domain": "[1.0.0, )",
+ "DodoSSH.Client.Ssh": "[1.0.0, )",
+ "DodoSSH.Client.Storage": "[1.0.0, )",
+ "DodoSSH.Client.Sync": "[1.0.0, )"
+ }
+ },
+ "dodossh.client.shell": {
+ "type": "Project",
+ "dependencies": {
+ "Avalonia": "[12.1.1, )",
+ "CommunityToolkit.Mvvm": "[8.4.2, )",
+ "DodoSSH.Client.Session": "[1.0.0, )",
+ "DodoSSH.Client.Ssh": "[1.0.0, )",
+ "DodoSSH.Client.Terminal": "[1.0.0, )",
+ "DodoSSH.Client.Transfer": "[1.0.0, )"
+ }
+ },
+ "dodossh.client.ssh": {
+ "type": "Project",
+ "dependencies": {
+ "SSH.NET": "[2025.1.0, )"
+ }
+ },
+ "dodossh.client.storage": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Contracts": "[1.0.0, )",
+ "DodoSSH.Crypto": "[1.0.0, )",
+ "EFCore.NamingConventions": "[10.0.1, )",
+ "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.10, )"
+ }
+ },
+ "dodossh.client.sync": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Client.Api": "[1.0.0, )",
+ "DodoSSH.Client.Domain": "[1.0.0, )",
+ "DodoSSH.Client.Storage": "[1.0.0, )",
+ "DodoSSH.Contracts": "[1.0.0, )",
+ "DodoSSH.Crypto": "[1.0.0, )"
+ }
+ },
+ "dodossh.client.terminal": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Client.Ssh": "[1.0.0, )"
+ }
+ },
+ "dodossh.client.transfer": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Client.Ssh": "[1.0.0, )"
+ }
+ },
+ "dodossh.contracts": {
+ "type": "Project"
+ },
+ "dodossh.crypto": {
+ "type": "Project",
+ "dependencies": {
+ "NSec.Cryptography": "[26.4.0, )"
+ }
+ },
+ "BouncyCastle.Cryptography": {
+ "type": "CentralTransitive",
+ "requested": "[2.6.2, )",
+ "resolved": "2.6.2",
+ "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
+ },
+ "EFCore.NamingConventions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1"
+ }
+ },
+ "libsodium": {
+ "type": "CentralTransitive",
+ "requested": "[1.0.22, )",
+ "resolved": "1.0.22",
+ "contentHash": "KPD9SloJFclrsjnhABu7dzWrcyYkwPbvx5l1gRSPAX/0n+OBtSiVCKtGFv4n+ecWUHU0tCG9LSSwoZZx673zBQ=="
+ },
+ "Microsoft.EntityFrameworkCore": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.10",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.10",
+ "Microsoft.Extensions.Caching.Memory": "10.0.10",
+ "Microsoft.Extensions.Logging": "10.0.10"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Relational": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "10.0.10",
+ "Microsoft.Extensions.Caching.Memory": "10.0.10",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Logging": "10.0.10"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Sqlite": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "kzg9MuQNJvZQxAU+piSkEzc7/1tpW6n1nVSGGMObu2GgxLK8Nf+6fvZundaznTZ+O2KhfPZ8HFNCzMH3PWDUmA==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.10",
+ "Microsoft.Extensions.Caching.Memory": "10.0.10",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
+ "Microsoft.Extensions.DependencyModel": "10.0.10",
+ "Microsoft.Extensions.Logging": "10.0.10",
+ "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
+ "SQLitePCLRaw.core": "2.1.11"
+ }
+ },
+ "NSec.Cryptography": {
+ "type": "CentralTransitive",
+ "requested": "[26.4.0, )",
+ "resolved": "26.4.0",
+ "contentHash": "0vsCtY5f+YgQROiWNqzgWp+l2pddfk9FkWoGV/bEo0MuEYPKlJWuoA8aOfO6qp3f+EnObKE3zSJhn1PspJeJVg==",
+ "dependencies": {
+ "libsodium": "[1.0.22, 1.0.23)"
+ }
+ },
+ "SQLitePCLRaw.bundle_e_sqlite3": {
+ "type": "CentralTransitive",
+ "requested": "[2.1.12, )",
+ "resolved": "2.1.12",
+ "contentHash": "mAgscpQMLw5/nfA1Q5oJVAT29yROUo1ifZGbbTpx/lwZpSxMUGoYbKfmvdm8oXER+RzxqBmmQzeBEVKfeHv2nw==",
+ "dependencies": {
+ "SQLitePCLRaw.lib.e_sqlite3.android": "2.1.12",
+ "SQLitePCLRaw.provider.e_sqlite3": "2.1.12"
+ }
+ },
+ "SQLitePCLRaw.core": {
+ "type": "CentralTransitive",
+ "requested": "[2.1.12, )",
+ "resolved": "2.1.12",
+ "contentHash": "ETpNw9DY3ckWLgRRAeCHj+GKOuPi61aeczkXhgHexUvqoZBAYg8RYESE2J7O1M7+o6QbdSEZwrw9bfqztUVWXg=="
+ },
+ "SQLitePCLRaw.provider.e_sqlite3": {
+ "type": "CentralTransitive",
+ "requested": "[2.1.12, )",
+ "resolved": "2.1.12",
+ "contentHash": "W3oH4XIfCzFrgUSDKHhN6N+dgzA5YHOR2VxX8GB6Qy7CyrJJgxPEG8NirgYWlPQC5P2jz2knSsexWu4tDUL33g==",
+ "dependencies": {
+ "SQLitePCLRaw.core": "2.1.12"
+ }
+ },
+ "SSH.NET": {
+ "type": "CentralTransitive",
+ "requested": "[2025.1.0, )",
+ "resolved": "2025.1.0",
+ "contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
+ "dependencies": {
+ "BouncyCastle.Cryptography": "2.6.2",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3"
+ }
+ }
+ },
+ "net10.0-android36.0/android-arm64": {
+ "HarfBuzzSharp.NativeAssets.Android": {
+ "type": "Transitive",
+ "resolved": "8.3.1.3",
+ "contentHash": "Yte9/yYql8ngAjo7YgHlXSinLJcJXIRBM9gegVXpJ2SVYT1i2O/wMA+H3jmYiYiTQxHpHKi4exZUcMzry171MA=="
+ },
+ "HarfBuzzSharp.NativeAssets.Linux": {
+ "type": "Transitive",
+ "resolved": "8.3.1.3",
+ "contentHash": "RI6A1LgmooU30+4QIyFt5rmBCzP0VzTR+587IJSGvYIsHHWlahFufihYxtraLfsIhW7I8dn6+xX+DZGygOPKWQ=="
+ },
+ "SkiaSharp.NativeAssets.Android": {
+ "type": "Transitive",
+ "resolved": "3.119.4",
+ "contentHash": "qfjNh5hZBZxpOIM1aeDByj2qNbcK2JZG5Y7YyGSeliaYnf1N/hVfsswIPUa+qzcMqS9Q0VCGk85zQLvwVXtrvQ=="
+ },
+ "SkiaSharp.NativeAssets.Linux": {
+ "type": "Transitive",
+ "resolved": "3.119.4",
+ "contentHash": "UAyVzbqNfZsZbKbzj68zXLyUyF/SbTKmzTfOO6qDu++dtIUMMTzPBe8oOuzU/DiewpfKoUUlOSsJmqWc6blxBw=="
+ },
+ "libsodium": {
+ "type": "CentralTransitive",
+ "requested": "[1.0.22, )",
+ "resolved": "1.0.22",
+ "contentHash": "KPD9SloJFclrsjnhABu7dzWrcyYkwPbvx5l1gRSPAX/0n+OBtSiVCKtGFv4n+ecWUHU0tCG9LSSwoZZx673zBQ=="
+ }
+ },
+ "net10.0-android36.0/android-x64": {
+ "HarfBuzzSharp.NativeAssets.Android": {
+ "type": "Transitive",
+ "resolved": "8.3.1.3",
+ "contentHash": "Yte9/yYql8ngAjo7YgHlXSinLJcJXIRBM9gegVXpJ2SVYT1i2O/wMA+H3jmYiYiTQxHpHKi4exZUcMzry171MA=="
+ },
+ "HarfBuzzSharp.NativeAssets.Linux": {
+ "type": "Transitive",
+ "resolved": "8.3.1.3",
+ "contentHash": "RI6A1LgmooU30+4QIyFt5rmBCzP0VzTR+587IJSGvYIsHHWlahFufihYxtraLfsIhW7I8dn6+xX+DZGygOPKWQ=="
+ },
+ "SkiaSharp.NativeAssets.Android": {
+ "type": "Transitive",
+ "resolved": "3.119.4",
+ "contentHash": "qfjNh5hZBZxpOIM1aeDByj2qNbcK2JZG5Y7YyGSeliaYnf1N/hVfsswIPUa+qzcMqS9Q0VCGk85zQLvwVXtrvQ=="
+ },
+ "SkiaSharp.NativeAssets.Linux": {
+ "type": "Transitive",
+ "resolved": "3.119.4",
+ "contentHash": "UAyVzbqNfZsZbKbzj68zXLyUyF/SbTKmzTfOO6qDu++dtIUMMTzPBe8oOuzU/DiewpfKoUUlOSsJmqWc6blxBw=="
+ },
+ "libsodium": {
+ "type": "CentralTransitive",
+ "requested": "[1.0.22, )",
+ "resolved": "1.0.22",
+ "contentHash": "KPD9SloJFclrsjnhABu7dzWrcyYkwPbvx5l1gRSPAX/0n+OBtSiVCKtGFv4n+ecWUHU0tCG9LSSwoZZx673zBQ=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DodoSSH.Client.App/App.axaml b/src/DodoSSH.Client.App/App.axaml
index f8fc514..3a181cc 100644
--- a/src/DodoSSH.Client.App/App.axaml
+++ b/src/DodoSSH.Client.App/App.axaml
@@ -12,73 +12,15 @@
- #0A0C0B
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- #3CE88F
-
-
-
-
-
-
-
-
-
-
-
-
-
- ui-monospace,Cascadia Mono,Consolas,monospace
+
+
+
+
+
diff --git a/src/DodoSSH.Client.App/App.axaml.cs b/src/DodoSSH.Client.App/App.axaml.cs
index cee0488..dd4c443 100644
--- a/src/DodoSSH.Client.App/App.axaml.cs
+++ b/src/DodoSSH.Client.App/App.axaml.cs
@@ -1,8 +1,8 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
-using DodoSSH.Client.App.Terminal;
-using DodoSSH.Client.App.ViewModels;
+using DodoSSH.Client.Shell.Terminal;
+using DodoSSH.Client.Shell.ViewModels;
using DodoSSH.Client.App.Views;
using DodoSSH.Client.Auth;
using DodoSSH.Client.Session;
diff --git a/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj b/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj
index ce05c36..1bce454 100644
--- a/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj
+++ b/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj
@@ -25,6 +25,11 @@
+
+
@@ -45,13 +50,4 @@
-
-
-
-
-
diff --git a/src/DodoSSH.Client.App/Views/ConfirmDeleteCard.axaml b/src/DodoSSH.Client.App/Views/ConfirmDeleteCard.axaml
index 8b40903..f55853e 100644
--- a/src/DodoSSH.Client.App/Views/ConfirmDeleteCard.axaml
+++ b/src/DodoSSH.Client.App/Views/ConfirmDeleteCard.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/Views/HostSidebar.axaml b/src/DodoSSH.Client.App/Views/HostSidebar.axaml
index a6f563b..0fe36fe 100644
--- a/src/DodoSSH.Client.App/Views/HostSidebar.axaml
+++ b/src/DodoSSH.Client.App/Views/HostSidebar.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/Views/HostSidebar.axaml.cs b/src/DodoSSH.Client.App/Views/HostSidebar.axaml.cs
index 383677d..afcbb2d 100644
--- a/src/DodoSSH.Client.App/Views/HostSidebar.axaml.cs
+++ b/src/DodoSSH.Client.App/Views/HostSidebar.axaml.cs
@@ -1,6 +1,6 @@
using Avalonia.Controls;
using Avalonia.Input;
-using DodoSSH.Client.App.ViewModels;
+using DodoSSH.Client.Shell.ViewModels;
namespace DodoSSH.Client.App.Views;
diff --git a/src/DodoSSH.Client.App/Views/MainWindow.axaml b/src/DodoSSH.Client.App/Views/MainWindow.axaml
index 2a1be7b..f21ba9c 100644
--- a/src/DodoSSH.Client.App/Views/MainWindow.axaml
+++ b/src/DodoSSH.Client.App/Views/MainWindow.axaml
@@ -1,7 +1,7 @@
diff --git a/src/DodoSSH.Client.App/Views/PreferencesScreen.axaml b/src/DodoSSH.Client.App/Views/PreferencesScreen.axaml
index 0bd35c3..b9d112a 100644
--- a/src/DodoSSH.Client.App/Views/PreferencesScreen.axaml
+++ b/src/DodoSSH.Client.App/Views/PreferencesScreen.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/Views/QuickConnect.axaml b/src/DodoSSH.Client.App/Views/QuickConnect.axaml
index c684a83..5f6162b 100644
--- a/src/DodoSSH.Client.App/Views/QuickConnect.axaml
+++ b/src/DodoSSH.Client.App/Views/QuickConnect.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/Views/QuickConnect.axaml.cs b/src/DodoSSH.Client.App/Views/QuickConnect.axaml.cs
index bed54d7..6aa8085 100644
--- a/src/DodoSSH.Client.App/Views/QuickConnect.axaml.cs
+++ b/src/DodoSSH.Client.App/Views/QuickConnect.axaml.cs
@@ -3,7 +3,7 @@ using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
-using DodoSSH.Client.App.ViewModels;
+using DodoSSH.Client.Shell.ViewModels;
namespace DodoSSH.Client.App.Views;
diff --git a/src/DodoSSH.Client.App/Views/SignOutCard.axaml b/src/DodoSSH.Client.App/Views/SignOutCard.axaml
index 0a1593e..a2af229 100644
--- a/src/DodoSSH.Client.App/Views/SignOutCard.axaml
+++ b/src/DodoSSH.Client.App/Views/SignOutCard.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/Views/StatusBar.axaml b/src/DodoSSH.Client.App/Views/StatusBar.axaml
index bea62f3..e2ebfcf 100644
--- a/src/DodoSSH.Client.App/Views/StatusBar.axaml
+++ b/src/DodoSSH.Client.App/Views/StatusBar.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/Views/TerminalTabs.axaml b/src/DodoSSH.Client.App/Views/TerminalTabs.axaml
index 08ee785..5a04313 100644
--- a/src/DodoSSH.Client.App/Views/TerminalTabs.axaml
+++ b/src/DodoSSH.Client.App/Views/TerminalTabs.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/Views/TitleBar.axaml b/src/DodoSSH.Client.App/Views/TitleBar.axaml
index 357d066..34cd55f 100644
--- a/src/DodoSSH.Client.App/Views/TitleBar.axaml
+++ b/src/DodoSSH.Client.App/Views/TitleBar.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/Views/TransfersScreen.axaml b/src/DodoSSH.Client.App/Views/TransfersScreen.axaml
index 6408529..49a268f 100644
--- a/src/DodoSSH.Client.App/Views/TransfersScreen.axaml
+++ b/src/DodoSSH.Client.App/Views/TransfersScreen.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/Views/TransfersScreen.axaml.cs b/src/DodoSSH.Client.App/Views/TransfersScreen.axaml.cs
index ece7bbd..2e1cd71 100644
--- a/src/DodoSSH.Client.App/Views/TransfersScreen.axaml.cs
+++ b/src/DodoSSH.Client.App/Views/TransfersScreen.axaml.cs
@@ -1,6 +1,6 @@
using Avalonia.Controls;
using Avalonia.Input;
-using DodoSSH.Client.App.ViewModels;
+using DodoSSH.Client.Shell.ViewModels;
namespace DodoSSH.Client.App.Views;
diff --git a/src/DodoSSH.Client.App/Views/UnlockCard.axaml b/src/DodoSSH.Client.App/Views/UnlockCard.axaml
index 714cead..5ac85bd 100644
--- a/src/DodoSSH.Client.App/Views/UnlockCard.axaml
+++ b/src/DodoSSH.Client.App/Views/UnlockCard.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/Views/VaultScreen.axaml b/src/DodoSSH.Client.App/Views/VaultScreen.axaml
index 2ec0f3f..3b235a5 100644
--- a/src/DodoSSH.Client.App/Views/VaultScreen.axaml
+++ b/src/DodoSSH.Client.App/Views/VaultScreen.axaml
@@ -1,6 +1,6 @@
diff --git a/src/DodoSSH.Client.App/packages.lock.json b/src/DodoSSH.Client.App/packages.lock.json
index 2eb2975..052b253 100644
--- a/src/DodoSSH.Client.App/packages.lock.json
+++ b/src/DodoSSH.Client.App/packages.lock.json
@@ -360,6 +360,17 @@
"DodoSSH.Client.Sync": "[1.0.0, )"
}
},
+ "dodossh.client.shell": {
+ "type": "Project",
+ "dependencies": {
+ "Avalonia": "[12.1.1, )",
+ "CommunityToolkit.Mvvm": "[8.4.2, )",
+ "DodoSSH.Client.Session": "[1.0.0, )",
+ "DodoSSH.Client.Ssh": "[1.0.0, )",
+ "DodoSSH.Client.Terminal": "[1.0.0, )",
+ "DodoSSH.Client.Transfer": "[1.0.0, )"
+ }
+ },
"dodossh.client.ssh": {
"type": "Project",
"dependencies": {
diff --git a/src/DodoSSH.Client.Shell/DodoSSH.Client.Shell.csproj b/src/DodoSSH.Client.Shell/DodoSSH.Client.Shell.csproj
new file mode 100644
index 0000000..3938921
--- /dev/null
+++ b/src/DodoSSH.Client.Shell/DodoSSH.Client.Shell.csproj
@@ -0,0 +1,54 @@
+
+
+
+ true
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DodoSSH.Client.App/Terminal/AvaloniaTerminalAssetProvider.cs b/src/DodoSSH.Client.Shell/Terminal/AvaloniaTerminalAssetProvider.cs
similarity index 96%
rename from src/DodoSSH.Client.App/Terminal/AvaloniaTerminalAssetProvider.cs
rename to src/DodoSSH.Client.Shell/Terminal/AvaloniaTerminalAssetProvider.cs
index 85d8a87..185c911 100644
--- a/src/DodoSSH.Client.App/Terminal/AvaloniaTerminalAssetProvider.cs
+++ b/src/DodoSSH.Client.Shell/Terminal/AvaloniaTerminalAssetProvider.cs
@@ -1,7 +1,7 @@
using Avalonia.Platform;
using DodoSSH.Client.Terminal;
-namespace DodoSSH.Client.App.Terminal;
+namespace DodoSSH.Client.Shell.Terminal;
///
/// Serves the renderer's files from the assembly's embedded resources.
@@ -13,7 +13,7 @@ namespace DodoSSH.Client.App.Terminal;
///
internal sealed class AvaloniaTerminalAssetProvider : ITerminalAssetProvider
{
- private const string ResourceRoot = "avares://DodoSSH.Client.App/WebAssets";
+ private const string ResourceRoot = "avares://DodoSSH.Client.Shell/WebAssets";
private static readonly (string Path, string File, string ContentType)[] Files =
[
diff --git a/src/DodoSSH.Client.Shell/Theme/Palette.axaml b/src/DodoSSH.Client.Shell/Theme/Palette.axaml
new file mode 100644
index 0000000..c191131
--- /dev/null
+++ b/src/DodoSSH.Client.Shell/Theme/Palette.axaml
@@ -0,0 +1,81 @@
+
+
+
+ #0A0C0B
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #3CE88F
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ui-monospace,Cascadia Mono,Consolas,monospace
+
+
diff --git a/src/DodoSSH.Client.App/ViewModels/MainWindowViewModel.cs b/src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs
similarity index 99%
rename from src/DodoSSH.Client.App/ViewModels/MainWindowViewModel.cs
rename to src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs
index 5deb450..4347427 100644
--- a/src/DodoSSH.Client.App/ViewModels/MainWindowViewModel.cs
+++ b/src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs
@@ -12,7 +12,7 @@ using DodoSSH.Client.Storage;
using DodoSSH.Client.Terminal;
using DodoSSH.Crypto;
-namespace DodoSSH.Client.App.ViewModels;
+namespace DodoSSH.Client.Shell.ViewModels;
/// Which of the shell's mutually exclusive screens is showing.
internal enum ShellState
diff --git a/src/DodoSSH.Client.App/ViewModels/TerminalTabViewModel.cs b/src/DodoSSH.Client.Shell/ViewModels/TerminalTabViewModel.cs
similarity index 98%
rename from src/DodoSSH.Client.App/ViewModels/TerminalTabViewModel.cs
rename to src/DodoSSH.Client.Shell/ViewModels/TerminalTabViewModel.cs
index 7d0a849..7dfccad 100644
--- a/src/DodoSSH.Client.App/ViewModels/TerminalTabViewModel.cs
+++ b/src/DodoSSH.Client.Shell/ViewModels/TerminalTabViewModel.cs
@@ -1,6 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
-namespace DodoSSH.Client.App.ViewModels;
+namespace DodoSSH.Client.Shell.ViewModels;
///
/// One open terminal, as a tab.
diff --git a/src/DodoSSH.Client.App/ViewModels/TransfersViewModel.cs b/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs
similarity index 99%
rename from src/DodoSSH.Client.App/ViewModels/TransfersViewModel.cs
rename to src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs
index a275d13..1ef7af6 100644
--- a/src/DodoSSH.Client.App/ViewModels/TransfersViewModel.cs
+++ b/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs
@@ -7,7 +7,7 @@ using DodoSSH.Client.Session;
using DodoSSH.Client.Ssh;
using DodoSSH.Client.Transfer;
-namespace DodoSSH.Client.App.ViewModels;
+namespace DodoSSH.Client.Shell.ViewModels;
/// One segment of a path, as a button in a breadcrumb trail.
/// What the segment is called.
diff --git a/src/DodoSSH.Client.App/ViewModels/VaultViewModel.cs b/src/DodoSSH.Client.Shell/ViewModels/VaultViewModel.cs
similarity index 99%
rename from src/DodoSSH.Client.App/ViewModels/VaultViewModel.cs
rename to src/DodoSSH.Client.Shell/ViewModels/VaultViewModel.cs
index bdcfbce..c2bc573 100644
--- a/src/DodoSSH.Client.App/ViewModels/VaultViewModel.cs
+++ b/src/DodoSSH.Client.Shell/ViewModels/VaultViewModel.cs
@@ -11,7 +11,7 @@ using DodoSSH.Client.Ssh;
using DodoSSH.Client.Sync;
using DodoSSH.Client.Terminal;
-namespace DodoSSH.Client.App.ViewModels;
+namespace DodoSSH.Client.Shell.ViewModels;
/// One host, as a row in the list.
///
diff --git a/src/DodoSSH.Client.App/WebAssets/terminal.css b/src/DodoSSH.Client.Shell/WebAssets/terminal.css
similarity index 100%
rename from src/DodoSSH.Client.App/WebAssets/terminal.css
rename to src/DodoSSH.Client.Shell/WebAssets/terminal.css
diff --git a/src/DodoSSH.Client.App/WebAssets/terminal.html b/src/DodoSSH.Client.Shell/WebAssets/terminal.html
similarity index 100%
rename from src/DodoSSH.Client.App/WebAssets/terminal.html
rename to src/DodoSSH.Client.Shell/WebAssets/terminal.html
diff --git a/src/DodoSSH.Client.App/WebAssets/terminal.js b/src/DodoSSH.Client.Shell/WebAssets/terminal.js
similarity index 100%
rename from src/DodoSSH.Client.App/WebAssets/terminal.js
rename to src/DodoSSH.Client.Shell/WebAssets/terminal.js
diff --git a/src/DodoSSH.Client.App/WebAssets/vendor/README.md b/src/DodoSSH.Client.Shell/WebAssets/vendor/README.md
similarity index 100%
rename from src/DodoSSH.Client.App/WebAssets/vendor/README.md
rename to src/DodoSSH.Client.Shell/WebAssets/vendor/README.md
diff --git a/src/DodoSSH.Client.App/WebAssets/vendor/addon-fit.js b/src/DodoSSH.Client.Shell/WebAssets/vendor/addon-fit.js
similarity index 100%
rename from src/DodoSSH.Client.App/WebAssets/vendor/addon-fit.js
rename to src/DodoSSH.Client.Shell/WebAssets/vendor/addon-fit.js
diff --git a/src/DodoSSH.Client.App/WebAssets/vendor/addon-webgl.js b/src/DodoSSH.Client.Shell/WebAssets/vendor/addon-webgl.js
similarity index 100%
rename from src/DodoSSH.Client.App/WebAssets/vendor/addon-webgl.js
rename to src/DodoSSH.Client.Shell/WebAssets/vendor/addon-webgl.js
diff --git a/src/DodoSSH.Client.App/WebAssets/vendor/xterm.css b/src/DodoSSH.Client.Shell/WebAssets/vendor/xterm.css
similarity index 100%
rename from src/DodoSSH.Client.App/WebAssets/vendor/xterm.css
rename to src/DodoSSH.Client.Shell/WebAssets/vendor/xterm.css
diff --git a/src/DodoSSH.Client.App/WebAssets/vendor/xterm.js b/src/DodoSSH.Client.Shell/WebAssets/vendor/xterm.js
similarity index 100%
rename from src/DodoSSH.Client.App/WebAssets/vendor/xterm.js
rename to src/DodoSSH.Client.Shell/WebAssets/vendor/xterm.js
diff --git a/src/DodoSSH.Client.Shell/packages.lock.json b/src/DodoSSH.Client.Shell/packages.lock.json
new file mode 100644
index 0000000..fed5920
--- /dev/null
+++ b/src/DodoSSH.Client.Shell/packages.lock.json
@@ -0,0 +1,344 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Avalonia": {
+ "type": "Direct",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "o8pZ1oE9AQ6gklpGM0lnOBp/JlVH0J/0mYszBf0GsSAcEnzHNCLM9NnrPZwKu4j2q9oNbVHYzzEPkszQeuqaKw==",
+ "dependencies": {
+ "Avalonia.BuildServices": "11.3.2",
+ "Avalonia.Remote.Protocol": "12.1.1",
+ "MicroCom.Runtime": "0.11.6"
+ }
+ },
+ "CommunityToolkit.Mvvm": {
+ "type": "Direct",
+ "requested": "[8.4.2, )",
+ "resolved": "8.4.2",
+ "contentHash": "WadCzGEc2U+3e20avRLng4qNtt4zoOGWrdUISqJWrHe3/FSnrYjuM5Sb4yQb09LhkBXrrI4Zt3dLKgRMbItsrg=="
+ },
+ "Meziantou.Analyzer": {
+ "type": "Direct",
+ "requested": "[3.0.137, )",
+ "resolved": "3.0.137",
+ "contentHash": "48n8WsokyefLOZt2qGPpk+hjfF9rQRa7MGK+8hMS6sO22QuIDtBztfbXV7RtVfX7JKjUwDI9e06I8aeCc5XZzQ=="
+ },
+ "Microsoft.CodeAnalysis.BannedApiAnalyzers": {
+ "type": "Direct",
+ "requested": "[5.6.0, )",
+ "resolved": "5.6.0",
+ "contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
+ },
+ "Avalonia.BuildServices": {
+ "type": "Transitive",
+ "resolved": "11.3.2",
+ "contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
+ },
+ "Avalonia.Remote.Protocol": {
+ "type": "Transitive",
+ "resolved": "12.1.1",
+ "contentHash": "0u77tnOwJnHtVLu+WBY7T56fN9W8n7++Uq9kHW6J+bfv5y13WZUMVS+PBzoBe32taYvz/oSomDGO1V41AHaFcQ=="
+ },
+ "MicroCom.Runtime": {
+ "type": "Transitive",
+ "resolved": "0.11.6",
+ "contentHash": "NdNWGDiZ6eS/Mf/9+QHR91cj1K7Hy+PX9yrHI/zM7xFYuj9IWT2uxtB6sCHjrnxAeLV9fut1R6zHDUGKX6f9lQ=="
+ },
+ "Microsoft.Data.Sqlite.Core": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "TPCs0ldm7AWqcKmp6f/Xr+14sat7hx4rHfRlS4RgCURBH2thEWbAKEyX7cCWr63zVJVOJIJZTg2cBiUXa8ys6g==",
+ "dependencies": {
+ "SQLitePCLRaw.core": "2.1.11"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA=="
+ },
+ "Microsoft.EntityFrameworkCore.Analyzers": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q=="
+ },
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "YbVWMIouzwTKBiLms8boa7xeRT88wI14R1msv3XExFk9n0/sa8nU7MwDa1CKtfLGMJs7O7QWuS9/xhcQ72AD2A==",
+ "dependencies": {
+ "Microsoft.Data.Sqlite.Core": "10.0.10",
+ "Microsoft.EntityFrameworkCore.Relational": "10.0.10",
+ "Microsoft.Extensions.Caching.Memory": "10.0.10",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
+ "Microsoft.Extensions.DependencyModel": "10.0.10",
+ "Microsoft.Extensions.Logging": "10.0.10",
+ "SQLitePCLRaw.core": "2.1.11"
+ }
+ },
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Caching.Memory": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.10",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Options": "10.0.10",
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA=="
+ },
+ "Microsoft.Extensions.DependencyModel": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA=="
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.10",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Options": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ=="
+ },
+ "dodossh.client.api": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Client.Auth": "[1.0.0, )",
+ "DodoSSH.Contracts": "[1.0.0, )",
+ "DodoSSH.Crypto": "[1.0.0, )"
+ }
+ },
+ "dodossh.client.auth": {
+ "type": "Project"
+ },
+ "dodossh.client.domain": {
+ "type": "Project"
+ },
+ "dodossh.client.session": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Client.Api": "[1.0.0, )",
+ "DodoSSH.Client.Auth": "[1.0.0, )",
+ "DodoSSH.Client.Domain": "[1.0.0, )",
+ "DodoSSH.Client.Ssh": "[1.0.0, )",
+ "DodoSSH.Client.Storage": "[1.0.0, )",
+ "DodoSSH.Client.Sync": "[1.0.0, )"
+ }
+ },
+ "dodossh.client.ssh": {
+ "type": "Project",
+ "dependencies": {
+ "SSH.NET": "[2025.1.0, )"
+ }
+ },
+ "dodossh.client.storage": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Contracts": "[1.0.0, )",
+ "DodoSSH.Crypto": "[1.0.0, )",
+ "EFCore.NamingConventions": "[10.0.1, )",
+ "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.10, )"
+ }
+ },
+ "dodossh.client.sync": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Client.Api": "[1.0.0, )",
+ "DodoSSH.Client.Domain": "[1.0.0, )",
+ "DodoSSH.Client.Storage": "[1.0.0, )",
+ "DodoSSH.Contracts": "[1.0.0, )",
+ "DodoSSH.Crypto": "[1.0.0, )"
+ }
+ },
+ "dodossh.client.terminal": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Client.Ssh": "[1.0.0, )"
+ }
+ },
+ "dodossh.client.transfer": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Client.Ssh": "[1.0.0, )"
+ }
+ },
+ "dodossh.contracts": {
+ "type": "Project"
+ },
+ "dodossh.crypto": {
+ "type": "Project",
+ "dependencies": {
+ "NSec.Cryptography": "[26.4.0, )"
+ }
+ },
+ "BouncyCastle.Cryptography": {
+ "type": "CentralTransitive",
+ "requested": "[2.6.2, )",
+ "resolved": "2.6.2",
+ "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
+ },
+ "EFCore.NamingConventions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1"
+ }
+ },
+ "libsodium": {
+ "type": "CentralTransitive",
+ "requested": "[1.0.22, )",
+ "resolved": "1.0.22",
+ "contentHash": "KPD9SloJFclrsjnhABu7dzWrcyYkwPbvx5l1gRSPAX/0n+OBtSiVCKtGFv4n+ecWUHU0tCG9LSSwoZZx673zBQ=="
+ },
+ "Microsoft.EntityFrameworkCore": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.10",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.10",
+ "Microsoft.Extensions.Caching.Memory": "10.0.10",
+ "Microsoft.Extensions.Logging": "10.0.10"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Relational": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "10.0.10",
+ "Microsoft.Extensions.Caching.Memory": "10.0.10",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Logging": "10.0.10"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Sqlite": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "kzg9MuQNJvZQxAU+piSkEzc7/1tpW6n1nVSGGMObu2GgxLK8Nf+6fvZundaznTZ+O2KhfPZ8HFNCzMH3PWDUmA==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.10",
+ "Microsoft.Extensions.Caching.Memory": "10.0.10",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
+ "Microsoft.Extensions.DependencyModel": "10.0.10",
+ "Microsoft.Extensions.Logging": "10.0.10",
+ "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
+ "SQLitePCLRaw.core": "2.1.11"
+ }
+ },
+ "NSec.Cryptography": {
+ "type": "CentralTransitive",
+ "requested": "[26.4.0, )",
+ "resolved": "26.4.0",
+ "contentHash": "0vsCtY5f+YgQROiWNqzgWp+l2pddfk9FkWoGV/bEo0MuEYPKlJWuoA8aOfO6qp3f+EnObKE3zSJhn1PspJeJVg==",
+ "dependencies": {
+ "libsodium": "[1.0.22, 1.0.23)"
+ }
+ },
+ "SQLitePCLRaw.bundle_e_sqlite3": {
+ "type": "CentralTransitive",
+ "requested": "[2.1.12, )",
+ "resolved": "2.1.12",
+ "contentHash": "mAgscpQMLw5/nfA1Q5oJVAT29yROUo1ifZGbbTpx/lwZpSxMUGoYbKfmvdm8oXER+RzxqBmmQzeBEVKfeHv2nw==",
+ "dependencies": {
+ "SQLitePCLRaw.lib.e_sqlite3": "2.1.12",
+ "SQLitePCLRaw.provider.e_sqlite3": "2.1.12"
+ }
+ },
+ "SQLitePCLRaw.core": {
+ "type": "CentralTransitive",
+ "requested": "[2.1.12, )",
+ "resolved": "2.1.12",
+ "contentHash": "ETpNw9DY3ckWLgRRAeCHj+GKOuPi61aeczkXhgHexUvqoZBAYg8RYESE2J7O1M7+o6QbdSEZwrw9bfqztUVWXg=="
+ },
+ "SQLitePCLRaw.lib.e_sqlite3": {
+ "type": "CentralTransitive",
+ "requested": "[2.1.12, )",
+ "resolved": "2.1.12",
+ "contentHash": "fWi8Dbknuhgg72fWinIdjXVaqO1hHL4YBBwVLnr7e1c9TAZwJ0QE38j9syW1hwx6HaqEVTwI+O07WPdZn8Rp0w=="
+ },
+ "SQLitePCLRaw.provider.e_sqlite3": {
+ "type": "CentralTransitive",
+ "requested": "[2.1.12, )",
+ "resolved": "2.1.12",
+ "contentHash": "W3oH4XIfCzFrgUSDKHhN6N+dgzA5YHOR2VxX8GB6Qy7CyrJJgxPEG8NirgYWlPQC5P2jz2knSsexWu4tDUL33g==",
+ "dependencies": {
+ "SQLitePCLRaw.core": "2.1.12"
+ }
+ },
+ "SSH.NET": {
+ "type": "CentralTransitive",
+ "requested": "[2025.1.0, )",
+ "resolved": "2025.1.0",
+ "contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
+ "dependencies": {
+ "BouncyCastle.Cryptography": "2.6.2",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/DodoSSH.Client.App.Layout.Tests/QuickConnectTests.cs b/tests/DodoSSH.Client.App.Layout.Tests/QuickConnectTests.cs
index b59ca2c..1f9f4fd 100644
--- a/tests/DodoSSH.Client.App.Layout.Tests/QuickConnectTests.cs
+++ b/tests/DodoSSH.Client.App.Layout.Tests/QuickConnectTests.cs
@@ -4,7 +4,7 @@ using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Threading;
using Avalonia.VisualTree;
-using DodoSSH.Client.App.ViewModels;
+using DodoSSH.Client.Shell.ViewModels;
using DodoSSH.Client.App.Views;
using DodoSSH.Client.Session;
using DodoSSH.Client.Session.Tests;
diff --git a/tests/DodoSSH.Client.App.Layout.Tests/ScreenLayoutTests.cs b/tests/DodoSSH.Client.App.Layout.Tests/ScreenLayoutTests.cs
index fd731b9..e959b38 100644
--- a/tests/DodoSSH.Client.App.Layout.Tests/ScreenLayoutTests.cs
+++ b/tests/DodoSSH.Client.App.Layout.Tests/ScreenLayoutTests.cs
@@ -4,7 +4,7 @@ using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Threading;
using Avalonia.VisualTree;
-using DodoSSH.Client.App.ViewModels;
+using DodoSSH.Client.Shell.ViewModels;
using DodoSSH.Client.App.Views;
using DodoSSH.Client.Session;
using DodoSSH.Client.Session.Tests;
diff --git a/tests/DodoSSH.Client.App.Layout.Tests/packages.lock.json b/tests/DodoSSH.Client.App.Layout.Tests/packages.lock.json
index 87f860e..16e33d0 100644
--- a/tests/DodoSSH.Client.App.Layout.Tests/packages.lock.json
+++ b/tests/DodoSSH.Client.App.Layout.Tests/packages.lock.json
@@ -486,6 +486,7 @@
"Avalonia.Themes.Fluent": "[12.1.1, )",
"CommunityToolkit.Mvvm": "[8.4.2, )",
"DodoSSH.Client.Session": "[1.0.0, )",
+ "DodoSSH.Client.Shell": "[1.0.0, )",
"DodoSSH.Client.Ssh": "[1.0.0, )",
"DodoSSH.Client.Terminal": "[1.0.0, )",
"DodoSSH.Client.Transfer": "[1.0.0, )"
@@ -508,6 +509,17 @@
"DodoSSH.Client.Sync": "[1.0.0, )"
}
},
+ "dodossh.client.shell": {
+ "type": "Project",
+ "dependencies": {
+ "Avalonia": "[12.1.1, )",
+ "CommunityToolkit.Mvvm": "[8.4.2, )",
+ "DodoSSH.Client.Session": "[1.0.0, )",
+ "DodoSSH.Client.Ssh": "[1.0.0, )",
+ "DodoSSH.Client.Terminal": "[1.0.0, )",
+ "DodoSSH.Client.Transfer": "[1.0.0, )"
+ }
+ },
"dodossh.client.ssh": {
"type": "Project",
"dependencies": {
diff --git a/tests/DodoSSH.Client.App.Tests/ShellFlowTests.cs b/tests/DodoSSH.Client.App.Tests/ShellFlowTests.cs
index e2f1737..2b2409c 100644
--- a/tests/DodoSSH.Client.App.Tests/ShellFlowTests.cs
+++ b/tests/DodoSSH.Client.App.Tests/ShellFlowTests.cs
@@ -1,4 +1,4 @@
-using DodoSSH.Client.App.ViewModels;
+using DodoSSH.Client.Shell.ViewModels;
using DodoSSH.Client.Auth;
using DodoSSH.Client.Session;
diff --git a/tests/DodoSSH.Client.App.Tests/packages.lock.json b/tests/DodoSSH.Client.App.Tests/packages.lock.json
index aa10825..9f2ad75 100644
--- a/tests/DodoSSH.Client.App.Tests/packages.lock.json
+++ b/tests/DodoSSH.Client.App.Tests/packages.lock.json
@@ -475,6 +475,7 @@
"Avalonia.Themes.Fluent": "[12.1.1, )",
"CommunityToolkit.Mvvm": "[8.4.2, )",
"DodoSSH.Client.Session": "[1.0.0, )",
+ "DodoSSH.Client.Shell": "[1.0.0, )",
"DodoSSH.Client.Ssh": "[1.0.0, )",
"DodoSSH.Client.Terminal": "[1.0.0, )",
"DodoSSH.Client.Transfer": "[1.0.0, )"
@@ -497,6 +498,17 @@
"DodoSSH.Client.Sync": "[1.0.0, )"
}
},
+ "dodossh.client.shell": {
+ "type": "Project",
+ "dependencies": {
+ "Avalonia": "[12.1.1, )",
+ "CommunityToolkit.Mvvm": "[8.4.2, )",
+ "DodoSSH.Client.Session": "[1.0.0, )",
+ "DodoSSH.Client.Ssh": "[1.0.0, )",
+ "DodoSSH.Client.Terminal": "[1.0.0, )",
+ "DodoSSH.Client.Transfer": "[1.0.0, )"
+ }
+ },
"dodossh.client.ssh": {
"type": "Project",
"dependencies": {