diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 96beef1d..89954cf9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,65 +2,82 @@ name: Release on: workflow_dispatch: + inputs: + version: + type: string + description: Version (Semver without leading v) + sentry-release: + type: boolean + description: Make Sentry Release? + default: false + release: types: [ published ] jobs: - release: - if: github.repository == 'ionite34/StabilityMatrix' - runs-on: windows-latest - + release-linux: + if: github.repository == 'ionite34/StabilityMatrix' || github.event_name == 'workflow_dispatch' + name: Release (linux-x64) + env: + platform-id: linux-x64 + out-name: StabilityMatrix.AppImage + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: olegtarasov/get-tag@v2.1.2 + if: github.event_name == 'release' id: tag_name with: tagRegex: "v(.*)" - - - name: Set Tag + + - name: Set Version from Tag + if: github.event_name == 'release' run: | echo "Using tag ${{ env.GIT_TAG_NAME }}" - echo "RELEASE_VERSION=${{ env.GIT_TAG_NAME }}.0" >> $env:GITHUB_ENV + echo "RELEASE_VERSION=${{ env.GIT_TAG_NAME }}" >> $GITHUB_ENV + + - name: Set Version from manual input + if: github.event_name == 'workflow_dispatch' + run: | + echo "Using version ${{ github.event.inputs.version }}" + echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - - name: Set up .NET + - name: Set up .NET 6 (for PupNet) uses: actions/setup-dotnet@v3 with: dotnet-version: '6.0.x' + + - name: Install PupNet + run: | + sudo apt-get -y install libfuse2 + dotnet tool install --framework net6.0 -g KuiperZone.PupNet - - name: Install dependencies - run: dotnet restore -p:PublishReadyToRun=true - - - name: Build + - name: Set up .NET 7 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '7.0.x' + + - name: PupNet Build env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - run: > - dotnet publish ./StabilityMatrix/StabilityMatrix.csproj - -o out -c Release -r win-x64 - -p:Version=$env:RELEASE_VERSION -p:FileVersion=$env:RELEASE_VERSION -p:AssemblyVersion=$env:RELEASE_VERSION - -p:PublishReadyToRun=true -p:PublishSingleFile=true - -p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }} - -p:SentryUploadSymbols=true -p:SentryUploadSources=true - --self-contained true - - - name: Remove old artifacts - uses: c-hive/gha-remove-artifacts@v1 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - age: '90 seconds' - skip-tags: false + run: pupnet -r linux-x64 -c Release --kind appimage --app-version $RELEASE_VERSION --clean -y + # Release/linux-x64/StabilityMatrix.x86_64.AppImage + - name: Post Build + run: mv ./Release/linux-x64/StabilityMatrix.x86_64.AppImage ${{ env.out-name }} + - name: Upload Artifact uses: actions/upload-artifact@v2 with: - name: StabilityMatrix - path: ./out/StabilityMatrix.exe + name: StabilityMatrix-${{ env.platform-id }} + path: ${{ env.out-name }} - name: Create Sentry release - if: ${{ env.MAKE_SENTRY_RELEASE == 'true' }} + if: ${{ github.event_name == 'release' }} uses: getsentry/action-release@v1 env: - MAKE_SENTRY_RELEASE: ${{ secrets.SENTRY_PROJECT != '' }} + MAKE_SENTRY_RELEASE: ${{ secrets.SENTRY_PROJECT != '' }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} @@ -68,3 +85,94 @@ jobs: environment: production ignore_missing: true version: StabilityMatrix@${{ env.GIT_TAG_NAME }} + + - name: Create Sentry release + if: ${{ github.event_name == 'workflow_dispatch' }} + uses: getsentry/action-release@v1 + env: + MAKE_SENTRY_RELEASE: ${{ secrets.SENTRY_PROJECT != '' }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + with: + environment: production + ignore_missing: true + version: StabilityMatrix@${{ github.event.inputs.version }} + + + release-windows: + if: github.repository == 'ionite34/StabilityMatrix' || github.event_name == 'workflow_dispatch' + name: Release (win-x64) + env: + platform-id: win-x64 + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + + - uses: olegtarasov/get-tag@v2.1.2 + if: github.event_name == 'release' + id: tag_name + with: + tagRegex: "v(.*)" + + - name: Set Version from Tag + if: github.event_name == 'release' + run: | + echo "Using tag ${{ env.GIT_TAG_NAME }}" + echo "RELEASE_VERSION=${{ env.GIT_TAG_NAME }}" >> $env:GITHUB_ENV + + - name: Set Version from manual input + if: github.event_name == 'workflow_dispatch' + run: | + echo "Using version ${{ github.event.inputs.version }}" + echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $env:GITHUB_ENV + + - name: Set up .NET 7 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '7.0.x' + + - name: Install dependencies + run: dotnet restore + + - name: .NET Publish + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: > + dotnet publish ./StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj + -o out -c Release -r ${{ env.platform-id }} + -p:Version=$env:RELEASE_VERSION + -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true + -p:PublishTrimmed=true + -p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }} + -p:SentryUploadSymbols=true -p:SentryUploadSources=true + + - name: Post Build + run: mv ./out/StabilityMatrix.Avalonia.exe ./out/${{ env.out-name }} + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: StabilityMatrix-${{ env.platform-id }} + path: ./out/${{ env.out-name }} + + + cleanup: + name: Artifact Cleanup + needs: [release-linux, release-windows] + if: github.repository == 'ionite34/StabilityMatrix' + runs-on: ubuntu-latest + continue-on-error: true + + steps: + - name: Remove old artifacts + uses: c-hive/gha-remove-artifacts@v1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + age: '1 hour' + skip-recent: 2 + skip-tags: false + + - name: Output + if: always() && true + run: exit 0 diff --git a/Jenkinsfile b/Jenkinsfile index ba10f8fb..6c03f2ad 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -20,15 +20,15 @@ node("Windows") { stage('Set Version') { script { if (env.TAG_NAME) { - version = env.TAG_NAME.replaceFirst(/^v/, '') + ".0" + version = env.TAG_NAME.replaceFirst(/^v/, '') } else { - version = VersionNumber projectStartDate: '2023-06-21', versionNumberString: '1.0.${BUILDS_ALL_TIME}.0', versionPrefix: '', worstResultForIncrement: 'SUCCESS' + version = VersionNumber projectStartDate: '2023-06-21', versionNumberString: '${BUILDS_ALL_TIME}', worstResultForIncrement: 'SUCCESS' } } } stage('Publish') { - bat "dotnet publish .\\StabilityMatrix\\StabilityMatrix.csproj -c Release -o out -r win-x64 -p:PublishSingleFile=true -p:Version=${version} -p:FileVersion=${version} -p:AssemblyVersion=${version} --self-contained true" + bat "dotnet publish .\\StabilityMatrix.Avalonia\\StabilityMatrix.Avalonia.csproj -c Release -o out -r win-x64 -p:PublishSingleFile=true -p:VersionPrefix=2.0.0 -p:VersionSuffix=${version} -p:IncludeNativeLibrariesForSelfExtract=true" } stage ('Archive Artifacts') { @@ -36,7 +36,7 @@ node("Windows") { } } else { stage('Publish') { - bat "dotnet publish .\\StabilityMatrix\\StabilityMatrix.csproj -c Release -o out -r win-x64 -p:PublishSingleFile=true --self-contained true" + bat "dotnet publish .\\StabilityMatrix.Avalonia\\StabilityMatrix.Avalonia.csproj -c Release -o out -r win-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true" } } } diff --git a/Jenkinsfile-linux b/Jenkinsfile-linux new file mode 100644 index 00000000..f520fb3f --- /dev/null +++ b/Jenkinsfile-linux @@ -0,0 +1,42 @@ +node("Diligence") { + def repoName = "StabilityMatrix" + def author = "ionite34" + def version = "" + + stage('Clean') { + deleteDir() + } + + stage('Checkout') { + git branch: env.BRANCH_NAME, credentialsId: 'Ionite', url: "https://github.com/${author}/${repoName}.git" + } + + // stage('Test') { + // sh "dotnet test StabilityMatrix.Tests" + // } + + if (env.BRANCH_NAME == 'main') { + + stage('Set Version') { + script { + if (env.TAG_NAME) { + version = env.TAG_NAME.replaceFirst(/^v/, '') + } else { + version = VersionNumber projectStartDate: '2023-06-21', versionNumberString: '${BUILDS_ALL_TIME}', worstResultForIncrement: 'SUCCESS' + } + } + } + + stage('Publish') { + sh "/home/jenkins/.dotnet/tools/pupnet --runtime linux-x64 --kind appimage --app-version ${version} --clean -y" + } + + stage ('Archive Artifacts') { + archiveArtifacts artifacts: 'out/*.appimage', followSymlinks: false + } + } else { + stage('Publish') { + sh "/home/jenkins/.dotnet/tools/pupnet --runtime linux-x64 --kind appimage --clean -y" + } + } +} diff --git a/StabilityMatrix.Avalonia.pupnet.conf b/StabilityMatrix.Avalonia.pupnet.conf new file mode 100644 index 00000000..747f4394 --- /dev/null +++ b/StabilityMatrix.Avalonia.pupnet.conf @@ -0,0 +1,292 @@ +################################################################################ +# PUPNET DEPLOY: 1.4.0 +################################################################################ + +######################################## +# APP PREAMBLE +######################################## + +# Mandatory application base name. This MUST BE the base name of the main executable file. It should NOT +# include any directory part or extension, i.e. do not append '.exe' or '.dll'. It should not contain +# spaces or invalid filename characters. +AppBaseName = StabilityMatrix.Avalonia + +# Mandatory application friendly name. +AppFriendlyName = Stability Matrix + +# Mandatory application ID in reverse DNS form. This should stay constant for lifetime of the software. +AppId = zone.lykos.stabilitymatrix + +# Mandatory application version and package release of form: 'VERSION[RELEASE]'. Use optional square +# brackets to denote package release, i.e. '1.2.3[1]'. Release refers to a change to the deployment +# package, rather the application. If release part is absent (i.e. '1.2.3'), the release value defaults +# to '1'. Note that the version-release value given here may be overridden from the command line. +AppVersionRelease = 2.0.0[1] + +# Mandatory single line application short summary description. +AppShortSummary = Package and checkpoint manager for Stable Diffusion. + +# Optional multi-line (surround with triple """ quotes) application description which may provide +# longer text than AppShortSummary. Text separated by an empty line will be treated as paragraphs +# (complex formatting should be avoided). The content is used by package builders where supported, +# including RPM and DEB, and may optionally be used to populate the '' element in the +# AppStream metadata through the use of a macro variable. +AppDescription = + +# Mandatory application license ID. This should be one of the recognised SPDX license +# identifiers, such as: 'MIT', 'GPL-3.0-or-later' or 'Apache-2.0'. For a proprietary or +# custom license, use 'LicenseRef-Proprietary' or 'LicenseRef-LICENSE'. +AppLicenseId = LicenseRef-Proprietary + +# Optional path to application copyright/license text file. If provided, it will be packaged with the +# application and used with package builders where supported. +AppLicenseFile = LICENSE + +# Optional path to application changelog file. IMPORTANT. If given, this file should contain version +# information in a predefined format. Namely, it should contain one or more version headings of form: +# '+ VERSION;DATE', under which are to be listed change items of form: '- Change description'. Formatted +# information will be parsed and used to populate AppStream metadata. Additionally, it will be packaged +# with the application and used with package builders where supported. NOTE. Superfluous text in the file +# is ignored, so the file may also contain README information. +# For information: https://github.com/kuiperzone/PupNet-Deploy. +AppChangeFile = + +######################################## +# PUBLISHER +######################################## + +# Mandatory publisher, group or creator. +PublisherName = Lykos + +# Optional copyright statement. +PublisherCopyright = Copyright (C) Lykos 2023 + +# Optional publisher or application web-link name. Note that Windows Setup packages +# require both PublisherLinkName and PublisherLinkUrl in order to include the link as +# an item in program menu entries. Do not modify name, as may leave old entries in updated installations. +PublisherLinkName = Home Page + +# Optional publisher or application web-link URL. +PublisherLinkUrl = https://lykos.ai + +# Publisher or maintainer email contact. Although optional, some package builders (i.e. DEB) require it +# and may warn or fail unless provided. +PublisherEmail = stability-matrix@lykos.ai + +######################################## +# DESKTOP INTEGRATION +######################################## + +# Boolean (true or false) which indicates whether the application is hidden on the desktop. It is used to +# populate the 'NoDisplay' field of the .desktop file. The default is false. Setting to true will also +# cause the main application start menu entry to be omitted for Windows Setup. +DesktopNoDisplay = false + +# Boolean (true or false) which indicates whether the application runs in the terminal, rather than +# providing a GUI. It is used to populate the 'Terminal' field of the .desktop file. +DesktopTerminal = false + +# Optional path to a Linux desktop file. If empty (default), one will be generated automatically from +# the information in this file. Supplying a custom file, however, allows for mime-types and +# internationalisation. If supplied, the file MUST contain the line: 'Exec=${INSTALL_EXEC}' +# in order to use the correct install location. Other macros may be used to help automate the content. +# Note. PupNet Deploy can generate you a desktop file. Use --help and 'pupnet --help macro' for reference. +# See: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html +DesktopFile = + +# Optional command name to start the application from the terminal. If, for example, AppBaseName is +# 'Zone.Kuiper.HelloWorld', the value here may be set to a simpler and/or lower-case variant such as +# 'helloworld'. It must not contain spaces or invalid filename characters. Do not add any extension such +# as '.exe'. If empty, the application will not be in the path and cannot be started from the command line. +# For Windows Setup packages, see also SetupCommandPrompt. StartCommand is not +# supported for all packages kinds (i.e. Flatpak). Default is empty (none). +StartCommand = stabilitymatrix + +# Optional category for the application. The value should be one of the recognised Freedesktop top-level +# categories, such as: Audio, Development, Game, Office, Utility etc. Only a single value should be +# provided here which will be used, where supported, to populate metadata. The default is empty. +# See: https://specifications.freedesktop.org/menu-spec/latest/apa.html +PrimeCategory = Utility + +# Path to AppStream metadata file. It is optional, but recommended as it is used by software centers. +# Note. The contents of the files may use macro variables. Use 'pupnet --help macro' for reference. +# See: https://docs.appimage.org/packaging-guide/optional/appstream.html +MetaFile = + +# Optional icon file paths. The value may include multiple filenames separated with semicolon or given +# in multi-line form. Valid types are SVG, PNG and ICO (ICO ignored on Linux). Note that the inclusion +# of a scalable SVG is preferable on Linux, whereas PNGs must be one of the standard sizes and MUST +# include the size in the filename in the form: name.32x32.png' or 'name.32.png'. +IconFiles = """ + StabilityMatrix.Avalonia/Assets/Icon.512x512.png + StabilityMatrix.Avalonia/Assets/Icon.ico +""" + +######################################## +# DOTNET PUBLISH +######################################## + +# Optional path relative to this file in which to find the dotnet project (.csproj) or solution (.sln) +# file, or the directory containing it. If empty (default), a single project or solution file is +# expected under the same directory as this file. IMPORTANT. If set to 'NONE', dotnet publish +# is disabled (not called). Instead, only DotnetPostPublish is called. +DotnetProjectPath = StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj + +# Optional arguments supplied to 'dotnet publish'. Do NOT include '-r' (runtime), or '-c' (configuration) +# here as they will be added according to command line arguments. Typically you want as a minimum: +# '-p:Version=${APP_VERSION} --self-contained true'. Additional useful arguments include: +# '-p:DebugType=None -p:DebugSymbols=false -p:PublishSingleFile=true -p:PublishReadyToRun=true +# -p:PublishTrimmed=true -p:TrimMode=link'. Note. This value may use macro variables. Use 'pupnet --help macro' +# for reference. See: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish +DotnetPublishArgs = -p:Version=${APP_VERSION} --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -p:PublishTrimmed=true + +# Post-publish (or standalone build) command on Linux (ignored on Windows). It is called after dotnet +# publish, but before the final output is built. This could, for example, be a script which copies +# additional files into the build directory given by ${BUILD_APP_BIN}. The working directory will be +# the location of this file. This value is optional, but becomes mandatory if DotnetProjectPath equals +# 'NONE'. Note. This value may use macro variables. Additionally, scripts may use these as environment +# variables. Use 'pupnet --help macro' for reference. +DotnetPostPublish = + +# Post-publish (or standalone build) command on Windows (ignored on Linux). This should perform +# the equivalent operation, as required, as DotnetPostPublish, but using DOS commands and batch +# scripts. Multiple commands may be specified, separated by semicolon or given in multi-line form. +# Note. This value may use macro variables. Additionally, scripts may use these as environment +# variables. Use 'pupnet --help macro' for reference. +DotnetPostPublishOnWindows = + +######################################## +# PACKAGE OUTPUT +######################################## + +# Optional package name (excludes version etc.). If empty, defaults to AppBaseName. However, it is +# used not only to specify the base output filename, but to identify the application in DEB and RPM +# packages. You may wish, therefore, to ensure that the value represents a unique name. Naming +# requirements are strict and must contain only alpha-numeric and '-', '+' and '.' characters. +PackageName = StabilityMatrix + +# Output directory, or subdirectory relative to this file. It will be created if it does not exist and +# will contain the final deploy output files. If empty, it defaults to the location of this file. +OutputDirectory = Release/linux-x64 + +######################################## +# APPIMAGE OPTIONS +######################################## + +# Additional arguments for use with appimagetool. Useful for signing. Default is empty. +AppImageArgs = + +# Boolean (true or false) which sets whether to include the application version in the AppImage filename, +# i.e. 'HelloWorld-1.2.3-x86_64.AppImage'. Default is false. It is ignored if the output filename is +# specified at command line. +AppImageVersionOutput = false + +######################################## +# FLATPAK OPTIONS +######################################## + +# The runtime platform. Invariably for .NET (inc. Avalonia), this should be 'org.freedesktop.Platform'. +# Refer: https://docs.flatpak.org/en/latest/available-runtimes.html +FlatpakPlatformRuntime = org.freedesktop.Platform + +# The platform SDK. Invariably for .NET (inc. Avalonia applications) this should be 'org.freedesktop.Sdk'. +# The SDK must be installed on the build system. +FlatpakPlatformSdk = org.freedesktop.Sdk + +# The platform runtime version. The latest available version may change periodically. +# Refer to Flatpak documentation. +FlatpakPlatformVersion = 22.08 + +# Flatpak manifest 'finish-args' sandbox permissions. Optional, but if empty, the application will have +# extremely limited access to the host environment. This option may be used to grant required +# application permissions. Values here should be prefixed with '--' and separated by semicolon or given +# in multi-line form. Refer: https://docs.flatpak.org/en/latest/sandbox-permissions.html +FlatpakFinishArgs = """ + --socket=wayland + --socket=x11 + --filesystem=host + --share=network +""" + +# Additional arguments for use with flatpak-builder. Useful for signing. Default is empty. +# See flatpak-builder --help. +FlatpakBuilderArgs = + +######################################## +# RPM OPTIONS +######################################## + +# Boolean (true or false) which specifies whether to build the RPM package with 'AutoReq' equal to yes or no. +# For dotnet application, the value should typically be false, but see RpmRequires below. +# Refer: https://rpm-software-management.github.io/rpm/manual/spec.html +RpmAutoReq = false + +# Boolean (true or false) which specifies whether to build the RPM package with 'AutoProv' equal to yes or no. +# Refer: https://rpm-software-management.github.io/rpm/manual/spec.html +RpmAutoProv = true + +# Optional list of RPM dependencies. The list may include multiple values separated with semicolon or given +# in multi-line form. If empty, a self-contained dotnet package will successfully run on many (but not all) +# Linux distros. In some cases, it will be necessary to explicitly specify additional dependencies. +# Default values are recommended for use with dotnet and RPM packages at the time of writing. +# For updated information, see: https://learn.microsoft.com/en-us/dotnet/core/install/linux-rhel#dependencies +RpmRequires = """ + krb5-libs + libicu + openssl-libs + zlib +""" + +######################################## +# DEBIAN OPTIONS +######################################## + +# Optional list of Debian dependencies. The list may include multiple values separated with semicolon or given +# in multi-line form. If empty, a self-contained dotnet package will successfully run on many (but not all) +# Linux distros. In some cases, it will be necessary to explicitly specify additional dependencies. +# Default values are recommended for use with dotnet and Debian packages at the time of writing. +# For updated information, see: https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu#dependencies +DebianRecommends = """ + libc6 + libgcc1 + libgcc-s1 + libgssapi-krb5-2 + libicu + libssl + libstdc++6 + libunwind + zlib1g +""" + +######################################## +# WINDOWS SETUP OPTIONS +######################################## + +# Boolean (true or false) which specifies whether the application is to be installed in administrative +# mode, or per-user. Default is false. See: https://jrsoftware.org/ishelp/topic_admininstallmode.htm +SetupAdminInstall = false + +# Optional command prompt title. The Windows installer will NOT add your application to the path. However, +# if your package contains a command-line utility, setting this value will ensure that a 'Command Prompt' +# program menu entry is added (with this title) which, when launched, will open a dedicated command +# window with your application directory in its path. Default is empty. See also StartCommand. +SetupCommandPrompt = Command Prompt + +# Mandatory value which specifies minimum version of Windows that your software runs on. Windows 8 = 6.2, +# Windows 10/11 = 10. Default: 10. See: https://jrsoftware.org/ishelp/topic_setup_minversion.htm +SetupMinWindowsVersion = 10 + +# Optional name and parameters of the Sign Tool to be used to digitally sign: the installer, +# uninstaller, and contained exe and dll files. If empty, files will not be signed. +# See: https://jrsoftware.org/ishelp/topic_setup_signtool.htm +SetupSignTool = + +# Optional suffix for the installer output filename. The default is empty, but you may wish set it to: +# 'Setup' or similar. This, for example, will output a file of name: HelloWorldSetup-x86_64.exe +# Ignored if the output filename is specified at command line. +SetupSuffixOutput = + +# Boolean (true or false) which sets whether to include the application version in the setup filename, +# i.e. 'HelloWorld-1.2.3-x86_64.exe'. Default is false. Ignored if the output filename is specified +# at command line. +SetupVersionOutput = false diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml new file mode 100644 index 00000000..d637c33d --- /dev/null +++ b/StabilityMatrix.Avalonia/App.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + 700 + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs new file mode 100644 index 00000000..b9eef0a7 --- /dev/null +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -0,0 +1,521 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Platform.Storage; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NLog; +using NLog.Config; +using NLog.Extensions.Logging; +using NLog.Targets; +using Octokit; +using Polly; +using Polly.Contrib.WaitAndRetry; +using Polly.Extensions.Http; +using Polly.Timeout; +using Refit; +using Sentry; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.DesignData; +using StabilityMatrix.Avalonia.Helpers; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels; +using StabilityMatrix.Avalonia.ViewModels.Dialogs; +using StabilityMatrix.Avalonia.Views; +using StabilityMatrix.Avalonia.Views.Dialogs; +using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Converters.Json; +using StabilityMatrix.Core.Database; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Helper.Factory; +using StabilityMatrix.Core.Models.Api; +using StabilityMatrix.Core.Models.Configs; +using StabilityMatrix.Core.Models.Packages; +using StabilityMatrix.Core.Models.Settings; +using StabilityMatrix.Core.Python; +using StabilityMatrix.Core.Services; +using StabilityMatrix.Core.Updater; +using Application = Avalonia.Application; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace StabilityMatrix.Avalonia; + +public sealed class App : Application +{ + [NotNull] public static IServiceProvider? Services { get; private set; } + [NotNull] public static Visual? VisualRoot { get; private set; } + [NotNull] public static IStorageProvider? StorageProvider { get; private set; } + [NotNull] public static IConfiguration? Config { get; private set; } + + // ReSharper disable once MemberCanBePrivate.Global + public IClassicDesktopStyleApplicationLifetime? DesktopLifetime => + ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; + + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + + // Set design theme + if (Design.IsDesignMode) + { + RequestedThemeVariant = ThemeVariant.Dark; + } + } + + public override void OnFrameworkInitializationCompleted() + { + base.OnFrameworkInitializationCompleted(); + + if (Design.IsDesignMode) + { + DesignData.DesignData.Initialize(); + Services = DesignData.DesignData.Services; + } + else + { + ConfigureServiceProvider(); + } + + if (DesktopLifetime is not null) + { + DesktopLifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; + + // First time setup if needed + var settingsManager = Services.GetRequiredService(); + if (!settingsManager.IsEulaAccepted()) + { + var setupWindow = Services.GetRequiredService(); + var setupViewModel = Services.GetRequiredService(); + setupWindow.DataContext = setupViewModel; + setupWindow.ShowAsDialog = true; + setupWindow.ShowActivated = true; + setupWindow.ShowAsyncCts = new CancellationTokenSource(); + + DesktopLifetime.MainWindow = setupWindow; + + setupWindow.ShowAsyncCts.Token.Register(() => + { + if (setupWindow.Result == ContentDialogResult.Primary) + { + settingsManager.SetEulaAccepted(); + ShowMainWindow(); + DesktopLifetime.MainWindow.Show(); + } + else + { + Shutdown(); + } + }); + } + else + { + ShowMainWindow(); + } + } + } + + private void ShowMainWindow() + { + if (DesktopLifetime is null) return; + + var mainViewModel = Services.GetRequiredService(); + var notificationService = Services.GetRequiredService(); + + var mainWindow = Services.GetRequiredService(); + mainWindow.DataContext = mainViewModel; + mainWindow.NotificationService = notificationService; + + var settingsManager = Services.GetRequiredService(); + var windowSettings = settingsManager.Settings.WindowSettings; + if (windowSettings != null) + { + mainWindow.Position = new PixelPoint(windowSettings.X, windowSettings.Y); + mainWindow.Width = windowSettings.Width; + mainWindow.Height = windowSettings.Height; + } + else + { + mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen; + } + + mainWindow.Closing += (_, _) => + { + settingsManager.Transaction(s => + { + s.WindowSettings = new WindowSettings( + mainWindow.Width, mainWindow.Height, + mainWindow.Position.X, mainWindow.Position.Y); + }, ignoreMissingLibraryDir: true); + }; + mainWindow.Closed += (_, _) => Shutdown(); + + VisualRoot = mainWindow; + StorageProvider = mainWindow.StorageProvider; + + DesktopLifetime.MainWindow = mainWindow; + DesktopLifetime.Exit += OnExit; + } + + private static void ConfigureServiceProvider() + { + var services = ConfigureServices(); + Services = services.BuildServiceProvider(); + + var settingsManager = Services.GetRequiredService(); + settingsManager.TryFindLibrary(); + Services.GetRequiredService().StartEventListener(); + } + + internal static void ConfigurePageViewModels(IServiceCollection services) + { + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + services.AddSingleton(provider => + new MainWindowViewModel(provider.GetRequiredService(), + provider.GetRequiredService>()) + { + Pages = + { + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + }, + FooterPages = + { + provider.GetRequiredService() + } + }); + + // Register disposable view models for shutdown cleanup + services.AddSingleton(p + => p.GetRequiredService()); + } + + internal static void ConfigureDialogViewModels(IServiceCollection services) + { + // Dialog view models (transient) + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + + // Other transients (usually sub view models) + services.AddTransient(); + services.AddTransient(); + + // Global progress + services.AddSingleton(); + + // Controls + services.AddTransient(); + + // Dialog factory + services.AddSingleton>(provider => + new ServiceManager() + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService)); + } + + internal static void ConfigureViews(IServiceCollection services) + { + // Pages + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Dialogs + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Controls + services.AddTransient(); + + // Windows + services.AddSingleton(); + services.AddSingleton(); + } + + internal static void ConfigurePackages(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + private static IServiceCollection ConfigureServices() + { + var services = new ServiceCollection(); + + services.AddMemoryCache(); + + ConfigurePageViewModels(services); + ConfigureDialogViewModels(services); + ConfigurePackages(services); + + // Other services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + Config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .Build(); + + services.Configure(Config.GetSection(nameof(DebugOptions))); + + if (Compat.IsWindows) + { + services.AddSingleton(); + } + else if (Compat.IsLinux || Compat.IsMacOS) + { + services.AddSingleton(); + } + + ConfigureViews(services); + + if (Design.IsDesignMode) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + services.AddSingleton(p => p.GetRequiredService()); + } + + services.AddTransient(_ => + { + var client = new GitHubClient(new ProductHeaderValue("StabilityMatrix")); + // var githubApiKey = Config["GithubApiKey"]; + // if (string.IsNullOrWhiteSpace(githubApiKey)) + // return client; + // + // client.Credentials = new Credentials(githubApiKey); + return client; + }); + + // Configure Refit and Polly + var jsonSerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + jsonSerializerOptions.Converters.Add(new ObjectToInferredTypesConverter()); + jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter()); + jsonSerializerOptions.Converters.Add( + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); + jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + + var defaultRefitSettings = new RefitSettings + { + ContentSerializer = + new SystemTextJsonContentSerializer(jsonSerializerOptions) + }; + + // HTTP Policies + var retryStatusCodes = new[] + { + HttpStatusCode.RequestTimeout, // 408 + HttpStatusCode.InternalServerError, // 500 + HttpStatusCode.BadGateway, // 502 + HttpStatusCode.ServiceUnavailable, // 503 + HttpStatusCode.GatewayTimeout // 504 + }; + var delay = Backoff + .DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromMilliseconds(80), + retryCount: 5); + var retryPolicy = HttpPolicyExtensions + .HandleTransientHttpError() + .Or() + .OrResult(r => retryStatusCodes.Contains(r.StatusCode)) + .WaitAndRetryAsync(delay); + + // Shorter timeout for local requests + var localTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(3)); + var localDelay = Backoff + .DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromMilliseconds(50), + retryCount: 3); + var localRetryPolicy = HttpPolicyExtensions + .HandleTransientHttpError() + .Or() + .OrResult(r => retryStatusCodes.Contains(r.StatusCode)) + .WaitAndRetryAsync(localDelay, onRetryAsync: (_, _) => + { + Debug.WriteLine("Retrying local request..."); + return Task.CompletedTask; + }); + + // named client for update + services.AddHttpClient("UpdateClient") + .AddPolicyHandler(retryPolicy); + + // Add Refit clients + services.AddRefitClient(defaultRefitSettings) + .ConfigureHttpClient(c => + { + c.BaseAddress = new Uri("https://civitai.com"); + c.Timeout = TimeSpan.FromSeconds(15); + }) + .AddPolicyHandler(retryPolicy); + + // Add Refit client managers + services.AddHttpClient("A3Client") + .AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy)); + + // Add logging + services.AddLogging(builder => + { + builder.ClearProviders(); + builder.AddFilter("Microsoft.Extensions.Http", LogLevel.Warning) + .AddFilter("Microsoft.Extensions.Http.DefaultHttpClientFactory", LogLevel.Warning) + .AddFilter("Microsoft", LogLevel.Warning) + .AddFilter("System", LogLevel.Warning); + builder.SetMinimumLevel(LogLevel.Debug); + builder.AddNLog(ConfigureLogging()); + }); + + return services; + } + + /// + /// Requests shutdown of the Current Application. + /// + /// This returns asynchronously *without waiting* for Shutdown + /// Exit code for the application. + /// If Application.Current is null + public static void Shutdown(int exitCode = 0) + { + if (Current is null) throw new NullReferenceException( + "Current Application was null when Shutdown called"); + if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.Shutdown(exitCode); + } + } + + private static void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args) + { + Debug.WriteLine("Start OnExit"); + // Services.GetRequiredService().OnShutdown(); + var settingsManager = Services.GetRequiredService(); + + // If RemoveFolderLinksOnShutdown is set, delete all package junctions + if (settingsManager is + { + IsLibraryDirSet: true, + Settings.RemoveFolderLinksOnShutdown: true + }) + { + var sharedFolders = Services.GetRequiredService(); + sharedFolders.RemoveLinksForAllPackages(); + } + + Debug.WriteLine("Start OnExit: Disposing services"); + // Dispose all services + foreach (var disposable in Services.GetServices()) + { + Debug.WriteLine($"Disposing {disposable.GetType().Name}"); + disposable.Dispose(); + } + + Debug.WriteLine("End OnExit"); + } + + private static LoggingConfiguration ConfigureLogging() + { + var logConfig = new LoggingConfiguration(); + + // File target + logConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, + new FileTarget("logfile") + { + Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}", + ArchiveOldFileOnStartup = true, + FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log", + ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log", + ArchiveNumbering = ArchiveNumberingMode.Rolling, + MaxArchiveFiles = 2 + }); + + // Debugger Target + logConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, + new DebuggerTarget("debugger") + { + Layout = "${message}" + }); + + // Sentry + if (SentrySdk.IsEnabled) + { + logConfig.AddSentry(o => + { + o.InitializeSdk = false; + o.Layout = "${message}"; + o.ShutdownTimeoutSeconds = 5; + o.IncludeEventDataOnBreadcrumbs = true; + o.BreadcrumbLayout = "${logger}: ${message}"; + // Debug and higher are stored as breadcrumbs (default is Info) + o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug; + // Error and higher is sent as event (default is Error) + o.MinimumEventLevel = NLog.LogLevel.Error; + }); + } + + LogManager.Configuration = logConfig; + + + return logConfig; + } +} diff --git a/StabilityMatrix.Avalonia/Assets.cs b/StabilityMatrix.Avalonia/Assets.cs new file mode 100644 index 00000000..f4f5c325 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Versioning; +using Avalonia.Platform; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Core.Helper; + +namespace StabilityMatrix.Avalonia; + +internal static class Assets +{ + /// + /// Fixed image for models with no images. + /// + public static Uri NoImage { get; } = + new("avares://StabilityMatrix.Avalonia/Assets/noimage.png"); + + public static AvaloniaResource LicensesJson => new( + "avares://StabilityMatrix.Avalonia/Assets/licenses.json"); + + [SupportedOSPlatform("windows")] + [SupportedOSPlatform("linux")] + public static AvaloniaResource SevenZipExecutable => Compat.Switch( + (PlatformKind.Windows, + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")), + (PlatformKind.Linux | PlatformKind.X64, + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", (UnixFileMode) 0x777))); + + [SupportedOSPlatform("windows")] + [SupportedOSPlatform("linux")] + public static AvaloniaResource SevenZipLicense => Compat.Switch( + (PlatformKind.Windows, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt")), + (PlatformKind.Linux | PlatformKind.X64, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt"))); + + public static AvaloniaResource PyScriptSiteCustomize => new( + "avares://StabilityMatrix.Avalonia/Assets/sitecustomize.py"); + + [SupportedOSPlatform("windows")] + public static AvaloniaResource PyScriptGetPip => new( + "avares://StabilityMatrix.Avalonia/Assets/win-x64/get-pip.pyc"); + + [SupportedOSPlatform("windows")] + public static IEnumerable<(AvaloniaResource resource, string relativePath)> PyModuleVenv => + FindAssets("win-x64/venv/"); + + [SupportedOSPlatform("windows")] + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + public static RemoteResource PythonDownloadUrl => Compat.Switch( + (PlatformKind.Windows | PlatformKind.X64, new RemoteResource( + new Uri("https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"), + "608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d")), + (PlatformKind.Linux | PlatformKind.X64, new RemoteResource( + new Uri("https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz"), + "c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79")), + (PlatformKind.MacOS | PlatformKind.Arm, new RemoteResource( + new Uri("https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz"), + "8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f"))); + + public static Uri DiscordServerUrl { get; } = + new("https://discord.com/invite/TUrgfECxHz"); + + public static Uri PatreonUrl { get; } = + new("https://patreon.com/StabilityMatrix"); + + /// + /// Yield AvaloniaResources given a relative directory path within the 'Assets' folder. + /// + public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets(string relativeAssetPath) + { + var baseUri = new Uri("avares://StabilityMatrix.Avalonia/Assets/"); + var targetUri = new Uri(baseUri, relativeAssetPath); + var files = AssetLoader.GetAssets(targetUri, null); + foreach (var file in files) + { + yield return (new AvaloniaResource(file), targetUri.MakeRelativeUri(file).ToString()); + } + } +} diff --git a/StabilityMatrix.Avalonia/Assets/Icon.512x512.png b/StabilityMatrix.Avalonia/Assets/Icon.512x512.png new file mode 100644 index 00000000..f39de2e5 Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/Icon.512x512.png differ diff --git a/StabilityMatrix.Avalonia/Assets/Icon.ico b/StabilityMatrix.Avalonia/Assets/Icon.ico new file mode 100644 index 00000000..c25e35de Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/Icon.ico differ diff --git a/StabilityMatrix.Avalonia/Assets/Icon.png b/StabilityMatrix.Avalonia/Assets/Icon.png new file mode 100644 index 00000000..f39de2e5 Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/Icon.png differ diff --git a/StabilityMatrix.Avalonia/Assets/avalonia-logo.ico b/StabilityMatrix.Avalonia/Assets/avalonia-logo.ico new file mode 100644 index 00000000..da8d49ff Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/avalonia-logo.ico differ diff --git a/StabilityMatrix.Avalonia/Assets/licenses.json b/StabilityMatrix.Avalonia/Assets/licenses.json new file mode 100644 index 00000000..7d510152 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/licenses.json @@ -0,0 +1 @@ +[{"PackageName":"AsyncImageLoader.Avalonia","PackageVersion":"3.0.0-avalonia11-preview6","PackageUrl":"https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia","Copyright":"","Authors":["SKProCH"],"Description":"Provides way to asynchronous bitmap loading from web for Avalonia Image control and more","LicenseUrl":"https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia/blob/master/LICENSE","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia.git","Commit":""}},{"PackageName":"Avalonia","PackageVersion":"11.0.0-rc1.1","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"8acc41c94f664ba67069beab173a1c0feb562dce"}},{"PackageName":"Avalonia","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Angle.Windows.Natives","PackageVersion":"2.1.0.2023020321","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2023 © The AvaloniaUI Project","Authors":["Avalonia"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://www.nuget.org/packages/Avalonia.Angle.Windows.Natives/2.1.0.2023020321/License","LicenseType":"LICENSE.txt","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/angle/","Commit":"1b9ed3b11888067a93a5ea552d705ddaab21adb1"}},{"PackageName":"Avalonia.AvaloniaEdit","PackageVersion":"11.0.0","PackageUrl":"","Copyright":"","Authors":["AvaloniaEdit"],"Description":"This project is a port of AvalonEdit, a WPF-based text editor for Avalonia.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"Avalonia.BuildServices","PackageVersion":"0.0.19","PackageUrl":"","Copyright":"","Authors":["Avalonia.BuildServices"],"Description":"Package Description","LicenseUrl":"","LicenseType":"","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"Avalonia.BuildServices","PackageVersion":"0.0.28","PackageUrl":"","Copyright":"","Authors":["Avalonia.BuildServices"],"Description":"Package Description","LicenseUrl":"","LicenseType":"","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"Avalonia.Controls.ColorPicker","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Controls.DataGrid","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Controls.ItemsRepeater","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Desktop","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Diagnostics","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Fonts.Inter","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.FreeDesktop","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Native","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Remote.Protocol","PackageVersion":"11.0.0-rc1.1","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"8acc41c94f664ba67069beab173a1c0feb562dce"}},{"PackageName":"Avalonia.Remote.Protocol","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Skia","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Svg","PackageVersion":"11.0.0","PackageUrl":"https://github.com/wieslawsoltes/Svg.Skia","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"An SVG image control for Avalonia.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/wieslawsoltes/Svg.Skia","Commit":"2d9ef9767e31fc5b87655cad1022c2632b3fd33a"}},{"PackageName":"Avalonia.Themes.Simple","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Win32","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.X11","PackageVersion":"11.0.0","PackageUrl":"https://avaloniaui.net/","Copyright":"Copyright 2013-2023 © The AvaloniaUI Project","Authors":["Avalonia Team"],"Description":"Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia/","Commit":"194044692eb3967b8c6bd0ed140a954f53b48e0e"}},{"PackageName":"Avalonia.Xaml.Behaviors","PackageVersion":"11.0.0.1","PackageUrl":"https://github.com/wieslawsoltes/AvaloniaBehaviors","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"Easily add interactivity to your Avalonia apps using XAML Behaviors. Behaviors encapsulate reusable functionalities for elements that can be easily added to your XAML without the need for more imperative code.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia.Xaml.Behaviors","Commit":"2fd5e3c6f58db51afd1f04e6c5704b08318c1599"}},{"PackageName":"Avalonia.Xaml.Interactions","PackageVersion":"11.0.0.1","PackageUrl":"https://github.com/wieslawsoltes/AvaloniaBehaviors","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"Easily add interactivity to your Avalonia apps using XAML Behaviors. Behaviors encapsulate reusable functionalities for elements that can be easily added to your XAML without the need for more imperative code.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia.Xaml.Behaviors","Commit":"2fd5e3c6f58db51afd1f04e6c5704b08318c1599"}},{"PackageName":"Avalonia.Xaml.Interactions.Custom","PackageVersion":"11.0.0.1","PackageUrl":"https://github.com/wieslawsoltes/AvaloniaBehaviors","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"Easily add interactivity to your Avalonia apps using XAML Behaviors. Behaviors encapsulate reusable functionalities for elements that can be easily added to your XAML without the need for more imperative code.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia.Xaml.Behaviors","Commit":"2fd5e3c6f58db51afd1f04e6c5704b08318c1599"}},{"PackageName":"Avalonia.Xaml.Interactions.DragAndDrop","PackageVersion":"11.0.0.1","PackageUrl":"https://github.com/wieslawsoltes/AvaloniaBehaviors","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"Easily add interactivity to your Avalonia apps using XAML Behaviors. Behaviors encapsulate reusable functionalities for elements that can be easily added to your XAML without the need for more imperative code.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia.Xaml.Behaviors","Commit":"2fd5e3c6f58db51afd1f04e6c5704b08318c1599"}},{"PackageName":"Avalonia.Xaml.Interactions.Draggable","PackageVersion":"11.0.0.1","PackageUrl":"https://github.com/wieslawsoltes/AvaloniaBehaviors","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"Easily add interactivity to your Avalonia apps using XAML Behaviors. Behaviors encapsulate reusable functionalities for elements that can be easily added to your XAML without the need for more imperative code.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia.Xaml.Behaviors","Commit":"2fd5e3c6f58db51afd1f04e6c5704b08318c1599"}},{"PackageName":"Avalonia.Xaml.Interactions.Events","PackageVersion":"11.0.0.1","PackageUrl":"https://github.com/wieslawsoltes/AvaloniaBehaviors","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"Easily add interactivity to your Avalonia apps using XAML Behaviors. Behaviors encapsulate reusable functionalities for elements that can be easily added to your XAML without the need for more imperative code.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia.Xaml.Behaviors","Commit":"2fd5e3c6f58db51afd1f04e6c5704b08318c1599"}},{"PackageName":"Avalonia.Xaml.Interactions.Reactive","PackageVersion":"11.0.0.1","PackageUrl":"https://github.com/wieslawsoltes/AvaloniaBehaviors","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"Easily add interactivity to your Avalonia apps using XAML Behaviors. Behaviors encapsulate reusable functionalities for elements that can be easily added to your XAML without the need for more imperative code.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia.Xaml.Behaviors","Commit":"2fd5e3c6f58db51afd1f04e6c5704b08318c1599"}},{"PackageName":"Avalonia.Xaml.Interactions.Responsive","PackageVersion":"11.0.0.1","PackageUrl":"https://github.com/wieslawsoltes/AvaloniaBehaviors","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"Easily add interactivity to your Avalonia apps using XAML Behaviors. Behaviors encapsulate reusable functionalities for elements that can be easily added to your XAML without the need for more imperative code.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia.Xaml.Behaviors","Commit":"2fd5e3c6f58db51afd1f04e6c5704b08318c1599"}},{"PackageName":"Avalonia.Xaml.Interactivity","PackageVersion":"11.0.0.1","PackageUrl":"https://github.com/wieslawsoltes/AvaloniaBehaviors","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"Easily add interactivity to your Avalonia apps using XAML Behaviors. Behaviors encapsulate reusable functionalities for elements that can be easily added to your XAML without the need for more imperative code.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/AvaloniaUI/Avalonia.Xaml.Behaviors","Commit":"2fd5e3c6f58db51afd1f04e6c5704b08318c1599"}},{"PackageName":"AvaloniaEdit.TextMate","PackageVersion":"11.0.0","PackageUrl":"","Copyright":"","Authors":["AvaloniaEdit.TextMate"],"Description":"TextMate integration for AvaloniaEdit.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"ColorTextBlock.Avalonia","PackageVersion":"11.0.0","PackageUrl":"https://github.com/whistyun/Markdown.Avalonia/tree/master/ColorTextBlock.Avalonia/","Copyright":"Copyright (c) 2020 whistyun","Authors":["whistyun"],"Description":"Package Description","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"CommunityToolkit.Mvvm","PackageVersion":"8.2.1","PackageUrl":"https://github.com/CommunityToolkit/dotnet","Copyright":"(c) .NET Foundation and Contributors. All rights reserved.","Authors":["Microsoft"],"Description":"This package includes a .NET MVVM library with helpers such as:\r\n - ObservableObject: a base class for objects implementing the INotifyPropertyChanged interface.\r\n - ObservableRecipient: a base class for observable objects with support for the IMessenger service.\r\n - ObservableValidator: a base class for objects implementing the INotifyDataErrorInfo interface.\r\n - RelayCommand: a simple delegate command implementing the ICommand interface.\r\n - AsyncRelayCommand: a delegate command supporting asynchronous operations and cancellation.\r\n - WeakReferenceMessenger: a messaging system to exchange messages through different loosely-coupled objects.\r\n - StrongReferenceMessenger: a high-performance messaging system that trades weak references for speed.\r\n - Ioc: a helper class to configure dependency injection service containers.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/CommunityToolkit/dotnet","Commit":"2258fd310359fb7434d2040b34f04366efbacbf8"}},{"PackageName":"ExCSS","PackageVersion":"4.1.4","PackageUrl":"","Copyright":"","Authors":["Tyler Brinks"],"Description":"ExCSS is a CSS 2.1 and CSS 3 parser for .NET. ExCSS makes it easy to read and parse stylesheets into a friendly object model with full LINQ support.","LicenseUrl":"","LicenseType":"","Repository":{"Type":"","Url":"https://github.com/TylerBrinks/ExCSS","Commit":""}},{"PackageName":"Fizzler","PackageVersion":"1.2.1","PackageUrl":"https://github.com/atifaziz/Fizzler","Copyright":"Copyright © 2009 Atif Aziz, Colin Ramsay. All rights reserved. Portions Copyright © 2008 Novell, Inc.","Authors":["Atif Aziz","Colin Ramsay"],"Description":"Fizzler is a W3C Selectors parser and generic selector framework for document hierarchies.","LicenseUrl":"https://www.nuget.org/packages/Fizzler/1.2.1/License","LicenseType":"COPYING.txt","Repository":{"Type":"Git","Url":"https://github.com/atifaziz/Fizzler","Commit":"8b3773e58a471266e4024f980b40099adccbab41"}},{"PackageName":"FluentAvaloniaUI","PackageVersion":"2.0.0","PackageUrl":"","Copyright":"","Authors":["FluentAvalonia"],"Description":"Control library focused on fluent design and bringing more WinUI controls into Avalonia","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/amwx/FluentAvalonia","Commit":"84a2a5cfd5e4a5d4a29856ff43253db57a908b33"}},{"PackageName":"FluentIcons.Avalonia","PackageVersion":"1.1.207","PackageUrl":"","Copyright":"","Authors":["davidxuang"],"Description":"FluentUI System Icons wrapper for Avalonia.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"https://github.com/davidxuang/FluentIcons","Commit":""}},{"PackageName":"FluentIcons.Common","PackageVersion":"1.1.207","PackageUrl":"","Copyright":"","Authors":["davidxuang"],"Description":"A shared library for FluentUI System Icons wrapper.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"https://github.com/davidxuang/FluentIcons","Commit":""}},{"PackageName":"FluentIcons.FluentAvalonia","PackageVersion":"1.1.207","PackageUrl":"","Copyright":"","Authors":["davidxuang"],"Description":"FluentUI System Icons wrapper for FluentAvalonia.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"https://github.com/davidxuang/FluentIcons","Commit":""}},{"PackageName":"FuzzySharp","PackageVersion":"2.0.2","PackageUrl":"https://github.com/JakeBayer/FuzzySharp","Copyright":"","Authors":["Jacob Bayer"],"Description":"Fuzzy string matcher based on FuzzyWuzzy algorithm from SeatGeek","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/JakeBayer/FuzzySharp","Commit":"53b71acd66e53a4ff9f4229348de48295f99c0a5"}},{"PackageName":"HarfBuzzSharp","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.Android","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.iOS","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.Linux","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.MacCatalyst","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.macOS","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.Tizen","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.tvOS","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.UWP","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.watchOS","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.WebAssembly","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HarfBuzzSharp.NativeAssets.Win32","PackageVersion":"2.8.2.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"HarfBuzzSharp is a cross-platform OpenType text shaping engine for .NET platforms.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"HtmlAgilityPack","PackageVersion":"1.11.42","PackageUrl":"http://html-agility-pack.net/","Copyright":"Copyright © ZZZ Projects Inc.","Authors":["ZZZ Projects","Simon Mourrier","Jeff Klawiter","Stephan Grell"],"Description":"This is an agile HTML parser that builds a read/write DOM and supports plain XPATH or XSLT (you actually don't HAVE to understand XPATH nor XSLT to use it, don't worry...). It is a .NET code library that allows you to parse \"out of the web\" HTML files. The parser is very tolerant with \"real world\" malformed HTML. The object model is very similar to what proposes System.Xml, but for HTML documents (or streams).","LicenseUrl":"https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/zzzprojects/html-agility-pack/","Commit":""}},{"PackageName":"K4os.Compression.LZ4","PackageVersion":"1.3.5","PackageUrl":"https://github.com/MiloszKrajewski/K4os.Compression.LZ4","Copyright":"","Authors":["Milosz Krajewski"],"Description":"Port of LZ4 compression algorithm for .NET","LicenseUrl":"https://raw.githubusercontent.com/MiloszKrajewski/K4os.Compression.LZ4/master/LICENSE","LicenseType":"","Repository":{"Type":"","Url":"https://github.com/MiloszKrajewski/K4os.Compression.LZ4","Commit":""}},{"PackageName":"Markdown.Avalonia","PackageVersion":"11.0.0","PackageUrl":"https://github.com/whistyun/Markdown.Avalonia","Copyright":"Copyright (c) 2010 Bevan Arps, 2020 whistyun","Authors":["Bevan Arps(original)","whistyun"],"Description":"Markdown Controls for Avalonia","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"Markdown.Avalonia.Html","PackageVersion":"11.0.0","PackageUrl":"https://github.com/whistyun/Markdown.Avalonia","Copyright":"© Simon Baynes 2013; whistyun 2023","Authors":["whistyun"],"Description":"html tag processor for Markdown.Avalonia","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"Markdown.Avalonia.Svg","PackageVersion":"11.0.0","PackageUrl":"https://github.com/whistyun/Markdown.Avalonia","Copyright":"Copyright (c) 2023 grifsun, whistyun","Authors":["grifsun","whistyun"],"Description":"Package Description","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"Markdown.Avalonia.SyntaxHigh","PackageVersion":"11.0.0","PackageUrl":"https://github.com/whistyun/Markdown.Avalonia","Copyright":"Copyright (c) 2021 whistyun","Authors":["whistyun"],"Description":"Package Description","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"Markdown.Avalonia.Tight","PackageVersion":"11.0.0","PackageUrl":"https://github.com/whistyun/Markdown.Avalonia","Copyright":"Copyright (c) 2010 Bevan Arps, 2020 whistyun","Authors":["Bevan Arps(original)","whistyun"],"Description":"Markdown Controls for Avalonia","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"MicroCom.CodeGenerator.MSBuild","PackageVersion":"0.11.0","PackageUrl":"","Copyright":"Copyright 2021 © Nikita Tsukanov","Authors":["MicroCom.CodeGenerator.MSBuild"],"Description":"IDL-based COM interop codegen. Consumes MIDL-like IDL file, generates efficient cross-platform C# interop and C++ header files.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/kekekeks/MicroCom","Commit":""}},{"PackageName":"MicroCom.Runtime","PackageVersion":"0.11.0","PackageUrl":"","Copyright":"Copyright 2021 © Nikita Tsukanov","Authors":["MicroCom.Runtime"],"Description":"IDL-based COM interop codegen. Consumes MIDL-like IDL file, generates efficient cross-platform C# interop and C++ header files.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/kekekeks/MicroCom","Commit":""}},{"PackageName":"NETStandard.Library","PackageVersion":"1.6.1","PackageUrl":"https://dot.net/","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"A set of standard .NET APIs that are prescribed to be used and supported together. This includes all of the APIs in the NETStandard.Platform package plus additional libraries that are core to .NET but built on top of NETStandard.Platform. \r\nWhen using NuGet 3.x this package requires at least version 3.4.","LicenseUrl":"http://go.microsoft.com/fwlink/?LinkId=329770","LicenseType":"MS-EULA","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"NETStandard.Library","PackageVersion":"1.6.0","PackageUrl":"https://dot.net/","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"A set of standard .NET APIs that are prescribed to be used and supported together. This includes all of the APIs in the NETStandard.Platform package plus additional libraries that are core to .NET but built on top of NETStandard.Platform. \r\nWhen using NuGet 3.x this package requires at least version 3.4.","LicenseUrl":"http://go.microsoft.com/fwlink/?LinkId=329770","LicenseType":"MS-EULA","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"Nito.AsyncEx","PackageVersion":"5.1.2","PackageUrl":"https://github.com/StephenCleary/AsyncEx","Copyright":"","Authors":["Stephen Cleary"],"Description":"A helper library for the Task-Based Asynchronous Pattern (TAP).","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/StephenCleary/AsyncEx","Commit":"0361015459938f2eb8f3c1ad1021d19ee01c93a4"}},{"PackageName":"Nito.AsyncEx.Context","PackageVersion":"5.1.2","PackageUrl":"https://github.com/StephenCleary/AsyncEx","Copyright":"","Authors":["Stephen Cleary"],"Description":"A single-threaded async-compatible context.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/StephenCleary/AsyncEx","Commit":"0361015459938f2eb8f3c1ad1021d19ee01c93a4"}},{"PackageName":"Nito.AsyncEx.Coordination","PackageVersion":"5.1.2","PackageUrl":"https://github.com/StephenCleary/AsyncEx","Copyright":"","Authors":["Stephen Cleary"],"Description":"Asynchronous coordination primitives.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/StephenCleary/AsyncEx","Commit":"0361015459938f2eb8f3c1ad1021d19ee01c93a4"}},{"PackageName":"Nito.AsyncEx.Interop.WaitHandles","PackageVersion":"5.1.2","PackageUrl":"https://github.com/StephenCleary/AsyncEx","Copyright":"","Authors":["Stephen Cleary"],"Description":"Task wrappers for WaitHandles.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/StephenCleary/AsyncEx","Commit":"0361015459938f2eb8f3c1ad1021d19ee01c93a4"}},{"PackageName":"Nito.AsyncEx.Oop","PackageVersion":"5.1.2","PackageUrl":"https://github.com/StephenCleary/AsyncEx","Copyright":"","Authors":["Stephen Cleary"],"Description":"Interfaces and utility methods for combining async with OOP.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/StephenCleary/AsyncEx","Commit":"0361015459938f2eb8f3c1ad1021d19ee01c93a4"}},{"PackageName":"Nito.AsyncEx.Tasks","PackageVersion":"5.1.2","PackageUrl":"https://github.com/StephenCleary/AsyncEx","Copyright":"","Authors":["Stephen Cleary"],"Description":"Common helper methods for tasks as used in asynchronous programming.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/StephenCleary/AsyncEx","Commit":"0361015459938f2eb8f3c1ad1021d19ee01c93a4"}},{"PackageName":"Nito.Cancellation","PackageVersion":"1.1.2","PackageUrl":"https://github.com/StephenCleary/Cancellation","Copyright":"","Authors":["Stephen Cleary"],"Description":"Helper types for working with cancellation tokens and sources.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/StephenCleary/Cancellation","Commit":"3f41c18da9f9bd71a17de6329aaea2bf7388d0d7"}},{"PackageName":"Nito.Collections.Deque","PackageVersion":"1.1.1","PackageUrl":"https://github.com/StephenCleary/Deque","Copyright":"","Authors":["Stephen Cleary"],"Description":"A double-ended queue.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/StephenCleary/Deque","Commit":"fb12d9918339a9affd11f6d5f82a29bb7e26c4a0"}},{"PackageName":"Nito.Disposables","PackageVersion":"2.2.1","PackageUrl":"https://github.com/StephenCleary/Disposables","Copyright":"","Authors":["Stephen Cleary"],"Description":"IDisposable and IAsyncDisposable helper types.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/StephenCleary/Disposables","Commit":"421e8c3af9e9baf1c885671887671971aa2d7a29"}},{"PackageName":"NLog","PackageVersion":"5.2.2","PackageUrl":"https://nlog-project.org/","Copyright":"Copyright (c) 2004-2023 NLog Project - https://nlog-project.org/","Authors":["Jarek Kowalski","Kim Christensen","Julian Verdurmen"],"Description":"NLog is a logging platform for .NET with rich log routing and management capabilities.\r\nNLog supports traditional logging, structured logging and the combination of both.\r\n\r\nSupported platforms:\r\n\r\n- .NET 5, 6 and 7\r\n- .NET Core 1, 2 and 3\r\n- .NET Standard 1.3+ and 2.0+\r\n- .NET Framework 3.5 - 4.8\r\n- Xamarin Android + iOS (.NET Standard)\r\n- Mono 4\r\n\r\nFor ASP.NET Core, check: https://www.nuget.org/packages/NLog.Web.AspNetCore","LicenseUrl":"https://licenses.nuget.org/BSD-3-Clause","LicenseType":"BSD-3-Clause","Repository":{"Type":"git","Url":"https://github.com/NLog/NLog.git","Commit":""}},{"PackageName":"NLog","PackageVersion":"4.5.11","PackageUrl":"http://nlog-project.org/","Copyright":"Copyright (c) 2004-2018 NLog Project - http://nlog-project.org/","Authors":["Jarek Kowalski","Kim Christensen","Julian Verdurmen"],"Description":"NLog is a logging platform for .NET with rich log routing and management capabilities.\r\nNLog supports traditional logging, structured logging and the combination of both.\r\n\r\nSupported platforms:\r\n\r\n- .NET Framework 3.5, 4, 4.5, 4.6 & 4.7\r\n- .NET Standard 1.3+ and 2.0+;\r\n- .NET Framework 4 client profile\r\n- Xamarin Android, Xamarin iOs\r\n- UWP\r\n- Windows Phone 8\r\n- Silverlight 4 and 5\r\n- Mono 4\r\n\r\nFor ASP.NET Core, check: https://www.nuget.org/packages/NLog.Web.AspNetCore","LicenseUrl":"https://github.com/NLog/NLog/blob/master/LICENSE.txt","LicenseType":"","Repository":{"Type":"git","Url":"git://github.com/NLog/NLog","Commit":""}},{"PackageName":"NLog.Extensions.Logging","PackageVersion":"5.3.2","PackageUrl":"https://github.com/NLog/NLog.Extensions.Logging","Copyright":"","Authors":["Microsoft","Julian Verdurmen"],"Description":"NLog LoggerProvider for Microsoft.Extensions.Logging for logging in .NET Standard libraries and .NET Core applications.\r\n\r\nFor ASP.NET Core, check: https://www.nuget.org/packages/NLog.Web.AspNetCore","LicenseUrl":"https://licenses.nuget.org/BSD-2-Clause","LicenseType":"BSD-2-Clause","Repository":{"Type":"git","Url":"https://github.com/NLog/NLog.Extensions.Logging.git","Commit":"d47e232ba867eedbff8f7972047a17715b8c1c2f"}},{"PackageName":"Polly","PackageVersion":"7.2.4","PackageUrl":"https://github.com/App-vNext/Polly","Copyright":"Copyright (c) 2023, App vNext","Authors":["Michael Wolfenden"," App vNext"],"Description":"Polly is a library that allows developers to express resilience and transient fault handling policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.","LicenseUrl":"https://licenses.nuget.org/BSD-3-Clause","LicenseType":"BSD-3-Clause","Repository":{"Type":"git","Url":"https://github.com/App-vNext/Polly","Commit":"b1a5a17de8ef5d0bc86e5a4502ad30891e674379"}},{"PackageName":"Polly","PackageVersion":"7.2.3","PackageUrl":"https://github.com/App-vNext/Polly","Copyright":"Copyright (c) 2022, App vNext","Authors":["Michael Wolfenden"," App vNext"],"Description":"Polly is a library that allows developers to express resilience and transient fault handling policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.","LicenseUrl":"https://licenses.nuget.org/BSD-3-Clause","LicenseType":"BSD-3-Clause","Repository":{"Type":"git","Url":"https://github.com/App-vNext/Polly.git","Commit":"c27faf89ef459fd9a8e0131ae1d4cd835ca4f31d"}},{"PackageName":"Polly.Contrib.WaitAndRetry","PackageVersion":"1.1.1","PackageUrl":"https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry","Copyright":"Copyright (c) 2020, App vNext and contributors","Authors":["Grant Dickinson"," App vNext"],"Description":"Polly.Contrib.WaitAndRetry is an extension library for Polly containing helper methods for a variety of wait-and-retry strategies.","LicenseUrl":"https://licenses.nuget.org/BSD-3-Clause","LicenseType":"BSD-3-Clause","Repository":{"Type":"git","Url":"https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry.git","Commit":"7596d2dacf22d88bbd814bc49c28424fb6e921e9"}},{"PackageName":"Polly.Extensions.Http","PackageVersion":"3.0.0","PackageUrl":"https://github.com/App-vNext/Polly.Extensions.Http","Copyright":"Copyright (c) 2019, App vNext","Authors":["App vNext"],"Description":"Polly.Extensions.Http is an extensions package containing opinionated convenience methods for configuring Polly policies to handle transient faults typical of calls through HttpClient.","LicenseUrl":"https://licenses.nuget.org/BSD-3-Clause","LicenseType":"BSD-3-Clause","Repository":{"Type":"git","Url":"https://github.com/App-vNext/Polly.Extensions.Http.git","Commit":"69fd292bc603cb3032e57b028522737255f03a49"}},{"PackageName":"Projektanker.Icons.Avalonia","PackageVersion":"6.6.0","PackageUrl":"","Copyright":"","Authors":["Sebastian Rumohr"],"Description":"A library to easily display icons in an Avalonia App.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"https://github.com/Projektanker/Icons.Avalonia","Commit":""}},{"PackageName":"Projektanker.Icons.Avalonia.FontAwesome","PackageVersion":"6.6.0","PackageUrl":"","Copyright":"","Authors":["Sebastian Rumohr"],"Description":"A library to easily display FontAwesome icons in an Avalonia App.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"https://github.com/Projektanker/Icons.Avalonia","Commit":""}},{"PackageName":"Sentry","PackageVersion":"3.33.1","PackageUrl":"https://sentry.io/","Copyright":"","Authors":["Sentry Team and Contributors"],"Description":"Official SDK for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/getsentry/sentry-dotnet","Commit":"2d6b2eb429da501e3da5cc7eb362345532058f09"}},{"PackageName":"Sentry.Android.AssemblyReader","PackageVersion":"3.33.1","PackageUrl":"https://sentry.io/","Copyright":"","Authors":["Sentry Team and Contributors"],"Description":".NET assembly reader for Android","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/getsentry/sentry-dotnet","Commit":"2d6b2eb429da501e3da5cc7eb362345532058f09"}},{"PackageName":"Sentry.Bindings.Android","PackageVersion":"3.33.1","PackageUrl":"https://sentry.io/","Copyright":"","Authors":["Sentry Team and Contributors"],"Description":".NET Bindings for the Sentry Android SDK","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/getsentry/sentry-dotnet","Commit":"2d6b2eb429da501e3da5cc7eb362345532058f09"}},{"PackageName":"Sentry.Bindings.Cocoa","PackageVersion":"3.33.1","PackageUrl":"https://sentry.io/","Copyright":"","Authors":["Sentry Team and Contributors"],"Description":".NET Bindings for the Sentry Cocoa SDK","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/getsentry/sentry-dotnet","Commit":"2d6b2eb429da501e3da5cc7eb362345532058f09"}},{"PackageName":"Sentry.NLog","PackageVersion":"3.33.1","PackageUrl":"https://sentry.io/","Copyright":"","Authors":["Sentry Team and Contributors"],"Description":"Official NLog integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/getsentry/sentry-dotnet","Commit":"2d6b2eb429da501e3da5cc7eb362345532058f09"}},{"PackageName":"ShimSkiaSharp","PackageVersion":"1.0.0","PackageUrl":"https://github.com/wieslawsoltes/Svg.Skia","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"SkiaSharp picture recorder object model shim.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/wieslawsoltes/Svg.Skia","Commit":"2d9ef9767e31fc5b87655cad1022c2632b3fd33a"}},{"PackageName":"SkiaSharp","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.Android","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.iOS","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.Linux","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.MacCatalyst","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.macOS","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.Tizen","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.tvOS","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.UWP","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.watchOS","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.WebAssembly","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"SkiaSharp.NativeAssets.Win32","PackageVersion":"2.88.3","PackageUrl":"https://go.microsoft.com/fwlink/?linkid=868515","Copyright":"© Microsoft Corporation. All rights reserved.","Authors":["Microsoft"],"Description":"SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library.\r\nIt provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.","LicenseUrl":"https://go.microsoft.com/fwlink/?linkid=868514","LicenseType":"","Repository":{"Type":"git","Url":"https://github.com/mono/SkiaSharp","Commit":"655cff084fa9365efde6e989004eb818294c9b0f"}},{"PackageName":"Svg.Custom","PackageVersion":"1.0.0","PackageUrl":"https://github.com/wieslawsoltes/Svg.Skia","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"Custom build of the SVG rendering library.","LicenseUrl":"https://licenses.nuget.org/MS-PL","LicenseType":"MS-PL","Repository":{"Type":"git","Url":"https://github.com/wieslawsoltes/Svg.Skia","Commit":"2d9ef9767e31fc5b87655cad1022c2632b3fd33a"}},{"PackageName":"Svg.Model","PackageVersion":"1.0.0","PackageUrl":"https://github.com/wieslawsoltes/Svg.Skia","Copyright":"Copyright © Wiesław Šoltés 2023","Authors":["Wiesław Šoltés"],"Description":"An SVG rendering object model library.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/wieslawsoltes/Svg.Skia","Commit":"2d9ef9767e31fc5b87655cad1022c2632b3fd33a"}},{"PackageName":"TextMateSharp","PackageVersion":"1.0.55","PackageUrl":"https://github.com/danipen/TextMateSharp","Copyright":"","Authors":["Daniel Peñalba"],"Description":"An interpreter for grammar files as defined by TextMate. TextMate grammars use the oniguruma dialect (https://github.com/kkos/oniguruma). Supports loading grammar files only from JSON format. Cross - grammar injections are currently not supported.\n\nTextMateSharp is a port of microsoft/vscode-textmate that brings TextMate grammars to dotnet ecosystem. The implementation is based the Java port eclipse/tm4e.\n\nTextMateSharp uses a wrapper around Oniguruma regex engine. Read below to learn how to build Oniguruma bindings.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/danipen/TextMateSharp","Commit":"c2d1d7d228a59b30d429b3f288092a3e326fa4c5"}},{"PackageName":"TextMateSharp.Grammars","PackageVersion":"1.0.55","PackageUrl":"https://github.com/danipen/TextMateSharp","Copyright":"","Authors":["Daniel Peñalba"],"Description":"A set of grammars and Themes that can be used by TextMateSharp","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"","Url":"","Commit":""}},{"PackageName":"Tmds.DBus.Protocol","PackageVersion":"0.15.0","PackageUrl":"","Copyright":"Tom Deseyn","Authors":["Tom Deseyn"],"Description":"Tmds.DBus.Protocol Library","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT","Repository":{"Type":"git","Url":"https://github.com/tmds/Tmds.DBus.git","Commit":"b2834c5e1b2800a2eb92d8c5932fd77639441e9a"}}] \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Assets/linux-x64/7zzs b/StabilityMatrix.Avalonia/Assets/linux-x64/7zzs new file mode 100644 index 00000000..c27d649d Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/linux-x64/7zzs differ diff --git a/StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt b/StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt new file mode 100644 index 00000000..8650d994 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt @@ -0,0 +1,88 @@ + 7-Zip + ~~~~~ + License for use and distribution + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + 7-Zip Copyright (C) 1999-2023 Igor Pavlov. + + The licenses for 7zz and 7zzs files are: + + - The "GNU LGPL" as main license for most of the code + - The "GNU LGPL" with "unRAR license restriction" for some code + - The "BSD 3-clause License" for some code + + Redistributions in binary form must reproduce related license information from this file. + + Note: + You can use 7-Zip on any computer, including a computer in a commercial + organization. You don't need to register or pay for 7-Zip. + + + GNU LGPL information + -------------------- + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You can receive a copy of the GNU Lesser General Public License from + http://www.gnu.org/ + + + + + BSD 3-clause License + -------------------- + + The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression. + That code was derived from the code in the "LZFSE compression library" developed by Apple Inc, + that also uses the "BSD 3-clause License": + + ---- + Copyright (c) 2015-2016, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ---- + + + + + unRAR license restriction + ------------------------- + + The decompression engine for RAR archives was developed using source + code of unRAR program. + All copyrights to original unRAR code are owned by Alexander Roshal. + + The license for original unRAR code has the following restriction: + + The unRAR sources cannot be used to re-create the RAR compression algorithm, + which is proprietary. Distribution of modified unRAR sources in separate form + or as a part of other software is permitted, provided that it is clearly + stated in the documentation and source comments that the code may + not be used to develop a RAR (WinRAR) compatible archiver. + + + -- + Igor Pavlov diff --git a/StabilityMatrix.Avalonia/Assets/noimage.png b/StabilityMatrix.Avalonia/Assets/noimage.png new file mode 100644 index 00000000..8b3cefed Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/noimage.png differ diff --git a/StabilityMatrix.Avalonia/Assets/sitecustomize.py b/StabilityMatrix.Avalonia/Assets/sitecustomize.py new file mode 100644 index 00000000..c51abc9d --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/sitecustomize.py @@ -0,0 +1,49 @@ +""" +Startup site customization for Stability Matrix. + +Currently this installs an audit hook to notify the parent process when input() is called, +so we can prompt the user to enter something. +""" + +import sys + +# Application Program Command escape sequence +# This wraps messages sent to the parent process. +esc_apc = "\x9F" +esc_prefix = "[SM;" +esc_st = "\x9C" + + +def send_apc(msg: str): + """Send an Application Program Command to the parent process.""" + sys.stdout.flush() + sys.stdout.write(esc_apc + esc_prefix + msg + esc_st) + sys.stdout.flush() + + +def send_apc_input(prompt: str): + """Apc message for input() prompt.""" + send_apc('{"type":"input","data":"' + str(prompt) + '"}') + + +def audit(event: str, *args): + """Main audit hook function.""" + # https://docs.python.org/3/library/functions.html#input + # input() raises audit event `builtins.input` with args (prompt: str) *before* reading from stdin. + # `builtins.input/result` raised after reading from stdin. + + if event == "builtins.input": + try: + prompts = args[0] if args else () + prompt = "".join(prompts) + send_apc_input(prompt) + except Exception: + pass + + +# Reconfigure stdout to UTF-8 +# noinspection PyUnresolvedReferences +sys.stdout.reconfigure(encoding="utf-8") + +# Install the audit hook +sys.addaudithook(audit) diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt b/StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt new file mode 100644 index 00000000..80473a66 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt @@ -0,0 +1,43 @@ +7-Zip Extra 18.01 +----------------- + +7-Zip Extra is package of extra modules of 7-Zip. + +7-Zip Copyright (C) 1999-2018 Igor Pavlov. + +7-Zip is free software. Read License.txt for more information about license. + +Source code of binaries can be found at: + http://www.7-zip.org/ + + +7-Zip Extra +~~~~~~~~~~~ +License for use and distribution +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Copyright (C) 1999-2018 Igor Pavlov. + +7-Zip Extra files are under the GNU LGPL license. + + +Notes: + You can use 7-Zip Extra on any computer, including a computer in a commercial + organization. You don't need to register or pay for 7-Zip. + + +GNU LGPL information +-------------------- + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You can receive a copy of the GNU Lesser General Public License from + http://www.gnu.org/ diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/7za.exe b/StabilityMatrix.Avalonia/Assets/win-x64/7za.exe new file mode 100644 index 00000000..a67de915 Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/win-x64/7za.exe differ diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/get-pip.pyc b/StabilityMatrix.Avalonia/Assets/win-x64/get-pip.pyc new file mode 100644 index 00000000..6a2e3984 Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/win-x64/get-pip.pyc differ diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/venv/__init__.py b/StabilityMatrix.Avalonia/Assets/win-x64/venv/__init__.py new file mode 100644 index 00000000..69155e5c --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/win-x64/venv/__init__.py @@ -0,0 +1,525 @@ +""" +Virtual environment (venv) package for Python. Based on PEP 405. + +Copyright (C) 2011-2014 Vinay Sajip. +Licensed to the PSF under a contributor agreement. +""" +import logging +import os +import shutil +import subprocess +import sys +import sysconfig +import types + + +CORE_VENV_DEPS = ('pip', 'setuptools') +logger = logging.getLogger(__name__) + + +class EnvBuilder: + """ + This class exists to allow virtual environment creation to be + customized. The constructor parameters determine the builder's + behaviour when called upon to create a virtual environment. + + By default, the builder makes the system (global) site-packages dir + *un*available to the created environment. + + If invoked using the Python -m option, the default is to use copying + on Windows platforms but symlinks elsewhere. If instantiated some + other way, the default is to *not* use symlinks. + + :param system_site_packages: If True, the system (global) site-packages + dir is available to created environments. + :param clear: If True, delete the contents of the environment directory if + it already exists, before environment creation. + :param symlinks: If True, attempt to symlink rather than copy files into + virtual environment. + :param upgrade: If True, upgrade an existing virtual environment. + :param with_pip: If True, ensure pip is installed in the virtual + environment + :param prompt: Alternative terminal prefix for the environment. + :param upgrade_deps: Update the base venv modules to the latest on PyPI + """ + + def __init__(self, system_site_packages=False, clear=False, + symlinks=False, upgrade=False, with_pip=False, prompt=None, + upgrade_deps=False): + self.system_site_packages = system_site_packages + self.clear = clear + self.symlinks = symlinks + self.upgrade = upgrade + self.with_pip = with_pip + if prompt == '.': # see bpo-38901 + prompt = os.path.basename(os.getcwd()) + self.prompt = prompt + self.upgrade_deps = upgrade_deps + + def create(self, env_dir): + """ + Create a virtual environment in a directory. + + :param env_dir: The target directory to create an environment in. + + """ + env_dir = os.path.abspath(env_dir) + context = self.ensure_directories(env_dir) + # See issue 24875. We need system_site_packages to be False + # until after pip is installed. + true_system_site_packages = self.system_site_packages + self.system_site_packages = False + self.create_configuration(context) + self.setup_python(context) + if self.with_pip: + self._setup_pip(context) + if not self.upgrade: + self.setup_scripts(context) + self.post_setup(context) + if true_system_site_packages: + # We had set it to False before, now + # restore it and rewrite the configuration + self.system_site_packages = True + self.create_configuration(context) + if self.upgrade_deps: + self.upgrade_dependencies(context) + + def clear_directory(self, path): + for fn in os.listdir(path): + fn = os.path.join(path, fn) + if os.path.islink(fn) or os.path.isfile(fn): + os.remove(fn) + elif os.path.isdir(fn): + shutil.rmtree(fn) + + def ensure_directories(self, env_dir): + """ + Create the directories for the environment. + + Returns a context object which holds paths in the environment, + for use by subsequent logic. + """ + + def create_if_needed(d): + if not os.path.exists(d): + os.makedirs(d) + elif os.path.islink(d) or os.path.isfile(d): + raise ValueError('Unable to create directory %r' % d) + + if os.path.exists(env_dir) and self.clear: + self.clear_directory(env_dir) + context = types.SimpleNamespace() + context.env_dir = env_dir + context.env_name = os.path.split(env_dir)[1] + prompt = self.prompt if self.prompt is not None else context.env_name + context.prompt = '(%s) ' % prompt + create_if_needed(env_dir) + executable = sys._base_executable + if not executable: # see gh-96861 + raise ValueError('Unable to determine path to the running ' + 'Python interpreter. Provide an explicit path or ' + 'check that your PATH environment variable is ' + 'correctly set.') + dirname, exename = os.path.split(os.path.abspath(executable)) + context.executable = executable + context.python_dir = dirname + context.python_exe = exename + if sys.platform == 'win32': + binname = 'Scripts' + incpath = 'Include' + libpath = os.path.join(env_dir, 'Lib', 'site-packages') + else: + binname = 'bin' + incpath = 'include' + libpath = os.path.join(env_dir, 'lib', + 'python%d.%d' % sys.version_info[:2], + 'site-packages') + context.inc_path = path = os.path.join(env_dir, incpath) + create_if_needed(path) + create_if_needed(libpath) + # Issue 21197: create lib64 as a symlink to lib on 64-bit non-OS X POSIX + if ((sys.maxsize > 2**32) and (os.name == 'posix') and + (sys.platform != 'darwin')): + link_path = os.path.join(env_dir, 'lib64') + if not os.path.exists(link_path): # Issue #21643 + os.symlink('lib', link_path) + context.bin_path = binpath = os.path.join(env_dir, binname) + context.bin_name = binname + context.env_exe = os.path.join(binpath, exename) + create_if_needed(binpath) + # Assign and update the command to use when launching the newly created + # environment, in case it isn't simply the executable script (e.g. bpo-45337) + context.env_exec_cmd = context.env_exe + if sys.platform == 'win32': + # bpo-45337: Fix up env_exec_cmd to account for file system redirections. + # Some redirects only apply to CreateFile and not CreateProcess + real_env_exe = os.path.realpath(context.env_exe) + if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe): + logger.warning('Actual environment location may have moved due to ' + 'redirects, links or junctions.\n' + ' Requested location: "%s"\n' + ' Actual location: "%s"', + context.env_exe, real_env_exe) + context.env_exec_cmd = real_env_exe + return context + + def create_configuration(self, context): + """ + Create a configuration file indicating where the environment's Python + was copied from, and whether the system site-packages should be made + available in the environment. + + :param context: The information for the environment creation request + being processed. + """ + context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg') + with open(path, 'w', encoding='utf-8') as f: + f.write('home = %s\n' % context.python_dir) + if self.system_site_packages: + incl = 'true' + else: + incl = 'false' + f.write('include-system-site-packages = %s\n' % incl) + f.write('version = %d.%d.%d\n' % sys.version_info[:3]) + if self.prompt is not None: + f.write(f'prompt = {self.prompt!r}\n') + + if os.name != 'nt': + def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): + """ + Try symlinking a file, and if that fails, fall back to copying. + """ + force_copy = not self.symlinks + if not force_copy: + try: + if not os.path.islink(dst): # can't link to itself! + if relative_symlinks_ok: + assert os.path.dirname(src) == os.path.dirname(dst) + os.symlink(os.path.basename(src), dst) + else: + os.symlink(src, dst) + except Exception: # may need to use a more specific exception + logger.warning('Unable to symlink %r to %r', src, dst) + force_copy = True + if force_copy: + shutil.copyfile(src, dst) + else: + def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): + """ + Try symlinking a file, and if that fails, fall back to copying. + """ + bad_src = os.path.lexists(src) and not os.path.exists(src) + if self.symlinks and not bad_src and not os.path.islink(dst): + try: + if relative_symlinks_ok: + assert os.path.dirname(src) == os.path.dirname(dst) + os.symlink(os.path.basename(src), dst) + else: + os.symlink(src, dst) + return + except Exception: # may need to use a more specific exception + logger.warning('Unable to symlink %r to %r', src, dst) + + # On Windows, we rewrite symlinks to our base python.exe into + # copies of venvlauncher.exe + basename, ext = os.path.splitext(os.path.basename(src)) + srcfn = os.path.join(os.path.dirname(__file__), + "scripts", + "nt", + basename + ext) + # Builds or venv's from builds need to remap source file + # locations, as we do not put them into Lib/venv/scripts + if sysconfig.is_python_build(True) or not os.path.isfile(srcfn): + if basename.endswith('_d'): + ext = '_d' + ext + basename = basename[:-2] + if basename == 'python': + basename = 'venvlauncher' + elif basename == 'pythonw': + basename = 'venvwlauncher' + src = os.path.join(os.path.dirname(src), basename + ext) + else: + src = srcfn + if not os.path.exists(src): + if not bad_src: + logger.warning('Unable to copy %r', src) + return + + shutil.copyfile(src, dst) + + def setup_python(self, context): + """ + Set up a Python executable in the environment. + + :param context: The information for the environment creation request + being processed. + """ + binpath = context.bin_path + path = context.env_exe + copier = self.symlink_or_copy + dirname = context.python_dir + if os.name != 'nt': + copier(context.executable, path) + if not os.path.islink(path): + os.chmod(path, 0o755) + for suffix in ('python', 'python3', f'python3.{sys.version_info[1]}'): + path = os.path.join(binpath, suffix) + if not os.path.exists(path): + # Issue 18807: make copies if + # symlinks are not wanted + copier(context.env_exe, path, relative_symlinks_ok=True) + if not os.path.islink(path): + os.chmod(path, 0o755) + else: + if self.symlinks: + # For symlinking, we need a complete copy of the root directory + # If symlinks fail, you'll get unnecessary copies of files, but + # we assume that if you've opted into symlinks on Windows then + # you know what you're doing. + suffixes = [ + f for f in os.listdir(dirname) if + os.path.normcase(os.path.splitext(f)[1]) in ('.exe', '.dll') + ] + if sysconfig.is_python_build(True): + suffixes = [ + f for f in suffixes if + os.path.normcase(f).startswith(('python', 'vcruntime')) + ] + else: + suffixes = {'python.exe', 'python_d.exe', 'pythonw.exe', 'pythonw_d.exe'} + base_exe = os.path.basename(context.env_exe) + suffixes.add(base_exe) + + for suffix in suffixes: + src = os.path.join(dirname, suffix) + if os.path.lexists(src): + copier(src, os.path.join(binpath, suffix)) + + if sysconfig.is_python_build(True): + # copy init.tcl + for root, dirs, files in os.walk(context.python_dir): + if 'init.tcl' in files: + tcldir = os.path.basename(root) + tcldir = os.path.join(context.env_dir, 'Lib', tcldir) + if not os.path.exists(tcldir): + os.makedirs(tcldir) + src = os.path.join(root, 'init.tcl') + dst = os.path.join(tcldir, 'init.tcl') + shutil.copyfile(src, dst) + break + + def _call_new_python(self, context, *py_args, **kwargs): + """Executes the newly created Python using safe-ish options""" + # gh-98251: We do not want to just use '-I' because that masks + # legitimate user preferences (such as not writing bytecode). All we + # really need is to ensure that the path variables do not overrule + # normal venv handling. + args = [context.env_exec_cmd, *py_args] + kwargs['env'] = env = os.environ.copy() + env['VIRTUAL_ENV'] = context.env_dir + env.pop('PYTHONHOME', None) + env.pop('PYTHONPATH', None) + kwargs['cwd'] = context.env_dir + kwargs['executable'] = context.env_exec_cmd + subprocess.check_output(args, **kwargs) + + def _setup_pip(self, context): + """Installs or upgrades pip in a virtual environment""" + self._call_new_python(context, '-m', 'ensurepip', '--upgrade', + '--default-pip', stderr=subprocess.STDOUT) + + def setup_scripts(self, context): + """ + Set up scripts into the created environment from a directory. + + This method installs the default scripts into the environment + being created. You can prevent the default installation by overriding + this method if you really need to, or if you need to specify + a different location for the scripts to install. By default, the + 'scripts' directory in the venv package is used as the source of + scripts to install. + """ + path = os.path.abspath(os.path.dirname(__file__)) + path = os.path.join(path, 'scripts') + self.install_scripts(context, path) + + def post_setup(self, context): + """ + Hook for post-setup modification of the venv. Subclasses may install + additional packages or scripts here, add activation shell scripts, etc. + + :param context: The information for the environment creation request + being processed. + """ + pass + + def replace_variables(self, text, context): + """ + Replace variable placeholders in script text with context-specific + variables. + + Return the text passed in , but with variables replaced. + + :param text: The text in which to replace placeholder variables. + :param context: The information for the environment creation request + being processed. + """ + text = text.replace('__VENV_DIR__', context.env_dir) + text = text.replace('__VENV_NAME__', context.env_name) + text = text.replace('__VENV_PROMPT__', context.prompt) + text = text.replace('__VENV_BIN_NAME__', context.bin_name) + text = text.replace('__VENV_PYTHON__', context.env_exe) + return text + + def install_scripts(self, context, path): + """ + Install scripts into the created environment from a directory. + + :param context: The information for the environment creation request + being processed. + :param path: Absolute pathname of a directory containing script. + Scripts in the 'common' subdirectory of this directory, + and those in the directory named for the platform + being run on, are installed in the created environment. + Placeholder variables are replaced with environment- + specific values. + """ + binpath = context.bin_path + plen = len(path) + for root, dirs, files in os.walk(path): + if root == path: # at top-level, remove irrelevant dirs + for d in dirs[:]: + if d not in ('common', os.name): + dirs.remove(d) + continue # ignore files in top level + for f in files: + if (os.name == 'nt' and f.startswith('python') + and f.endswith(('.exe', '.pdb'))): + continue + srcfile = os.path.join(root, f) + suffix = root[plen:].split(os.sep)[2:] + if not suffix: + dstdir = binpath + else: + dstdir = os.path.join(binpath, *suffix) + if not os.path.exists(dstdir): + os.makedirs(dstdir) + dstfile = os.path.join(dstdir, f) + with open(srcfile, 'rb') as f: + data = f.read() + if not srcfile.endswith(('.exe', '.pdb')): + try: + data = data.decode('utf-8') + data = self.replace_variables(data, context) + data = data.encode('utf-8') + except UnicodeError as e: + data = None + logger.warning('unable to copy script %r, ' + 'may be binary: %s', srcfile, e) + if data is not None: + with open(dstfile, 'wb') as f: + f.write(data) + shutil.copymode(srcfile, dstfile) + + def upgrade_dependencies(self, context): + logger.debug( + f'Upgrading {CORE_VENV_DEPS} packages in {context.bin_path}' + ) + self._call_new_python(context, '-m', 'pip', 'install', '--upgrade', + *CORE_VENV_DEPS) + + +def create(env_dir, system_site_packages=False, clear=False, + symlinks=False, with_pip=False, prompt=None, upgrade_deps=False): + """Create a virtual environment in a directory.""" + builder = EnvBuilder(system_site_packages=system_site_packages, + clear=clear, symlinks=symlinks, with_pip=with_pip, + prompt=prompt, upgrade_deps=upgrade_deps) + builder.create(env_dir) + +def main(args=None): + compatible = True + if sys.version_info < (3, 3): + compatible = False + elif not hasattr(sys, 'base_prefix'): + compatible = False + if not compatible: + raise ValueError('This script is only for use with Python >= 3.3') + else: + import argparse + + parser = argparse.ArgumentParser(prog=__name__, + description='Creates virtual Python ' + 'environments in one or ' + 'more target ' + 'directories.', + epilog='Once an environment has been ' + 'created, you may wish to ' + 'activate it, e.g. by ' + 'sourcing an activate script ' + 'in its bin directory.') + parser.add_argument('dirs', metavar='ENV_DIR', nargs='+', + help='A directory to create the environment in.') + parser.add_argument('--system-site-packages', default=False, + action='store_true', dest='system_site', + help='Give the virtual environment access to the ' + 'system site-packages dir.') + if os.name == 'nt': + use_symlinks = False + else: + use_symlinks = True + group = parser.add_mutually_exclusive_group() + group.add_argument('--symlinks', default=use_symlinks, + action='store_true', dest='symlinks', + help='Try to use symlinks rather than copies, ' + 'when symlinks are not the default for ' + 'the platform.') + group.add_argument('--copies', default=not use_symlinks, + action='store_false', dest='symlinks', + help='Try to use copies rather than symlinks, ' + 'even when symlinks are the default for ' + 'the platform.') + parser.add_argument('--clear', default=False, action='store_true', + dest='clear', help='Delete the contents of the ' + 'environment directory if it ' + 'already exists, before ' + 'environment creation.') + parser.add_argument('--upgrade', default=False, action='store_true', + dest='upgrade', help='Upgrade the environment ' + 'directory to use this version ' + 'of Python, assuming Python ' + 'has been upgraded in-place.') + parser.add_argument('--without-pip', dest='with_pip', + default=True, action='store_false', + help='Skips installing or upgrading pip in the ' + 'virtual environment (pip is bootstrapped ' + 'by default)') + parser.add_argument('--prompt', + help='Provides an alternative prompt prefix for ' + 'this environment.') + parser.add_argument('--upgrade-deps', default=False, action='store_true', + dest='upgrade_deps', + help='Upgrade core dependencies: {} to the latest ' + 'version in PyPI'.format( + ' '.join(CORE_VENV_DEPS))) + options = parser.parse_args(args) + if options.upgrade and options.clear: + raise ValueError('you cannot supply --upgrade and --clear together.') + builder = EnvBuilder(system_site_packages=options.system_site, + clear=options.clear, + symlinks=options.symlinks, + upgrade=options.upgrade, + with_pip=options.with_pip, + prompt=options.prompt, + upgrade_deps=options.upgrade_deps) + for d in options.dirs: + builder.create(d) + +if __name__ == '__main__': + rc = 1 + try: + main() + rc = 0 + except Exception as e: + print('Error: %s' % e, file=sys.stderr) + sys.exit(rc) diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/venv/__main__.py b/StabilityMatrix.Avalonia/Assets/win-x64/venv/__main__.py new file mode 100644 index 00000000..912423e4 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/win-x64/venv/__main__.py @@ -0,0 +1,10 @@ +import sys +from . import main + +rc = 1 +try: + main() + rc = 0 +except Exception as e: + print('Error: %s' % e, file=sys.stderr) +sys.exit(rc) diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/common/Activate.ps1 b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/common/Activate.ps1 new file mode 100644 index 00000000..eeea3583 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/common/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/common/activate b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/common/activate new file mode 100644 index 00000000..6fbc2b88 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/common/activate @@ -0,0 +1,69 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="__VENV_DIR__" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1="__VENV_PROMPT__${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT="__VENV_PROMPT__" + export VIRTUAL_ENV_PROMPT +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null +fi diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/nt/activate.bat b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/nt/activate.bat new file mode 100644 index 00000000..c1c3c82e --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/nt/activate.bat @@ -0,0 +1,34 @@ +@echo off + +rem This file is UTF-8 encoded, so we need to update the current code page while executing it +for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do ( + set _OLD_CODEPAGE=%%a +) +if defined _OLD_CODEPAGE ( + "%SystemRoot%\System32\chcp.com" 65001 > nul +) + +set VIRTUAL_ENV=__VENV_DIR__ + +if not defined PROMPT set PROMPT=$P$G + +if defined _OLD_VIRTUAL_PROMPT set PROMPT=%_OLD_VIRTUAL_PROMPT% +if defined _OLD_VIRTUAL_PYTHONHOME set PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME% + +set _OLD_VIRTUAL_PROMPT=%PROMPT% +set PROMPT=__VENV_PROMPT__%PROMPT% + +if defined PYTHONHOME set _OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME% +set PYTHONHOME= + +if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH% +if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH% + +set PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH% +set VIRTUAL_ENV_PROMPT=__VENV_PROMPT__ + +:END +if defined _OLD_CODEPAGE ( + "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul + set _OLD_CODEPAGE= +) diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/nt/deactivate.bat b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/nt/deactivate.bat new file mode 100644 index 00000000..62a39a75 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/nt/deactivate.bat @@ -0,0 +1,22 @@ +@echo off + +if defined _OLD_VIRTUAL_PROMPT ( + set "PROMPT=%_OLD_VIRTUAL_PROMPT%" +) +set _OLD_VIRTUAL_PROMPT= + +if defined _OLD_VIRTUAL_PYTHONHOME ( + set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%" + set _OLD_VIRTUAL_PYTHONHOME= +) + +if defined _OLD_VIRTUAL_PATH ( + set "PATH=%_OLD_VIRTUAL_PATH%" +) + +set _OLD_VIRTUAL_PATH= + +set VIRTUAL_ENV= +set VIRTUAL_ENV_PROMPT= + +:END diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/posix/activate.csh b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/posix/activate.csh new file mode 100644 index 00000000..d6f697c5 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/posix/activate.csh @@ -0,0 +1,26 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV "__VENV_DIR__" + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = "__VENV_PROMPT__$prompt" + setenv VIRTUAL_ENV_PROMPT "__VENV_PROMPT__" +endif + +alias pydoc python -m pydoc + +rehash diff --git a/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/posix/activate.fish b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/posix/activate.fish new file mode 100644 index 00000000..9aa44460 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/win-x64/venv/scripts/posix/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/); you cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV "__VENV_DIR__" + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) "__VENV_PROMPT__" (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT "__VENV_PROMPT__" +end diff --git a/StabilityMatrix.Avalonia/Controls/AppWindowBase.cs b/StabilityMatrix.Avalonia/Controls/AppWindowBase.cs new file mode 100644 index 00000000..fd422c0f --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/AppWindowBase.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using AsyncAwaitBestPractices; +using Avalonia.Interactivity; +using Avalonia.Threading; +using FluentAvalonia.UI.Windowing; +using StabilityMatrix.Avalonia.ViewModels; + +namespace StabilityMatrix.Avalonia.Controls; + +[SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")] +public class AppWindowBase : AppWindow +{ + public CancellationTokenSource? ShowAsyncCts { get; set; } + + protected AppWindowBase() + { + } + + public void ShowWithCts(CancellationTokenSource cts) + { + ShowAsyncCts?.Cancel(); + ShowAsyncCts = cts; + Show(); + } + + public Task ShowAsync() + { + ShowAsyncCts?.Cancel(); + ShowAsyncCts = new CancellationTokenSource(); + + var tcs = new TaskCompletionSource(); + ShowAsyncCts.Token.Register(s => + { + ((TaskCompletionSource) s!).SetResult(true); + }, tcs); + + Show(); + + return tcs.Task; + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + + if (ShowAsyncCts is not null) + { + ShowAsyncCts.Cancel(); + ShowAsyncCts = null; + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + if (DataContext is ViewModelBase viewModel) + { + // Run synchronous load then async load + viewModel.OnLoaded(); + + // Can't block here so we'll run as async on UI thread + Dispatcher.UIThread.InvokeAsync(async () => + { + await viewModel.OnLoadedAsync(); + }).SafeFireAndForget(); + } + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + + if (DataContext is not ViewModelBase viewModel) + return; + + // Run synchronous load then async unload + viewModel.OnUnloaded(); + + // Can't block here so we'll run as async on UI thread + Dispatcher.UIThread.InvokeAsync(async () => + { + await viewModel.OnUnloadedAsync(); + }).SafeFireAndForget(); + } +} diff --git a/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs b/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs new file mode 100644 index 00000000..8f5f4502 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs @@ -0,0 +1,133 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using AsyncImageLoader; +using Avalonia; +using Avalonia.Layout; +using Avalonia.Media; + +namespace StabilityMatrix.Avalonia.Controls; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +[SuppressMessage("ReSharper", "UnusedMember.Local")] +public class BetterAdvancedImage : AdvancedImage +{ + #region Reflection Shenanigans to access private parent fields + [NotNull] + private static readonly FieldInfo? IsCornerRadiusUsedField = typeof(AdvancedImage).GetField( + "_isCornerRadiusUsed",BindingFlags.Instance | BindingFlags.NonPublic); + + [NotNull] + private static readonly FieldInfo? CornerRadiusClipField = typeof(AdvancedImage).GetField( + "_cornerRadiusClip",BindingFlags.Instance | BindingFlags.NonPublic); + + private bool IsCornerRadiusUsed + { + get => IsCornerRadiusUsedField.GetValue(this) as bool? ?? false; + set => IsCornerRadiusUsedField.SetValue(this, value); + } + + private RoundedRect CornerRadiusClip + { + get => (RoundedRect) CornerRadiusClipField.GetValue(this)!; + set => CornerRadiusClipField.SetValue(this, value); + } + + static BetterAdvancedImage() + { + if (IsCornerRadiusUsedField is null) + { + throw new NullReferenceException("IsCornerRadiusUsedField was not resolved"); + } + if (CornerRadiusClipField is null) + { + throw new NullReferenceException("CornerRadiusClipField was not resolved"); + } + } + #endregion + + protected override Type StyleKeyOverride { get; } = typeof(AdvancedImage); + + public BetterAdvancedImage(Uri? baseUri) : base(baseUri) + { + } + + public BetterAdvancedImage(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + /// + /// + public override void Render(DrawingContext context) + { + var source = CurrentImage; + + if (source != null && Bounds is { Width: > 0, Height: > 0 }) + { + var viewPort = new Rect(Bounds.Size); + var sourceSize = source.Size; + + var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); + var scaledSize = sourceSize * scale; + + // Calculate starting points for dest + var destX = HorizontalContentAlignment switch + { + HorizontalAlignment.Left => 0, + HorizontalAlignment.Center => (int) (viewPort.Width - scaledSize.Width) / 2, + HorizontalAlignment.Right => (int) (viewPort.Width - scaledSize.Width), + // Stretch is default, use center + HorizontalAlignment.Stretch => (int) (viewPort.Width - scaledSize.Width) / 2, + _ => throw new ArgumentException(nameof(HorizontalContentAlignment)) + }; + var destY = VerticalContentAlignment switch + { + VerticalAlignment.Top => 0, + VerticalAlignment.Center => (int) (viewPort.Height - scaledSize.Height) / 2, + VerticalAlignment.Bottom => (int) (viewPort.Height - scaledSize.Height), + VerticalAlignment.Stretch => 0, // Stretch is default, use top + _ => throw new ArgumentException(nameof(VerticalContentAlignment)) + }; + + var destRect = viewPort + .CenterRect(new Rect(scaledSize)) + .WithX(destX) + .WithY(destY) + .Intersect(viewPort); + var destRectUnscaledSize = destRect.Size / scale; + + // Calculate starting points for source + var sourceX = HorizontalContentAlignment switch + { + HorizontalAlignment.Left => 0, + HorizontalAlignment.Center => (int) (sourceSize - destRectUnscaledSize).Width / 2, + HorizontalAlignment.Right => (int) (sourceSize - destRectUnscaledSize).Width, + // Stretch is default, use center + HorizontalAlignment.Stretch => (int) (sourceSize - destRectUnscaledSize).Width / 2, + _ => throw new ArgumentException(nameof(HorizontalContentAlignment)) + }; + var sourceY = VerticalContentAlignment switch + { + VerticalAlignment.Top => 0, + VerticalAlignment.Center => (int) (sourceSize - destRectUnscaledSize).Height / 2, + VerticalAlignment.Bottom => (int) (sourceSize - destRectUnscaledSize).Height, + VerticalAlignment.Stretch => 0, // Stretch is default, use top + _ => throw new ArgumentException(nameof(VerticalContentAlignment)) + }; + + var sourceRect = new Rect(sourceSize) + .CenterRect(new Rect(destRect.Size / scale)) + .WithX(sourceX) + .WithY(sourceY); + + DrawingContext.PushedState? pushedState = + IsCornerRadiusUsed ? context.PushClip(CornerRadiusClip) : null; + context.DrawImage(source, sourceRect, destRect); + pushedState?.Dispose(); + } + else + { + base.Render(context); + } + } +} diff --git a/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs b/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs new file mode 100644 index 00000000..e6f50833 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs @@ -0,0 +1,254 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using StabilityMatrix.Avalonia.ViewModels; +using StabilityMatrix.Avalonia.ViewModels.Dialogs; + +namespace StabilityMatrix.Avalonia.Controls; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +[SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] +public class BetterContentDialog : ContentDialog +{ + #region Reflection Shenanigans for setting content dialog result + [NotNull] + protected static readonly FieldInfo? ResultField = typeof(ContentDialog).GetField( + "_result",BindingFlags.Instance | BindingFlags.NonPublic); + + protected ContentDialogResult Result + { + get => (ContentDialogResult) ResultField.GetValue(this)!; + set => ResultField.SetValue(this, value); + } + + [NotNull] + protected static readonly MethodInfo? HideCoreMethod = typeof(ContentDialog).GetMethod( + "HideCore", BindingFlags.Instance | BindingFlags.NonPublic); + + protected void HideCore() + { + HideCoreMethod.Invoke(this, null); + } + + // Also get button properties to hide on command execution change + [NotNull] + protected static readonly FieldInfo? PrimaryButtonField = typeof(ContentDialog).GetField( + "_primaryButton", BindingFlags.Instance | BindingFlags.NonPublic); + + protected Button? PrimaryButton + { + get => (Button?) PrimaryButtonField.GetValue(this)!; + set => PrimaryButtonField.SetValue(this, value); + } + + [NotNull] + protected static readonly FieldInfo? SecondaryButtonField = typeof(ContentDialog).GetField( + "_secondaryButton", BindingFlags.Instance | BindingFlags.NonPublic); + + protected Button? SecondaryButton + { + get => (Button?) SecondaryButtonField.GetValue(this)!; + set => SecondaryButtonField.SetValue(this, value); + } + + [NotNull] + protected static readonly FieldInfo? CloseButtonField = typeof(ContentDialog).GetField( + "_closeButton", BindingFlags.Instance | BindingFlags.NonPublic); + + protected Button? CloseButton + { + get => (Button?) CloseButtonField.GetValue(this)!; + set => CloseButtonField.SetValue(this, value); + } + + static BetterContentDialog() + { + if (ResultField is null) + { + throw new NullReferenceException("ResultField was not resolved"); + } + if (HideCoreMethod is null) + { + throw new NullReferenceException("HideCoreMethod was not resolved"); + } + if (PrimaryButtonField is null || SecondaryButtonField is null || CloseButtonField is null) + { + throw new NullReferenceException("Button fields were not resolved"); + } + } + #endregion + + protected override Type StyleKeyOverride { get; } = typeof(ContentDialog); + + public static readonly StyledProperty IsFooterVisibleProperty = AvaloniaProperty.Register( + "IsFooterVisible", true); + + public bool IsFooterVisible + { + get => GetValue(IsFooterVisibleProperty); + set => SetValue(IsFooterVisibleProperty, value); + } + + public static readonly StyledProperty ContentVerticalScrollBarVisibilityProperty + = AvaloniaProperty.Register( + "ContentScrollBarVisibility", ScrollBarVisibility.Auto); + + public ScrollBarVisibility ContentVerticalScrollBarVisibility + { + get => GetValue(ContentVerticalScrollBarVisibilityProperty); + set => SetValue(ContentVerticalScrollBarVisibilityProperty, value); + } + + public static readonly StyledProperty MaxDialogWidthProperty = AvaloniaProperty.Register( + "MaxDialogWidth"); + + public double MaxDialogWidth + { + get => GetValue(MaxDialogWidthProperty); + set => SetValue(MaxDialogWidthProperty, value); + } + + public static readonly StyledProperty MaxDialogHeightProperty = AvaloniaProperty.Register( + "MaxDialogHeight"); + + public double MaxDialogHeight + { + get => GetValue(MaxDialogHeightProperty); + set => SetValue(MaxDialogHeightProperty, value); + } + + + public BetterContentDialog() + { + AddHandler(LoadedEvent, OnLoaded); + } + + private void TryBindButtons() + { + if ((Content as Control)?.DataContext is ContentDialogViewModelBase viewModel) + { + viewModel.PrimaryButtonClick += OnDialogButtonClick; + viewModel.SecondaryButtonClick += OnDialogButtonClick; + viewModel.CloseButtonClick += OnDialogButtonClick; + } + + // If commands provided, bind OnCanExecuteChanged to hide buttons + // otherwise link visibility to IsEnabled + if (PrimaryButton is not null) + { + if (PrimaryButtonCommand is not null) + { + PrimaryButtonCommand.CanExecuteChanged += (_, _) => + PrimaryButton.IsEnabled = PrimaryButtonCommand.CanExecute(null); + // Also set initial state + PrimaryButton.IsEnabled = PrimaryButtonCommand.CanExecute(null); + } + else + { + PrimaryButton.IsVisible = IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText); + } + } + + if (SecondaryButton is not null) + { + if (SecondaryButtonCommand is not null) + { + SecondaryButtonCommand.CanExecuteChanged += (_, _) => + SecondaryButton.IsEnabled = SecondaryButtonCommand.CanExecute(null); + // Also set initial state + SecondaryButton.IsEnabled = SecondaryButtonCommand.CanExecute(null); + } + else + { + SecondaryButton.IsVisible = IsSecondaryButtonEnabled && !string.IsNullOrEmpty(SecondaryButtonText); + } + } + + if (CloseButton is not null) + { + if (CloseButtonCommand is not null) + { + CloseButtonCommand.CanExecuteChanged += (_, _) => + CloseButton.IsEnabled = CloseButtonCommand.CanExecute(null); + // Also set initial state + CloseButton.IsEnabled = CloseButtonCommand.CanExecute(null); + } + } + } + + protected void OnDialogButtonClick(object? sender, ContentDialogResult e) + { + Result = e; + HideCore(); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + TryBindButtons(); + } + + private void OnLoaded(object? sender, RoutedEventArgs? e) + { + TryBindButtons(); + + // Find the named grid + // https://github.com/amwx/FluentAvalonia/blob/master/src/FluentAvalonia/Styling/ + // ControlThemes/FAControls/ContentDialogStyles.axaml#L96 + var border = VisualChildren[0] as Border; + var panel = border?.Child as Panel; + var faBorder = panel?.Children[0] as FABorder; + + // Set dialog maximums + if (MaxDialogWidth > 0) + { + faBorder!.MaxWidth = MaxDialogWidth; + } + if (MaxDialogHeight > 0) + { + faBorder!.MaxHeight = MaxDialogHeight; + } + + var border2 = faBorder?.Child as Border; + // Named Grid 'DialogSpace' + if (border2?.Child is not Grid dialogSpaceGrid) throw new InvalidOperationException("Could not find DialogSpace grid"); + + var scrollViewer = dialogSpaceGrid.Children[0] as ScrollViewer; + var actualBorder = dialogSpaceGrid.Children[1] as Border; + + // Get the parent border, which is what we want to hide + if (scrollViewer is null || actualBorder is null) + { + throw new InvalidOperationException("Could not find parent border"); + } + + var subBorder = scrollViewer.Content as Border; + var subGrid = subBorder?.Child as Grid; + if (subGrid is null) throw new InvalidOperationException("Could not find sub grid"); + var contentControlTitle = subGrid.Children[0] as ContentControl; + // Hide title if empty + if (Title is null or string {Length: 0}) + { + contentControlTitle!.IsVisible = false; + } + + // Set footer and scrollbar visibility states + actualBorder.IsVisible = IsFooterVisible; + scrollViewer.VerticalScrollBarVisibility = ContentVerticalScrollBarVisibility; + + // Also call the vm's OnLoad + if (Content is Control {DataContext: ViewModelBase viewModel}) + { + viewModel.OnLoaded(); + Dispatcher.UIThread.InvokeAsync( + async () => await viewModel.OnLoadedAsync()); + } + } +} diff --git a/StabilityMatrix.Avalonia/Controls/BetterFlyout.cs b/StabilityMatrix.Avalonia/Controls/BetterFlyout.cs new file mode 100644 index 00000000..3fb01662 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/BetterFlyout.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.VisualTree; + +namespace StabilityMatrix.Avalonia.Controls; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class BetterFlyout : Flyout +{ + public static readonly StyledProperty VerticalScrollBarVisibilityProperty = AvaloniaProperty.Register( + "VerticalScrollBarVisibility"); + + public ScrollBarVisibility VerticalScrollBarVisibility + { + get => GetValue(VerticalScrollBarVisibilityProperty); + set => SetValue(VerticalScrollBarVisibilityProperty, value); + } + + public static readonly StyledProperty HorizontalScrollBarVisibilityProperty = AvaloniaProperty.Register( + "HorizontalScrollBarVisibility"); + + public ScrollBarVisibility HorizontalScrollBarVisibility + { + get => GetValue(HorizontalScrollBarVisibilityProperty); + set => SetValue(HorizontalScrollBarVisibilityProperty, value); + } + + protected override void OnOpened() + { + base.OnOpened(); + var presenter = Popup.Child; + if (presenter.FindDescendantOfType() is { } scrollViewer) + { + scrollViewer.VerticalScrollBarVisibility = VerticalScrollBarVisibility; + scrollViewer.HorizontalScrollBarVisibility = HorizontalScrollBarVisibility; + } + } +} diff --git a/StabilityMatrix.Avalonia/Controls/BetterImage.cs b/StabilityMatrix.Avalonia/Controls/BetterImage.cs new file mode 100644 index 00000000..8eb77eee --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/BetterImage.cs @@ -0,0 +1,140 @@ +using Avalonia; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Controls; +using Avalonia.Controls.Automation.Peers; +using Avalonia.Media; +using Avalonia.Metadata; + +namespace StabilityMatrix.Avalonia.Controls; + +public class BetterImage : Control +{ + /// + /// Defines the property. + /// + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StretchProperty = + AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StretchDirectionProperty = + AvaloniaProperty.Register( + nameof(StretchDirection), + StretchDirection.Both); + + static BetterImage() + { + AffectsRender(SourceProperty, StretchProperty, StretchDirectionProperty); + AffectsMeasure(SourceProperty, StretchProperty, StretchDirectionProperty); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue( + AutomationControlType.Image); + } + + /// + /// Gets or sets the image that will be displayed. + /// + [Content] + public IImage? Source + { + get { return GetValue(SourceProperty); } + set { SetValue(SourceProperty, value); } + } + + /// + /// Gets or sets a value controlling how the image will be stretched. + /// + public Stretch Stretch + { + get { return GetValue(StretchProperty); } + set { SetValue(StretchProperty, value); } + } + + /// + /// Gets or sets a value controlling in what direction the image will be stretched. + /// + public StretchDirection StretchDirection + { + get { return GetValue(StretchDirectionProperty); } + set { SetValue(StretchDirectionProperty, value); } + } + + /// + protected override bool BypassFlowDirectionPolicies => true; + + /// + /// Renders the control. + /// + /// The drawing context. + public sealed override void Render(DrawingContext context) + { + var source = Source; + + if (source != null && Bounds.Width > 0 && Bounds.Height > 0) + { + Rect viewPort = new Rect(Bounds.Size); + Size sourceSize = source.Size; + + Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); + Size scaledSize = sourceSize * scale; + Rect destRect = viewPort + .CenterRect(new Rect(scaledSize)) + .WithX(0) + .WithY(0) + .Intersect(viewPort); + Rect sourceRect = new Rect(sourceSize) + .CenterRect(new Rect(destRect.Size / scale)) + .WithX(0) + .WithY(0); + + context.DrawImage(source, sourceRect, destRect); + } + } + + /// + /// Measures the control. + /// + /// The available size. + /// The desired size of the control. + protected override Size MeasureOverride(Size availableSize) + { + var source = Source; + var result = new Size(); + + if (source != null) + { + result = Stretch.CalculateSize(availableSize, source.Size, StretchDirection); + } + + return result; + } + + /// + protected override Size ArrangeOverride(Size finalSize) + { + var source = Source; + + if (source != null) + { + var sourceSize = source.Size; + var result = Stretch.CalculateSize(finalSize, sourceSize); + return result; + } + else + { + return new Size(); + } + } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ImageAutomationPeer(this); + } +} diff --git a/StabilityMatrix.Avalonia/Controls/Card.cs b/StabilityMatrix.Avalonia/Controls/Card.cs new file mode 100644 index 00000000..24df9e40 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/Card.cs @@ -0,0 +1,15 @@ +using System; +using Avalonia.Controls; + +namespace StabilityMatrix.Avalonia.Controls; + +public class Card : ContentControl +{ + protected override Type StyleKeyOverride => typeof(Card); + + public Card() + { + MinHeight = 8; + MinWidth = 8; + } +} diff --git a/StabilityMatrix.Avalonia/Controls/FASymbolIconSource.cs b/StabilityMatrix.Avalonia/Controls/FASymbolIconSource.cs new file mode 100644 index 00000000..bc5c84e6 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/FASymbolIconSource.cs @@ -0,0 +1,88 @@ +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Avalonia; +using Avalonia.Controls.Documents; +using Avalonia.Media; +using FluentAvalonia.UI.Controls; +using Projektanker.Icons.Avalonia; + +namespace StabilityMatrix.Avalonia.Controls; + +[TypeConverter(typeof(FASymbolIconSourceConverter))] +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +[SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] +public class FASymbolIconSource : PathIconSource +{ + public static readonly StyledProperty SymbolProperty = + AvaloniaProperty.Register(nameof(Symbol)); + + public static readonly StyledProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner(); + + + public FASymbolIconSource() + { + Stretch = Stretch.None; + // FontSize = 20; // Override value inherited from visual parents. + InvalidateData(); + } + + public string Symbol + { + get => GetValue(SymbolProperty); + set => SetValue(SymbolProperty, value); + } + + public double FontSize + { + get => GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SymbolProperty || change.Property == FontSizeProperty) + { + InvalidateData(); + } + } + + private void InvalidateData() + { + var path = IconProvider.Current.GetIconPath(Symbol); + var geometry = Geometry.Parse(path); + + var scale = FontSize / 20; + + Data = geometry; + // TODO: Scaling not working + Data.Transform = new ScaleTransform(scale, scale); + } +} + +public class FASymbolIconSourceConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + if (sourceType == typeof(string)) + { + return true; + } + return base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + return value switch + { + string val => new FASymbolIconSource + { + Symbol = val, + }, + _ => base.ConvertFrom(context, culture, value) + }; + } +} diff --git a/StabilityMatrix.Avalonia/Controls/LaunchOptionCardTemplateSelector.cs b/StabilityMatrix.Avalonia/Controls/LaunchOptionCardTemplateSelector.cs new file mode 100644 index 00000000..55731932 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/LaunchOptionCardTemplateSelector.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Metadata; +using StabilityMatrix.Core.Models; + +namespace StabilityMatrix.Avalonia.Controls; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class LaunchOptionCardTemplateSelector : IDataTemplate +{ + // public bool SupportsRecycling => false; + + // ReSharper disable once CollectionNeverUpdated.Global + [Content] + public Dictionary Templates { get; } = new(); + + // Check if we can accept the provided data + public bool Match(object? data) + { + return data is LaunchOptionCard; + } + + // Build the DataTemplate here + public Control Build(object? data) + { + if (data is not LaunchOptionCard card) throw new ArgumentException(null, nameof(data)); + return Templates[card.Type].Build(card)!; + } +} diff --git a/StabilityMatrix.Avalonia/Controls/ProgressRing.cs b/StabilityMatrix.Avalonia/Controls/ProgressRing.cs new file mode 100644 index 00000000..f1a5b56c --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/ProgressRing.cs @@ -0,0 +1,130 @@ +using System.Diagnostics.CodeAnalysis; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; + +namespace StabilityMatrix.Avalonia.Controls; + +/// +/// A control used to indicate the progress of an operation. +/// +[PseudoClasses(":preserveaspect", ":indeterminate")] +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class ProgressRing : RangeBase +{ + public static readonly StyledProperty IsIndeterminateProperty = + ProgressBar.IsIndeterminateProperty.AddOwner(); + + public static readonly StyledProperty PreserveAspectProperty = + AvaloniaProperty.Register(nameof(PreserveAspect), true); + + public static readonly StyledProperty ValueAngleProperty = + AvaloniaProperty.Register(nameof(ValueAngle)); + + public static readonly StyledProperty StartAngleProperty = + AvaloniaProperty.Register(nameof(StartAngle)); + + public static readonly StyledProperty EndAngleProperty = + AvaloniaProperty.Register(nameof(EndAngle), 360); + + static ProgressRing() + { + MinimumProperty.Changed.AddClassHandler(OnMinimumPropertyChanged); + MaximumProperty.Changed.AddClassHandler(OnMaximumPropertyChanged); + ValueProperty.Changed.AddClassHandler(OnValuePropertyChanged); + MaximumProperty.Changed.AddClassHandler(OnStartAnglePropertyChanged); + MaximumProperty.Changed.AddClassHandler(OnEndAnglePropertyChanged); + } + + public ProgressRing() + { + UpdatePseudoClasses(IsIndeterminate, PreserveAspect); + } + + public bool IsIndeterminate + { + get => GetValue(IsIndeterminateProperty); + set => SetValue(IsIndeterminateProperty, value); + } + + public bool PreserveAspect + { + get => GetValue(PreserveAspectProperty); + set => SetValue(PreserveAspectProperty, value); + } + + public double ValueAngle + { + get => GetValue(ValueAngleProperty); + private set => SetValue(ValueAngleProperty, value); + } + + public double StartAngle + { + get => GetValue(StartAngleProperty); + set => SetValue(StartAngleProperty, value); + } + + public double EndAngle + { + get => GetValue(EndAngleProperty); + set => SetValue(EndAngleProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + var e = change as AvaloniaPropertyChangedEventArgs; + if (e is null) return; + + if (e.Property == IsIndeterminateProperty) + { + UpdatePseudoClasses(e.NewValue.GetValueOrDefault(), null); + } + else if (e.Property == PreserveAspectProperty) + { + UpdatePseudoClasses(null, e.NewValue.GetValueOrDefault()); + } + } + + private void UpdatePseudoClasses( + bool? isIndeterminate, + bool? preserveAspect) + { + if (isIndeterminate.HasValue) + { + PseudoClasses.Set(":indeterminate", isIndeterminate.Value); + } + + if (preserveAspect.HasValue) + { + PseudoClasses.Set(":preserveaspect", preserveAspect.Value); + } + } + + private static void OnMinimumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) + { + sender.Minimum = (double) e.NewValue!; + } + + private static void OnMaximumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) + { + sender.Maximum = (double) e.NewValue!; + } + + private static void OnValuePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) + { + sender.ValueAngle = ((double) e.NewValue! - sender.Minimum) * (sender.EndAngle - sender.StartAngle) / (sender.Maximum - sender.Minimum); + } + + private static void OnStartAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) + { + sender.StartAngle = (double) e.NewValue!; + } + + private static void OnEndAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) + { + sender.EndAngle = (double) e.NewValue!; + } +} diff --git a/StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml b/StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml new file mode 100644 index 00000000..911d135b --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml @@ -0,0 +1,41 @@ + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml.cs b/StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml.cs new file mode 100644 index 00000000..f5620f12 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml.cs @@ -0,0 +1,16 @@ +using Avalonia.Markup.Xaml; + +namespace StabilityMatrix.Avalonia.Controls; + +public partial class RefreshBadge : UserControlBase +{ + public RefreshBadge() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} diff --git a/StabilityMatrix.Avalonia/Controls/UserControlBase.cs b/StabilityMatrix.Avalonia/Controls/UserControlBase.cs new file mode 100644 index 00000000..f0932e55 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/UserControlBase.cs @@ -0,0 +1,51 @@ +using System.Diagnostics.CodeAnalysis; +using AsyncAwaitBestPractices; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; +using StabilityMatrix.Avalonia.ViewModels; + +namespace StabilityMatrix.Avalonia.Controls; + +[SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")] +public class UserControlBase : UserControl +{ + static UserControlBase() + { + LoadedEvent.AddClassHandler( + (cls, args) => cls.OnLoadedEvent(args)); + + UnloadedEvent.AddClassHandler( + (cls, args) => cls.OnUnloadedEvent(args)); + } + + // ReSharper disable once UnusedParameter.Global + protected virtual void OnLoadedEvent(RoutedEventArgs? e) + { + if (DataContext is not ViewModelBase viewModel) return; + + // Run synchronous load then async load + viewModel.OnLoaded(); + + // Can't block here so we'll run as async on UI thread + Dispatcher.UIThread.InvokeAsync(async () => + { + await viewModel.OnLoadedAsync(); + }).SafeFireAndForget(); + } + + // ReSharper disable once UnusedParameter.Global + protected virtual void OnUnloadedEvent(RoutedEventArgs? e) + { + if (DataContext is not ViewModelBase viewModel) return; + + // Run synchronous load then async load + viewModel.OnUnloaded(); + + // Can't block here so we'll run as async on UI thread + Dispatcher.UIThread.InvokeAsync(async () => + { + await viewModel.OnUnloadedAsync(); + }).SafeFireAndForget(); + } +} diff --git a/StabilityMatrix.Avalonia/Converters/FitSquarelyWithinAspectRatioConverter.cs b/StabilityMatrix.Avalonia/Converters/FitSquarelyWithinAspectRatioConverter.cs new file mode 100644 index 00000000..695ee031 --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/FitSquarelyWithinAspectRatioConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace StabilityMatrix.Avalonia.Converters; + +public class FitSquarelyWithinAspectRatioConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var bounds = value is Rect rect ? rect : default; + return Math.Min(bounds.Width, bounds.Height); + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/StabilityMatrix.Avalonia/Converters/LaunchOptionConverter.cs b/StabilityMatrix.Avalonia/Converters/LaunchOptionConverter.cs new file mode 100644 index 00000000..ba48b904 --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/LaunchOptionConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace StabilityMatrix.Avalonia.Converters; + +public class LaunchOptionConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (targetType == typeof(string)) + { + return value?.ToString() ?? ""; + } + + if (targetType == typeof(bool?)) + { + return bool.TryParse(value?.ToString(), out var boolValue) && boolValue; + } + + if (targetType == typeof(double?)) + { + if (value == null) + { + return null; + } + return double.TryParse(value.ToString(), out var doubleValue) ? doubleValue : 0; + } + + if (targetType == typeof(int?)) + { + if (value == null) + { + return null; + } + return int.TryParse(value.ToString(), out var intValue) ? intValue : 0; + } + + throw new ArgumentException("Unsupported type"); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value; + } +} diff --git a/StabilityMatrix.Avalonia/Converters/LaunchOptionIntDoubleConverter.cs b/StabilityMatrix.Avalonia/Converters/LaunchOptionIntDoubleConverter.cs new file mode 100644 index 00000000..0c26f390 --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/LaunchOptionIntDoubleConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace StabilityMatrix.Avalonia.Converters; + +public class LaunchOptionIntDoubleConverter : IValueConverter +{ + // Convert from int to double + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (targetType == typeof(double?)) + { + if (value == null) + { + return null; + } + return System.Convert.ToDouble(value); + } + + throw new ArgumentException($"Unsupported type {targetType}"); + } + + // Convert from double to object int (floor) + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (targetType == typeof(int?) || targetType == typeof(object)) + { + return System.Convert.ToInt32(value); + } + + throw new ArgumentException($"Unsupported type {targetType}"); + } +} diff --git a/StabilityMatrix.Avalonia/Converters/ValueConverterGroup.cs b/StabilityMatrix.Avalonia/Converters/ValueConverterGroup.cs new file mode 100644 index 00000000..fdd0336c --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/ValueConverterGroup.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Data.Converters; + +namespace StabilityMatrix.Avalonia.Converters; + +public class ValueConverterGroup : List, IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture)); + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs new file mode 100644 index 00000000..e64de5ea --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels; +using StabilityMatrix.Avalonia.ViewModels.Dialogs; +using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Database; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Helper.Factory; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Api; +using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Python; +using StabilityMatrix.Core.Services; +using StabilityMatrix.Core.Updater; + +namespace StabilityMatrix.Avalonia.DesignData; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public static class DesignData +{ + [NotNull] public static IServiceProvider? Services { get; set; } + + private static bool isInitialized; + + // This needs to be static method instead of static constructor + // or else Avalonia analyzers won't work. + public static void Initialize() + { + if (isInitialized) throw new InvalidOperationException("DesignData is already initialized."); + + var services = new ServiceCollection(); + + var activePackageId = Guid.NewGuid(); + services.AddSingleton(_ => new MockSettingsManager + { + Settings = + { + InstalledPackages = new List + { + new() + { + Id = activePackageId, + DisplayName = "My Installed Package", + PackageName = "stable-diffusion-webui", + PackageVersion = "v1.0.0", + LibraryPath = $"Packages{Environment.NewLine}example-webui", + LastUpdateCheck = DateTimeOffset.Now + } + }, + ActiveInstalledPackage = activePackageId + } + }); + + // General services + services.AddLogging() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + // Mock services + services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + // Placeholder services that nobody should need during design time + services + .AddSingleton(_ => null!) + .AddSingleton(_ => null!) + .AddSingleton(_ => null!) + .AddSingleton(_ => null!) + .AddSingleton(_ => null!); + + // Using some default service implementations from App + App.ConfigurePackages(services); + App.ConfigurePageViewModels(services); + App.ConfigureDialogViewModels(services); + App.ConfigureViews(services); + + Services = services.BuildServiceProvider(); + + var dialogFactory = Services.GetRequiredService>(); + var settingsManager = Services.GetRequiredService(); + var downloadService = Services.GetRequiredService(); + var modelFinder = Services.GetRequiredService(); + var packageFactory = Services.GetRequiredService(); + var notificationService = Services.GetRequiredService(); + + // Sample data + var sampleCivitVersions = new List + { + new() + { + Name = "BB95 Furry Mix", + Description = "v1.0.0", + } + }; + + var sampleViewModel = new ModelVersionViewModel(settingsManager, sampleCivitVersions[0]); + + // Sample data for dialogs + SelectModelVersionViewModel.Versions = new[] {sampleViewModel}; + SelectModelVersionViewModel.SelectedVersionViewModel = sampleViewModel; + + LaunchOptionsViewModel = Services.GetRequiredService(); + LaunchOptionsViewModel.Cards = new[] + { + LaunchOptionCard.FromDefinition(new LaunchOptionDefinition + { + Name = "Host", + Type = LaunchOptionType.String, + Description = "The host name for the Web UI", + DefaultValue = "localhost", + Options = { "--host" } + }), + LaunchOptionCard.FromDefinition(new LaunchOptionDefinition + { + Name = "API", + Type = LaunchOptionType.Bool, + Options = { "--api" } + }) + }; + LaunchOptionsViewModel.UpdateFilterCards(); + + InstallerViewModel = Services.GetRequiredService(); + InstallerViewModel.AvailablePackages = + packageFactory.GetAllAvailablePackages().ToImmutableArray(); + InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0]; + InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note."; + + // Checkpoints page + CheckpointsPageViewModel.CheckpointFolders = new ObservableCollection + { + new(settingsManager, downloadService, modelFinder) + { + Title = "Lora", + DirectoryPath = "Packages/lora", + CheckpointFiles = new ObservableCollection + { + new() + { + FilePath = "~/Models/Lora/electricity-light.safetensors", + Title = "Auroral Background", + ConnectedModel = new ConnectedModelInfo + { + VersionName = "Lightning Auroral", + BaseModel = "SD 1.5", + ModelName = "Auroral Background", + ModelType = CivitModelType.LORA, + FileMetadata = new CivitFileMetadata + { + Format = CivitModelFormat.SafeTensor, + Fp = CivitModelFpType.fp16, + Size = CivitModelSize.pruned, + } + } + }, + new() + { + FilePath = "~/Models/Lora/model.safetensors", + Title = "Some model" + }, + } + }, + new(settingsManager, downloadService, modelFinder) + { + Title = "VAE", + DirectoryPath = "Packages/VAE", + CheckpointFiles = new ObservableCollection + { + new() + { + FilePath = "~/Models/VAE/vae_v2.pt", + Title = "VAE v2", + } + } + } + }; + + CheckpointBrowserViewModel.ModelCards = new + ObservableCollection + { + new(new CivitModel + { + Name = "BB95 Furry Mix", + Description = "A furry mix of BB95", + }, downloadService, settingsManager, + dialogFactory, notificationService) + }; + + ProgressManagerViewModel.ProgressItems = new ObservableCollection + { + new(new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading..."))), + new(new ProgressItem(Guid.NewGuid(), "Test File 2.uwu", new ProgressReport(0.25f, "Extracting..."))) + }; + + UpdateViewModel = Services.GetRequiredService(); + UpdateViewModel.UpdateText = + $"Stability Matrix v2.0.1 is now available! You currently have v2.0.0. Would you like to update now?"; + UpdateViewModel.ReleaseNotes = "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature"; + + isInitialized = true; + } + + [NotNull] public static InstallerViewModel? InstallerViewModel { get; private set; } + [NotNull] public static LaunchOptionsViewModel? LaunchOptionsViewModel { get; private set; } + [NotNull] public static UpdateViewModel? UpdateViewModel { get; private set; } + + public static ServiceManager DialogFactory => + Services.GetRequiredService>(); + public static MainWindowViewModel MainWindowViewModel => + Services.GetRequiredService(); + public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel => + Services.GetRequiredService(); + public static LaunchPageViewModel LaunchPageViewModel => + Services.GetRequiredService(); + public static PackageManagerViewModel PackageManagerViewModel => + Services.GetRequiredService(); + public static CheckpointsPageViewModel CheckpointsPageViewModel => + Services.GetRequiredService(); + public static SettingsViewModel SettingsViewModel => + Services.GetRequiredService(); + public static CheckpointBrowserViewModel CheckpointBrowserViewModel => + Services.GetRequiredService(); + public static SelectModelVersionViewModel SelectModelVersionViewModel => + Services.GetRequiredService(); + public static OneClickInstallViewModel OneClickInstallViewModel => + Services.GetRequiredService(); + public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel => + Services.GetRequiredService(); + public static ProgressManagerViewModel ProgressManagerViewModel => + Services.GetRequiredService(); + public static ExceptionViewModel ExceptionViewModel => + DialogFactory.Get(viewModel => + { + // Use try-catch to generate traceback information + try + { + try + { + throw new OperationCanceledException("Example"); + } + catch (OperationCanceledException e) + { + throw new AggregateException(e); + } + } + catch (AggregateException e) + { + viewModel.Exception = e; + } + }); + + public static RefreshBadgeViewModel RefreshBadgeViewModel => new() + { + State = ProgressState.Success + }; +} diff --git a/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs b/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs new file mode 100644 index 00000000..419d972e --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs @@ -0,0 +1,21 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.DesignData; + +public class MockDownloadService : IDownloadService +{ + public Task DownloadToFileAsync(string downloadUrl, string downloadPath, + IProgress? progress = null, string? httpClientName = null) + { + return Task.CompletedTask; + } + + public Task GetImageStreamFromUrl(string url) + { + return Task.FromResult(new MemoryStream(new byte[24]) as Stream); + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/MockHttpClientFactory.cs b/StabilityMatrix.Avalonia/DesignData/MockHttpClientFactory.cs new file mode 100644 index 00000000..452af8c4 --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockHttpClientFactory.cs @@ -0,0 +1,12 @@ +using System; +using System.Net.Http; + +namespace StabilityMatrix.Avalonia.DesignData; + +public class MockHttpClientFactory : IHttpClientFactory +{ + public HttpClient CreateClient(string name) + { + throw new NotImplementedException(); + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs new file mode 100644 index 00000000..c102805c --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LiteDB.Async; +using StabilityMatrix.Core.Database; +using StabilityMatrix.Core.Models.Api; +using StabilityMatrix.Core.Models.Database; + +namespace StabilityMatrix.Avalonia.DesignData; + +public class MockLiteDbContext : ILiteDbContext +{ + public LiteDatabaseAsync Database => throw new NotImplementedException(); + public ILiteCollectionAsync CivitModels => throw new NotImplementedException(); + public ILiteCollectionAsync CivitModelVersions => throw new NotImplementedException(); + public ILiteCollectionAsync CivitModelQueryCache => throw new NotImplementedException(); + public Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3) + { + return Task.FromResult<(CivitModel?, CivitModelVersion?)>((null, null)); + } + + public Task UpsertCivitModelAsync(CivitModel civitModel) + { + return Task.FromResult(true); + } + + public Task UpsertCivitModelAsync(IEnumerable civitModels) + { + return Task.FromResult(true); + } + + public Task UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry entry) + { + return Task.FromResult(true); + } + + public Task GetGithubCacheEntry(string cacheKey) + { + return Task.FromResult(null); + } + + public Task UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) + { + return Task.FromResult(true); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs b/StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs new file mode 100644 index 00000000..790488ba --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls.Notifications; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Core.Models; + +namespace StabilityMatrix.Avalonia.DesignData; + +public class MockNotificationService : INotificationService +{ + public void Initialize(Visual? visual, + NotificationPosition position = NotificationPosition.BottomRight, int maxItems = 3) + { + } + + public void Show(INotification notification) + { + } + + public Task> TryAsync(Task task, string title = "Error", string? message = null, + NotificationType appearance = NotificationType.Error) + { + return Task.FromResult(new TaskResult(default!)); + } + + public Task> TryAsync(Task task, string title = "Error", string? message = null, + NotificationType appearance = NotificationType.Error) + { + return Task.FromResult(new TaskResult(true)); + } + + public void Show(string title, string message, NotificationType appearance = NotificationType.Information) + { + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs b/StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs new file mode 100644 index 00000000..86564211 --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs @@ -0,0 +1,9 @@ +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.DesignData; + +public class MockSettingsManager : SettingsManager +{ + protected override void LoadSettings() {} + protected override void SaveSettings() {} +} diff --git a/StabilityMatrix.Avalonia/DesignData/MockSharedFolders.cs b/StabilityMatrix.Avalonia/DesignData/MockSharedFolders.cs new file mode 100644 index 00000000..4e5a631f --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockSharedFolders.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Packages; + +namespace StabilityMatrix.Avalonia.DesignData; + +public class MockSharedFolders : ISharedFolders +{ + public void SetupLinksForPackage(BasePackage basePackage, DirectoryPath installDirectory) + { + } + + public Task UpdateLinksForPackage(BasePackage basePackage, DirectoryPath installDirectory) + { + return Task.CompletedTask; + } + + public void RemoveLinksForAllPackages() + { + } +} diff --git a/StabilityMatrix.Avalonia/DialogHelper.cs b/StabilityMatrix.Avalonia/DialogHelper.cs new file mode 100644 index 00000000..32d071cc --- /dev/null +++ b/StabilityMatrix.Avalonia/DialogHelper.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; +using FluentAvalonia.UI.Controls; +using Markdown.Avalonia; +using StabilityMatrix.Avalonia.Controls; + +namespace StabilityMatrix.Avalonia; + +public static class DialogHelper +{ + /// + /// Create a generic textbox entry content dialog. + /// + public static BetterContentDialog CreateTextEntryDialog( + string title, + string description, + IReadOnlyList textFields) + { + Dispatcher.UIThread.VerifyAccess(); + + var stackPanel = new StackPanel(); + var grid = new Grid + { + RowDefinitions = + { + new RowDefinition(GridLength.Auto), + new RowDefinition(GridLength.Star) + }, + Children = + { + new TextBlock + { + Text = description + }, + stackPanel + } + }; + grid.Loaded += (_, _) => + { + // Focus first textbox + var firstTextBox = stackPanel.Children.OfType().First(); + firstTextBox.Focus(); + firstTextBox.CaretIndex = firstTextBox.Text?.LastIndexOf('.') ?? 0; + }; + + // Disable primary button if any textboxes are invalid + var primaryCommand = new RelayCommand(delegate { }, + () => + { + var invalidCount = textFields.Count(field => !field.IsValid); + Debug.WriteLine($"Checking can execute: {invalidCount} invalid fields"); + return invalidCount == 0; + }); + + // Create textboxes + foreach (var field in textFields) + { + var label = new TextBlock + { + Text = field.Label + }; + stackPanel.Children.Add(label); + + var textBox = new TextBox + { + [!TextBox.TextProperty] = new Binding("TextProperty"), + Watermark = field.Watermark, + DataContext = field, + }; + stackPanel.Children.Add(textBox); + + // When IsValid property changes, update invalid count and primary button + field.PropertyChanged += (_, args) => + { + if (args.PropertyName == nameof(TextBoxField.IsValid)) + { + primaryCommand.NotifyCanExecuteChanged(); + } + }; + + // Set initial value + textBox.Text = field.Text; + + // See if initial value is valid + try + { + field.Validator?.Invoke(field.Text); + } + catch (Exception) + { + field.IsValid = false; + } + } + + return new BetterContentDialog + { + Title = title, + Content = grid, + PrimaryButtonText = "OK", + CloseButtonText = "Cancel", + IsPrimaryButtonEnabled = true, + PrimaryButtonCommand = primaryCommand, + DefaultButton = ContentDialogButton.Primary + }; + } + + /// + /// Create a generic dialog for showing a markdown document + /// + public static BetterContentDialog CreateMarkdownDialog(string markdown, string? title = null) + { + Dispatcher.UIThread.VerifyAccess(); + + var viewer = new MarkdownScrollViewer + { + Markdown = markdown + }; + + return new BetterContentDialog + { + Title = title, + Content = viewer, + CloseButtonText = "Close", + IsPrimaryButtonEnabled = false, + }; + } +} + +// Text fields +public sealed class TextBoxField : INotifyPropertyChanged +{ + // Label above the textbox + public string Label { get; init; } = string.Empty; + // Actual text value + public string Text { get; set; } = string.Empty; + // Watermark text + public string Watermark { get; init; } = string.Empty; + + /// + /// Validation action on text changes. Throw exception if invalid. + /// + public Action? Validator { get; init; } + + public string TextProperty + { + get => Text; + [DebuggerStepThrough] + set + { + try + { + Validator?.Invoke(value); + } + catch (Exception e) + { + IsValid = false; + throw new DataValidationException(e.Message); + } + Text = value; + IsValid = true; + OnPropertyChanged(); + } + } + + // Default to true if no validator is provided + private bool isValid; + public bool IsValid + { + get => Validator == null || isValid; + set + { + isValid = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs new file mode 100644 index 00000000..26051cf8 --- /dev/null +++ b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs @@ -0,0 +1,222 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using System.Threading.Tasks; +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using NLog; +using StabilityMatrix.Core.Exceptions; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Processes; +using StabilityMatrix.Core.Python; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.Helpers; + +[SupportedOSPlatform("macos")] +[SupportedOSPlatform("linux")] +public class UnixPrerequisiteHelper : IPrerequisiteHelper +{ + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly IDownloadService downloadService; + private readonly ISettingsManager settingsManager; + private readonly IPyRunner pyRunner; + + private DirectoryPath HomeDir => settingsManager.LibraryDir; + private DirectoryPath AssetsDir => HomeDir + "Assets"; + + private DirectoryPath PythonDir => AssetsDir + "Python310"; + private FilePath PythonDllPath => PythonDir + "python310.dll"; + public bool IsPythonInstalled => PythonDllPath.Exists; + + private DirectoryPath PortableGitInstallDir => HomeDir + "PortableGit"; + public string GitBinPath => PortableGitInstallDir + "bin"; + + // Cached store of whether or not git is installed + private bool? isGitInstalled; + + public UnixPrerequisiteHelper( + IDownloadService downloadService, + ISettingsManager settingsManager, + IPyRunner pyRunner) + { + this.downloadService = downloadService; + this.settingsManager = settingsManager; + this.pyRunner = pyRunner; + } + + private async Task CheckIsGitInstalled() + { + var result = await ProcessRunner.RunBashCommand("git --version"); + isGitInstalled = result.ExitCode == 0; + return isGitInstalled == true; + } + + public async Task InstallAllIfNecessary(IProgress? progress = null) + { + await UnpackResourcesIfNecessary(progress); + await InstallPythonIfNecessary(progress); + } + + public async Task UnpackResourcesIfNecessary(IProgress? progress = null) + { + // Array of (asset_uri, extract_to) + var assets = new[] + { + (Assets.SevenZipExecutable, AssetsDir), + (Assets.SevenZipLicense, AssetsDir), + }; + + progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)); + + Directory.CreateDirectory(AssetsDir); + foreach (var (asset, extractDir) in assets) + { + await asset.ExtractToDir(extractDir); + } + + progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)); + } + + public async Task InstallGitIfNecessary(IProgress? progress = null) + { + if (isGitInstalled == true || (isGitInstalled == null && await CheckIsGitInstalled())) return; + + // Show prompt to install git + var dialog = new ContentDialog + { + Title = "Git not found", + Content = new StackPanel + { + Children = + { + new TextBlock + { + Text = "The current operation requires Git. Please install it to continue." + }, + new SelectableTextBlock + { + Text = "$ sudo apt install git" + }, + } + }, + PrimaryButtonText = "Retry", + CloseButtonText = "Close", + DefaultButton = ContentDialogButton.Primary, + }; + + while (true) + { + // Return if installed + if (await CheckIsGitInstalled()) return; + if (await dialog.ShowAsync() == ContentDialogResult.None) + { + // Cancel + throw new OperationCanceledException("Git installation canceled"); + } + // Otherwise continue to retry indefinitely + } + } + + public async Task RunGit(string? workingDirectory = null, params string[] args) + { + var command = args.Length == 0 ? "git" : + "git " + string.Join(" ", args.Select(ProcessRunner.Quote)); + + var result = await ProcessRunner.RunBashCommand(command, workingDirectory ?? ""); + if (result.ExitCode != 0) + { + Logger.Error("Git command [{Command}] failed with exit code " + + "{ExitCode}:\n{StdOut}\n{StdErr}", + command, result.ExitCode, result.StandardOutput, result.StandardError); + + throw new ProcessException($"Git command [{command}] failed with exit code" + + $" {result.ExitCode}:\n{result.StandardOutput}\n{result.StandardError}"); + } + } + + public async Task InstallPythonIfNecessary(IProgress? progress = null) + { + if (IsPythonInstalled) return; + + Directory.CreateDirectory(AssetsDir); + + // Download + var remote = Assets.PythonDownloadUrl; + var url = remote.Url; + var hashSha256 = remote.HashSha256; + + var fileName = Path.GetFileName(url.LocalPath); + var downloadPath = Path.Combine(AssetsDir, fileName); + Logger.Info($"Downloading Python from {url} to {downloadPath}"); + try + { + await downloadService.DownloadToFileAsync(url.ToString(), downloadPath, progress); + + // Verify hash + var actualHash = await FileHash.GetSha256Async(downloadPath); + Logger.Info($"Verifying Python hash: (expected: {hashSha256}, actual: {actualHash})"); + if (actualHash != hashSha256) + { + throw new Exception($"Python download hash mismatch: expected {hashSha256}, actual {actualHash}"); + } + + // Extract + Logger.Info($"Extracting Python Zip: {downloadPath} to {PythonDir}"); + if (PythonDir.Exists) + { + await PythonDir.DeleteAsync(true); + } + progress?.Report(new ProgressReport(0, "Installing Python", isIndeterminate: true)); + await ArchiveHelper.Extract7ZAuto(downloadPath, PythonDir); + + // For Linux, move the inner 'python' folder up to the root PythonDir + if (Compat.IsLinux) + { + var innerPythonDir = PythonDir.JoinDir("python"); + if (!innerPythonDir.Exists) + { + throw new Exception($"Python download did not contain expected inner 'python' folder: {innerPythonDir}"); + } + + foreach (var folder in Directory.EnumerateDirectories(innerPythonDir)) + { + var folderName = Path.GetFileName(folder); + var dest = Path.Combine(PythonDir, folderName); + Directory.Move(folder, dest); + } + Directory.Delete(innerPythonDir); + } + } + finally + { + // Cleanup download file + if (File.Exists(downloadPath)) + { + File.Delete(downloadPath); + } + } + + // Initialize pyrunner and install virtualenv + await pyRunner.Initialize(); + await pyRunner.InstallPackage("virtualenv"); + + progress?.Report(new ProgressReport(1, "Installing Python", isIndeterminate: false)); + } + + public Task GetGitOutput(string? workingDirectory = null, params string[] args) + { + throw new NotImplementedException(); + } + + [UnsupportedOSPlatform("Linux")] + [UnsupportedOSPlatform("macOS")] + public Task InstallVcRedistIfNecessary(IProgress? progress = null) + { + throw new PlatformNotSupportedException(); + } +} diff --git a/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs new file mode 100644 index 00000000..e5ee642f --- /dev/null +++ b/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Versioning; +using System.Threading.Tasks; +using Microsoft.Win32; +using NLog; +using Octokit; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Processes; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.Helpers; + +[SupportedOSPlatform("windows")] +public class WindowsPrerequisiteHelper : IPrerequisiteHelper +{ + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly IGitHubClient gitHubClient; + private readonly IDownloadService downloadService; + private readonly ISettingsManager settingsManager; + + private const string VcRedistDownloadUrl = "https://aka.ms/vs/16/release/vc_redist.x64.exe"; + + private string HomeDir => settingsManager.LibraryDir; + + private string VcRedistDownloadPath => Path.Combine(HomeDir, "vcredist.x64.exe"); + + private string AssetsDir => Path.Combine(HomeDir, "Assets"); + private string SevenZipPath => Path.Combine(AssetsDir, "7za.exe"); + + private string PythonDownloadPath => Path.Combine(AssetsDir, "python-3.10.11-embed-amd64.zip"); + private string PythonDir => Path.Combine(AssetsDir, "Python310"); + private string PythonDllPath => Path.Combine(PythonDir, "python310.dll"); + private string PythonLibraryZipPath => Path.Combine(PythonDir, "python310.zip"); + private string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc"); + // Temporary directory to extract venv to during python install + private string VenvTempDir => Path.Combine(PythonDir, "venv"); + + private string PortableGitInstallDir => Path.Combine(HomeDir, "PortableGit"); + private string PortableGitDownloadPath => Path.Combine(HomeDir, "PortableGit.7z.exe"); + private string GitExePath => Path.Combine(PortableGitInstallDir, "bin", "git.exe"); + public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin"); + + public bool IsPythonInstalled => File.Exists(PythonDllPath); + + public WindowsPrerequisiteHelper( + IGitHubClient gitHubClient, + IDownloadService downloadService, + ISettingsManager settingsManager) + { + this.gitHubClient = gitHubClient; + this.downloadService = downloadService; + this.settingsManager = settingsManager; + } + + public async Task RunGit(string? workingDirectory = null, params string[] args) + { + var process = ProcessRunner.StartAnsiProcess(GitExePath, args, + workingDirectory: workingDirectory, + environmentVariables: new Dictionary + { + {"PATH", Compat.GetEnvPathWithExtensions(GitBinPath)} + }); + + await ProcessRunner.WaitForExitConditionAsync(process); + } + + public async Task GetGitOutput(string? workingDirectory = null, params string[] args) + { + var process = await ProcessRunner.GetProcessOutputAsync( + GitExePath, string.Join(" ", args), + workingDirectory: workingDirectory, + environmentVariables: new Dictionary + { + {"PATH", Compat.GetEnvPathWithExtensions(GitBinPath)} + }); + + return process; + } + + public async Task InstallAllIfNecessary(IProgress? progress = null) + { + await InstallVcRedistIfNecessary(progress); + await UnpackResourcesIfNecessary(progress); + await InstallPythonIfNecessary(progress); + await InstallGitIfNecessary(progress); + } + + public async Task UnpackResourcesIfNecessary(IProgress? progress = null) + { + // Array of (asset_uri, extract_to) + var assets = new[] + { + (Assets.SevenZipExecutable, AssetsDir), + (Assets.SevenZipLicense, AssetsDir), + }; + + progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)); + + Directory.CreateDirectory(AssetsDir); + foreach (var (asset, extractDir) in assets) + { + await asset.ExtractToDir(extractDir); + } + + progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)); + } + + public async Task InstallPythonIfNecessary(IProgress? progress = null) + { + if (File.Exists(PythonDllPath)) + { + Logger.Debug("Python already installed at {PythonDllPath}", PythonDllPath); + return; + } + + Logger.Info("Python not found at {PythonDllPath}, downloading...", PythonDllPath); + + Directory.CreateDirectory(AssetsDir); + + // Delete existing python zip if it exists + if (File.Exists(PythonLibraryZipPath)) + { + File.Delete(PythonLibraryZipPath); + } + + var remote = Assets.PythonDownloadUrl; + var url = remote.Url.ToString(); + Logger.Info($"Downloading Python from {url} to {PythonLibraryZipPath}"); + + // Cleanup to remove zip if download fails + try + { + // Download python zip + await downloadService.DownloadToFileAsync(url, PythonDownloadPath, progress: progress); + + // Verify python hash + var downloadHash = await FileHash.GetSha256Async(PythonDownloadPath, progress); + if (downloadHash != remote.HashSha256) + { + var fileExists = File.Exists(PythonDownloadPath); + var fileSize = new FileInfo(PythonDownloadPath).Length; + var msg = $"Python download hash mismatch: {downloadHash} != {remote.HashSha256} " + + $"(file exists: {fileExists}, size: {fileSize})"; + throw new Exception(msg); + } + + progress?.Report(new ProgressReport(progress: 1f, message: "Python download complete")); + + progress?.Report(new ProgressReport(-1, "Installing Python...", isIndeterminate: true)); + + // We also need 7z if it's not already unpacked + if (!File.Exists(SevenZipPath)) + { + await Assets.SevenZipExecutable.ExtractToDir(AssetsDir); + await Assets.SevenZipLicense.ExtractToDir(AssetsDir); + } + + // Delete existing python dir + if (Directory.Exists(PythonDir)) + { + Directory.Delete(PythonDir, true); + } + + // Unzip python + await ArchiveHelper.Extract7Z(PythonDownloadPath, PythonDir); + + try + { + // Extract embedded venv folder + Directory.CreateDirectory(VenvTempDir); + foreach (var (resource, relativePath) in Assets.PyModuleVenv) + { + var path = Path.Combine(VenvTempDir, relativePath); + // Create missing directories + var dir = Path.GetDirectoryName(path); + if (dir != null) + { + Directory.CreateDirectory(dir); + } + + await resource.ExtractTo(path); + } + // Add venv to python's library zip + + await ArchiveHelper.AddToArchive7Z(PythonLibraryZipPath, VenvTempDir); + } + finally + { + // Remove venv + if (Directory.Exists(VenvTempDir)) + { + Directory.Delete(VenvTempDir, true); + } + } + + // Extract get-pip.pyc + await Assets.PyScriptGetPip.ExtractToDir(PythonDir); + + // We need to uncomment the #import site line in python310._pth for pip to work + var pythonPthPath = Path.Combine(PythonDir, "python310._pth"); + var pythonPthContent = await File.ReadAllTextAsync(pythonPthPath); + pythonPthContent = pythonPthContent.Replace("#import site", "import site"); + await File.WriteAllTextAsync(pythonPthPath, pythonPthContent); + + progress?.Report(new ProgressReport(1f, "Python install complete")); + } + finally + { + // Always delete zip after download + if (File.Exists(PythonDownloadPath)) + { + File.Delete(PythonDownloadPath); + } + } + } + + public async Task InstallGitIfNecessary(IProgress? progress = null) + { + if (File.Exists(GitExePath)) + { + Logger.Debug("Git already installed at {GitExePath}", GitExePath); + return; + } + + Logger.Info("Git not found at {GitExePath}, downloading...", GitExePath); + + var portableGitUrl = + "https://github.com/git-for-windows/git/releases/download/v2.41.0.windows.1/PortableGit-2.41.0-64-bit.7z.exe"; + + if (!File.Exists(PortableGitDownloadPath)) + { + await downloadService.DownloadToFileAsync(portableGitUrl, PortableGitDownloadPath, progress: progress); + progress?.Report(new ProgressReport(progress: 1f, message: "Git download complete")); + } + + await UnzipGit(progress); + } + + [SupportedOSPlatform("windows")] + public async Task InstallVcRedistIfNecessary(IProgress? progress = null) + { + var registry = Registry.LocalMachine; + var key = registry.OpenSubKey( + @"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64", false); + if (key != null) + { + var buildId = Convert.ToUInt32(key.GetValue("Bld")); + if (buildId >= 30139) + { + return; + } + } + + Logger.Info("Downloading VC Redist"); + + await downloadService.DownloadToFileAsync(VcRedistDownloadUrl, VcRedistDownloadPath, progress: progress); + progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ download complete", + type: ProgressType.Download)); + + Logger.Info("Installing VC Redist"); + progress?.Report(new ProgressReport(progress: 0.5f, isIndeterminate: true, type: ProgressType.Generic, message: "Installing prerequisites...")); + var process = ProcessRunner.StartAnsiProcess(VcRedistDownloadPath, "/install /quiet /norestart"); + await process.WaitForExitAsync(); + progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ install complete", + type: ProgressType.Generic)); + + File.Delete(VcRedistDownloadPath); + } + + private async Task UnzipGit(IProgress? progress = null) + { + if (progress == null) + { + await ArchiveHelper.Extract7Z(PortableGitDownloadPath, PortableGitInstallDir); + } + else + { + await ArchiveHelper.Extract7Z(PortableGitDownloadPath, PortableGitInstallDir, progress); + } + + Logger.Info("Extracted Git"); + + File.Delete(PortableGitDownloadPath); + } + +} diff --git a/StabilityMatrix.Avalonia/Models/AvaloniaResource.cs b/StabilityMatrix.Avalonia/Models/AvaloniaResource.cs new file mode 100644 index 00000000..1c09b315 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/AvaloniaResource.cs @@ -0,0 +1,66 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Platform; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.FileInterfaces; + +namespace StabilityMatrix.Avalonia.Models; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public readonly record struct AvaloniaResource( + Uri UriPath, + UnixFileMode WriteUnixFileMode = UnixFileMode.None) +{ + /// + /// File name component of the Uri path. + /// + public string FileName => Path.GetFileName(UriPath.ToString()); + + /// + /// File path relative to the 'Assets' folder. + /// + public Uri RelativeAssetPath => + new Uri("avares://StabilityMatrix.Avalonia/Assets/").MakeRelativeUri(UriPath); + + public AvaloniaResource(string uriPath, UnixFileMode writeUnixFileMode = UnixFileMode.None) + : this(new Uri(uriPath), writeUnixFileMode) + { + } + + /// + /// Opens a stream to this resource. + /// + public Stream Open() => AssetLoader.Open(UriPath); + + /// + /// Extracts this resource to a target file path. + /// + public async Task ExtractTo(FilePath outputPath, bool overwrite = true) + { + if (outputPath.Exists) + { + // Skip if not overwriting + if (!overwrite) return; + // Otherwise delete the file + outputPath.Delete(); + } + var stream = AssetLoader.Open(UriPath); + await using var fileStream = File.Create(outputPath); + await stream.CopyToAsync(fileStream); + // Write permissions + if (!Compat.IsWindows && Compat.IsUnix && WriteUnixFileMode != UnixFileMode.None) + { + File.SetUnixFileMode(outputPath, WriteUnixFileMode); + } + } + + /// + /// Extracts this resource to the output directory. + /// + public Task ExtractToDir(DirectoryPath outputDir, bool overwrite = true) + { + return ExtractTo(outputDir.JoinFile(FileName), overwrite); + } +} diff --git a/StabilityMatrix.Avalonia/Models/ObservableDictionary.cs b/StabilityMatrix.Avalonia/Models/ObservableDictionary.cs new file mode 100644 index 00000000..d5831460 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/ObservableDictionary.cs @@ -0,0 +1,125 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace StabilityMatrix.Avalonia.Models; + +public class ObservableDictionary : IDictionary, + INotifyCollectionChanged, INotifyPropertyChanged where TKey : notnull +{ + private readonly IDictionary dictionary; + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event PropertyChangedEventHandler? PropertyChanged; + public IEnumerator> GetEnumerator() => dictionary.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public ObservableDictionary() + { + dictionary = new Dictionary(); + } + + public ObservableDictionary(Dictionary dictionary) + { + this.dictionary = dictionary; + } + + public void Add(KeyValuePair item) + { + dictionary.Add(item); + CollectionChanged?.Invoke(this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Keys))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Values))); + } + + public void Clear() + { + dictionary.Clear(); + CollectionChanged?.Invoke(this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Keys))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Values))); + } + + public bool Contains(KeyValuePair item) => dictionary.Contains(item); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + dictionary.CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + var success = dictionary.Remove(item); + if (!success) return false; + + CollectionChanged?.Invoke(this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Keys))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Values))); + + return success; + } + + public int Count => dictionary.Count; + public bool IsReadOnly => dictionary.IsReadOnly; + + public void Add(TKey key, TValue value) + { + dictionary.Add(key, value); + CollectionChanged?.Invoke(this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, + new KeyValuePair(key, value))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Keys))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Values))); + } + + public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); + + public bool Remove(TKey key) + { + var success = dictionary.TryGetValue(key, out var value) && dictionary.Remove(key); + if (!success) return false; + + CollectionChanged?.Invoke(this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, + new KeyValuePair(key, value!))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Keys))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Values))); + + return success; + } + + public bool TryGetValue([NotNull] TKey key, [MaybeNullWhen(false)] out TValue value) + => dictionary.TryGetValue(key, out value); + + public TValue this[TKey key] + { + get => dictionary[key]; + set + { + var exists = dictionary.ContainsKey(key); + var action = exists + ? NotifyCollectionChangedAction.Replace + : NotifyCollectionChangedAction.Add; + dictionary[key] = value; + CollectionChanged?.Invoke(this, + new NotifyCollectionChangedEventArgs(action, dictionary[key])); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Keys))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Values))); + } + } + + public ICollection Keys => dictionary.Keys; + public ICollection Values => dictionary.Values; +} diff --git a/StabilityMatrix.Avalonia/Models/RemoteResource.cs b/StabilityMatrix.Avalonia/Models/RemoteResource.cs new file mode 100644 index 00000000..8b0ba59e --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/RemoteResource.cs @@ -0,0 +1,33 @@ +using System; + +namespace StabilityMatrix.Avalonia.Models; + +/// +/// Defines a remote downloadable resource. +/// +public readonly record struct RemoteResource +{ + public Uri Url { get; } + + public Uri[]? FallbackUrls { get; } + + public string HashSha256 { get; } + + public RemoteResource(Uri url, string hashSha256) + { + Url = url; + HashSha256 = hashSha256; + } + + public RemoteResource(Uri[] urls, string hashSha256) + { + if (urls.Length == 0) + { + throw new ArgumentException("Must have at least one url.", nameof(urls)); + } + + Url = urls[0]; + FallbackUrls = urls.Length > 1 ? urls[1..] : null; + HashSha256 = hashSha256; + } +} diff --git a/StabilityMatrix.Avalonia/Models/SharedState.cs b/StabilityMatrix.Avalonia/Models/SharedState.cs new file mode 100644 index 00000000..d2dd8fe1 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/SharedState.cs @@ -0,0 +1,14 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace StabilityMatrix.Avalonia.Models; + +/// +/// Singleton DI service for observable shared UI state. +/// +public partial class SharedState : ObservableObject +{ + /// + /// Whether debug mode enabled from settings page version tap. + /// + [ObservableProperty] private bool isDebugMode; +} diff --git a/StabilityMatrix.Avalonia/Program.cs b/StabilityMatrix.Avalonia/Program.cs new file mode 100644 index 00000000..5d614db1 --- /dev/null +++ b/StabilityMatrix.Avalonia/Program.cs @@ -0,0 +1,198 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; +using NLog; +using Polly.Contrib.WaitAndRetry; +using Projektanker.Icons.Avalonia; +using Projektanker.Icons.Avalonia.FontAwesome; +using Semver; +using Sentry; +using StabilityMatrix.Avalonia.ViewModels.Dialogs; +using StabilityMatrix.Avalonia.Views.Dialogs; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Updater; + +namespace StabilityMatrix.Avalonia; + +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class Program +{ + private static bool isExceptionDialogEnabled; + + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + { + HandleUpdateReplacement(); + + var infoVersion = Assembly.GetExecutingAssembly() + .GetCustomAttribute()?.InformationalVersion; + Compat.AppVersion = SemVersion.Parse(infoVersion ?? "0.0.0", SemVersionStyles.Strict); + + // Configure exception dialog for unhandled exceptions + if (!Debugger.IsAttached || args.Contains("--debug-exception-dialog")) + { + isExceptionDialogEnabled = true; + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + } + + // Configure Sentry + if ((Debugger.IsAttached && args.Contains("--debug-sentry")) || !args.Contains("--no-sentry")) + { + ConfigureSentry(); + } + + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + { + IconProvider.Current.Register(); + + return AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); + } + + private static void HandleUpdateReplacement() + { + // Check if we're in the named update folder + if (Compat.AppCurrentDir.Parent is {Name: UpdateHelper.UpdateFolderName} parentDir) + { + var retryDelays = Backoff.DecorrelatedJitterBackoffV2( + TimeSpan.FromMilliseconds(350), retryCount: 5); + + foreach (var delay in retryDelays) + { + // Copy our current file to the parent directory, overwriting the old app file + var currentExe = Compat.AppCurrentDir.JoinFile(Compat.GetExecutableName()); + var targetExe = parentDir.JoinFile(Compat.GetExecutableName()); + try + { + currentExe.CopyTo(targetExe, true); + + // Start the new app + Process.Start(targetExe); + + // Shutdown the current app + Environment.Exit(0); + } + catch (Exception) + { + Thread.Sleep(delay); + } + } + } + + // Delete update folder if it exists in current directory + var updateDir = UpdateHelper.UpdateFolder; + if (updateDir.Exists) + { + try + { + updateDir.Delete(true); + } + catch (Exception e) + { + var logger = LogManager.GetCurrentClassLogger(); + logger.Error(e, "Failed to delete update file"); + } + } + } + + private static void ConfigureSentry() + { + SentrySdk.Init(o => + { + o.Dsn = "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328"; + o.StackTraceMode = StackTraceMode.Enhanced; + o.TracesSampleRate = 1.0; + o.IsGlobalModeEnabled = true; + // Enables Sentry's "Release Health" feature. + o.AutoSessionTracking = true; + // 1.0 to capture 100% of transactions for performance monitoring. + o.TracesSampleRate = 1.0; +#if DEBUG + o.Environment = "Development"; +#endif + }); + } + + private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + if (e.ExceptionObject is not Exception ex) return; + + var logger = LogManager.GetCurrentClassLogger(); + logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message); + + if (SentrySdk.IsEnabled) + { + SentrySdk.CaptureException(ex); + } + + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + var dialog = new ExceptionDialog + { + DataContext = new ExceptionViewModel + { + Exception = ex + } + }; + + var mainWindow = lifetime.MainWindow; + // We can only show dialog if main window exists, and is visible + if (mainWindow is {PlatformImpl: not null, IsVisible: true}) + { + // Configure for dialog mode + dialog.ShowAsDialog = true; + dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner; + + // Show synchronously without blocking UI thread + // https://github.com/AvaloniaUI/Avalonia/issues/4810#issuecomment-704259221 + var cts = new CancellationTokenSource(); + + dialog.ShowDialog(mainWindow).ContinueWith(_ => + { + cts.Cancel(); + ExitWithException(ex); + }, TaskScheduler.FromCurrentSynchronizationContext()); + + Dispatcher.UIThread.MainLoop(cts.Token); + } + else + { + // No parent window available + var cts = new CancellationTokenSource(); + // Exit on token cancellation + cts.Token.Register(() => ExitWithException(ex)); + + dialog.ShowWithCts(cts); + + Dispatcher.UIThread.MainLoop(cts.Token); + } + } + } + + [DoesNotReturn] + private static void ExitWithException(Exception exception) + { + App.Shutdown(1); + Dispatcher.UIThread.InvokeShutdown(); + Environment.Exit(Marshal.GetHRForException(exception)); + } +} diff --git a/StabilityMatrix.Avalonia/Services/INotificationService.cs b/StabilityMatrix.Avalonia/Services/INotificationService.cs new file mode 100644 index 00000000..c8a0039c --- /dev/null +++ b/StabilityMatrix.Avalonia/Services/INotificationService.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls.Notifications; +using StabilityMatrix.Core.Models; + +namespace StabilityMatrix.Avalonia.Services; + +public interface INotificationService +{ + public void Initialize( + Visual? visual, + NotificationPosition position = NotificationPosition.BottomRight, + int maxItems = 3); + + public void Show(INotification notification); + + /// + /// Attempt to run the given task, showing a generic error notification if it fails. + /// + /// The task to run. + /// The title to show in the notification. + /// The message to show, default to exception.Message + /// The appearance of the notification. + Task> TryAsync( + Task task, + string title = "Error", + string? message = null, + NotificationType appearance = NotificationType.Error); + + /// + /// Attempt to run the given void task, showing a generic error notification if it fails. + /// Return a TaskResult with true if the task succeeded, false if it failed. + /// + /// The task to run. + /// The title to show in the notification. + /// The message to show, default to exception.Message + /// The appearance of the notification. + Task> TryAsync( + Task task, + string title = "Error", + string? message = null, + NotificationType appearance = NotificationType.Error); + + void Show(string title, string message, + NotificationType appearance = NotificationType.Information); +} diff --git a/StabilityMatrix.Avalonia/Services/NotificationService.cs b/StabilityMatrix.Avalonia/Services/NotificationService.cs new file mode 100644 index 00000000..153506da --- /dev/null +++ b/StabilityMatrix.Avalonia/Services/NotificationService.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Notifications; +using StabilityMatrix.Core.Models; + +namespace StabilityMatrix.Avalonia.Services; + +public class NotificationService : INotificationService +{ + private WindowNotificationManager? notificationManager; + + public void Initialize( + Visual? visual, + NotificationPosition position = NotificationPosition.BottomRight, + int maxItems = 3) + { + if (notificationManager is not null) return; + notificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(visual)) + { + Position = position, + MaxItems = maxItems + }; + } + + public void Show(INotification notification) + { + notificationManager?.Show(notification); + } + + public void Show(string title, string message, + NotificationType appearance = NotificationType.Information) + { + Show(new Notification(title, message, appearance)); + } + + /// + public async Task> TryAsync( + Task task, + string title = "Error", + string? message = null, + NotificationType appearance = NotificationType.Error) + { + try + { + return new TaskResult(await task); + } + catch (Exception e) + { + Show(new Notification(title, message ?? e.Message, appearance)); + return TaskResult.FromException(e); + } + } + + /// + public async Task> TryAsync( + Task task, + string title = "Error", + string? message = null, + NotificationType appearance = NotificationType.Error) + { + try + { + await task; + return new TaskResult(true); + } + catch (Exception e) + { + Show(new Notification(title, message ?? e.Message, appearance)); + return new TaskResult(false, e); + } + } +} diff --git a/StabilityMatrix.Avalonia/Services/ServiceManager.cs b/StabilityMatrix.Avalonia/Services/ServiceManager.cs new file mode 100644 index 00000000..dc7387cf --- /dev/null +++ b/StabilityMatrix.Avalonia/Services/ServiceManager.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; + +namespace StabilityMatrix.Avalonia.Services; + +public class ServiceManager +{ + // Holds providers + private readonly Dictionary> providers = new(); + + // Holds singleton instances + private readonly Dictionary instances = new(); + + /// + /// Register a new dialog view model (singleton instance) + /// + public ServiceManager Register(TService instance) where TService : T + { + lock (instances) + { + if (instances.ContainsKey(typeof(TService)) || providers.ContainsKey(typeof(TService))) + { + throw new ArgumentException( + $"Service of type {typeof(TService)} is already registered for {typeof(T)}"); + } + + instances[typeof(TService)] = instance; + } + + return this; + } + + /// + /// Register a new dialog view model provider action (called on each dialog creation) + /// + public ServiceManager Register(Func provider) where TService : T + { + lock (providers) + { + if (instances.ContainsKey(typeof(TService)) || providers.ContainsKey(typeof(TService))) + { + throw new ArgumentException( + $"Service of type {typeof(TService)} is already registered for {typeof(T)}"); + } + + // Return type is wrong during build with method group syntax + // ReSharper disable once RedundantCast + providers[typeof(TService)] = () => (TService) provider(); + } + + return this; + } + + /// + /// Register a new dialog view model instance using a service provider + /// Equal to Register[TService](serviceProvider.GetRequiredService[TService]) + /// + public ServiceManager RegisterProvider(IServiceProvider provider) where TService : notnull, T + { + lock (providers) + { + if (instances.ContainsKey(typeof(TService)) || providers.ContainsKey(typeof(TService))) + { + throw new ArgumentException( + $"Service of type {typeof(TService)} is already registered for {typeof(T)}"); + } + + // Return type is wrong during build with method group syntax + // ReSharper disable once RedundantCast + providers[typeof(TService)] = () => (TService) provider.GetRequiredService(); + } + + return this; + } + + /// + /// Get a view model instance + /// + [SuppressMessage("ReSharper", "InconsistentlySynchronizedField")] + public TService Get() where TService : T + { + if (instances.TryGetValue(typeof(TService), out var instance)) + { + if (instance is null) + { + throw new ArgumentException( + $"Service of type {typeof(TService)} was registered as null"); + } + return (TService) instance; + } + + if (providers.TryGetValue(typeof(TService), out var provider)) + { + if (provider is null) + { + throw new ArgumentException( + $"Service of type {typeof(TService)} was registered as null"); + } + var result = provider(); + if (result is null) + { + throw new ArgumentException( + $"Service provider for type {typeof(TService)} returned null"); + } + return (TService) result; + } + + throw new ArgumentException( + $"Service of type {typeof(TService)} is not registered for {typeof(T)}"); + } + + /// + /// Get a view model instance with an initializer parameter + /// + public TService Get(Func initializer) where TService : T + { + var instance = Get(); + return initializer(instance); + } + + /// + /// Get a view model instance with an initializer for a mutable instance + /// + public TService Get(Action initializer) where TService : T + { + var instance = Get(); + initializer(instance); + return instance; + } +} diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj new file mode 100644 index 00000000..4c9bc694 --- /dev/null +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -0,0 +1,79 @@ + + + WinExe + net7.0 + win-x64;linux-x64;osx-x64;osx-arm64 + enable + true + app.manifest + true + partial + true + ./Assets/Icon.ico + 2.0.0-dev.1 + $(Version) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings new file mode 100644 index 00000000..8ee7adac --- /dev/null +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings @@ -0,0 +1,4 @@ + + UI diff --git a/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml b/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml new file mode 100644 index 00000000..ab433d39 --- /dev/null +++ b/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml @@ -0,0 +1,305 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs new file mode 100644 index 00000000..f61dae4c --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Markup.Xaml; +using StabilityMatrix.Avalonia.Controls; + +namespace StabilityMatrix.Avalonia.Views; + +public partial class CheckpointBrowserPage : UserControlBase +{ + public CheckpointBrowserPage() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml new file mode 100644 index 00000000..dbc0c022 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs new file mode 100644 index 00000000..b0c88c58 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs @@ -0,0 +1,60 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.ViewModels; + +namespace StabilityMatrix.Avalonia.Views; + +public partial class CheckpointsPage : UserControlBase +{ + public CheckpointsPage() + { + InitializeComponent(); + + AddHandler(DragDrop.DragEnterEvent, OnDragEnter); + AddHandler(DragDrop.DragLeaveEvent, OnDragExit); + AddHandler(DragDrop.DropEvent, OnDrop); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + private static async void OnDrop(object? sender, DragEventArgs e) + { + var sourceDataContext = (e.Source as Control)?.DataContext; + if (sourceDataContext is CheckpointFolder folder) + { + await folder.OnDrop(e); + } + } + + private static void OnDragExit(object? sender, DragEventArgs e) + { + var sourceDataContext = (e.Source as Control)?.DataContext; + if (sourceDataContext is CheckpointFolder folder) + { + folder.IsCurrentDragTarget = false; + } + } + + private static void OnDragEnter(object? sender, DragEventArgs e) + { + // Only allow Copy or Link as Drop Operations. + e.DragEffects &= DragDropEffects.Copy | DragDropEffects.Link; + + // Only allow if the dragged data contains text or filenames. + if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.Files)) + { + e.DragEffects = DragDropEffects.None; + } + + // Forward to view model + var sourceDataContext = (e.Source as Control)?.DataContext; + if (sourceDataContext is CheckpointFolder folder) + { + folder.IsCurrentDragTarget = true; + } + } +} diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml new file mode 100644 index 00000000..80e41576 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +