diff --git a/.github/actions/configure-appinstaller/action.yml b/.github/actions/configure-appinstaller/action.yml new file mode 100644 index 000000000..4f02855cf --- /dev/null +++ b/.github/actions/configure-appinstaller/action.yml @@ -0,0 +1,26 @@ +name: Configure AppInstaller +inputs: + manifest: + required: true + bundle: + required: true + version: + required: true + branch: + required: true +runs: + using: 'composite' + steps: + - name: Configure manifest + shell: powershell + run: | + $manifestPath = "${{ inputs.manifest }}" + + $manifest = [xml](Get-Content $manifestPath) + $manifest.AppInstaller.Uri = "https://install.eartrumpet.app/${{inputs.branch}}/EarTrumpet.Package.appinstaller" + $manifest.AppInstaller.Version = [string](Get-Content "${{ inputs.version }}") + + $bundleName = "${{inputs.bundle}}".Replace("*", $manifest.AppInstaller.Version) + $manifest.AppInstaller.MainBundle.Uri = "https://install.eartrumpet.app/${{inputs.branch}}/$bundleName" + $manifest.AppInstaller.MainBundle.Version = [string](Get-Content "${{ inputs.version }}") + $manifest.Save($manifestPath) diff --git a/.github/actions/configure-manifest/action.yml b/.github/actions/configure-manifest/action.yml new file mode 100644 index 000000000..16b40f0dd --- /dev/null +++ b/.github/actions/configure-manifest/action.yml @@ -0,0 +1,36 @@ +name: Configure Manifest +inputs: + manifest: + required: true + store-association: + required: true + publisher: + required: true + version: + required: true + prefix: + required: true + branch: + required: true +runs: + using: 'composite' + steps: + - name: Configure manifest + shell: powershell + run: | + $manifestPath = "${{ inputs.manifest }}" + $associationPath = "${{ inputs.store-association }}" + + $manifest = [xml](Get-Content $manifestPath) + $association = [xml](Get-Content $associationPath) + + $manifest.Package.Identity.Publisher = "${{ inputs.publisher }}" + $manifest.Package.Identity.Version = [string](Get-Content "${{ inputs.version }}") + if ("${{ inputs.branch }}" -ne "master") { + $manifest.Package.Properties.DisplayName = $manifest.Package.Properties.DisplayName + " (${{ inputs.branch }})" + $manifest.Package.Applications.Application.VisualElements.DisplayName = $manifest.Package.Properties.DisplayName + $manifest.Package.Applications.Application.Extensions.Extension.StartupTask.DisplayName = $manifest.Package.Properties.DisplayName + $association.StoreAssociation.ProductReservedInfo.ReservedNames.ReservedName = $manifest.Package.Properties.DisplayName + } + $manifest.Save($manifestPath) + $association.Save($associationPath) diff --git a/.github/actions/configure-nuspec/action.yml b/.github/actions/configure-nuspec/action.yml new file mode 100644 index 000000000..ac901d48a --- /dev/null +++ b/.github/actions/configure-nuspec/action.yml @@ -0,0 +1,18 @@ +name: Configure Nuspec +inputs: + nuspec: + required: true + version: + required: true +runs: + using: 'composite' + steps: + - name: Configure Nuspec + shell: pwsh + run: | + $Version = [Version](Get-Content "${{ inputs.version }}") + $NuspecPath = "${{ inputs.nuspec }}" + + $nuspec = [xml](Get-Content -Path $NuspecPath) + $nuspec.package.metadata.version = $Version + $nuspec.Save($NuspecPath) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5196d009f..19550daae 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,473 +1,601 @@ -name: EarTrumpet-CI +name: EarTrumpet on: push: branches: - master - dev - rafael/* - - dave/* - - david/* paths-ignore: - - "**/*.md" - - ".github/ISSUE_TEMPLATE/*" - - ".github/workflows/sponsors.yml" - - "Graphics/*" - pull_request: - branches: - - dev - paths-ignore: - - "**/*.md" - - crowdin.yml + - '**/*.md' + - '.github/ISSUE_TEMPLATE/*' + - '.github/workflows/sponsors.yml' + - '.github/workflows/translators.yml' + - 'Graphics/*' + env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true - BUILD_CONFIGURATION: Release - BUILD_PLATFORM: x86 - ARTIFACTS_BASE: '${{ github.workspace }}\artifacts' + GHA_HASH_PATHS: | + EarTrumpet/** + !EarTrumpet/**/bin/** + !EarTrumpet/**/obj/** + !EarTrumpet/**/.vs/** + EarTrumpet.Package/** + !EarTrumpet.Package/**/bin/** + !EarTrumpet.Package/**/obj/** + !EarTrumpet/**/.vs/** + Packaging/** jobs: - build: - runs-on: windows-2019 - strategy: - matrix: - channel: [AppInstaller, Store, Chocolatey] - include: - - channel: AppInstaller - publisher: - "CN=File-New-Project, O=File-New-Project, L=Purcellville, S=Virginia, C=US" - - channel: Store - publisher: CN=6099D0EF-9374-47ED-BDFE-A82136831235 - - channel: Chocolatey - publisher: - "CN=File-New-Project, O=File-New-Project, L=Purcellville, S=Virginia, C=US" - max-parallel: 3 + # + # Base build + # + + base: + name: 🔨 Build Base + runs-on: windows-latest + steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Check for existing base + uses: actions/cache/restore@v4 + id: base-exists + with: + path: .artifacts + key: base-${{ hashFiles(env.GHA_HASH_PATHS) }} + lookup-only: true - name: Fetch all history for all tags and branches run: git fetch --prune --unshallow + if: steps.base-exists.outputs.cache-hit != 'true' - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.15 + uses: gittools/actions/gitversion/setup@v1.2.0 with: - versionSpec: "5.x" + versionSpec: '5.x' includePrerelease: false + if: steps.base-exists.outputs.cache-hit != 'true' - name: Use GitVersion id: gitversion - uses: gittools/actions/gitversion/execute@v0.9.15 + uses: gittools/actions/gitversion/execute@v1.2.0 + if: steps.base-exists.outputs.cache-hit != 'true' - - name: Create artifact layout - shell: powershell + - name: Create artifacts folder + shell: pwsh run: | - $ErrorActionPreference = 'Ignore' - New-Item -ItemType Directory "$env:ARTIFACTS_BASE" - New-Item -ItemType Directory "$env:ARTIFACTS_BASE\appxupload" - New-Item -ItemType Directory "$env:ARTIFACTS_BASE\sideload" - New-Item -ItemType Directory "$env:ARTIFACTS_BASE\chocolatey" - New-Item -ItemType Directory "$env:ARTIFACTS_BASE\loose" - New-Item -ItemType Directory "$env:ARTIFACTS_BASE\metadata" - - - name: Generate versioning metadata - shell: powershell + New-Item -ItemType Directory -Force -Path .artifacts/ + if: steps.base-exists.outputs.cache-hit != 'true' + + - name: Generate versioning artifact + shell: pwsh run: | - Set-Content "$env:ARTIFACTS_BASE\metadata\semver.txt" "${{ steps.gitversion.outputs.semVer }}" - Set-Content "$env:ARTIFACTS_BASE\metadata\branch.txt" "${{ steps.gitversion.outputs.branchName }}" - Set-Content "$env:ARTIFACTS_BASE\metadata\commits.txt" "${{ steps.gitversion.outputs.commitsSinceVersionSource }}" - - if("${{ matrix.channel }}" -eq "Store") { - $Version = "${{ steps.gitversion.outputs.majorMinorPatch }}.0" - } else { - $Version = "${{ steps.gitversion.outputs.majorMinorPatch }}.${{ steps.gitversion.outputs.commitsSinceVersionSource }}" - } + "${{ steps.gitversion.outputs.majorMinorPatch }}.${{ steps.gitversion.outputs.commitsSinceVersionSource }}" | + Out-File .artifacts/version_4.txt + "${{ steps.gitversion.outputs.majorMinorPatch }}.0" | + Out-File .artifacts/version_3.txt + if: steps.base-exists.outputs.cache-hit != 'true' + + - name: Restore packages + run: dotnet restore EarTrumpet.sln + shell: cmd + if: steps.base-exists.outputs.cache-hit != 'true' - Set-Content "$env:ARTIFACTS_BASE\metadata\${{ matrix.channel }}.version.txt" $Version + - name: Build EarTrumpet + shell: cmd + run: > + dotnet publish + /p:PublishProfile=x86 + /p:Platform=x86 + EarTrumpet\EarTrumpet.csproj + + dotnet publish + /p:PublishProfile=x64 + /p:Platform=x64 + EarTrumpet\EarTrumpet.csproj + + dotnet publish + /p:PublishProfile=arm64 + /p:Platform=arm64 + EarTrumpet\EarTrumpet.csproj + if: steps.base-exists.outputs.cache-hit != 'true' + + - name: Set Bugsnag API key + shell: pwsh + run: | + Get-ChildItem .artifacts\base\**\app.config | ForEach-Object { + $cfg = Get-Content $_ + $cfg | ForEach-Object { $_.Replace("{bugsnag.apikey}", "${{ secrets.bugsnag_api_key }}") } | Set-Content $_ + } + if: steps.base-exists.outputs.cache-hit != 'true' - - name: Install NuGet - uses: NuGet/setup-nuget@v1 + - name: Sign all files + uses: azure/trusted-signing-action@v0.5.0 + with: + azure-tenant-id: ${{ secrets.azure_tenant_id }} + azure-client-id: ${{ secrets.azure_client_id }} + azure-client-secret: ${{ secrets.azure_client_secret }} + endpoint: https://wus2.codesigning.azure.net/ + trusted-signing-account-name: main + certificate-profile-name: profileTWN + files-catalog: packaging\catalog.txt + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + if: steps.base-exists.outputs.cache-hit != 'true' + + - name: Add to cache + uses: actions/cache/save@v4 with: - nuget-version: latest + key: base-${{ hashFiles(env.GHA_HASH_PATHS) }} + path: .artifacts + if: steps.base-exists.outputs.cache-hit != 'true' - - name: Restore NuGet Packages - run: nuget restore EarTrumpet.vs15.sln + # + # Packaging: AppInstaller + # - - name: Set Bugsnag API Key - shell: powershell - run: | - $cfg = Get-Content ".\EarTrumpet\app.config" - $cfg | ForEach-Object { $_.Replace("{bugsnag.apikey}", "${{ secrets.bugsnag_api_key }}") } | Set-Content ".\EarTrumpet\app.config" + appinstaller: + name: 📦 Package for AppInstaller + runs-on: windows-latest + needs: base - - name: Adjust manifest and store association - if: matrix.channel == 'Store' || matrix.channel == 'AppInstaller' - shell: powershell - run: | - $manifestPath = ".\EarTrumpet.Package\Package.appxmanifest" - $storeAssociationPath = ".\EarTrumpet.Package\Package.StoreAssociation.xml" - - $manifest = [xml](Get-Content $manifestPath) - $manifest.Package.Identity.Publisher = "${{ matrix.publisher }}" - if("${{ matrix.channel }}" -eq "AppInstaller") { - if("${{ steps.gitversion.outputs.branchName }}" -eq "master") { - $manifest.Package.Properties.DisplayName = "EarTrumpet" - $manifest.Package.Applications.Application.VisualElements.DisplayName = "EarTrumpet" - } else { - $manifest.Package.Properties.DisplayName = $manifest.Package.Properties.DisplayName + " (${{ steps.gitversion.outputs.branchName }})" - $manifest.Package.Applications.Application.VisualElements.DisplayName = "EarTrumpet (${{ steps.gitversion.outputs.branchName }})" - } - } - $manifest.Save($manifestPath) - - $storeAssociation = [xml](Get-Content $storeAssociationPath) - $storeAssociation.StoreAssociation.Publisher = "${{ matrix.publisher }}" - if("${{ matrix.channel }}" -eq "AppInstaller") { - if("${{ steps.gitversion.outputs.branchName }}" -eq "master") { - $storeAssociation.StoreAssociation.ProductReservedInfo.ReservedNames.ReservedName = "EarTrumpet" - } else { - $storeAssociation.StoreAssociation.ProductReservedInfo.ReservedNames.ReservedName = "EarTrumpet (${{ steps.gitversion.outputs.branchName }})" - } - } - $storeAssociation.Save($storeAssociationPath) + env: + channel: 'AppInstaller' + publisher: 'CN=File-New-Project, O=File-New-Project, L=Purcellville, S=Virginia, C=US' - - name: Set up MSBuild - uses: microsoft/setup-msbuild@v1 + steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Build EarTrumpet appxupload package - if: matrix.channel == 'Store' - shell: cmd - run: - msbuild EarTrumpet.Package/EarTrumpet.Package.wapproj - /p:Platform=%BUILD_PLATFORM% /p:Configuration=%BUILD_CONFIGURATION% - /p:AppxBundle=Always /p:Channel=${{ matrix.channel }} - /p:AppxPackageDir=%ARTIFACTS_BASE%\appxupload\ - /p:AppxPackageSigningEnabled=false /p:UapAppxPackageBuildMode=CI - -maxcpucount - - - name: Upload appxupload artifact - if: matrix.channel == 'Store' && github.event_name != 'pull_request' - uses: actions/upload-artifact@v3 + - name: Fetch all history for all tags and branches + run: git fetch --prune --unshallow + + - name: Restore base + uses: actions/cache/restore@v4 with: - name: appxupload - path: artifacts/appxupload + path: .artifacts + key: base-${{ hashFiles(env.GHA_HASH_PATHS) }} + fail-on-cache-miss: true - - name: Build EarTrumpet - if: matrix.channel == 'Chocolatey' - shell: cmd - run: - msbuild EarTrumpet/EarTrumpet.csproj /p:Platform=%BUILD_PLATFORM% - /p:Configuration=%BUILD_CONFIGURATION% /p:Channel=${{ matrix.channel - }} /p:OutputPath=%ARTIFACTS_BASE%\loose\ -maxcpucount - - - name: Upload loose artifacts - if: - matrix.channel == 'Chocolatey' && github.event_name != 'pull_request' - uses: actions/upload-artifact@v3 + - name: Configure manifest + uses: ./.github/actions/configure-manifest with: - name: loose - path: artifacts/loose + manifest: EarTrumpet.Package/Package.appxmanifest + store-association: EarTrumpet.Package/Package.StoreAssociation.xml + publisher: ${{ env.publisher }} + branch: ${{ github.ref_name }} + prefix: 'EarTrumpet' + version: .artifacts/version_4.txt - - name: Build EarTrumpet appinstaller/sideload package - if: matrix.channel == 'AppInstaller' || matrix.channel == 'Chocolatey' - shell: cmd - run: - msbuild EarTrumpet.Package/EarTrumpet.Package.wapproj - /p:Platform=%BUILD_PLATFORM% /p:Configuration=%BUILD_CONFIGURATION% - /p:AppxBundle=Always /p:Channel=${{ matrix.channel }} - /p:AppxPackageDir=%ARTIFACTS_BASE%\sideload\ + - name: Set up MSBuild + uses: microsoft/setup-msbuild@v2 + with: + msbuild-architecture: x64 + + - name: Restore packages + shell: pwsh + run: > + dotnet restore + EarTrumpet.sln + + - name: Create msixbundle + shell: pwsh + run: > + msbuild + /p:AppxBundle=Always /p:AppxPackageSigningEnabled=false - /p:UapAppxPackageBuildMode=SideloadOnly + /p:AppxPackageDir=..\.artifacts\sideload\ + /p:AppxPackageTestDir=..\.artifacts\sideload\ + /p:AppInstallerUri="https://install.eartrumpet.app" + /p:Configuration=Release + /p:Channel=AppInstaller /p:GenerateAppInstallerFile=true - /p:AppxPackageTestDir=%ARTIFACTS_BASE%\sideload\ - /p:AppInstallerUri="https://install.eartrumpet.app" -maxcpucount - - - name: Adjust appinstaller manifest - if: - matrix.channel == 'AppInstaller' && github.event_name != - 'pull_request' - shell: powershell - run: | - $manifestPath = "$env:ARTIFACTS_BASE/sideload/EarTrumpet.Package.appinstaller" - $manifest = [xml](Get-Content $manifestPath) - $manifest.AppInstaller.Uri = "https://install.eartrumpet.app/${{ steps.gitversion.outputs.branchName }}/EarTrumpet.Package.appinstaller" - $manifest.AppInstaller.MainBundle.Uri = "https://install.eartrumpet.app/${{ steps.gitversion.outputs.branchName }}/EarTrumpet.Package_${{ steps.gitversion.outputs.majorMinorPatch }}.${{ steps.gitversion.outputs.commitsSinceVersionSource }}_x86.appxbundle" - $manifest.AppInstaller.MainBundle.Publisher = "${{ matrix.publisher }}" - - $fragment = [xml]'' - $manifest.AppInstaller.InsertAfter($manifest.ImportNode($fragment.AppInstaller.Dependencies, $true), $manifest.AppInstaller.MainBundle) - - $manifest.Save($manifestPath) - - - name: Upload appinstaller/sideload package artifacts - if: - matrix.channel == 'AppInstaller' && github.event_name != - 'pull_request' - uses: actions/upload-artifact@v3 - with: - name: sideload - path: artifacts/sideload + /p:Platform=x86 + /p:UapAppxPackageBuildMode=SideloadOnly + /p:WapAlwaysBuildDependentProjects=false + "/p:AppxBundlePlatforms=x86|x64|arm64" + EarTrumpet.Package/EarTrumpet.Package.wapproj - - name: Fix up PDPs - if: matrix.channel == 'Store' && github.event_name != 'pull_request' + - name: Stage AppInstaller manifest shell: pwsh - run: | - Set-Location packaging\MicrosoftStore\PDPs - Get-ChildItem | ForEach-Object { - $locale = $_.Name - $pdp = [xml](Get-Content "$locale\pdp.xml") - $pdp.ProductDescription.language = $locale - $pdp.ProductDescription.lang = $locale - $pdp.ProductDescription - $pdp.Save((Resolve-Path "$locale\pdp.xml")) - } + run: > + Copy-Item Packaging\AppInstaller\EarTrumpet.Package.appinstaller .artifacts\sideload\ + -Force - - name: Stage msix packaging metadata - if: matrix.channel == 'Store' && github.event_name != 'pull_request' - shell: powershell - run: | - Copy-Item packaging\ -Recurse "$env:ARTIFACTS_BASE\metadata\" + - name: Configure AppInstaller manifest + uses: ./.github/actions/configure-appinstaller + with: + manifest: .artifacts\sideload\EarTrumpet.Package.appinstaller + bundle: 'EarTrumpet.Package_*_x86_x64_arm64.msixbundle' + version: .artifacts/version_4.txt + branch: ${{ github.ref_name }} - - name: Upload metadata artifacts - uses: actions/upload-artifact@v3 + - name: Sign package + uses: azure/trusted-signing-action@v0.5.0 + with: + azure-tenant-id: ${{ secrets.azure_tenant_id }} + azure-client-id: ${{ secrets.azure_client_id }} + azure-client-secret: ${{ secrets.azure_client_secret }} + endpoint: https://wus2.codesigning.azure.net/ + trusted-signing-account-name: main + certificate-profile-name: profileTWN + files-folder: .artifacts/sideload/ + files-folder-filter: msixbundle + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + + - name: Upload artifact + uses: actions/upload-artifact@v4 with: - name: metadata - path: artifacts/metadata + name: AppInstaller + path: | + .artifacts/sideload/EarTrumpet.Package_*_x86_x64_arm64.msixbundle + .artifacts/sideload/EarTrumpet.Package.appinstaller - - name: Stage chocolatey packaging metadata - if: - matrix.channel == 'Chocolatey' && github.event_name != 'pull_request' - shell: powershell - run: | - Copy-Item .chocolatey\* -Recurse "$env:ARTIFACTS_BASE\chocolatey\" + # + # Packaging: Chocolatey + # + + chocolatey: + name: 📦 Package for Chocolatey + runs-on: windows-latest + needs: base - - name: Upload chocolatey artifacts - uses: actions/upload-artifact@v3 - with: - name: chocolatey - path: artifacts/chocolatey - release: - needs: build - runs-on: windows-2019 - if: github.event_name != 'pull_request' - strategy: - matrix: - channel: [AppInstaller, Store, Chocolatey] - max-parallel: 3 env: - AZURE_TENANT_ID: ${{ secrets.azure_tenant_id }} - AZURE_CLIENT_ID: ${{ secrets.azure_client_id }} - AZURE_CLIENT_SECRET: ${{ secrets.azure_client_secret }} + channel: 'Chocolatey' + publisher: 'CN=File-New-Project, O=File-New-Project, L=Purcellville, S=Virginia, C=US' + steps: - - name: Download artifacts - uses: actions/download-artifact@v3 - with: - path: artifacts + - name: Checkout + uses: actions/checkout@v4 - - name: Install NuGet - uses: NuGet/setup-nuget@v1 + - name: Restore base + uses: actions/cache/restore@v4 with: - nuget-version: latest + path: .artifacts + key: base-${{ hashFiles(env.GHA_HASH_PATHS) }} + fail-on-cache-miss: true - - name: Install Build Tools - run: nuget install Microsoft.Windows.SDK.BuildTools + - name: Adjust nuspec + uses: ./.github/actions/configure-nuspec + with: + nuspec: Packaging\Chocolatey\eartrumpet.nuspec + version: .artifacts/version_4.txt - - name: Install Azure Codesigning + - name: Stage packaging shell: pwsh - env: - ACS_PACKAGE_URI: ${{ secrets.acs_package_uri }} - ACS_METADATA_URI: ${{ secrets.acs_metadata_uri }} - GITHUB_RUN_ID: ${{ github.run_id }} run: | - Invoke-WebRequest $env:ACS_PACKAGE_URI -UseBasicParsing -OutFile package.zip - Expand-Archive package.zip -DestinationPath acs - Invoke-WebRequest $env:ACS_METADATA_URI -UseBasicParsing -OutFile acs\metadata.json + Copy-Item Packaging\Chocolatey -Recurse .artifacts - - name: Sign and repackage Store artifacts - if: matrix.channel == 'Store' + - name: Create release archive shell: pwsh - run: | - $MetadataPath = "$env:ARTIFACTS_BASE\metadata" - $Version = [Version](Get-Content "$MetadataPath\Store.version.txt") - $AppxUploadPath = "$env:ARTIFACTS_BASE\AppxUpload" - $BundleFilename = "EarTrumpet.Package_${Version}_x86.appxbundle" - $SymbolsBundleFilename = "EarTrumpet.Package_${Version}_x86.appxsym" - $AppxFilename = "EarTrumpet.Package_${Version}_x86.appx" - $StoreBundleFilename = "EarTrumpet.Package_${Version}_x86_bundle.appxupload" - - ### Expand bundle and appx package within - $ExtractedPath = "$env:ARTIFACTS_BASE\Extracted" - Expand-Archive "$AppxUploadPath\$StoreBundleFilename" "$ExtractedPath\AppxUpload" - Expand-Archive "$ExtractedPath\AppxUpload\$SymbolsBundleFilename" "$ExtractedPath\Symbols" - Expand-Archive "$ExtractedPath\AppxUpload\$BundleFilename" "$ExtractedPath\Bundle" - Expand-Archive "$ExtractedPath\Bundle\$AppxFilename" "$ExtractedPath\Package" - - ### Place symbols next to executable image - Copy-Item "$ExtractedPath\Symbols\EarTrumpet.pdb" "$ExtractedPath\Package\EarTrumpet\" - - ### Sign executable image - & (Resolve-Path "Microsoft.Windows.SDK.BuildTools.*\bin\*\x64\signtool.exe") sign /v /fd SHA256 /td SHA256 /tr http://timestamp.acs.microsoft.com /dlib "acs\bin\x64\Azure.CodeSigning.Dlib.dll" /dmdf "acs\metadata.json" "$ExtractedPath\Package\EarTrumpet\EarTrumpet.exe" - - $SignedPath = "$env:ARTIFACTS_BASE\Signed" - New-Item -ItemType Directory "$SignedPath" - New-Item -ItemType Directory "$SignedPath\Package" - New-Item -ItemType Directory "$SignedPath\Bundle" - New-Item -ItemType Directory "$SignedPath\AppxUpload" - - ### Repackage appx package - & "C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\makeappx.exe" pack /l /h sha256 /d "$ExtractedPath\Package" /o /p "$SignedPath\Package\$AppxFilename" - - Set-ItemProperty "$SignedPath\Package\$AppxFilename" -Name IsReadOnly -Value $true - Copy-Item "$ExtractedPath\Bundle\*.appx" "$SignedPath\Package\" -ErrorAction Ignore - Set-ItemProperty "$SignedPath\Package\$AppxFilename" -Name IsReadOnly -Value $false - - ### Repackage appx bundle - & "C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\makeappx.exe" bundle /d "$SignedPath\Package" /bv $Version /o /p "$SignedPath\Bundle\$BundleFilename" - - ### Repackage appxupload - Copy-Item "$ExtractedPath\AppxUpload\$SymbolsBundleFilename" "$SignedPath\Bundle" - Compress-Archive -Path "$SignedPath\Bundle\*" -DestinationPath "$SignedPath\AppxUpload\$StoreBundleFilename" -CompressionLevel Optimal - - - name: Sign AppInstaller artifacts - if: matrix.channel == 'AppInstaller' + run: > + Compress-Archive + -Path .artifacts\base\x86\** + -DestinationPath .artifacts\chocolatey\tools\release.zip + -CompressionLevel Optimal + + - name: Create package shell: pwsh - run: | - $MetadataPath = "$env:ARTIFACTS_BASE\metadata" - $Version = [Version](Get-Content "$MetadataPath\AppInstaller.version.txt") - $Branch = Get-Content "$MetadataPath\branch.txt" - $Semver= Get-Content "$MetadataPath\semver.txt" - $BundleFilename = "EarTrumpet.Package_${Version}_x86.appxbundle" - $SymbolsBundleFilename = "EarTrumpet.Package_${Version}_x86.appxsym" - $AppxFilename = "EarTrumpet.Package_${Version}_x86.appx" + run: > + choco + pack ".artifacts\chocolatey\eartrumpet.nuspec" + --out ".artifacts\chocolatey" - $SideloadPath = "$env:ARTIFACTS_BASE\sideload" - $SignedPath = "$env:ARTIFACTS_BASE\sideload\signed" + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: Chocolatey + path: | + .artifacts/chocolatey/eartrumpet.*.nupkg - ### Expand bundle and appx package within - $ExtractedPath = "$env:TEMP\extracted" - Expand-Archive "$SideloadPath\$SymbolsBundleFilename" "$ExtractedPath\Symbols" - Expand-Archive "$SideloadPath\$BundleFilename" "$ExtractedPath\Bundle" - Write-Output "Expand $ExtractedPath\Bundle\EarTrumpet.Package_${Version}_x86.appx" - Expand-Archive "$ExtractedPath\Bundle\EarTrumpet.Package_${Version}_x86.appx" "$ExtractedPath\Package" + # + # Packaging: Store + # - ### Place symbols next to executable image - Copy-Item "$ExtractedPath\Symbols\EarTrumpet.pdb" "$ExtractedPath\Package\EarTrumpet\" + store: + name: 📦 Package for Microsoft Store + runs-on: windows-latest + needs: base - ### Sign executable image - Write-Output "Signing $ExtractedPath\Package\EarTrumpet\EarTrumpet.exe" - & (Resolve-Path "Microsoft.Windows.SDK.BuildTools.*\bin\*\x64\signtool.exe") sign /v /fd SHA256 /td SHA256 /tr http://timestamp.acs.microsoft.com /dlib "acs\bin\x64\Azure.CodeSigning.Dlib.dll" /dmdf "acs\metadata.json" "$ExtractedPath\Package\EarTrumpet\EarTrumpet.exe" + env: + channel: 'Store' + publisher: 'CN=6099D0EF-9374-47ED-BDFE-A82136831235' - New-Item -ItemType Directory "$SignedPath" - New-Item -ItemType Directory "$SignedPath\Package" - New-Item -ItemType Directory "$SignedPath\Bundle" + steps: + - name: Checkout + uses: actions/checkout@v4 - ### Repackage appx package - & "C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\makeappx.exe" pack /l /h sha256 /d "$ExtractedPath\Package" /o /p "$SignedPath\Package\$AppxFilename" + - name: Fetch all history for all tags and branches + run: git fetch --prune --unshallow - Set-ItemProperty "$SignedPath\Package\EarTrumpet.Package_${Version}_x86.appx" -Name IsReadOnly -Value $true - Copy-Item "$ExtractedPath\Bundle\*.appx" "$SignedPath\Package\" -ErrorAction Ignore - Set-ItemProperty "$SignedPath\Package\EarTrumpet.Package_${Version}_x86.appx" -Name IsReadOnly -Value $false + - name: Restore base + uses: actions/cache/restore@v4 + with: + path: .artifacts + key: base-${{ hashFiles(env.GHA_HASH_PATHS) }} + fail-on-cache-miss: true - ### Repackage appx bundle - & "C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\makeappx.exe" bundle /d "$SignedPath\Package" /bv $Version /o /p "$SignedPath\Bundle\$BundleFilename" + - name: Configure manifest + uses: ./.github/actions/configure-manifest + with: + manifest: EarTrumpet.Package/Package.appxmanifest + store-association: EarTrumpet.Package/Package.StoreAssociation.xml + publisher: ${{ env.publisher }} + branch: ${{ github.ref_name }} + prefix: 'EarTrumpet' + version: .artifacts/version_3.txt - ### Sign appx bundle - Write-Output "Signing $SignedPath\Bundle\$BundleFilename" - & (Resolve-Path "Microsoft.Windows.SDK.BuildTools.*\bin\*\x64\signtool.exe") sign /v /fd SHA256 /td SHA256 /tr http://timestamp.acs.microsoft.com /dlib "acs\bin\x64\Azure.CodeSigning.Dlib.dll" /dmdf "acs\metadata.json" "$SignedPath\Bundle\$BundleFilename" + - name: Set up MSBuild + uses: microsoft/setup-msbuild@v2 + with: + msbuild-architecture: x64 - Copy-Item "$SideloadPath\*.appinstaller" "$SignedPath\Bundle" + - name: Restore packages + shell: pwsh + run: > + dotnet restore + EarTrumpet.sln - - name: Sign and repackage Chocolatey artifacts - if: matrix.channel == 'Chocolatey' + - name: Create msixbundle shell: pwsh - run: | - $MetadataPath = "$env:ARTIFACTS_BASE\metadata" - $Branch = Get-Content "$MetadataPath\branch.txt" - $Semver= Get-Content "$MetadataPath\semver.txt" - $LooseFilesPath = "$env:ARTIFACTS_BASE\loose" + run: > + msbuild + /p:AppxBundle=Always + /p:AppxPackageSigningEnabled=false + /p:AppxPackageDir=..\.artifacts\store\ + /p:Configuration=Release + /p:Channel=Store + /p:GenerateAppInstallerFile=false + /p:Platform=x86 + /p:UapAppxPackageBuildMode=CI + /p:WapAlwaysBuildDependentProjects=false + "/p:AppxBundlePlatforms=x86|x64|arm64" + EarTrumpet.Package/EarTrumpet.Package.wapproj + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: MicrosoftStore + path: | + .artifacts/store/EarTrumpet.Package_*_x86_x64_arm64.msixbundle + .artifacts/store/EarTrumpet.Package_*_x86_x64_arm64_bundle.msixupload - ### Sign executable image - & (Resolve-Path "Microsoft.Windows.SDK.BuildTools.*\bin\*\x64\signtool.exe") sign /v /fd SHA256 /td SHA256 /tr http://timestamp.acs.microsoft.com /dlib "acs\bin\x64\Azure.CodeSigning.Dlib.dll" /dmdf "acs\metadata.json" "$LooseFilesPath\EarTrumpet.exe" + # + # Packaging: Loose + # - ### Package for release - Compress-Archive -Path "$LooseFilesPath\*" -DestinationPath "$env:ARTIFACTS_BASE\chocolatey\tools\release.zip" -CompressionLevel Optimal + loose: + name: 📦 Package for Loose scenarios + runs-on: windows-latest + needs: base - - name: Adjust nuspec - if: matrix.channel == 'Chocolatey' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Restore base + uses: actions/cache/restore@v4 + with: + path: .artifacts + key: base-${{ hashFiles(env.GHA_HASH_PATHS) }} + fail-on-cache-miss: true + + - name: Create release archives shell: pwsh - run: | - $MetadataPath = "$env:ARTIFACTS_BASE\metadata" - $Version = [Version](Get-Content "$MetadataPath\Chocolatey.version.txt") - $NuspecPath = "$env:ARTIFACTS_BASE\chocolatey\eartrumpet.nuspec" + run: > + New-Item + -ItemType Directory + -Path .artifacts\loose + -Force + + @("x86", "x64", "arm64") | ForEach-Object { + Compress-Archive ` + -Path .artifacts\base\$_\* ` + -DestinationPath .artifacts\loose\EarTrumpet_$(Get-Content .artifacts/version_4.txt)_$($_).zip ` + -CompressionLevel Optimal + } + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: Loose + path: | + .artifacts/loose/*.zip + + # + # Deployment: AppInstaller + # + deploy-appinstaller: + name: Deploy AppInstaller package + runs-on: windows-latest + needs: appinstaller + if: | + github.event_name != 'pull_request' && + ( + startsWith(github.ref, 'refs/heads/dev') || + startsWith(github.ref, 'refs/heads/master') || + startsWith(github.ref, 'refs/heads/feature') || + startsWith(github.ref, 'refs/heads/rafael') + ) - $nuspec = [xml](Get-Content -Path $NuspecPath) - $nuspec.package.metadata.version = $Version - $nuspec.Save($NuspecPath) + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: AppInstaller + path: . - - name: Create chocolatey package - if: matrix.channel == 'Chocolatey' - shell: powershell + - name: Upload files to Cloudflare + shell: pwsh + env: + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + AWS_REQUEST_CHECKSUM_CALCULATION: when_required run: | - choco pack "$env:ARTIFACTS_BASE\chocolatey\eartrumpet.nuspec" --out "$env:ARTIFACTS_BASE\chocolatey" + # Upload MSIX bundle + Get-ChildItem -File EarTrumpet.Package_*_x86_x64_arm64.msixbundle | ForEach-Object { + aws s3 cp $_.FullName "s3://${{ secrets.R2_BUCKET_NAME }}/${{ github.ref_name }}/$($_.Name)" ` + --content-type "application/msixbundle" ` + --endpoint-url "https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com" + } + + # Upload appinstaller file + aws s3 cp EarTrumpet.Package.appinstaller "s3://${{ secrets.R2_BUCKET_NAME }}/${{ github.ref_name }}/EarTrumpet.Package.appinstaller" ` + --content-type "application/appinstaller" ` + --endpoint-url "https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com" + + # + # Deployment: Microsoft Store + # + + deploy-store: + name: 🚀 Deploy Microsoft Store package + runs-on: windows-latest + needs: store + if: | + github.event_name != 'pull_request' && + ( + startsWith(github.ref, 'refs/heads/master') + ) - - name: Upload chocolatey artifact - if: matrix.channel == 'Chocolatey' - uses: actions/upload-artifact@v3 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download artifact + uses: actions/download-artifact@v4 with: - name: chocolatey-package - path: artifacts/chocolatey/*.nupkg + name: MicrosoftStore + path: . - - name: Install OpenSSH FOD - if: matrix.channel == 'AppInstaller' || matrix.channel == 'Store' + - name: Push to Microsoft Store shell: powershell - run: | - Set-Service -Name wuauserv -StartupType Manual - Start-Service -Name wuauserv - Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 + run: > + Set-PSRepository + -Name "PSGallery" + -InstallationPolicy Trusted + + Install-Module + -Name StoreBroker + + $Password = ConvertTo-SecureString '${{ secrets.partnercenter_client_secret }}' + -AsPlainText + -Force + + $Credentials = New-Object System.Management.Automation.PSCredential ('${{ secrets.partnercenter_client_id }}', $Password) + + Set-StoreBrokerAuthentication + -TenantId '${{ secrets.partnercenter_tenant_id }}' + -Credential $Credentials + -Verbose + + $SubmissionRoot = "$env:Temp\Packaging\Submission" + + $Bundle = Resolve-Path "EarTrumpet.Package_*_x86_x64_arm64_bundle.msixupload" | Split-Path -Leaf + + New-SubmissionPackage + -ConfigPath "Packaging\MicrosoftStore\SBConfig.json" + -PDPRootPath "Packaging\MicrosoftStore\PDPs" + -ImagesRootPath "Packaging\MicrosoftStore\PDPs" + -AppxPath "$Bundle" + -MediaFallbackLanguage en-US + -OutPath "$SubmissionRoot" + -OutName EarTrumpet + -Verbose + + $SubmissionId, $SubmissionUrl = + Update-ApplicationSubmission + -AppId 9NBLGGH516XP + -SubmissionDataPath "$SubmissionRoot\EarTrumpet.json" + -PackagePath "$SubmissionRoot\EarTrumpet.zip" + -AddPackages + -UpdateListings + -UpdatePublishModeAndVisibility + -UpdatePricingAndAvailability + -UpdateAppProperties + -UpdateNotesForCertification + -TargetPublishMode Manual + -Force + -Verbose + + Complete-ApplicationSubmission + -AppId 9NBLGGH516XP + -SubmissionId $SubmissionId + -Verbose + + # + # Deployment: GitHub + # + + deploy-github: + name: 🚀 Deploy GitHub Release + runs-on: windows-latest + needs: loose + if: | + github.event_name != 'pull_request' && + ( + startsWith(github.ref, 'refs/heads/master') + ) - - name: Prepare for staging - if: matrix.channel == 'AppInstaller' || matrix.channel == 'Store' - shell: powershell - run: | - "${{ secrets.staging_userkey }}" | Out-File -Encoding ascii staging.key | Out-Null + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Restore base + uses: actions/cache/restore@v4 + with: + path: .artifacts + key: base-${{ hashFiles(env.GHA_HASH_PATHS) }} + fail-on-cache-miss: true + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: Loose + path: .artifacts/loose - - name: Stage AppInstaller artifacts via SCP - if: matrix.channel == 'AppInstaller' + - name: Generate changelog + id: changelog + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: pwsh - run: | - icacls .\staging.key /inheritance:r - icacls .\staging.key /grant:r "$env:USERNAME`:(R)" - $Branch = Get-Content $env:ARTIFACTS_BASE\metadata\branch.txt - ssh -i staging.key -o "StrictHostKeyChecking no" ${{ secrets.staging_username }}@${{ secrets.staging_host }} "mkdir -p /var/www/html/$Branch" - scp -B -i staging.key -o "StrictHostKeyChecking no" $env:ARTIFACTS_BASE\sideload\signed\bundle\* ${{ secrets.staging_username }}@${{ secrets.staging_host }}:/var/www/html/$Branch - del staging.key - - - name: Stage Store artifacts via SCP - if: matrix.channel == 'Store' + run: > + $lastTagRef = git describe --match "[0-9]*.[0-9]*.[0-9]*.[0-9]*" --abbrev=0 --tags HEAD 2>$null + + $lastTagDate = git show -s --format=%aI "$lastTagRef^{commit}" + + $query = (Get-Content ".github/workflows/queries/commits.graphql" -Raw) + -replace '%%OWNER%%', '${{ github.repository_owner }}' + -replace '%%REPO%%', '${{ github.event.repository.name }}' + + $template = Get-Content ".github/workflows/templates/commits.md" -Raw + + $changelog = gh api graphql + --paginate + -F query="$query" + -F lastTagDate="$lastTagDate" + -F ref="HEAD" + --template "$template" + + $changelog | Out-File -FilePath .artifacts/changelog.md + + - name: Create Release shell: pwsh - run: | - icacls .\staging.key /inheritance:r - icacls .\staging.key /grant:r "$env:USERNAME`:(R)" - $Branch = Get-Content $env:ARTIFACTS_BASE\metadata\branch.txt - ssh -i staging.key -o "StrictHostKeyChecking no" ${{ secrets.staging_username }}@${{ secrets.staging_host }} "mkdir -p /var/www/html/store/$Branch" - scp -B -i staging.key -o "StrictHostKeyChecking no" $env:ARTIFACTS_BASE\signed\appxupload\* ${{ secrets.staging_username }}@${{ secrets.staging_host }}:/var/www/html/store/$Branch - del staging.key - - - name: Push release to Partner Center via StoreBroker - if: matrix.channel == 'Store' - shell: powershell - run: | - Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted - Install-Module -Name StoreBroker - - $Password = ConvertTo-SecureString '${{ secrets.partnercenter_clientkey }}' -AsPlainText -Force - $Credentials = New-Object System.Management.Automation.PSCredential ('${{ secrets.partnercenter_clientid }}', $Password) - Set-StoreBrokerAuthentication -TenantId '${{ secrets.partnercenter_tenantid }}' -Credential $Credentials -Verbose - - $MetadataPath = "$env:ARTIFACTS_BASE\metadata" - $PackagingRoot = "$MetadataPath\Packaging\MicrosoftStore" - $SubmissionRoot = "$env:TEMP\Packaging\Submission" - $Version = [Version](Get-Content "$MetadataPath\Store.version.txt") - $StoreBundleFilename = "EarTrumpet.Package_${Version}_x86_bundle.appxupload" - - New-SubmissionPackage -ConfigPath "$PackagingRoot\SBConfig.json" -PDPRootPath "$PackagingRoot\PDPs" -ImagesRootPath "$PackagingRoot\PDPs" -AppxPath "$env:ARTIFACTS_BASE\Signed\AppxUpload\$StoreBundleFilename" -MediaFallbackLanguage en-US -OutPath "$SubmissionRoot" -OutName EarTrumpet -Verbose - $submissionId, $submissionUrl = Update-ApplicationSubmission -AppId "${{ secrets.partnercenter_appid }}" -SubmissionDataPath "$SubmissionRoot\EarTrumpet.json" -PackagePath "$SubmissionRoot\EarTrumpet.zip" -AddPackages -UpdateListings -UpdatePublishModeAndVisibility -UpdatePricingAndAvailability -UpdateAppProperties -UpdateNotesForCertification -TargetPublishMode Manual -Force -Verbose - Complete-ApplicationSubmission -AppId "${{ secrets.partnercenter_appid }}" -SubmissionId $submissionId -Verbose + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: > + $version = Get-Content .artifacts/version_4.txt + + gh release create "v$version" + --draft + --title "$version" + --notes-file .artifacts/changelog.md + (Get-ChildItem ".artifacts/loose/*.zip").FullName \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 000000000..361e393fe --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,46 @@ +name: EarTrumpet (PR Build) +on: + pull_request: + branches: + - dev + paths-ignore: + - '**/*.md' + - crowdin.yml + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +jobs: + pr-build: + name: 🔨 Build Base + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Restore packages + run: dotnet restore EarTrumpet.sln + shell: cmd + + - name: Build EarTrumpet (unsigned for PR validation) + shell: cmd + run: > + dotnet publish + /p:PublishProfile=x86 + /p:Platform=x86 + /p:DisableGitVersionTask=true + EarTrumpet\EarTrumpet.csproj + + dotnet publish + /p:PublishProfile=x64 + /p:Platform=x64 + /p:DisableGitVersionTask=true + EarTrumpet\EarTrumpet.csproj + + dotnet publish + /p:PublishProfile=arm64 + /p:Platform=arm64 + /p:DisableGitVersionTask=true + EarTrumpet\EarTrumpet.csproj diff --git a/.github/workflows/queries/commits.graphql b/.github/workflows/queries/commits.graphql new file mode 100644 index 000000000..df435c632 --- /dev/null +++ b/.github/workflows/queries/commits.graphql @@ -0,0 +1,20 @@ +query($lastTagDate: GitTimestamp!, $ref: String!) { + repository(owner: "%%OWNER%%", name: "%%REPO%%") { + object(expression: $ref) { + ... on Commit { + history(first: 100, since: $lastTagDate) { + nodes { + messageHeadline + oid + associatedPullRequests(first: 1) { + nodes { + title + number + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 1fe2fbb99..228c14442 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -1,8 +1,8 @@ name: Generate Sponsors on: workflow_dispatch: - schedule: - - cron: 0 12 1-31 * * + # schedule: + # - cron: 0 12 1-31 * * jobs: deploy: runs-on: ubuntu-latest @@ -16,7 +16,7 @@ jobs: token: ${{ secrets.SPONSORS_PAT }} file: 'README.md' organization: true - template: '{{{ name }}} ' + template: '{{{ name }}} ' - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@v4 diff --git a/.github/workflows/templates/commits.md b/.github/workflows/templates/commits.md new file mode 100644 index 000000000..8cfe64bef --- /dev/null +++ b/.github/workflows/templates/commits.md @@ -0,0 +1,8 @@ +{{ range .data.repository.object.history.nodes }} + {{ $headline := .messageHeadline }} + {{- range .associatedPullRequests.nodes }} +* {{$headline}} + {{- else }} +* {{$headline}} ({{ slice .oid 0 7 }}) + {{- end }} +{{- end }} diff --git a/.github/workflows/translators.yml b/.github/workflows/translators.yml new file mode 100644 index 000000000..11046de4d --- /dev/null +++ b/.github/workflows/translators.yml @@ -0,0 +1,47 @@ +name: Update Translators List + +on: + schedule: + - cron: '0 0 * * 0' # Run weekly on Sunday at 00:00 + workflow_dispatch: + +jobs: + update-translators: + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Update README.md + shell: pwsh + run: | + $headers = @{ + "Authorization" = "Bearer ${{ secrets.CROWDIN_API_TOKEN }}" + } + + $response = Invoke-RestMethod ` + -Uri "https://api.crowdin.com/api/v2/projects/407880/members" ` + -Headers $headers + $translators = $response.data.data + + $html = ($translators | Sort-Object -Property @{Expression={ $_.fullName }}, @{Expression={ $_.username }} | ForEach-Object { + $translator = $_ + $translatorName = if ($translator.fullName) { $translator.fullName } else { $translator.username } + $avatarUrl = $translator.avatarUrl + "`"$translatorName`"" + }) -join " " + $html = "`n$html`n" + + $readmeContent = Get-Content -Path .\README.md -Raw + $readmeContent -match "(?[\s\S]*?)[\s\S]*?(?[\s\S]*)" + $readmeContent = $matches["sof"] + $html + $matches["eof"] + + Set-Content -Path .\README.md -Value $readmeContent + + - name: Commit changes + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add README.md + git commit -m "Update translators via GitHub Actions" || exit 0 + git push origin master diff --git a/CHANGELOG.md b/CHANGELOG.md index bd8f9f27f..46a3067d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## x.x.x.x +- Added support for StartAllBack multitaskbar notification area (thanks @Simplestas!) +- Fixed various process lifecycle trigger bugs (thanks @spacechase0!) +- Fixed an issue with EarTrumpet tooltips not updating in some scenarios (thanks @Tester798!) +- Fixed an issue with EarTrumpet Actions getting disconnected with updated packaged applications +- Fixed an issue where volume mixer entries were not properly differentiated +- Added setting to show the full mixer window on startup +- Added support for adjusting volumes by 10% in one step from the flyout when the `Ctrl` key is pressed in combination with `Right`/`Left` or `+`/`-` (thanks @ryanspain!) +- Migrated EarTrumpet from .NET Framework 4.6.2 to .NET 8.0 +- Fixed an issue with EarTrumpet packaging that affected the languages shown in the Microsoft Store +- Dropped support for some end-of-life versions of Windows 10 +- Added a help dialog to assist when EarTrumpet can't start automatically due to missing Windows policies +- Fixed the duplicate Windows Legacy > Volume Mixer entries that appear on Windows 11 +- Updated translations for Turkish, Russian, Croatian, German, Portuguese, Japanese, Swedish, Catalan, Korean, Hebrew, and Spanish +- Fixed an issue with EarTrumpet Actions not recording packaged application IDs correctly +- Added new and improved logarithmic volume implementation (thanks @Dwscdv3!) +- Added logarithmic peak metering (thanks @Dwscdv3!) +- Added adjustable logarithmic slider range (thanks @Dwscdv3!) + ## 2.3.0.0 - Added setting to turn on/off ability to change volume with the scroll wheel anywhere (thanks @Tester798!) - Added setting to turn on/off ability to change volume with the scroll wheel when hovering over the EarTrumpet icon (thanks @Tester798!) @@ -118,7 +137,7 @@ - Added text to notification area icon tooltip to indicate mute state - Re-added flyout window shadow and borders - Added additional telemetry points -- Removed Arabic, Hungarian, Korean, Norwegian Bokm�l, Portuguese, Romanian, and Turkish until we complete localization +- Removed Arabic, Hungarian, Korean, Norwegian Bokmål, Portuguese, Romanian, and Turkish until we complete localization - Additional bugfixes ## 2.0.8.0 diff --git a/COMPILING.md b/COMPILING.md index ab0b020e1..4236e8ea2 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -1,18 +1,19 @@ # Compiling EarTrumpet ## Requirements -* [Visual Studio 2017](https://visualstudio.microsoft.com/vs/community/) (or newer) +* [Visual Studio 2022](https://visualstudio.microsoft.com/vs/community/) (or newer) * [Git for Windows](https://git-scm.com/download/win) -* [Windows 10 Anniversary Update](https://blogs.windows.com/windowsexperience/2016/08/02/how-to-get-the-windows-10-anniversary-update/#GD97Eq04wJA7S4P7.97) (or newer) -* [.NET Framework 4.6.2 Developer Pack](https://www.microsoft.com/net/download/thank-you/net462-developer-pack) -* [Windows 10 SDK (10.0.14393.0)](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive) +* [Windows 10 November 2021 Update](https://learn.microsoft.com/windows/whats-new/whats-new-windows-10-version-21h2) (or newer) +* [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) +* [Windows SDK for Windows 11 (10.0.26100.4188)](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive) (or newer) + ## Step-by-step -1. Install Visual Studio 2017 with the `.NET desktop development` and `Universal Windows Platform development` workloads. -2. Install the `Windows 10 SDK (10.0.14393.0)` SDK. -3. Install the .NET Framework 4.6.2 Developer Pack. +1. Install Visual Studio 2022 with the `.NET desktop development` and `Universal Windows Platform development` workloads. +2. Install the Windows SDK. +3. Install the .NET 8 SDK. 4. Install Git for Windows. 5. Clone the EarTrumpet repository (`git clone https://github.com/File-New-Project/EarTrumpet.git`). -6. Open `EarTrumpet.vs15.sln` in Visual Studio. -7. Change the target platform to `x86` and build the `EarTrumpet.Package` project. +6. Open `EarTrumpet.sln` in Visual Studio. +7. Change the target platform as needed and build the `EarTrumpet.Package` project. 8. You're done. If you plan on submitting your changes to us, please review the [Contributing guide](https://github.com/File-New-Project/EarTrumpet/blob/master/CONTRIBUTING.md) first. diff --git a/EarTrumpet.ColorTool/EarTrumpet.ColorTool.csproj b/EarTrumpet.ColorTool/EarTrumpet.ColorTool.csproj index b0c2c11f2..585f1445b 100644 --- a/EarTrumpet.ColorTool/EarTrumpet.ColorTool.csproj +++ b/EarTrumpet.ColorTool/EarTrumpet.ColorTool.csproj @@ -9,7 +9,6 @@ EarTrumpet.ColorTool EarTrumpet.ColorTool v4.6.2 - MinimumRecommendedRules.ruleset true x86 prompt diff --git a/EarTrumpet.Package/Directory.Build.props b/EarTrumpet.Package/Directory.Build.props new file mode 100644 index 000000000..e7d63e32c --- /dev/null +++ b/EarTrumpet.Package/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + $(NoWarn);NU1701 + + + \ No newline at end of file diff --git a/EarTrumpet.Package/EarTrumpet.Package.wapproj b/EarTrumpet.Package/EarTrumpet.Package.wapproj index 1d7fa6f5a..0823a3537 100644 --- a/EarTrumpet.Package/EarTrumpet.Package.wapproj +++ b/EarTrumpet.Package/EarTrumpet.Package.wapproj @@ -2,8 +2,17 @@ 15.0 + MSB4011;$(NoWarn) + + Debug + x64 + + + Release + x64 + Debug x86 @@ -12,6 +21,14 @@ Release x86 + + Debug + ARM64 + + + Release + ARM64 + $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ @@ -19,32 +36,52 @@ ea5510ed-f014-4587-a505-64c59d5b2627 - 10.0.14393.0 - 10.0.14393.0 + 10.0.26100.0 + 10.0.19041.0 en-US EarTrumpet.Package_StoreKey.pfx ..\EarTrumpet\EarTrumpet.csproj False False x86 - 1 + 0 OnApplicationRun false - 3E10717CB4915430F2920FECAC4F20570137B9FC + NU1702 + Always + SHA256 + False + 0 - + + Always + + Always Always + + Always + + + Always + + + Always + Designer - + + Properties\PublishProfiles\x86.pubxml + Properties\PublishProfiles\x64.pubxml + Properties\PublishProfiles\ARM64.pubxml + @@ -98,6 +135,53 @@ + + + + + + + + + + + + + + + + + + + + + + $([System.Text.RegularExpressions.Regex]::Match('%(Filename)', 'Resources\.(.+)').Groups[1].Value) + + + + Language=en-us;@(EarTrumpetReferencedResxFiles->'%(LanguageTag)', ';')|DXFeatureLevel=DX9|Scale=100|Scale=125|Scale=150|Scale=200|Scale=400|TargetSize=16|TargetSize=24|TargetSize=32|TargetSize=48|TargetSize=256|AlternateForm=UNPLATED|Contrast=standard|HomeRegion=001|LayoutDirection=LTR|Configuration=|Platform=UAP + + + + + %(FinalAppxManifest.Identity) + + <Resource Language="en-us" /> + @(EarTrumpetReferencedResxFiles->'<Resource Language="%(LanguageTag)" />', '') + + + + \ No newline at end of file diff --git a/EarTrumpet.Package/Package.appxmanifest b/EarTrumpet.Package/Package.appxmanifest index 70d269b16..3cf70c606 100644 --- a/EarTrumpet.Package/Package.appxmanifest +++ b/EarTrumpet.Package/Package.appxmanifest @@ -1,38 +1,99 @@  - - - - EarTrumpet - File-New-Project - Assets\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + EarTrumpet + File-New-Project + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EarTrumpet.sln b/EarTrumpet.sln new file mode 100644 index 000000000..873269e34 --- /dev/null +++ b/EarTrumpet.sln @@ -0,0 +1,96 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8F56ECD6-00C6-4C4A-AB1F-C01ED14EF53B}" + ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md + COMPILING.md = COMPILING.md + CONTRIBUTING.md = CONTRIBUTING.md + GitVersion.yml = GitVersion.yml + LICENSE = LICENSE + PRIVACY.md = PRIVACY.md + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EarTrumpet", "EarTrumpet\EarTrumpet.csproj", "{DF42BC76-DFB5-41C2-9308-1A1F13A66A38}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Chocolatey", "Chocolatey", "{EF7F607E-FD43-4BA4-B32B-128EF2944A26}" + ProjectSection(SolutionItems) = preProject + .chocolatey\tools\chocolateybeforemodify.ps1 = .chocolatey\tools\chocolateybeforemodify.ps1 + .chocolatey\tools\chocolateyinstall.ps1 = .chocolatey\tools\chocolateyinstall.ps1 + .chocolatey\tools\chocolateyuninstall.ps1 = .chocolatey\tools\chocolateyuninstall.ps1 + EndProjectSection +EndProject +Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "EarTrumpet.Package", "EarTrumpet.Package\EarTrumpet.Package.wapproj", "{EA5510ED-F014-4587-A505-64C59D5B2627}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + VSDebug|ARM64 = VSDebug|ARM64 + VSDebug|x64 = VSDebug|x64 + VSDebug|x86 = VSDebug|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Debug|ARM64.Build.0 = Debug|ARM64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Debug|x64.ActiveCfg = Debug|x64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Debug|x64.Build.0 = Debug|x64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Debug|x86.ActiveCfg = Debug|x86 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Debug|x86.Build.0 = Debug|x86 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Release|ARM64.ActiveCfg = Release|ARM64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Release|ARM64.Build.0 = Release|ARM64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Release|x64.ActiveCfg = Release|x64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Release|x64.Build.0 = Release|x64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Release|x86.ActiveCfg = Release|x86 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.Release|x86.Build.0 = Release|x86 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.VSDebug|ARM64.ActiveCfg = VSDebug|ARM64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.VSDebug|ARM64.Build.0 = VSDebug|ARM64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.VSDebug|x64.ActiveCfg = VSDebug|x64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.VSDebug|x64.Build.0 = VSDebug|x64 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.VSDebug|x86.ActiveCfg = VSDebug|x86 + {DF42BC76-DFB5-41C2-9308-1A1F13A66A38}.VSDebug|x86.Build.0 = VSDebug|x86 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|ARM64.Build.0 = Debug|ARM64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|x64.ActiveCfg = Debug|x64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|x64.Build.0 = Debug|x64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|x64.Deploy.0 = Debug|x64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|x86.ActiveCfg = Debug|x86 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|x86.Build.0 = Debug|x86 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|x86.Deploy.0 = Debug|x86 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|ARM64.ActiveCfg = Release|ARM64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|ARM64.Build.0 = Release|ARM64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|ARM64.Deploy.0 = Release|ARM64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|x64.ActiveCfg = Release|x64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|x64.Build.0 = Release|x64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|x64.Deploy.0 = Release|x64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|x86.ActiveCfg = Release|x86 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|x86.Build.0 = Release|x86 + {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|x86.Deploy.0 = Release|x86 + {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|ARM64.ActiveCfg = Debug|ARM64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|ARM64.Build.0 = Debug|ARM64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|ARM64.Deploy.0 = Debug|ARM64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x64.ActiveCfg = Debug|x64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x64.Build.0 = Debug|x64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x64.Deploy.0 = Debug|x64 + {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.ActiveCfg = Debug|x86 + {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.Build.0 = Debug|x86 + {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.Deploy.0 = Debug|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {EF7F607E-FD43-4BA4-B32B-128EF2944A26} = {8F56ECD6-00C6-4C4A-AB1F-C01ED14EF53B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {228AD06A-DAC2-406B-8F21-3E7B980B1B37} + EndGlobalSection +EndGlobal diff --git a/EarTrumpet.vs15.sln b/EarTrumpet.vs15.sln deleted file mode 100644 index 18536d925..000000000 --- a/EarTrumpet.vs15.sln +++ /dev/null @@ -1,58 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29306.81 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EarTrumpet", "EarTrumpet\EarTrumpet.csproj", "{BA3C7B42-84B0-468C-8640-217E2A24CF81}" -EndProject -Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "EarTrumpet.Package", "EarTrumpet.Package\EarTrumpet.Package.wapproj", "{EA5510ED-F014-4587-A505-64C59D5B2627}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8F56ECD6-00C6-4C4A-AB1F-C01ED14EF53B}" - ProjectSection(SolutionItems) = preProject - CHANGELOG.md = CHANGELOG.md - COMPILING.md = COMPILING.md - CONTRIBUTING.md = CONTRIBUTING.md - GitVersion.yml = GitVersion.yml - LICENSE = LICENSE - PRIVACY.md = PRIVACY.md - README.md = README.md - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EarTrumpet.ColorTool", "EarTrumpet.ColorTool\EarTrumpet.ColorTool.csproj", "{E5B2C3B5-4CED-4C82-8A82-D290A7E0FC5D}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 - VSDebug|x86 = VSDebug|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BA3C7B42-84B0-468C-8640-217E2A24CF81}.Debug|x86.ActiveCfg = Debug|x86 - {BA3C7B42-84B0-468C-8640-217E2A24CF81}.Debug|x86.Build.0 = Debug|x86 - {BA3C7B42-84B0-468C-8640-217E2A24CF81}.Release|x86.ActiveCfg = Release|x86 - {BA3C7B42-84B0-468C-8640-217E2A24CF81}.Release|x86.Build.0 = Release|x86 - {BA3C7B42-84B0-468C-8640-217E2A24CF81}.VSDebug|x86.ActiveCfg = VSDebug|x86 - {BA3C7B42-84B0-468C-8640-217E2A24CF81}.VSDebug|x86.Build.0 = VSDebug|x86 - {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|x86.ActiveCfg = Debug|x86 - {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|x86.Build.0 = Debug|x86 - {EA5510ED-F014-4587-A505-64C59D5B2627}.Debug|x86.Deploy.0 = Debug|x86 - {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|x86.ActiveCfg = Release|x86 - {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|x86.Build.0 = Release|x86 - {EA5510ED-F014-4587-A505-64C59D5B2627}.Release|x86.Deploy.0 = Release|x86 - {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.ActiveCfg = Debug|x86 - {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.Build.0 = Debug|x86 - {EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.Deploy.0 = Debug|x86 - {E5B2C3B5-4CED-4C82-8A82-D290A7E0FC5D}.Debug|x86.ActiveCfg = Debug|x86 - {E5B2C3B5-4CED-4C82-8A82-D290A7E0FC5D}.Debug|x86.Build.0 = Debug|x86 - {E5B2C3B5-4CED-4C82-8A82-D290A7E0FC5D}.Release|x86.ActiveCfg = Release|x86 - {E5B2C3B5-4CED-4C82-8A82-D290A7E0FC5D}.Release|x86.Build.0 = Release|x86 - {E5B2C3B5-4CED-4C82-8A82-D290A7E0FC5D}.VSDebug|x86.ActiveCfg = Debug|x86 - {E5B2C3B5-4CED-4C82-8A82-D290A7E0FC5D}.VSDebug|x86.Build.0 = Debug|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {228AD06A-DAC2-406B-8F21-3E7B980B1B37} - EndGlobalSection -EndGlobal diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/AddonResources.xaml b/EarTrumpet/Addons/EarTrumpet.Actions/AddonResources.xaml index 482df820d..17a97f562 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/AddonResources.xaml +++ b/EarTrumpet/Addons/EarTrumpet.Actions/AddonResources.xaml @@ -317,8 +317,9 @@ ((LinkedTextBlock)d).DataItemChanged(); + private static void DataItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((LinkedTextBlock)d).DataItemChanged(); - public string FormatText - { - get { return (string)this.GetValue(FormatTextProperty); } - set { this.SetValue(FormatTextProperty, value); } - } - public static readonly DependencyProperty FormatTextProperty = DependencyProperty.Register( - "FormatText", typeof(string), typeof(LinkedTextBlock), new PropertyMetadata("", new PropertyChangedCallback(FormatTextChanged))); + public string FormatText + { + get { return (string)this.GetValue(FormatTextProperty); } + set { this.SetValue(FormatTextProperty, value); } + } + public static readonly DependencyProperty FormatTextProperty = DependencyProperty.Register( + "FormatText", typeof(string), typeof(LinkedTextBlock), new PropertyMetadata("", new PropertyChangedCallback(FormatTextChanged))); - private static void FormatTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((LinkedTextBlock)d).PropertiesChanged(); + private static void FormatTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((LinkedTextBlock)d).PropertiesChanged(); - public Style HyperlinkStyle - { - get { return (Style)this.GetValue(HyperlinkStyleProperty); } - set { this.SetValue(HyperlinkStyleProperty, value); } - } - public static readonly DependencyProperty HyperlinkStyleProperty = DependencyProperty.Register( - "HyperlinkStyle", typeof(Style), typeof(LinkedTextBlock), new PropertyMetadata(null, new PropertyChangedCallback(HyperlinkStyleChanged))); + public Style HyperlinkStyle + { + get { return (Style)this.GetValue(HyperlinkStyleProperty); } + set { this.SetValue(HyperlinkStyleProperty, value); } + } + public static readonly DependencyProperty HyperlinkStyleProperty = DependencyProperty.Register( + "HyperlinkStyle", typeof(Style), typeof(LinkedTextBlock), new PropertyMetadata(null, new PropertyChangedCallback(HyperlinkStyleChanged))); + + public Style RunStyle + { + get { return (Style)this.GetValue(RunStyleProperty); } + set { this.SetValue(RunStyleProperty, value); } + } + public static readonly DependencyProperty RunStyleProperty = DependencyProperty.Register( + "RunStyle", typeof(Style), typeof(LinkedTextBlock), new PropertyMetadata(null, new PropertyChangedCallback(RunStyleChanged))); - public Style RunStyle - { - get { return (Style)this.GetValue(RunStyleProperty); } - set { this.SetValue(RunStyleProperty, value); } - } - public static readonly DependencyProperty RunStyleProperty = DependencyProperty.Register( - "RunStyle", typeof(Style), typeof(LinkedTextBlock), new PropertyMetadata(null, new PropertyChangedCallback(RunStyleChanged))); + private static void HyperlinkStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((LinkedTextBlock)d).PropertiesChanged(); + private static void RunStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((LinkedTextBlock)d).PropertiesChanged(); - private static void HyperlinkStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((LinkedTextBlock)d).PropertiesChanged(); - private static void RunStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((LinkedTextBlock)d).PropertiesChanged(); + private void DataItemChanged() + { + ((INotifyPropertyChanged)DataItem).PropertyChanged += (s, e) => PropertiesChanged(); + } + + private void PropertiesChanged() + { + this.Inlines.Clear(); - private void DataItemChanged() + if (ContextMenu != null && Popup != null) { - ((INotifyPropertyChanged)DataItem).PropertyChanged += (s, e) => PropertiesChanged(); + ContextMenu.Loaded += ContextMenu_Loaded; + Popup.Loaded += Popup_Loaded; } - private void PropertiesChanged() + ReadLinksAndText(FormatText, (text, isLink) => { - this.Inlines.Clear(); - - if (ContextMenu != null && Popup != null) + text = text.Trim(); + if (!isLink) { - ContextMenu.Loaded += ContextMenu_Loaded; - Popup.Loaded += Popup_Loaded; + var run = new Run(text) + { + Style = RunStyle + }; + this.Inlines.Add(run); } - - ReadLinksAndText(FormatText, (text, isLink) => + else { - text = text.Trim(); - if (!isLink) + var resolvedPropertyObject = DataItem.GetType().GetProperty(text).GetValue(DataItem, null); + var link = new Hyperlink(new Run(resolvedPropertyObject.ToString())) { - var run = new Run(text); - run.Style = RunStyle; - this.Inlines.Add(run); - } - else + NavigateUri = new Uri("about:none"), + Style = HyperlinkStyle + }; + + link.RequestNavigate += (s, e) => { - var resolvedPropertyObject = DataItem.GetType().GetProperty(text).GetValue(DataItem, null); - var link = new Hyperlink(new Run(resolvedPropertyObject.ToString())); - link.NavigateUri = new Uri("about:none"); - link.Style = HyperlinkStyle; + // Take focus now so that we get return focus when the user leaves. + link.Focus(); + var dpiX = Window.GetWindow(this).DpiX(); + var dpiY = Window.GetWindow(this).DpiY(); - link.RequestNavigate += (s, e) => + if (resolvedPropertyObject is IOptionViewModel) { - // Take focus now so that we get return focus when the user leaves. - link.Focus(); - var dpiX = Window.GetWindow(this).DpiX(); - var dpiY = Window.GetWindow(this).DpiY(); - - if (resolvedPropertyObject is IOptionViewModel) + ContextMenu2.Opacity = 0; + ContextMenu2.ItemsSource = GetContextMenuFromOptionViewModel((IOptionViewModel)resolvedPropertyObject).OrderBy(menu => menu.DisplayName); + ContextMenu2.UpdateLayout(); + ContextMenu2.IsOpen = true; + ContextMenu2.Dispatcher.BeginInvoke((Action)(() => { - ContextMenu2.Opacity = 0; - ContextMenu2.ItemsSource = GetContextMenuFromOptionViewModel((IOptionViewModel)resolvedPropertyObject).OrderBy(menu => menu.DisplayName); - ContextMenu2.UpdateLayout(); - ContextMenu2.IsOpen = true; - ContextMenu2.Dispatcher.BeginInvoke((Action)(() => - { - ContextMenu2.Opacity = 1; - ContextMenu2.HorizontalOffset = -1 * (ContextMenu2.RenderSize.Width / dpiX) / 2; - ContextMenu2.VerticalOffset = -1 * (ContextMenu2.RenderSize.Height / dpiY) / 2; - ContextMenu2.Focus(); - }), - System.Windows.Threading.DispatcherPriority.DataBind, null); - } - else + ContextMenu2.Opacity = 1; + ContextMenu2.HorizontalOffset = -1 * (ContextMenu2.RenderSize.Width / dpiX) / 2; + ContextMenu2.VerticalOffset = -1 * (ContextMenu2.RenderSize.Height / dpiY) / 2; + ContextMenu2.Focus(); + }), + System.Windows.Threading.DispatcherPriority.DataBind, null); + } + else + { + Popup.PreviewKeyDown += (_, ee) => { - Popup.PreviewKeyDown += (_, ee) => + if (ee.Key == Key.Escape) { - if (ee.Key == Key.Escape) - { - Popup.IsOpen = false; - } - }; - Popup.Opacity = 0; - Popup.DataContext = resolvedPropertyObject; - Popup.UpdateLayout(); - Popup.Child.UpdateLayout(); - Popup.IsOpen = true; - Popup.Dispatcher.BeginInvoke((Action)(() => - { - Popup.Opacity = 1; - Popup.HorizontalOffset = -1 * (Popup.Child.RenderSize.Width / dpiX) / 2; - Popup.VerticalOffset = -1 * (Popup.Child.RenderSize.Height / dpiY) / 2; - Keyboard.Focus(Popup.Child.FindVisualChild()); - }), - System.Windows.Threading.DispatcherPriority.DataBind, null); - } - }; - this.Inlines.Add(link); - } - this.Inlines.Add(new Run(" ")); - }); - } + Popup.IsOpen = false; + } + }; + Popup.Opacity = 0; + Popup.DataContext = resolvedPropertyObject; + Popup.UpdateLayout(); + Popup.Child.UpdateLayout(); + Popup.IsOpen = true; + Popup.Dispatcher.BeginInvoke((Action)(() => + { + Popup.Opacity = 1; + Popup.HorizontalOffset = -1 * (Popup.Child.RenderSize.Width / dpiX) / 2; + Popup.VerticalOffset = -1 * (Popup.Child.RenderSize.Height / dpiY) / 2; + Keyboard.Focus(Popup.Child.FindVisualChild()); + }), + System.Windows.Threading.DispatcherPriority.DataBind, null); + } + }; + this.Inlines.Add(link); + } + this.Inlines.Add(new Run(" ")); + }); + } - private void Popup_Loaded(object sender, RoutedEventArgs e) - { - Popup.UpdateLayout(); - Popup.Child.UpdateLayout(); - Popup.HorizontalOffset = -1 * Popup.Child.RenderSize.Width / 2; - Popup.VerticalOffset = -1 * Popup.Child.RenderSize.Height / 2; - } + private void Popup_Loaded(object sender, RoutedEventArgs e) + { + Popup.UpdateLayout(); + Popup.Child.UpdateLayout(); + Popup.HorizontalOffset = -1 * Popup.Child.RenderSize.Width / 2; + Popup.VerticalOffset = -1 * Popup.Child.RenderSize.Height / 2; + } - private void ContextMenu_Loaded(object sender, RoutedEventArgs e) - { - ContextMenu2.UpdateLayout(); - ContextMenu2.HorizontalOffset = -1 * ContextMenu2.RenderSize.Width / 2; - ContextMenu2.VerticalOffset = -1 * ContextMenu2.RenderSize.Height / 2; - } + private void ContextMenu_Loaded(object sender, RoutedEventArgs e) + { + ContextMenu2.UpdateLayout(); + ContextMenu2.HorizontalOffset = -1 * ContextMenu2.RenderSize.Width / 2; + ContextMenu2.VerticalOffset = -1 * ContextMenu2.RenderSize.Height / 2; + } - private List GetContextMenuFromOptionViewModel(IOptionViewModel options) + private static List GetContextMenuFromOptionViewModel(IOptionViewModel options) + { + return options.All.Select(item => new ContextMenuItem { - return options.All.Select(item => new ContextMenuItem - { - DisplayName = item.DisplayName, - IsChecked = (item == options.Selected), - Command = new RelayCommand(() => options.Selected = item), - }).ToList(); - } + DisplayName = item.DisplayName, + IsChecked = (item == options.Selected), + Command = new RelayCommand(() => options.Selected = item), + }).ToList(); + } - private void ReadLinksAndText(string text, Action callback) + private static void ReadLinksAndText(string text, Action callback) + { + var ptr = 0; + for (var i = 0; i < text.Length; i++) { - int ptr = 0; - for (int i = 0; i < text.Length; i++) + if (text[i] == '{') { - if (text[i] == '{') + if (i > 0) { - if (i > 0) - { - callback(text.Substring(ptr, i - 1 - ptr), false); - } - ptr = i + 1; - } - else if (text[i] == '}') - { - callback(text.Substring(ptr, i - ptr), true); - ptr = i + 1; + callback(text.Substring(ptr, i - 1 - ptr), false); } + ptr = i + 1; } - - if (ptr < text.Length - 1) + else if (text[i] == '}') { - callback(text.Substring(ptr, text.Length - ptr), false); + callback(text.Substring(ptr, i - ptr), true); + ptr = i + 1; } } + + if (ptr < text.Length - 1) + { + callback(text.Substring(ptr, text.Length - ptr), false); + } } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/Controls/MenuButton.cs b/EarTrumpet/Addons/EarTrumpet.Actions/Controls/MenuButton.cs index 88f630043..48bdedace 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/Controls/MenuButton.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/Controls/MenuButton.cs @@ -1,27 +1,26 @@ using System.Windows.Controls; using System.Windows.Controls.Primitives; -namespace EarTrumpet.Actions.Controls +namespace EarTrumpet.Actions.Controls; + +public class MenuButton : Button { - public class MenuButton : Button + public MenuButton() { - public MenuButton() - { - Click += Button_Click; - } + Click += Button_Click; + } - private void Button_Click(object sender, System.Windows.RoutedEventArgs e) - { - var btn = (Button)sender; + private void Button_Click(object sender, System.Windows.RoutedEventArgs e) + { + var btn = (Button)sender; - btn.ContextMenu.Opened += (_, __) => - { - ((Popup)btn.ContextMenu.Parent).PopupAnimation = PopupAnimation.None; - }; + btn.ContextMenu.Opened += (_, __) => + { + ((Popup)btn.ContextMenu.Parent).PopupAnimation = PopupAnimation.None; + }; - btn.ContextMenu.PlacementTarget = btn; - btn.ContextMenu.Placement = PlacementMode.Bottom; - btn.ContextMenu.IsOpen = true; - } + btn.ContextMenu.PlacementTarget = btn; + btn.ContextMenu.Placement = PlacementMode.Bottom; + btn.ContextMenu.IsOpen = true; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/AudioAppEventKind.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/AudioAppEventKind.cs index 0ef769bf5..12fcfdd12 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/AudioAppEventKind.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/AudioAppEventKind.cs @@ -1,12 +1,11 @@ -namespace EarTrumpet.Actions.DataModel.Enum +namespace EarTrumpet.Actions.DataModel.Enum; + +public enum AudioAppEventKind { - public enum AudioAppEventKind - { - Added, - Removed, - PlayingSound, - NotPlayingSound, - Muted, - Unmuted, - } + Added, + Removed, + PlayingSound, + NotPlayingSound, + Muted, + Unmuted, } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/AudioDeviceEventKind.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/AudioDeviceEventKind.cs index 87241790d..95cca187d 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/AudioDeviceEventKind.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/AudioDeviceEventKind.cs @@ -1,10 +1,9 @@ -namespace EarTrumpet.Actions.DataModel.Enum +namespace EarTrumpet.Actions.DataModel.Enum; + +public enum AudioDeviceEventKind { - public enum AudioDeviceEventKind - { - Added, - Removed, - BecomingDefault, - LeavingDefault, - } + Added, + Removed, + BecomingDefault, + LeavingDefault, } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/BoolValue.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/BoolValue.cs index c5f0a3fa9..4f0d186ed 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/BoolValue.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/BoolValue.cs @@ -1,8 +1,7 @@ -namespace EarTrumpet.Actions.DataModel.Enum +namespace EarTrumpet.Actions.DataModel.Enum; + +public enum BoolValue { - public enum BoolValue - { - True, - False, - } + True, + False, } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ComparisonBoolKind.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ComparisonBoolKind.cs index 4ab31bdeb..3b58dedbe 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ComparisonBoolKind.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ComparisonBoolKind.cs @@ -1,8 +1,7 @@ -namespace EarTrumpet.Actions.DataModel.Enum +namespace EarTrumpet.Actions.DataModel.Enum; + +public enum ComparisonBoolKind { - public enum ComparisonBoolKind - { - Is, - IsNot, - } + Is, + IsNot, } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/EarTrumpetEventKind.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/EarTrumpetEventKind.cs index 5e54ec3db..a24ece309 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/EarTrumpetEventKind.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/EarTrumpetEventKind.cs @@ -1,8 +1,7 @@ -namespace EarTrumpet.Actions.DataModel.Enum +namespace EarTrumpet.Actions.DataModel.Enum; + +public enum EarTrumpetEventKind { - public enum EarTrumpetEventKind - { - Startup, - Shutdown, - }; -} + Startup, + Shutdown, +}; diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/MuteKind.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/MuteKind.cs index e5ddec5b4..2c065fc64 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/MuteKind.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/MuteKind.cs @@ -1,9 +1,8 @@ -namespace EarTrumpet.Actions.DataModel.Enum +namespace EarTrumpet.Actions.DataModel.Enum; + +public enum MuteKind { - public enum MuteKind - { - Mute, - Unmute, - ToggleMute, - } + Mute, + Unmute, + ToggleMute, } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ProcessEventKind.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ProcessEventKind.cs index 319422b23..4f99534fe 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ProcessEventKind.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ProcessEventKind.cs @@ -1,8 +1,7 @@ -namespace EarTrumpet.Actions.DataModel.Enum +namespace EarTrumpet.Actions.DataModel.Enum; + +public enum ProcessEventKind { - public enum ProcessEventKind - { - Start, - Stop, - } + Start, + Stop, } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ProcessStateKind.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ProcessStateKind.cs index fa75a24d9..7d2bd0986 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ProcessStateKind.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ProcessStateKind.cs @@ -1,8 +1,7 @@ -namespace EarTrumpet.Actions.DataModel.Enum +namespace EarTrumpet.Actions.DataModel.Enum; + +public enum ProcessStateKind { - public enum ProcessStateKind - { - Running, - NotRunning - } + Running, + NotRunning } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/SetVolumeKind.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/SetVolumeKind.cs index c69d87f2b..e941b4fc4 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/SetVolumeKind.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/SetVolumeKind.cs @@ -1,9 +1,8 @@ -namespace EarTrumpet.Actions.DataModel.Enum +namespace EarTrumpet.Actions.DataModel.Enum; + +public enum SetVolumeKind { - public enum SetVolumeKind - { - Set, - Increment, - Decrement, - } + Set, + Increment, + Decrement, } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/VolumeUnit.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/VolumeUnit.cs new file mode 100644 index 000000000..e311ed5a3 --- /dev/null +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/VolumeUnit.cs @@ -0,0 +1,7 @@ +namespace EarTrumpet.Actions.DataModel.Enum; + +public enum VolumeUnit +{ + Percentage, + Decibel, +} diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithApp.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithApp.cs index b8f3abefd..62fcd1009 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithApp.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithApp.cs @@ -1,9 +1,8 @@ using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.DataModel +namespace EarTrumpet.Actions.DataModel; + +internal interface IPartWithApp { - interface IPartWithApp - { - AppRef App { get; set; } - } + AppRef App { get; set; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithDevice.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithDevice.cs index 81f65808c..faf75e259 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithDevice.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithDevice.cs @@ -1,9 +1,8 @@ using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.DataModel +namespace EarTrumpet.Actions.DataModel; + +public interface IPartWithDevice { - public interface IPartWithDevice - { - Device Device { get; set; } - } + Device Device { get; set; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithText.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithText.cs index abf7e43cd..2ad0b49d1 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithText.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithText.cs @@ -1,7 +1,6 @@ -namespace EarTrumpet.Actions.DataModel +namespace EarTrumpet.Actions.DataModel; + +internal interface IPartWithText { - interface IPartWithText - { - string Text { get; set; } - } + string Text { get; set; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithVolume.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithVolume.cs index aae5e4b56..74eca4234 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithVolume.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithVolume.cs @@ -1,7 +1,10 @@ -namespace EarTrumpet.Actions.DataModel +using EarTrumpet.Actions.DataModel.Enum; + +namespace EarTrumpet.Actions.DataModel; + +public interface IPartWithVolume { - public interface IPartWithVolume - { - double Volume { get; set; } - } + double Volume { get; set; } + VolumeUnit Unit { get; set; } + SetVolumeKind Option { get; set; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/LocalVariablesContainer.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/LocalVariablesContainer.cs index 3d9ab8587..c7448e2fe 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/LocalVariablesContainer.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/LocalVariablesContainer.cs @@ -1,21 +1,20 @@ using EarTrumpet.DataModel.Storage; -namespace EarTrumpet.Actions.DataModel +namespace EarTrumpet.Actions.DataModel; + +public class LocalVariablesContainer { - public class LocalVariablesContainer + public bool this[string key] { - public bool this[string key] - { - get => _settings.Get($"{s_localVariablePrefix}{key}", false); - set => _settings.Set($"{s_localVariablePrefix}{key}", value); - } + get => _settings.Get($"{s_localVariablePrefix}{key}", false); + set => _settings.Set($"{s_localVariablePrefix}{key}", value); + } - private const string s_localVariablePrefix = "LocalVariable."; - private readonly ISettingsBag _settings; + private const string s_localVariablePrefix = "LocalVariable."; + private readonly ISettingsBag _settings; - public LocalVariablesContainer(ISettingsBag settings) - { - _settings = settings; - } + public LocalVariablesContainer(ISettingsBag settings) + { + _settings = settings; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Part.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Part.cs index 0a02c809a..e1290201a 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Part.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Part.cs @@ -1,4 +1,3 @@ -namespace EarTrumpet.Actions.DataModel -{ - public abstract class Part { } -} +namespace EarTrumpet.Actions.DataModel; + +public abstract class Part { } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/ProcessWatcher.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/ProcessWatcher.cs index 1ecf313c9..7cf40f6d6 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/ProcessWatcher.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/ProcessWatcher.cs @@ -1,150 +1,161 @@ -using EarTrumpet.Interop; -using EarTrumpet.Actions.Interop.Helpers; -using System; +using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading; +using EarTrumpet.Actions.Interop.Helpers; +using Windows.Win32; -namespace EarTrumpet.Actions.DataModel +namespace EarTrumpet.Actions.DataModel; + +public sealed class ProcessWatcher : IDisposable { - public class ProcessWatcher + private class ProcessInfo { - class ProcessInfo - { - public List Windows = new List(); - } + public List Windows = []; + } - class WatcherInfo - { - public List StartCallbacks = new List(); - public List StopCallbacks = new List(); - public Dictionary RunningProcesses = new Dictionary(); - } + private class WatcherInfo + { + public List StartCallbacks = []; + public List StopCallbacks = []; + public Dictionary RunningProcesses = []; + } - public static ProcessWatcher Current { get; } = new ProcessWatcher(); + public static ProcessWatcher Current { get; } = new ProcessWatcher(); - WindowWatcher _watcher = new WindowWatcher(); - Dictionary _info = new Dictionary(); + private readonly WindowWatcher _watcher = new(); + private Dictionary _info = []; + + public ProcessWatcher() + { + _watcher.WindowCreated += OnWindowCreated; + } - public ProcessWatcher() + // Used only by the condition processor, so we use realtime data only. + public static bool IsRunning(string procName) + { + try { - _watcher.WindowCreated += OnWindowCreated; + return Process.GetProcessesByName(procName).Length != 0; } - - // Used only by the condition processor, so we use realtime data only. - public bool IsRunning(string procName) + catch (Exception ex) { - try - { - return Process.GetProcessesByName(procName).Any(); - } - catch (Exception ex) - { - Trace.WriteLine(ex); - } - return false; + Trace.WriteLine(ex); } + return false; + } - private void OnWindowCreated(IntPtr hwnd) + private void OnWindowCreated(IntPtr hwnd) + { + try { - try + var pid = 0U; + unsafe { - User32.GetWindowThreadProcessId(hwnd, out uint pid); - - using (var proc = Process.GetProcessById((int)pid)) - { - if (_info.ContainsKey(proc.ProcessName.ToLower())) - { - FoundNewRelevantProcess(proc); - } - } + _ = PInvoke.GetWindowThreadProcessId(new HWND(hwnd.ToPointer()), &pid); } - catch (Exception ex) + + using var proc = Process.GetProcessById((int)pid); + if (_info.ContainsKey(proc.ProcessName.ToLowerInvariant())) { - Trace.WriteLine(ex); + FoundNewRelevantProcess(proc); } } + catch (Exception ex) + { + Trace.WriteLine(ex); + } + } + + private bool FoundNewRelevantProcess(Process proc) + { + var info = _info[proc.ProcessName.ToLowerInvariant()]; - bool FoundNewRelevantProcess(Process proc) + if (!info.RunningProcesses.ContainsKey(proc.Id)) { - var info = _info[proc.ProcessName.ToLower()]; + var procInfo = new ProcessInfo(); + info.RunningProcesses[proc.Id] = procInfo; + + var procId = proc.Id; - if (!info.RunningProcesses.ContainsKey(proc.Id)) + new Thread(() => { - var procInfo = new ProcessInfo(); - info.RunningProcesses[proc.Id] = procInfo; - - new Thread(() => - { - Thread.CurrentThread.IsBackground = true; - - var procName = proc.ProcessName; - proc.WaitForExit(); - Trace.WriteLine($"ProcessWatcher STOP {procName}"); - info.StopCallbacks.ForEach(s => s.Invoke()); - }).Start(); - - Trace.WriteLine($"ProcessWatcher START {proc.ProcessName}"); - info.StartCallbacks.ForEach(s => s.Invoke()); - return true; - } - return false; + Thread.CurrentThread.IsBackground = true; + + var actualProc = Process.GetProcessById(procId); + + var procName = actualProc.ProcessName; + actualProc.WaitForExit(); + Trace.WriteLine($"ProcessWatcher STOP {procName}"); + App.Current.Dispatcher.Invoke(() => info.StopCallbacks.ForEach(s => s.Invoke())); + }).Start(); + + Trace.WriteLine($"ProcessWatcher START {proc.ProcessName}"); + info.StartCallbacks.ForEach(s => s.Invoke()); + return true; } + return false; + } - public void RegisterStop(string text, Action callback) - { - Trace.WriteLine($"ProcessWatcher RegisterStop {text}"); - text = text.ToLower(); - WatcherInfo info = _info.ContainsKey(text) ? _info[text] : _info[text] = new WatcherInfo(); - info.StopCallbacks.Add(callback); + public void RegisterStop(string text, Action callback) + { + Trace.WriteLine($"ProcessWatcher RegisterStop {text}"); + text = text.ToLower(CultureInfo.CurrentCulture); + var info = _info.TryGetValue(text, out var value) ? value : _info[text] = new WatcherInfo(); + info.StopCallbacks.Add(callback); - try - { - var runningProcs = Process.GetProcessesByName(text); - foreach (var proc in runningProcs) - { - FoundNewRelevantProcess(proc); - } - } - catch (Exception ex) + try + { + var runningProcs = Process.GetProcessesByName(text); + foreach (var proc in runningProcs) { - Trace.WriteLine(ex); + FoundNewRelevantProcess(proc); } } - - public void RegisterStart(string text, Action callback) + catch (Exception ex) { - Trace.WriteLine($"ProcessWatcher RegisterStart {text}"); - text = text.ToLower(); - WatcherInfo info = _info.ContainsKey(text) ? _info[text] : new WatcherInfo(); - info.StartCallbacks.Add(callback); + Trace.WriteLine(ex); + } + } + + public void RegisterStart(string text, Action callback) + { + Trace.WriteLine($"ProcessWatcher RegisterStart {text}"); + text = text.ToLower(CultureInfo.CurrentCulture); + var info = _info.TryGetValue(text, out var value) ? value : _info[text] = new WatcherInfo(); + info.StartCallbacks.Add(callback); - try + try + { + var didSignal = false; + var runningProcs = Process.GetProcessesByName(text); + foreach (var proc in runningProcs) { - bool didSignal = false; - var runningProcs = Process.GetProcessesByName(text); - foreach (var proc in runningProcs) - { - didSignal = didSignal || FoundNewRelevantProcess(proc); - } - - if (runningProcs.Any() && !didSignal) - { - // We were already watching so we didn't signal but the process is running. - callback(); - } + didSignal = didSignal || FoundNewRelevantProcess(proc); } - catch (Exception ex) + + if (runningProcs.Length != 0 && !didSignal) { - Trace.WriteLine(ex); + // We were already watching so we didn't signal but the process is running. + callback(); } } - - public void Clear() + catch (Exception ex) { - Trace.WriteLine("ProcessWatcher Clear"); - _info = new Dictionary(); + Trace.WriteLine(ex); } } + + public void Clear() + { + Trace.WriteLine("ProcessWatcher Clear"); + _info = []; + } + + public void Dispose() + { + _watcher.Dispose(); + } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ActionProcessor.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ActionProcessor.cs index 95a38a93c..e9b24325c 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ActionProcessor.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ActionProcessor.cs @@ -1,197 +1,225 @@ -using EarTrumpet.Interop; -using EarTrumpet.Actions.DataModel.Serialization; -using EarTrumpet.Actions.DataModel.Enum; -using System; +using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; -using System.Text; +using EarTrumpet.Actions.DataModel.Enum; +using EarTrumpet.Actions.DataModel.Serialization; +using EarTrumpet.DataModel.AppInformation; using EarTrumpet.DataModel.Audio; using EarTrumpet.DataModel.WindowsAudio; -using EarTrumpet.DataModel.AppInformation; +using EarTrumpet.Extensions; +using Windows.Win32; -namespace EarTrumpet.Actions.DataModel.Processing +namespace EarTrumpet.Actions.DataModel.Processing; + +internal class ActionProcessor { - class ActionProcessor + public static void Invoke(BaseAction a) { - public static void Invoke(BaseAction a) + Trace.WriteLine($"ActionProcessor Invoke: {a.GetType().Name}"); + if (a is SetVariableAction setVariableAction) + { + EarTrumpetActionsAddon.Current.LocalVariables[setVariableAction.Text] = (setVariableAction.Value == BoolValue.True); + } + else if (a is SetDefaultDeviceAction setDefaultDeviceAction) { - Trace.WriteLine($"ActionProcessor Invoke: {a.GetType().Name}"); - if (a is SetVariableAction) + var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), setDefaultDeviceAction.Device.Kind)); + + var dev = mgr.Devices.FirstOrDefault(d => d.Id == setDefaultDeviceAction.Device.Id); + if (dev != null) { - EarTrumpetActionsAddon.Current.LocalVariables[((SetVariableAction)a).Text] = (((SetVariableAction)a).Value == BoolValue.True); + mgr.Default = dev; } - else if (a is SetDefaultDeviceAction) - { - var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((SetDefaultDeviceAction)a).Device.Kind)); + } + else if (a is SetAppVolumeAction setAppVolumeAction) + { + var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((SetAppVolumeAction)a).Device.Kind)); - var dev = mgr.Devices.FirstOrDefault(d => d.Id == ((SetDefaultDeviceAction)a).Device.Id); - if (dev != null) - { - mgr.Default = dev; - } - } - else if (a is SetAppVolumeAction) + var device = (setAppVolumeAction.Device?.Id == null) ? + mgr.Default : mgr.Devices.FirstOrDefault(d => d.Id == setAppVolumeAction.Device.Id); + if (device != null) { - var action = (SetAppVolumeAction)a; - var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((SetAppVolumeAction)a).Device.Kind)); - - var device = (action.Device?.Id == null) ? - mgr.Default : mgr.Devices.FirstOrDefault(d => d.Id == action.Device.Id); - if (device != null) + if (setAppVolumeAction.App.Id == AppRef.ForegroundAppId) { - if (action.App.Id == AppRef.ForegroundAppId) + var app = FindForegroundApp(device.Groups); + if (app != null) { - var app = FindForegroundApp(device.Groups); - if (app != null) - { - DoAudioAction(action.Option, app, action); - } + DoAudioAction(setAppVolumeAction.Option, app, setAppVolumeAction); } - else + } + else + { + foreach (var app in device.Groups.Where(app => setAppVolumeAction.App.Id == AppRef.EveryAppId || app.AppId == setAppVolumeAction.App.Id)) { - foreach (var app in device.Groups.Where(app => action.App.Id == AppRef.EveryAppId || app.AppId == action.App.Id)) - { - DoAudioAction(action.Option, app, action); - } + DoAudioAction(setAppVolumeAction.Option, app, setAppVolumeAction); } } } - else if (a is SetAppMuteAction) - { - var action = (SetAppMuteAction)a; - var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((SetAppMuteAction)a).Device.Kind)); + } + else if (a is SetAppMuteAction setAppMuteAction) + { + var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((SetAppMuteAction)a).Device.Kind)); - var device = (action.Device?.Id == null) ? - mgr.Default : mgr.Devices.FirstOrDefault(d => d.Id == action.Device.Id); - if (device != null) + var device = (setAppMuteAction.Device?.Id == null) ? + mgr.Default : mgr.Devices.FirstOrDefault(d => d.Id == setAppMuteAction.Device.Id); + if (device != null) + { + if (setAppMuteAction.App.Id == AppRef.ForegroundAppId) { - if (action.App.Id == AppRef.ForegroundAppId) + var app = FindForegroundApp(device.Groups); + if (app != null) { - var app = FindForegroundApp(device.Groups); - if (app != null) - { - DoAudioAction(action.Option, app); - } + DoAudioAction(setAppMuteAction.Option, app); } - else + } + else + { + foreach (var app in device.Groups.Where(app => setAppMuteAction.App.Id == AppRef.EveryAppId || app.AppId == setAppMuteAction.App.Id)) { - foreach (var app in device.Groups.Where(app => action.App.Id == AppRef.EveryAppId || app.AppId == action.App.Id)) - { - DoAudioAction(action.Option, app); - } + DoAudioAction(setAppMuteAction.Option, app); } } } - else if (a is SetDeviceVolumeAction) - { - var action = (SetDeviceVolumeAction)a; - - var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((SetDeviceVolumeAction)a).Device.Kind)); + } + else if (a is SetDeviceVolumeAction setDeviceVolumeAction) + { + var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((SetDeviceVolumeAction)a).Device.Kind)); - var device = (action.Device?.Id == null) ? - mgr.Default : mgr.Devices.FirstOrDefault(d => d.Id == action.Device.Id); - if (device != null) - { - DoAudioAction(action.Option, device, action); - } - } - else if (a is SetDeviceMuteAction) + var device = (setDeviceVolumeAction.Device?.Id == null) ? + mgr.Default : mgr.Devices.FirstOrDefault(d => d.Id == setDeviceVolumeAction.Device.Id); + if (device != null) { - var action = (SetDeviceMuteAction)a; - var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((SetDeviceMuteAction)a).Device.Kind)); - - var device = (action.Device?.Id == null) ? - mgr.Default : mgr.Devices.FirstOrDefault(d => d.Id == action.Device.Id); - if (device != null) - { - DoAudioAction(action.Option, device); - } + DoAudioAction(setDeviceVolumeAction.Option, device, setDeviceVolumeAction); } - else + } + else if (a is SetDeviceMuteAction setDeviceMuteAction) + { + var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((SetDeviceMuteAction)a).Device.Kind)); + + var device = (setDeviceMuteAction.Device?.Id == null) ? + mgr.Default : mgr.Devices.FirstOrDefault(d => d.Id == setDeviceMuteAction.Device.Id); + if (device != null) { - throw new NotImplementedException(); + DoAudioAction(setDeviceMuteAction.Option, device); } } + else + { + throw new NotImplementedException(); + } + } - private static IAudioDeviceSession FindForegroundApp(ObservableCollection groups) + private static IAudioDeviceSession FindForegroundApp(ObservableCollection groups) + { + var hWnd = PInvoke.GetForegroundWindow(); + if (hWnd == (HWND)null) { - var hWnd = User32.GetForegroundWindow(); - var foregroundClassName = new StringBuilder(User32.MAX_CLASSNAME_LENGTH); - User32.GetClassName(hWnd, foregroundClassName, foregroundClassName.Capacity); + Trace.WriteLine($"ActionProcessor FindForegroundApp: No Window (1)"); + return null; + } - if (hWnd == IntPtr.Zero) + var className = string.Empty; + unsafe + { + Span classNameBuffer = stackalloc char[(int)PInvoke.MAX_CLASS_NAME_LEN]; + fixed (char* pClassNameBuffer = classNameBuffer) { - Trace.WriteLine($"ActionProcessor FindForegroundApp: No Window (1)"); - return null; + _ = PInvoke.GetClassName(hWnd, pClassNameBuffer, classNameBuffer.Length); + className = new PWSTR(pClassNameBuffer).ToString(); } + } - // ApplicationFrameWindow.exe, find the real hosted process in the child CoreWindow. - if (foregroundClassName.ToString() == "ApplicationFrameWindow") - { - hWnd = User32.FindWindowEx(hWnd, IntPtr.Zero, "Windows.UI.Core.CoreWindow", IntPtr.Zero); - } + // ApplicationFrameWindow.exe, find the real hosted process in the child CoreWindow. + if (className.ToString() == "ApplicationFrameWindow") + { + hWnd = PInvoke.FindWindowEx(hWnd, (HWND)null, "Windows.UI.Core.CoreWindow", null); + } + if (hWnd == (HWND)null) + { + Trace.WriteLine($"ActionProcessor FindForegroundApp: No Window (2)"); + return null; + } - if (hWnd == IntPtr.Zero) - { - Trace.WriteLine($"ActionProcessor FindForegroundApp: No Window (2)"); - return null; - } + var processId = 0U; + unsafe + { + _ = PInvoke.GetWindowThreadProcessId(hWnd, &processId); + } - User32.GetWindowThreadProcessId(hWnd, out uint processId); + try + { + var appInfo = AppInformationFactory.CreateForProcess(processId); - try + foreach(var group in groups) { - var appInfo = AppInformationFactory.CreateForProcess((int)processId); - - foreach(var group in groups) + if (group.AppId == appInfo.PackageInstallPath || group.AppId == appInfo.AppId) { - if (group.AppId == appInfo.PackageInstallPath) - { - Trace.WriteLine($"ActionProcessor FindForegroundApp: {group.DisplayName}"); - return group; - } + Trace.WriteLine($"ActionProcessor FindForegroundApp: {group.DisplayName}"); + return group; } } - catch(Exception ex) - { - Trace.WriteLine(ex); - } - Trace.WriteLine("ActionProcessor FindForegroundApp Didn't locate foreground app"); - return null; } + catch(Exception ex) + { + Trace.WriteLine(ex); + } + Trace.WriteLine("ActionProcessor FindForegroundApp Didn't locate foreground app"); + return null; + } - private static void DoAudioAction(MuteKind action, IStreamWithVolumeControl stream) + private static void DoAudioAction(MuteKind action, IStreamWithVolumeControl stream) + { + switch (action) { - switch (action) - { - case MuteKind.Mute: - stream.IsMuted = true; - break; - case MuteKind.ToggleMute: - stream.IsMuted = !stream.IsMuted; - break; - case MuteKind.Unmute: - stream.IsMuted = false; - break; - } + case MuteKind.Mute: + stream.IsMuted = true; + break; + case MuteKind.ToggleMute: + stream.IsMuted = !stream.IsMuted; + break; + case MuteKind.Unmute: + stream.IsMuted = false; + break; } + } - private static void DoAudioAction(SetVolumeKind action, IStreamWithVolumeControl stream, IPartWithVolume part) + private static void DoAudioAction(SetVolumeKind action, IStreamWithVolumeControl stream, IPartWithVolume part) + { + try { - var vol = (float)(part.Volume / 100f); - switch (action) + switch (part.Unit) { - case SetVolumeKind.Set: - stream.Volume = vol; - break; - case SetVolumeKind.Increment: - stream.Volume += vol; + case VolumeUnit.Percentage: + { + var vol = (float)(part.Volume / 100); + var prevVol = stream.GetVolumeScalar(); + stream.SetVolumeScalar(action switch { + SetVolumeKind.Set => vol, + SetVolumeKind.Increment => (prevVol + vol).Bound(0, 1), + SetVolumeKind.Decrement => (prevVol - vol).Bound(0, 1), + _ => throw new ArgumentException("Invalid volume action.") + }); + } break; - case SetVolumeKind.Decrement: - stream.Volume -= vol; + case VolumeUnit.Decibel: + { + var vol = (float)part.Volume; + var prevVol = stream.GetVolumeLogarithmic(); + stream.SetVolumeLogarithmic(action switch { + SetVolumeKind.Set => vol, + SetVolumeKind.Increment => (prevVol + vol).Bound(App.Settings.LogarithmicVolumeMinDb, 0), + SetVolumeKind.Decrement => (prevVol - vol).Bound(App.Settings.LogarithmicVolumeMinDb, 0), + _ => throw new ArgumentException("Invalid volume action.") + }); + } break; + default: + throw new ArgumentException("Invalid volume unit."); } } + catch (Exception ex) when (ex.Is(HRESULT.AUDCLNT_E_DEVICE_INVALIDATED)) + { + // Expected in some cases. + } } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/AudioTriggerManager.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/AudioTriggerManager.cs index 1bda8c6c0..95e60056f 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/AudioTriggerManager.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/AudioTriggerManager.cs @@ -6,160 +6,168 @@ using EarTrumpet.DataModel.WindowsAudio; using EarTrumpet.Extensibility.Shared; -namespace EarTrumpet.Actions.DataModel.Processing +namespace EarTrumpet.Actions.DataModel.Processing; + +internal class AudioTriggerManager { - class AudioTriggerManager - { - public event Action Triggered; + public event Action Triggered; + + private readonly PlaybackDataModelHost _playbackManager; + private readonly IAudioDeviceManager _recordingManager; + private readonly List _appTriggers = []; + private readonly List _deviceTriggers = []; + private IAudioDevice _defaultPlaybackDevice; + private IAudioDevice _defaultRecordingDevice; - private readonly PlaybackDataModelHost _playbackManager; - private readonly IAudioDeviceManager _recordingManager; - private readonly List _appTriggers = new List(); - private readonly List _deviceTriggers = new List(); - private IAudioDevice _defaultPlaybackDevice; - private IAudioDevice _defaultRecordingDevice; + public AudioTriggerManager() + { + _playbackManager = PlaybackDataModelHost.Current; + _playbackManager.AppPropertyChanged += OnAppPropertyChanged; + _playbackManager.AppAdded += (a) => OnAppAddOrRemove(a, AudioAppEventKind.Added); + _playbackManager.AppRemoved += (a) => OnAppAddOrRemove(a, AudioAppEventKind.Removed); + _playbackManager.DeviceAdded += (d) => OnDeviceAddOrRemove(d, AudioDeviceEventKind.Added); + _playbackManager.DeviceRemoved += (d) => OnDeviceAddOrRemove(d, AudioDeviceEventKind.Removed); + _playbackManager.DeviceManager.DefaultChanged += PlaybackDeviceManager_DefaultChanged; + _defaultPlaybackDevice = _playbackManager.DeviceManager.Default; + + _recordingManager = WindowsAudioFactory.Create(AudioDeviceKind.Recording); + _recordingManager.DefaultChanged += RecordingMgr_DefaultChanged; + _defaultRecordingDevice = _recordingManager.Default; + } - public AudioTriggerManager() + public void Register(BaseTrigger trigger) + { + if (trigger is DeviceEventTrigger) { - _playbackManager = PlaybackDataModelHost.Current; - _playbackManager.AppPropertyChanged += OnAppPropertyChanged; - _playbackManager.AppAdded += (a) => OnAppAddOrRemove(a, AudioAppEventKind.Added); - _playbackManager.AppRemoved += (a) => OnAppAddOrRemove(a, AudioAppEventKind.Removed); - _playbackManager.DeviceAdded += (d) => OnDeviceAddOrRemove(d, AudioDeviceEventKind.Added); - _playbackManager.DeviceRemoved += (d) => OnDeviceAddOrRemove(d, AudioDeviceEventKind.Removed); - _playbackManager.DeviceManager.DefaultChanged += PlaybackDeviceManager_DefaultChanged; - _defaultPlaybackDevice = _playbackManager.DeviceManager.Default; - - _recordingManager = WindowsAudioFactory.Create(AudioDeviceKind.Recording); - _recordingManager.DefaultChanged += RecordingMgr_DefaultChanged; - _defaultRecordingDevice = _recordingManager.Default; + _deviceTriggers.Add((DeviceEventTrigger)trigger); } - - public void Register(BaseTrigger trigger) + else if (trigger is AppEventTrigger) { - if (trigger is DeviceEventTrigger) - { - _deviceTriggers.Add((DeviceEventTrigger)trigger); - } - else if (trigger is AppEventTrigger) - { - _appTriggers.Add((AppEventTrigger)trigger); - } - else throw new NotImplementedException(); + _appTriggers.Add((AppEventTrigger)trigger); } - - public void Clear() + else { - _appTriggers.Clear(); - _deviceTriggers.Clear(); + throw new NotImplementedException(); } + } + + public void Clear() + { + _appTriggers.Clear(); + _deviceTriggers.Clear(); + } - private void PlaybackDeviceManager_DefaultChanged(object sender, EarTrumpet.DataModel.Audio.IAudioDevice newDefault) + private void PlaybackDeviceManager_DefaultChanged(object sender, EarTrumpet.DataModel.Audio.IAudioDevice newDefault) + { + if (newDefault == null) { - if (newDefault == null) return; + return; + } - ProcessDefaultChanged(newDefault); + ProcessDefaultChanged(newDefault); - _defaultPlaybackDevice = newDefault; - } + _defaultPlaybackDevice = newDefault; + } - private void RecordingMgr_DefaultChanged(object sender, IAudioDevice newDefault) + private void RecordingMgr_DefaultChanged(object sender, IAudioDevice newDefault) + { + if (newDefault == null) { - if (newDefault == null) return; + return; + } - ProcessDefaultChanged(newDefault); + ProcessDefaultChanged(newDefault); - _defaultRecordingDevice = newDefault; - } + _defaultRecordingDevice = newDefault; + } - private void ProcessDefaultChanged(IAudioDevice newDefault) + private void ProcessDefaultChanged(IAudioDevice newDefault) + { + foreach (var trigger in _deviceTriggers) { - foreach (var trigger in _deviceTriggers) + if (trigger.Device.Id == _defaultPlaybackDevice?.Id && + trigger.Option == AudioDeviceEventKind.LeavingDefault) { - if (trigger.Device.Id == _defaultPlaybackDevice?.Id && - trigger.Option == AudioDeviceEventKind.LeavingDefault) - { - Triggered?.Invoke(trigger); - } + Triggered?.Invoke(trigger); + } - if (trigger.Device.Id == newDefault.Id && - trigger.Option == AudioDeviceEventKind.BecomingDefault) - { - Triggered?.Invoke(trigger); - } + if (trigger.Device.Id == newDefault.Id && + trigger.Option == AudioDeviceEventKind.BecomingDefault) + { + Triggered?.Invoke(trigger); } } + } - private void OnDeviceAddOrRemove(IAudioDevice device, AudioDeviceEventKind option) + private void OnDeviceAddOrRemove(IAudioDevice device, AudioDeviceEventKind option) + { + foreach (var trigger in _deviceTriggers) { - foreach (var trigger in _deviceTriggers) + if (trigger.Option == option) { - if (trigger.Option == option) + // Default device: not supported + if (trigger.Device.Id == device.Id) { - // Default device: not supported - if (trigger.Device.Id == device.Id) - { - Triggered?.Invoke(trigger); - } + Triggered?.Invoke(trigger); } } } + } - private void OnAppAddOrRemove(IAudioDeviceSession app, AudioAppEventKind option) + private void OnAppAddOrRemove(IAudioDeviceSession app, AudioAppEventKind option) + { + foreach (var trigger in _appTriggers) { - foreach (var trigger in _appTriggers) + if (trigger.Option == option) { - if (trigger.Option == option) + var device = app.Parent; + if ((trigger.Device?.Id == null && device == _playbackManager.DeviceManager.Default) || + trigger.Device?.Id == device.Id) { - var device = app.Parent; - if ((trigger.Device?.Id == null && device == _playbackManager.DeviceManager.Default) || - trigger.Device?.Id == device.Id) + if (trigger.App.Id == AppRef.EveryAppId || trigger.App.Id == app.AppId) { - if (trigger.App.Id == app.AppId) - { - Triggered?.Invoke(trigger); - } + Triggered?.Invoke(trigger); } } } } + } - private void OnAppPropertyChanged(IAudioDeviceSession app, string propertyName) + private void OnAppPropertyChanged(IAudioDeviceSession app, string propertyName) + { + foreach (var trigger in _appTriggers) { - foreach (var trigger in _appTriggers) + var device = app.Parent; + if ((trigger.Device?.Id == null && device == _playbackManager.DeviceManager.Default) || trigger.Device?.Id == device.Id) { - var device = app.Parent; - if ((trigger.Device?.Id == null && device == _playbackManager.DeviceManager.Default) || trigger.Device?.Id == device.Id) + if (trigger.App.Id == app.AppId) { - if (trigger.App.Id == app.AppId) + switch (trigger.Option) { - switch (trigger.Option) - { - case AudioAppEventKind.Muted: - if (propertyName == nameof(app.IsMuted) && app.IsMuted) - { - Triggered?.Invoke(trigger); - } - break; - case AudioAppEventKind.Unmuted: - if (propertyName == nameof(app.IsMuted) && !app.IsMuted) - { - Triggered?.Invoke(trigger); - } - break; - case AudioAppEventKind.PlayingSound: - if (propertyName == nameof(app.State) && app.State == SessionState.Active) - { - Triggered?.Invoke(trigger); - } - break; - case AudioAppEventKind.NotPlayingSound: - if (propertyName == nameof(app.State) && app.State != SessionState.Active) - { - Triggered?.Invoke(trigger); - } - break; - } + case AudioAppEventKind.Muted: + if (propertyName == nameof(app.IsMuted) && app.IsMuted) + { + Triggered?.Invoke(trigger); + } + break; + case AudioAppEventKind.Unmuted: + if (propertyName == nameof(app.IsMuted) && !app.IsMuted) + { + Triggered?.Invoke(trigger); + } + break; + case AudioAppEventKind.PlayingSound: + if (propertyName == nameof(app.State) && app.State == SessionState.Active) + { + Triggered?.Invoke(trigger); + } + break; + case AudioAppEventKind.NotPlayingSound: + if (propertyName == nameof(app.State) && app.State != SessionState.Active) + { + Triggered?.Invoke(trigger); + } + break; } } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ConditionProcessor.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ConditionProcessor.cs index 46533879b..1baf2f486 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ConditionProcessor.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ConditionProcessor.cs @@ -3,45 +3,44 @@ using System; using EarTrumpet.DataModel.WindowsAudio; -namespace EarTrumpet.Actions.DataModel.Processing +namespace EarTrumpet.Actions.DataModel.Processing; + +internal class ConditionProcessor { - class ConditionProcessor + public static bool IsMet(BaseCondition condition) { - public static bool IsMet(BaseCondition condition) + if (condition is ProcessCondition) { - if (condition is ProcessCondition) + var isProcessRunning = ProcessWatcher.IsRunning(((ProcessCondition)condition).Text); + switch (((ProcessCondition)condition).Option) { - bool isProcessRunning = ProcessWatcher.Current.IsRunning(((ProcessCondition)condition).Text); - switch (((ProcessCondition)condition).Option) - { - case ProcessStateKind.Running: - return isProcessRunning; - case ProcessStateKind.NotRunning: - return !isProcessRunning; - default: - throw new NotImplementedException(); - } + case ProcessStateKind.Running: + return isProcessRunning; + case ProcessStateKind.NotRunning: + return !isProcessRunning; + default: + throw new NotImplementedException(); } - else if (condition is DefaultDeviceCondition) - { - var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((DefaultDeviceCondition)condition).Device.Kind)); + } + else if (condition is DefaultDeviceCondition) + { + var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((DefaultDeviceCondition)condition).Device.Kind)); - var isDeviceCurrentlyDefault = ((DefaultDeviceCondition)condition).Device.Id == mgr.Default?.Id; - switch (((DefaultDeviceCondition)condition).Option) - { - case ComparisonBoolKind.Is: - return isDeviceCurrentlyDefault; - case ComparisonBoolKind.IsNot: - return !isDeviceCurrentlyDefault; - default: - throw new NotImplementedException(); - } - } - else if (condition is VariableCondition) + var isDeviceCurrentlyDefault = ((DefaultDeviceCondition)condition).Device.Id == mgr.Default?.Id; + switch (((DefaultDeviceCondition)condition).Option) { - return (EarTrumpetActionsAddon.Current.LocalVariables[((VariableCondition)condition).Text] == (((VariableCondition)condition).Value == BoolValue.True)); + case ComparisonBoolKind.Is: + return isDeviceCurrentlyDefault; + case ComparisonBoolKind.IsNot: + return !isDeviceCurrentlyDefault; + default: + throw new NotImplementedException(); } - throw new NotImplementedException(); } + else if (condition is VariableCondition) + { + return (EarTrumpetActionsAddon.Current.LocalVariables[((VariableCondition)condition).Text] == (((VariableCondition)condition).Value == BoolValue.True)); + } + throw new NotImplementedException(); } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/TriggerManager.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/TriggerManager.cs index a2e1da1aa..0af336648 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/TriggerManager.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/TriggerManager.cs @@ -5,87 +5,89 @@ using System; using System.Collections.Generic; -namespace EarTrumpet.Actions.DataModel.Processing +namespace EarTrumpet.Actions.DataModel.Processing; + +internal class TriggerManager { - class TriggerManager - { - public event Action Triggered; + public event Action Triggered; - private List _eventTriggers = new List(); - private AudioTriggerManager _audioManager; + private List _eventTriggers = []; + private AudioTriggerManager _audioManager; - public TriggerManager() - { - _audioManager = new AudioTriggerManager(); - _audioManager.Triggered += (t) => Triggered?.Invoke(t); - } + public TriggerManager() + { + _audioManager = new AudioTriggerManager(); + _audioManager.Triggered += (t) => Triggered?.Invoke(t); + } - public void Clear() - { - ProcessWatcher.Current.Clear(); - _eventTriggers.Clear(); - _audioManager.Clear(); - } + public void Clear() + { + ProcessWatcher.Current.Clear(); + _eventTriggers.Clear(); + _audioManager.Clear(); + } - public void OnEvent(AddonEventKind evt) + public void OnEvent(AddonEventKind evt) + { + foreach (var trigger in _eventTriggers) { - foreach (var trigger in _eventTriggers) + if ((trigger.Option == EarTrumpetEventKind.Startup && evt == AddonEventKind.InitializeAddon) || + (trigger.Option == EarTrumpetEventKind.Shutdown && evt == AddonEventKind.AppShuttingDown)) { - if ((trigger.Option == EarTrumpetEventKind.Startup && evt == AddonEventKind.InitializeAddon) || - (trigger.Option == EarTrumpetEventKind.Shutdown && evt == AddonEventKind.AppShuttingDown)) - { - Triggered?.Invoke(trigger); - } + Triggered?.Invoke(trigger); } } + } - public void Register(BaseTrigger trig) + public void Register(BaseTrigger trig) + { + if (trig is ProcessTrigger) { - if (trig is ProcessTrigger) + var trigger = (ProcessTrigger)trig; + if (!string.IsNullOrWhiteSpace(trigger.Text)) { - var trigger = (ProcessTrigger)trig; - if (!string.IsNullOrWhiteSpace(trigger.Text)) + if (trigger.Option == ProcessEventKind.Start) { - if (trigger.Option == ProcessEventKind.Start) - { - ProcessWatcher.Current.RegisterStart(trigger.Text, () => Triggered?.Invoke(trig)); - } - else - { - ProcessWatcher.Current.RegisterStop(trigger.Text, () => Triggered?.Invoke(trig)); - } + ProcessWatcher.Current.RegisterStart(trigger.Text, () => Triggered?.Invoke(trig)); } - } - else if (trig is EventTrigger) - { - _eventTriggers.Add((EventTrigger)trig); - } - else if (trig is DeviceEventTrigger) - { - _audioManager.Register(trig); - } - else if (trig is AppEventTrigger) - { - _audioManager.Register(trig); - } - else if (trig is HotkeyTrigger) - { - var trigger = (HotkeyTrigger)trig; - - HotkeyManager.Current.Register(trigger.Option); - HotkeyManager.Current.KeyPressed += (data) => + else { - if (data.Equals(trigger.Option)) - { - Triggered?.Invoke(trig); - } - }; + ProcessWatcher.Current.RegisterStop(trigger.Text, () => Triggered?.Invoke(trig)); + } } - else if (trig is ContextMenuTrigger) + } + else if (trig is EventTrigger) + { + _eventTriggers.Add((EventTrigger)trig); + } + else if (trig is DeviceEventTrigger) + { + _audioManager.Register(trig); + } + else if (trig is AppEventTrigger) + { + _audioManager.Register(trig); + } + else if (trig is HotkeyTrigger) + { + var trigger = (HotkeyTrigger)trig; + + HotkeyManager.Current.Register(trigger.Option); + HotkeyManager.Current.KeyPressed += (data) => { - // Nothing to do. - } - else throw new NotImplementedException(); + if (data.Equals(trigger.Option)) + { + Triggered?.Invoke(trig); + } + }; + } + else if (trig is ContextMenuTrigger) + { + // Nothing to do. + } + else + { + throw new NotImplementedException(); } } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Actions.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Actions.cs index dae3b3b18..d02ff836d 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Actions.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Actions.cs @@ -1,52 +1,53 @@ using EarTrumpet.Actions.DataModel.Enum; using System.Xml.Serialization; -namespace EarTrumpet.Actions.DataModel.Serialization +namespace EarTrumpet.Actions.DataModel.Serialization; + +[XmlInclude(typeof(SetAppVolumeAction))] +[XmlInclude(typeof(SetAppMuteAction))] +[XmlInclude(typeof(SetDeviceVolumeAction))] +[XmlInclude(typeof(SetDeviceMuteAction))] +[XmlInclude(typeof(SetDefaultDeviceAction))] +[XmlInclude(typeof(SetVariableAction))] +public abstract class BaseAction : Part { } + +public class SetAppMuteAction : BaseAction, IPartWithDevice, IPartWithApp +{ + public Device Device { get; set; } + public AppRef App { get; set; } + public MuteKind Option { get; set; } +} + +public class SetAppVolumeAction : BaseAction, IPartWithVolume, IPartWithDevice, IPartWithApp +{ + public Device Device { get; set; } + public AppRef App { get; set; } + public SetVolumeKind Option { get; set; } + public double Volume { get; set; } + public VolumeUnit Unit { get; set; } +} + +public class SetDefaultDeviceAction : BaseAction, IPartWithDevice +{ + public Device Device { get; set; } +} + +public class SetDeviceMuteAction : BaseAction, IPartWithDevice +{ + public Device Device { get; set; } + public MuteKind Option { get; set; } +} + +public class SetDeviceVolumeAction : BaseAction, IPartWithDevice, IPartWithVolume +{ + public Device Device { get; set; } + public SetVolumeKind Option { get; set; } + public double Volume { get; set; } + public VolumeUnit Unit { get; set; } +} + +public class SetVariableAction : BaseAction, IPartWithText { - [XmlInclude(typeof(SetAppVolumeAction))] - [XmlInclude(typeof(SetAppMuteAction))] - [XmlInclude(typeof(SetDeviceVolumeAction))] - [XmlInclude(typeof(SetDeviceMuteAction))] - [XmlInclude(typeof(SetDefaultDeviceAction))] - [XmlInclude(typeof(SetVariableAction))] - public abstract class BaseAction : Part { } - - public class SetAppMuteAction : BaseAction, IPartWithDevice, IPartWithApp - { - public Device Device { get; set; } - public AppRef App { get; set; } - public MuteKind Option { get; set; } - } - - public class SetAppVolumeAction : BaseAction, IPartWithVolume, IPartWithDevice, IPartWithApp - { - public Device Device { get; set; } - public AppRef App { get; set; } - public SetVolumeKind Option { get; set; } - public double Volume { get; set; } - } - - public class SetDefaultDeviceAction : BaseAction, IPartWithDevice - { - public Device Device { get; set; } - } - - public class SetDeviceMuteAction : BaseAction, IPartWithDevice - { - public Device Device { get; set; } - public MuteKind Option { get; set; } - } - - public class SetDeviceVolumeAction : BaseAction, IPartWithDevice, IPartWithVolume - { - public Device Device { get; set; } - public SetVolumeKind Option { get; set; } - public double Volume { get; set; } - } - - public class SetVariableAction : BaseAction, IPartWithText - { - public string Text { get; set; } - public BoolValue Value { get; set; } - } + public string Text { get; set; } + public BoolValue Value { get; set; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/App.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/App.cs index c71af15f4..ebf6f7a1b 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/App.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/App.cs @@ -1,20 +1,19 @@ -namespace EarTrumpet.Actions.DataModel.Serialization +namespace EarTrumpet.Actions.DataModel.Serialization; + +public class AppRef { - public class AppRef - { - public static readonly string EveryAppId = "EarTrumpet.EveryApp"; - public static readonly string ForegroundAppId = "EarTrumpet.ForegroundApp"; + public static readonly string EveryAppId = "EarTrumpet.EveryApp"; + public static readonly string ForegroundAppId = "EarTrumpet.ForegroundApp"; - public string Id { get; set; } + public string Id { get; set; } - public override int GetHashCode() - { - return Id == null ? 0 : Id.GetHashCode(); - } + public override int GetHashCode() + { + return Id == null ? 0 : Id.GetHashCode(); + } - public bool Equals(AppRef other) - { - return other.Id == Id; - } + public bool Equals(AppRef other) + { + return other.Id == Id; } } \ No newline at end of file diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Conditions.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Conditions.cs index 99b2ae84a..0fcca75f1 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Conditions.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Conditions.cs @@ -1,28 +1,27 @@ using EarTrumpet.Actions.DataModel.Enum; using System.Xml.Serialization; -namespace EarTrumpet.Actions.DataModel.Serialization -{ - [XmlInclude(typeof(DefaultDeviceCondition))] - [XmlInclude(typeof(ProcessCondition))] - [XmlInclude(typeof(VariableCondition))] - public abstract class BaseCondition : Part { } +namespace EarTrumpet.Actions.DataModel.Serialization; - public class DefaultDeviceCondition : BaseCondition, IPartWithDevice - { - public Device Device { get; set; } - public ComparisonBoolKind Option { get; set; } - } +[XmlInclude(typeof(DefaultDeviceCondition))] +[XmlInclude(typeof(ProcessCondition))] +[XmlInclude(typeof(VariableCondition))] +public abstract class BaseCondition : Part { } - public class ProcessCondition : BaseCondition, IPartWithText - { - public string Text { get; set; } - public ProcessStateKind Option { get; set; } - } +public class DefaultDeviceCondition : BaseCondition, IPartWithDevice +{ + public Device Device { get; set; } + public ComparisonBoolKind Option { get; set; } +} - public class VariableCondition : BaseCondition, IPartWithText - { - public string Text { get; set; } - public BoolValue Value { get; set; } - } +public class ProcessCondition : BaseCondition, IPartWithText +{ + public string Text { get; set; } + public ProcessStateKind Option { get; set; } +} + +public class VariableCondition : BaseCondition, IPartWithText +{ + public string Text { get; set; } + public BoolValue Value { get; set; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Device.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Device.cs index 736fd7bdd..acf5fe1cf 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Device.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Device.cs @@ -1,19 +1,18 @@ -namespace EarTrumpet.Actions.DataModel.Serialization +namespace EarTrumpet.Actions.DataModel.Serialization; + +public class Device { - public class Device - { - public string Id { get; set; } + public string Id { get; set; } - public string Kind { get; set; } + public string Kind { get; set; } - public override int GetHashCode() - { - return Id == null ? 0 : Id.GetHashCode(); - } + public override int GetHashCode() + { + return Id == null ? 0 : Id.GetHashCode(); + } - public bool Equals(Device other) - { - return other.Id == Id; - } + public bool Equals(Device other) + { + return other.Id == Id; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/EarTrumpetAction.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/EarTrumpetAction.cs index 778e58a64..1ba1f9b4f 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/EarTrumpetAction.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/EarTrumpetAction.cs @@ -1,14 +1,13 @@ using System; using System.Collections.ObjectModel; -namespace EarTrumpet.Actions.DataModel.Serialization +namespace EarTrumpet.Actions.DataModel.Serialization; + +public class EarTrumpetAction { - public class EarTrumpetAction - { - public string DisplayName { get; set; } - public Guid Id { get; set; } = Guid.NewGuid(); - public ObservableCollection Triggers { get; set; } = new ObservableCollection(); - public ObservableCollection Conditions { get; set; } = new ObservableCollection(); - public ObservableCollection Actions { get; set; } = new ObservableCollection(); - } + public string DisplayName { get; set; } + public Guid Id { get; set; } = Guid.NewGuid(); + public ObservableCollection Triggers { get; set; } = []; + public ObservableCollection Conditions { get; set; } = []; + public ObservableCollection Actions { get; set; } = []; } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Triggers.cs b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Triggers.cs index 8a15ef5b4..002737a99 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Triggers.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Triggers.cs @@ -2,44 +2,43 @@ using EarTrumpet.Actions.DataModel.Enum; using System.Xml.Serialization; -namespace EarTrumpet.Actions.DataModel.Serialization +namespace EarTrumpet.Actions.DataModel.Serialization; + +[XmlInclude(typeof(EventTrigger))] +[XmlInclude(typeof(HotkeyTrigger))] +[XmlInclude(typeof(DeviceEventTrigger))] +[XmlInclude(typeof(AppEventTrigger))] +[XmlInclude(typeof(ProcessTrigger))] +[XmlInclude(typeof(ContextMenuTrigger))] +public abstract class BaseTrigger : Part { } + +public class AppEventTrigger : BaseTrigger, IPartWithDevice, IPartWithApp +{ + public Device Device { get; set; } + public AppRef App { get; set; } + public AudioAppEventKind Option { get; set; } +} + +public class ContextMenuTrigger : BaseTrigger { } + +public class DeviceEventTrigger : BaseTrigger, IPartWithDevice +{ + public Device Device { get; set; } + public AudioDeviceEventKind Option { get; set; } +} + +public class EventTrigger : BaseTrigger +{ + public EarTrumpetEventKind Option { get; set; } +} + +public class HotkeyTrigger : BaseTrigger +{ + public HotkeyData Option { get; set; } = new HotkeyData(); +} + +public class ProcessTrigger : BaseTrigger, IPartWithText { - [XmlInclude(typeof(EventTrigger))] - [XmlInclude(typeof(HotkeyTrigger))] - [XmlInclude(typeof(DeviceEventTrigger))] - [XmlInclude(typeof(AppEventTrigger))] - [XmlInclude(typeof(ProcessTrigger))] - [XmlInclude(typeof(ContextMenuTrigger))] - public abstract class BaseTrigger : Part { } - - public class AppEventTrigger : BaseTrigger, IPartWithDevice, IPartWithApp - { - public Device Device { get; set; } - public AppRef App { get; set; } - public AudioAppEventKind Option { get; set; } - } - - public class ContextMenuTrigger : BaseTrigger { } - - public class DeviceEventTrigger : BaseTrigger, IPartWithDevice - { - public Device Device { get; set; } - public AudioDeviceEventKind Option { get; set; } - } - - public class EventTrigger : BaseTrigger - { - public EarTrumpetEventKind Option { get; set; } - } - - public class HotkeyTrigger : BaseTrigger - { - public HotkeyData Option { get; set; } = new HotkeyData(); - } - - public class ProcessTrigger : BaseTrigger, IPartWithText - { - public string Text { get; set; } - public ProcessEventKind Option { get; set; } - } + public string Text { get; set; } + public ProcessEventKind Option { get; set; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/EarTrumpetActionsAddon.cs b/EarTrumpet/Addons/EarTrumpet.Actions/EarTrumpetActionsAddon.cs index ad8a2ead6..b44b14e72 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/EarTrumpetActionsAddon.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/EarTrumpetActionsAddon.cs @@ -8,121 +8,120 @@ using EarTrumpet.UI.ViewModels; using System; using System.Collections.Generic; -using System.ComponentModel.Composition; +using System.Composition; using System.IO; using System.Linq; -namespace EarTrumpet.Actions +namespace EarTrumpet.Actions; + +[Export(typeof(EarTrumpetAddon))] +public class EarTrumpetActionsAddon : EarTrumpetAddon, IEarTrumpetAddonEvents, IEarTrumpetAddonSettingsPage, IEarTrumpetAddonNotificationAreaContextMenu { - [Export(typeof(EarTrumpetAddon))] - public class EarTrumpetActionsAddon : EarTrumpetAddon, IEarTrumpetAddonEvents, IEarTrumpetAddonSettingsPage, IEarTrumpetAddonNotificationAreaContextMenu - { - public static EarTrumpetActionsAddon Current { get; private set; } - public LocalVariablesContainer LocalVariables { get; private set; } + public static EarTrumpetActionsAddon Current { get; private set; } + public LocalVariablesContainer LocalVariables { get; private set; } - public EarTrumpetActionsAddon() : base() - { - DisplayName = Properties.Resources.MyActionsText; - } + public EarTrumpetActionsAddon() : base() + { + DisplayName = Properties.Resources.MyActionsText; + } - public EarTrumpetAction[] Actions + public EarTrumpetAction[] Actions + { + get => _actions; + set { - get => _actions; - set - { - Settings.Set(c_actionsSettingKey, value); - LoadAndRegister(); - } + Settings.Set(c_actionsSettingKey, value); + LoadAndRegister(); } + } - private readonly string c_actionsSettingKey = "ActionsData"; - private EarTrumpetAction[] _actions = new EarTrumpetAction[] { }; - private TriggerManager _triggerManager = new TriggerManager(); + private readonly string c_actionsSettingKey = "ActionsData"; + private EarTrumpetAction[] _actions = Array.Empty(); + private TriggerManager _triggerManager = new(); - public void OnAddonEvent(AddonEventKind evt) + public void OnAddonEvent(AddonEventKind evt) + { + if (evt == AddonEventKind.AddonsInitialized) { - if (evt == AddonEventKind.AddonsInitialized) - { - Current = this; - LocalVariables = new LocalVariablesContainer(Settings); + Current = this; + LocalVariables = new LocalVariablesContainer(Settings); - _triggerManager.Triggered += OnTriggered; - LoadAndRegister(); + _triggerManager.Triggered += OnTriggered; + LoadAndRegister(); - _triggerManager.OnEvent(AddonEventKind.InitializeAddon); - } - else if (evt == AddonEventKind.AppShuttingDown) - { - _triggerManager.OnEvent(AddonEventKind.AppShuttingDown); - } + _triggerManager.OnEvent(AddonEventKind.InitializeAddon); } - - public SettingsCategoryViewModel GetSettingsCategory() + else if (evt == AddonEventKind.AppShuttingDown) { - LoadAddonResources(); - return new ActionsCategoryViewModel(); + _triggerManager.OnEvent(AddonEventKind.AppShuttingDown); } + } - public IEnumerable NotificationAreaContextMenuItems - { - get - { - var ret = new List(); + public SettingsCategoryViewModel GetSettingsCategory() + { + LoadAddonResources(); + return new ActionsCategoryViewModel(); + } - if (EarTrumpetActionsAddon.Current == null) - { - return ret; - } + public IEnumerable NotificationAreaContextMenuItems + { + get + { + var ret = new List(); - foreach (var item in EarTrumpetActionsAddon.Current.Actions.Where(a => a.Triggers.FirstOrDefault(ax => ax is ContextMenuTrigger) != null)) - { - ret.Add(new ContextMenuItem - { - Glyph = "\xE1CE", - IsChecked = true, - DisplayName = item.DisplayName, - Command = new RelayCommand(() => EarTrumpetActionsAddon.Current.TriggerAction(item)) - }); - } + if (EarTrumpetActionsAddon.Current == null) + { return ret; } - } - - private void LoadAndRegister() - { - _triggerManager.Clear(); - _actions = Settings.Get(c_actionsSettingKey, new EarTrumpetAction[] { }); - _actions.SelectMany(a => a.Triggers).ToList().ForEach(t => _triggerManager.Register(t)); - } - public void Import(string fileName) - { - var imported = Serializer.FromString(File.ReadAllText(fileName)).ToList(); - foreach(var imp in imported) + foreach (var item in EarTrumpetActionsAddon.Current.Actions.Where(a => a.Triggers.FirstOrDefault(ax => ax is ContextMenuTrigger) != null)) { - imp.Id = Guid.NewGuid(); + ret.Add(new ContextMenuItem + { + Glyph = "\xE1CE", + IsChecked = true, + DisplayName = item.DisplayName, + Command = new RelayCommand(() => TriggerAction(item)) + }); } - imported.AddRange(Actions); - Actions = imported.ToArray(); + return ret; } + } - public string Export() - { - return Settings.Get(c_actionsSettingKey, ""); - } + private void LoadAndRegister() + { + _triggerManager.Clear(); + _actions = Settings.Get(c_actionsSettingKey, Array.Empty()); + _actions.SelectMany(a => a.Triggers).ToList().ForEach(t => _triggerManager.Register(t)); + } - private void OnTriggered(BaseTrigger trigger) + public void Import(string fileName) + { + var imported = Serializer.FromString(File.ReadAllText(fileName)).ToList(); + foreach(var imp in imported) { - var action = Actions.FirstOrDefault(a => a.Triggers.Contains(trigger)); - if (action != null && action.Conditions.All(c => ConditionProcessor.IsMet(c))) - { - TriggerAction(action); - } + imp.Id = Guid.NewGuid(); } + imported.AddRange(Actions); + Actions = [.. imported]; + } + + public string Export() + { + return Settings.Get(c_actionsSettingKey, ""); + } - public void TriggerAction(EarTrumpetAction action) + private void OnTriggered(BaseTrigger trigger) + { + var action = Actions.FirstOrDefault(a => a.Triggers.Contains(trigger)); + if (action != null && action.Conditions.All(c => ConditionProcessor.IsMet(c))) { - action.Actions.ToList().ForEach(a => ActionProcessor.Invoke(a)); + TriggerAction(action); } } + + public static void TriggerAction(EarTrumpetAction action) + { + action.Actions.ToList().ForEach(a => ActionProcessor.Invoke(a)); + } } \ No newline at end of file diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/Interop/Helpers/WindowWatcher.cs b/EarTrumpet/Addons/EarTrumpet.Actions/Interop/Helpers/WindowWatcher.cs index dd739d0d8..b89087a33 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/Interop/Helpers/WindowWatcher.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/Interop/Helpers/WindowWatcher.cs @@ -3,39 +3,44 @@ using System.Diagnostics; using System.Windows.Forms; -namespace EarTrumpet.Actions.Interop.Helpers +namespace EarTrumpet.Actions.Interop.Helpers; + +internal class WindowWatcher : IDisposable { - class WindowWatcher - { - public event Action WindowCreated; - public event Action WindowDestroyed; - readonly Win32Window _window; - readonly uint _ShellNotifyMsg; + public event Action WindowCreated; + public event Action WindowDestroyed; - public WindowWatcher() + private readonly Win32Window _window; + private readonly uint _ShellNotifyMsg; + + public WindowWatcher() + { + _window = new Win32Window(); + _window.Initialize(WndProc); + _ShellNotifyMsg = User32.RegisterWindowMessageW(User32.SHELLHOOK); + if (!User32.RegisterShellHookWindow(_window.Handle)) { - _window = new Win32Window(); - _window.Initialize(WndProc); - _ShellNotifyMsg = User32.RegisterWindowMessageW(User32.SHELLHOOK); - if (!User32.RegisterShellHookWindow(_window.Handle)) - { - Trace.WriteLine("Failed to register shell hook window"); - } + Trace.WriteLine("Failed to register shell hook window"); } + } - void WndProc(Message m) + private void WndProc(Message m) + { + if (m.Msg == _ShellNotifyMsg) { - if (m.Msg == _ShellNotifyMsg) + if (m.WParam.ToInt32() == User32.HSHELL_WINDOWCREATED) + { + WindowCreated?.Invoke(m.LParam); + } + else if (m.WParam.ToInt32() == User32.HSHELL_WINDOWDESTROYED) { - if (m.WParam.ToInt32() == User32.HSHELL_WINDOWCREATED) - { - WindowCreated?.Invoke(m.LParam); - } - else if (m.WParam.ToInt32() == User32.HSHELL_WINDOWDESTROYED) - { - WindowDestroyed?.Invoke(m.LParam); - } + WindowDestroyed?.Invoke(m.LParam); } } } + + public void Dispose() + { + _window.Dispose(); + } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/Interop/User32.cs b/EarTrumpet/Addons/EarTrumpet.Actions/Interop/User32.cs index d6f1757a4..188fde177 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/Interop/User32.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/Interop/User32.cs @@ -1,19 +1,18 @@ using System; using System.Runtime.InteropServices; -namespace EarTrumpet.Actions.Interop +namespace EarTrumpet.Actions.Interop; + +internal class User32 { - class User32 - { - public static readonly string SHELLHOOK = "SHELLHOOK"; - public const int HSHELL_WINDOWCREATED = 1; - public const int HSHELL_WINDOWDESTROYED = 2; + public static readonly string SHELLHOOK = "SHELLHOOK"; + public const int HSHELL_WINDOWCREATED = 1; + public const int HSHELL_WINDOWDESTROYED = 2; - [DllImport("user32.dll", PreserveSig = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool RegisterShellHookWindow(IntPtr hWnd); + [DllImport("user32.dll", PreserveSig = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool RegisterShellHookWindow(IntPtr hWnd); - [DllImport("user32.dll", PreserveSig = true)] - public static extern uint RegisterWindowMessageW([MarshalAs(UnmanagedType.LPWStr)] string msg); - } + [DllImport("user32.dll", PreserveSig = true)] + public static extern uint RegisterWindowMessageW([MarshalAs(UnmanagedType.LPWStr)] string msg); } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetAppMuteActionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetAppMuteActionViewModel.cs index 1557a5772..2b90c78db 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetAppMuteActionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetAppMuteActionViewModel.cs @@ -1,26 +1,25 @@ using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.ViewModel.Actions +namespace EarTrumpet.Actions.ViewModel.Actions; + +internal class SetAppMuteActionViewModel : PartViewModel { - class SetAppMuteActionViewModel : PartViewModel - { - public OptionViewModel Option { get; } - public DeviceListViewModel Device { get; } - public AppListViewModel App { get; } + public OptionViewModel Option { get; } + public DeviceListViewModel Device { get; } + public AppListViewModel App { get; } - private SetAppMuteAction _action; + private SetAppMuteAction _action; - public SetAppMuteActionViewModel(SetAppMuteAction action) : base(action) - { - _action = action; + public SetAppMuteActionViewModel(SetAppMuteAction action) : base(action) + { + _action = action; - Option = new OptionViewModel(action, nameof(action.Option)); - App = new AppListViewModel(action, AppListViewModel.AppKind.EveryApp | AppListViewModel.AppKind.ForegroundApp); - Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.DefaultPlayback); + Option = new OptionViewModel(action, nameof(action.Option)); + App = new AppListViewModel(action, AppListViewModel.AppKind.EveryApp | AppListViewModel.AppKind.ForegroundApp); + Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.DefaultPlayback); - Attach(Option); - Attach(App); - Attach(Device); - } + Attach(Option); + Attach(App); + Attach(Device); } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetAppVolumeActionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetAppVolumeActionViewModel.cs index 998033efc..3cb687c4a 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetAppVolumeActionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetAppVolumeActionViewModel.cs @@ -1,44 +1,71 @@ -using EarTrumpet.Actions.DataModel.Serialization; -using EarTrumpet.Actions.DataModel.Enum; +using EarTrumpet.Actions.DataModel.Enum; +using EarTrumpet.Actions.DataModel.Serialization; +using EarTrumpet.Extensions; +using System; -namespace EarTrumpet.Actions.ViewModel.Actions +namespace EarTrumpet.Actions.ViewModel.Actions; + +internal class SetAppVolumeActionViewModel : PartViewModel { - class SetAppVolumeActionViewModel : PartViewModel - { - public OptionViewModel Option { get; } - public DeviceListViewModel Device { get; } - public AppListViewModel App { get; } - public VolumeViewModel Volume { get; } + public OptionViewModel Option { get; } + public OptionViewModel Unit { get; } + public DeviceListViewModel Device { get; } + public AppListViewModel App { get; } + public VolumeViewModel Volume { get; } - private SetAppVolumeAction _action; + private SetAppVolumeAction _action; - public SetAppVolumeActionViewModel(SetAppVolumeAction action) : base(action) - { - _action = action; + public SetAppVolumeActionViewModel(SetAppVolumeAction action) : base(action) + { + _action = action; - Option = new OptionViewModel(action, nameof(action.Option)); - App = new AppListViewModel(action, AppListViewModel.AppKind.EveryApp | AppListViewModel.AppKind.ForegroundApp); - Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.DefaultPlayback); - Volume = new VolumeViewModel(action); + Option = new OptionViewModel(action, nameof(action.Option)); + Unit = new OptionViewModel(action, nameof(action.Unit)); + App = new AppListViewModel(action, AppListViewModel.AppKind.EveryApp | AppListViewModel.AppKind.ForegroundApp); + Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.DefaultPlayback); + Volume = new VolumeViewModel(action); - Attach(Option); - Attach(App); - Attach(Device); - Attach(Volume); - } + Attach(Option); + Attach(Unit); + Attach(App); + Attach(Device); + Attach(Volume); - public override string LinkText + Option.PropertyChanged += (s, e) => { - get + if (e.PropertyName == nameof(OptionViewModel.Selected)) { - if (_action.Option == SetVolumeKind.Set) - { - return base.LinkText; - } - else + Volume.UpdateRange(); + Volume.Volume = 0; + } + }; + + Unit.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(OptionViewModel.Selected)) + { + Volume.UpdateRange(); + Volume.Volume = (VolumeUnit)Unit.Selected.Value switch { - return Properties.Resources.SetAppVolumeAction_LinkTextIncrement; - } + VolumeUnit.Percentage => Math.Round(Volume.Volume.LogToLinear() * 100), + VolumeUnit.Decibel => Math.Round((Volume.Volume / 100).LinearToLog(), 1), + _ => throw new ArgumentException("Invalid volume unit."), + }; + } + }; + } + + public override string LinkText + { + get + { + if (_action.Option == SetVolumeKind.Set) + { + return base.LinkText; + } + else + { + return Properties.Resources.SetAppVolumeAction_LinkTextIncrement; } } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDefaultDeviceActionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDefaultDeviceActionViewModel.cs index 78338b6b9..741dd7a6d 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDefaultDeviceActionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDefaultDeviceActionViewModel.cs @@ -1,15 +1,14 @@ using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.ViewModel.Actions +namespace EarTrumpet.Actions.ViewModel.Actions; + +internal class SetDefaultDeviceActionViewModel : PartViewModel { - class SetDefaultDeviceActionViewModel : PartViewModel - { - public DeviceListViewModel Device { get; } + public DeviceListViewModel Device { get; } - public SetDefaultDeviceActionViewModel(SetDefaultDeviceAction action) : base(action) - { - Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.Recording); - Attach(Device); - } + public SetDefaultDeviceActionViewModel(SetDefaultDeviceAction action) : base(action) + { + Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.Recording); + Attach(Device); } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDeviceMuteActionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDeviceMuteActionViewModel.cs index e52f00466..a95f4e58c 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDeviceMuteActionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDeviceMuteActionViewModel.cs @@ -1,36 +1,35 @@ using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.ViewModel.Actions +namespace EarTrumpet.Actions.ViewModel.Actions; + +internal class SetDeviceMuteActionViewModel : PartViewModel { - class SetDeviceMuteActionViewModel : PartViewModel - { - public OptionViewModel Option { get; } - public DeviceListViewModel Device { get; } + public OptionViewModel Option { get; } + public DeviceListViewModel Device { get; } - private SetDeviceMuteAction _action; + private SetDeviceMuteAction _action; - public SetDeviceMuteActionViewModel(SetDeviceMuteAction action) : base(action) - { - _action = action; - Option = new OptionViewModel(action, nameof(action.Option)); - Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.Recording | DeviceListViewModel.DeviceListKind.DefaultPlayback); + public SetDeviceMuteActionViewModel(SetDeviceMuteAction action) : base(action) + { + _action = action; + Option = new OptionViewModel(action, nameof(action.Option)); + Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.Recording | DeviceListViewModel.DeviceListKind.DefaultPlayback); - Attach(Option); - Attach(Device); - } + Attach(Option); + Attach(Device); + } - public override string LinkText + public override string LinkText + { + get { - get + if (_action.Option == DataModel.Enum.MuteKind.ToggleMute) + { + return Properties.Resources.SetDeviceMuteAction_LinkTextToggle; + } + else { - if (_action.Option == DataModel.Enum.MuteKind.ToggleMute) - { - return Properties.Resources.SetDeviceMuteAction_LinkTextToggle; - } - else - { - return base.LinkText; - } + return base.LinkText; } } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDeviceVolumeActionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDeviceVolumeActionViewModel.cs index b27139ee7..2e8a12271 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDeviceVolumeActionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDeviceVolumeActionViewModel.cs @@ -1,40 +1,67 @@ using EarTrumpet.Actions.DataModel.Enum; using EarTrumpet.Actions.DataModel.Serialization; +using EarTrumpet.Extensions; +using System; -namespace EarTrumpet.Actions.ViewModel.Actions +namespace EarTrumpet.Actions.ViewModel.Actions; + +internal class SetDeviceVolumeActionViewModel : PartViewModel { - class SetDeviceVolumeActionViewModel : PartViewModel + public OptionViewModel Option { get; } + public OptionViewModel Unit { get; } + public DeviceListViewModel Device { get; } + public VolumeViewModel Volume { get; } + + private SetDeviceVolumeAction _action; + + public SetDeviceVolumeActionViewModel(SetDeviceVolumeAction action) : base(action) { - public OptionViewModel Option { get; } - public DeviceListViewModel Device { get; } - public VolumeViewModel Volume { get; } + _action = action; + Option = new OptionViewModel(action, nameof(action.Option)); + Unit = new OptionViewModel(action, nameof(action.Unit)); + Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.Recording | DeviceListViewModel.DeviceListKind.DefaultPlayback); + Volume = new VolumeViewModel(action); - private SetDeviceVolumeAction _action; + Attach(Option); + Attach(Unit); + Attach(Device); + Attach(Volume); - public SetDeviceVolumeActionViewModel(SetDeviceVolumeAction action) : base(action) + Option.PropertyChanged += (s, e) => { - _action = action; - Option = new OptionViewModel(action, nameof(action.Option)); - Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.Recording | DeviceListViewModel.DeviceListKind.DefaultPlayback); - Volume = new VolumeViewModel(action); - - Attach(Option); - Attach(Device); - Attach(Volume); - } + if (e.PropertyName == nameof(OptionViewModel.Selected)) + { + Volume.UpdateRange(); + Volume.Volume = 0; + } + }; - public override string LinkText + Unit.PropertyChanged += (s, e) => { - get + if (e.PropertyName == nameof(OptionViewModel.Selected)) { - if (_action.Option == SetVolumeKind.Set) + Volume.UpdateRange(); + Volume.Volume = (VolumeUnit)Unit.Selected.Value switch { - return base.LinkText; - } - else - { - return Properties.Resources.SetDeviceVolumeAction_LinkTextIncrement; - } + VolumeUnit.Percentage => 100, + VolumeUnit.Decibel => 0, + _ => throw new ArgumentException("Invalid volume unit."), + }; + } + }; + } + + public override string LinkText + { + get + { + if (_action.Option == SetVolumeKind.Set) + { + return base.LinkText; + } + else + { + return Properties.Resources.SetDeviceVolumeAction_LinkTextIncrement; } } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetVariableActionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetVariableActionViewModel.cs index 24fa32673..83c19a858 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetVariableActionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetVariableActionViewModel.cs @@ -1,19 +1,18 @@ using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.ViewModel.Actions +namespace EarTrumpet.Actions.ViewModel.Actions; + +internal class SetVariableActionViewModel : PartViewModel { - class SetVariableActionViewModel : PartViewModel - { - public OptionViewModel Option { get; } - public TextViewModel Text { get; } + public OptionViewModel Option { get; } + public TextViewModel Text { get; } - public SetVariableActionViewModel(SetVariableAction action) : base(action) - { - Option = new OptionViewModel(action, nameof(action.Value)); - Text = new TextViewModel(action); + public SetVariableActionViewModel(SetVariableAction action) : base(action) + { + Option = new OptionViewModel(action, nameof(action.Value)); + Text = new TextViewModel(action); - Attach(Option); - Attach(Text); - } + Attach(Option); + Attach(Text); } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ActionsCategoryViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ActionsCategoryViewModel.cs index aaa08526a..358b16c78 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ActionsCategoryViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ActionsCategoryViewModel.cs @@ -1,115 +1,112 @@ -using EarTrumpet.Extensions; +using System.Collections.ObjectModel; +using System.Linq; +using EarTrumpet.Actions.DataModel.Serialization; +using EarTrumpet.Extensions; using EarTrumpet.UI.Helpers; using EarTrumpet.UI.ViewModels; -using EarTrumpet.Actions.DataModel.Serialization; -using System; -using System.Collections.ObjectModel; -using System.Linq; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +public class ActionsCategoryViewModel : SettingsCategoryViewModel { - public class ActionsCategoryViewModel : SettingsCategoryViewModel + public ActionsCategoryViewModel() + : base(Properties.Resources.MyActionsText, "\xE950", Properties.Resources.AddonDescriptionText, EarTrumpetActionsAddon.Current.Manifest.Id, new ObservableCollection()) { - public ActionsCategoryViewModel() - : base(Properties.Resources.MyActionsText, "\xE950", Properties.Resources.AddonDescriptionText, EarTrumpetActionsAddon.Current.Manifest.Id, new ObservableCollection()) - { - // Get a 'fresh' copy so that we can edit the objects and still go back later. - var actions = EarTrumpetActionsAddon.Current.Actions; - EarTrumpetActionsAddon.Current.Actions = EarTrumpetActionsAddon.Current.Actions; + // Get a 'fresh' copy so that we can edit the objects and still go back later. + var actions = EarTrumpetActionsAddon.Current.Actions; + EarTrumpetActionsAddon.Current.Actions = EarTrumpetActionsAddon.Current.Actions; - Pages.AddRange(actions.Select(a => new EarTrumpetActionViewModel(this, a))); - Pages.Add(new ImportExportPageViewModel(this)); + Pages.AddRange(actions.Select(a => new EarTrumpetActionViewModel(this, a))); + Pages.Add(new ImportExportPageViewModel(this)); - Toolbar = new ToolbarItemViewModel[] { new ToolbarItemViewModel{ - Command = new RelayCommand(() => - { - var vm = new EarTrumpetActionViewModel(this, new EarTrumpetAction { DisplayName = Properties.Resources.NewActionText }); - vm.IsWorkSaved = false; - vm.IsPersisted = false; + Toolbar = [ new() { + Command = new RelayCommand(() => + { + var vm = new EarTrumpetActionViewModel(this, new EarTrumpetAction { DisplayName = Properties.Resources.NewActionText }) { IsWorkSaved = false, + IsPersisted = false }; - vm.PropertyChanged += (_, e) => + vm.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(vm.IsSelected) && + vm.IsSelected && !Pages.Contains(vm)) { - if (e.PropertyName == nameof(vm.IsSelected) && - vm.IsSelected && !Pages.Contains(vm)) - { - Pages.Insert(0, vm); - } - }; + Pages.Insert(0, vm); + } + }; - Selected = vm; - }), - DisplayName = Properties.Resources.NewActionText, - Glyph = "\xE948", - GlyphFontSize = 15, - } }; - - if (Pages.Count == 2) - { - Toolbar[0].Command.Execute(null); - } - } + Selected = vm; + }), + DisplayName = Properties.Resources.NewActionText, + Glyph = "\xE948", + GlyphFontSize = 15, + } ]; - internal void ReloadSavedPages() + if (Pages.Count == 2) { - foreach (var item in Pages.Where(p => p is EarTrumpetActionViewModel).ToList()) - { - Pages.Remove(item); - } - - Pages.InsertRange(0, new System.Collections.ObjectModel.ObservableCollection(EarTrumpetActionsAddon.Current.Actions.Select(a => new EarTrumpetActionViewModel(this, a)))); - Selected = Pages[0]; + Toolbar[0].Command.Execute(null); } + } - public void Delete(EarTrumpetActionViewModel earTrumpetActionViewModel, bool promptOverride = false) + internal void ReloadSavedPages() + { + foreach (var item in Pages.Where(p => p is EarTrumpetActionViewModel).ToList()) { - Action doRemove = () => - { - var actions = EarTrumpetActionsAddon.Current.Actions.ToList(); - if (actions.Any(a => a.Id == earTrumpetActionViewModel.Id)) - { - actions.Remove(item => item.Id == earTrumpetActionViewModel.Id); - } - EarTrumpetActionsAddon.Current.Actions = actions.ToArray(); - - if (Pages.Any(a => a == earTrumpetActionViewModel)) - { - Pages.Remove(earTrumpetActionViewModel); - } - }; - - if (earTrumpetActionViewModel.IsPersisted && !promptOverride) - { - _parent.ShowDialog(Properties.Resources.DeleteActionDialogTitle, Properties.Resources.DeleteActionDialogText, - Properties.Resources.DeleteActionDialogYesText, Properties.Resources.DeleteActionDialogNoText, doRemove, () => { }); - } - else - { - doRemove(); - } + Pages.Remove(item); } - public void Save(EarTrumpetActionViewModel earTrumpetActionViewModel) + Pages.InsertRange(0, new System.Collections.ObjectModel.ObservableCollection(EarTrumpetActionsAddon.Current.Actions.Select(a => new EarTrumpetActionViewModel(this, a)))); + Selected = Pages[0]; + } + + public void Delete(EarTrumpetActionViewModel earTrumpetActionViewModel, bool promptOverride = false) + { + void doRemove() { var actions = EarTrumpetActionsAddon.Current.Actions.ToList(); if (actions.Any(a => a.Id == earTrumpetActionViewModel.Id)) { actions.Remove(item => item.Id == earTrumpetActionViewModel.Id); } - actions.Insert(0, earTrumpetActionViewModel.GetAction()); - EarTrumpetActionsAddon.Current.Actions = actions.ToArray(); - earTrumpetActionViewModel.IsWorkSaved = true; + EarTrumpetActionsAddon.Current.Actions = [.. actions]; if (Pages.Any(a => a == earTrumpetActionViewModel)) { Pages.Remove(earTrumpetActionViewModel); } - Pages.Insert(0, earTrumpetActionViewModel); - Selected = Pages[0]; } - public void CompleteNavigation(NavigationCookie cookie) + if (earTrumpetActionViewModel.IsPersisted && !promptOverride) { - _parent.CompleteNavigation(cookie); + _parent.ShowDialog(Properties.Resources.DeleteActionDialogTitle, Properties.Resources.DeleteActionDialogText, + Properties.Resources.DeleteActionDialogYesText, Properties.Resources.DeleteActionDialogNoText, doRemove, () => { }); } + else + { + doRemove(); + } + } + + public void Save(EarTrumpetActionViewModel earTrumpetActionViewModel) + { + var actions = EarTrumpetActionsAddon.Current.Actions.ToList(); + if (actions.Any(a => a.Id == earTrumpetActionViewModel.Id)) + { + actions.Remove(item => item.Id == earTrumpetActionViewModel.Id); + } + actions.Insert(0, earTrumpetActionViewModel.GetAction()); + EarTrumpetActionsAddon.Current.Actions = [.. actions]; + earTrumpetActionViewModel.IsWorkSaved = true; + + if (Pages.Any(a => a == earTrumpetActionViewModel)) + { + Pages.Remove(earTrumpetActionViewModel); + } + Pages.Insert(0, earTrumpetActionViewModel); + Selected = Pages[0]; + } + + public void CompleteNavigation(NavigationCookie cookie) + { + _parent.CompleteNavigation(cookie); } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/AppListViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/AppListViewModel.cs index d3348917d..d03f97cba 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/AppListViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/AppListViewModel.cs @@ -10,70 +10,79 @@ using EarTrumpet.DataModel.WindowsAudio; using EarTrumpet.DataModel.Audio; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +internal class AppListViewModel : BindableBase { - class AppListViewModel : BindableBase + [Flags] + public enum AppKind { - [Flags] - public enum AppKind + Default = 0, + EveryApp = 1, + ForegroundApp = 2, + } + + public ObservableCollection All { get; } + + private IPartWithApp _part; + + public AppListViewModel(IPartWithApp part, AppKind flags) + { + _part = part; + All = []; + + GetApps(flags); + + if (part.App?.Id == null) { - Default = 0, - EveryApp = 1, - ForegroundApp = 2, + _part.App = new AppRef { Id = All[0].Id }; } + } - public ObservableCollection All { get; } + public void OnInvoked(object sender, IAppItemViewModel viewModel) + { + _part.App = new AppRef { Id = viewModel.Id }; + RaisePropertyChanged(""); // Signal change so ToString will be called. - private IPartWithApp _part; + var popup = ((DependencyObject)sender).FindVisualParent(); + popup.IsOpen = false; + } - public AppListViewModel(IPartWithApp part, AppKind flags) - { - _part = part; - All = new ObservableCollection(); + public override string ToString() + { + var existing = All.FirstOrDefault(d => d.Id == _part.App?.Id); - GetApps(flags); + // Fallback to checking against package full path, for compatibility with older actions + // that predate changes to how we track packaged applications. + // https://github.com/File-New-Project/EarTrumpet/issues/1524 - if (part.App?.Id == null) - { - _part.App = new AppRef { Id = All[0].Id }; - } + if (existing == null) + { + existing = All.FirstOrDefault(d => d.PackageInstallPath == _part.App?.Id); } - public void OnInvoked(object sender, IAppItemViewModel vivewModel) + if (existing != null) { - _part.App = new AppRef { Id = vivewModel.Id }; - RaisePropertyChanged(""); // Signal change so ToString will be called. + return existing.DisplayName; + } + return _part.App?.Id; + } - var popup = ((DependencyObject)sender).FindVisualParent(); - popup.IsOpen = false; + public void GetApps(AppKind flags) + { + if ((flags & AppKind.EveryApp) == AppKind.EveryApp) + { + All.Add(new EveryAppViewModel()); } - public override string ToString() + if ((flags & AppKind.ForegroundApp) == AppKind.ForegroundApp) { - var existing = All.FirstOrDefault(d => d.Id == _part.App?.Id); - if (existing != null) - { - return existing.DisplayName; - } - return _part.App?.Id; + All.Add(new ForegroundAppViewModel()); } - public void GetApps(AppKind flags) + foreach (var app in WindowsAudioFactory.Create(AudioDeviceKind.Playback).Devices.SelectMany(d => d.Groups).Distinct(IAudioDeviceSessionComparer.Instance).OrderBy(d => d.DisplayName).OrderBy(d => d.DisplayName)) { - if ((flags & AppKind.EveryApp) == AppKind.EveryApp) - { - All.Add(new EveryAppViewModel()); - } - - if ((flags & AppKind.ForegroundApp) == AppKind.ForegroundApp) - { - All.Add(new ForegroundAppViewModel()); - } - - foreach (var app in WindowsAudioFactory.Create(AudioDeviceKind.Playback).Devices.SelectMany(d => d.Groups).Distinct(IAudioDeviceSessionComparer.Instance).OrderBy(d => d.DisplayName).OrderBy(d => d.DisplayName)) - { - All.Add(new SettingsAppItemViewModel(app)); - } + All.Add(new SettingsAppItemViewModel(app)); } } } \ No newline at end of file diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/DefaultDeviceConditionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/DefaultDeviceConditionViewModel.cs index 7a50995bb..af45c1a17 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/DefaultDeviceConditionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/DefaultDeviceConditionViewModel.cs @@ -1,20 +1,19 @@ using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.ViewModel.Conditions +namespace EarTrumpet.Actions.ViewModel.Conditions; + +internal class DefaultDeviceConditionViewModel : PartViewModel { - class DefaultDeviceConditionViewModel : PartViewModel - { - public DeviceListViewModel Device { get; } + public DeviceListViewModel Device { get; } - public OptionViewModel Option { get; } + public OptionViewModel Option { get; } - public DefaultDeviceConditionViewModel(DefaultDeviceCondition condition) : base(condition) - { - Option = new OptionViewModel(condition, nameof(condition.Option)); - Device = new DeviceListViewModel(condition, DeviceListViewModel.DeviceListKind.Recording); + public DefaultDeviceConditionViewModel(DefaultDeviceCondition condition) : base(condition) + { + Option = new OptionViewModel(condition, nameof(condition.Option)); + Device = new DeviceListViewModel(condition, DeviceListViewModel.DeviceListKind.Recording); - Attach(Option); - Attach(Device); - } + Attach(Option); + Attach(Device); } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/ProcessConditionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/ProcessConditionViewModel.cs index 6aea1a4e3..3a8bbe6ef 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/ProcessConditionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/ProcessConditionViewModel.cs @@ -1,20 +1,19 @@ using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.ViewModel.Conditions +namespace EarTrumpet.Actions.ViewModel.Conditions; + +internal class ProcessConditionViewModel : PartViewModel { - class ProcessConditionViewModel : PartViewModel - { - public OptionViewModel Option { get; } + public OptionViewModel Option { get; } - public TextViewModel Text { get; } + public TextViewModel Text { get; } - public ProcessConditionViewModel(ProcessCondition condition) : base(condition) - { - Option = new OptionViewModel(condition, nameof(condition.Option)); - Text = new TextViewModel(condition); + public ProcessConditionViewModel(ProcessCondition condition) : base(condition) + { + Option = new OptionViewModel(condition, nameof(condition.Option)); + Text = new TextViewModel(condition); - Attach(Option); - Attach(Text); - } + Attach(Option); + Attach(Text); } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/VariableConditionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/VariableConditionViewModel.cs index 6747d34db..19bb5bfbf 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/VariableConditionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/VariableConditionViewModel.cs @@ -1,19 +1,18 @@ using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.ViewModel.Conditions +namespace EarTrumpet.Actions.ViewModel.Conditions; + +internal class VariableConditionViewModel : PartViewModel { - class VariableConditionViewModel : PartViewModel - { - public OptionViewModel Option { get; } - public TextViewModel Text { get; } + public OptionViewModel Option { get; } + public TextViewModel Text { get; } - public VariableConditionViewModel(VariableCondition condition) : base(condition) - { - Option = new OptionViewModel(condition, nameof(condition.Value)); - Text = new TextViewModel(condition); + public VariableConditionViewModel(VariableCondition condition) : base(condition) + { + Option = new OptionViewModel(condition, nameof(condition.Value)); + Text = new TextViewModel(condition); - Attach(Option); - Attach(Text); - } + Attach(Option); + Attach(Text); } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DefaultPlaybackDeviceViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DefaultPlaybackDeviceViewModel.cs index 1a6b0e39e..5bc75b09a 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DefaultPlaybackDeviceViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DefaultPlaybackDeviceViewModel.cs @@ -1,14 +1,13 @@ using EarTrumpet.DataModel.WindowsAudio; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +internal class DefaultPlaybackDeviceViewModel : DeviceViewModelBase { - class DefaultPlaybackDeviceViewModel : DeviceViewModelBase + public DefaultPlaybackDeviceViewModel() { - public DefaultPlaybackDeviceViewModel() - { - DisplayName = Properties.Resources.DefaultPlaybackDeviceText; - Kind = AudioDeviceKind.Playback.ToString(); - GroupName = Properties.Resources.PlaybackDeviceGroupText; - } + DisplayName = Properties.Resources.DefaultPlaybackDeviceText; + Kind = AudioDeviceKind.Playback.ToString(); + GroupName = Properties.Resources.PlaybackDeviceGroupText; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceListViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceListViewModel.cs index 59406f789..54eb7ba83 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceListViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceListViewModel.cs @@ -8,74 +8,73 @@ using System.Windows.Controls.Primitives; using EarTrumpet.DataModel.WindowsAudio; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +public class DeviceListViewModel : BindableBase { - public class DeviceListViewModel : BindableBase + [Flags] + public enum DeviceListKind { - [Flags] - public enum DeviceListKind - { - Playback = 0, - Recording = 1, - DefaultPlayback = 2 - } + Playback = 0, + Recording = 1, + DefaultPlayback = 2 + } - public ObservableCollection All { get; } + public ObservableCollection All { get; } - public void OnInvoked(object sender, DeviceViewModelBase vivewModel) - { - _part.Device = new Device { Id = vivewModel.Id, Kind = vivewModel.Kind }; - RaisePropertyChanged(""); // Signal change so ToString will be called. + public void OnInvoked(object sender, DeviceViewModelBase vivewModel) + { + _part.Device = new Device { Id = vivewModel.Id, Kind = vivewModel.Kind }; + RaisePropertyChanged(""); // Signal change so ToString will be called. - var popup = ((DependencyObject)sender).FindVisualParent(); - popup.IsOpen = false; - } + var popup = ((DependencyObject)sender).FindVisualParent(); + popup.IsOpen = false; + } - private IPartWithDevice _part; + private IPartWithDevice _part; - public DeviceListViewModel(IPartWithDevice part, DeviceListKind flags) - { - _part = part; - All = new ObservableCollection(); - GetDevices(flags); + public DeviceListViewModel(IPartWithDevice part, DeviceListKind flags) + { + _part = part; + All = []; + GetDevices(flags); - if (_part.Device == null) - { - _part.Device = new Device { Id = All[0].Id, Kind = All[0].Kind }; - } + if (_part.Device == null) + { + _part.Device = new Device { Id = All[0].Id, Kind = All[0].Kind }; } + } - public override string ToString() + public override string ToString() + { + var existing = All.FirstOrDefault(d => d.Id == _part.Device?.Id); + if (existing != null) { - var existing = All.FirstOrDefault(d => d.Id == _part.Device?.Id); - if (existing != null) - { - return existing.DisplayName; - } - return _part.Device?.Id; + return existing.DisplayName; } + return _part.Device?.Id; + } - void GetDevices(DeviceListKind flags) + private void GetDevices(DeviceListKind flags) + { + var isRecording = (flags & DeviceListKind.Recording) == DeviceListKind.Recording; + + if ((flags & DeviceListKind.DefaultPlayback) == DeviceListKind.DefaultPlayback) { - bool isRecording = (flags & DeviceListKind.Recording) == DeviceListKind.Recording; + All.Add(new DefaultPlaybackDeviceViewModel()); + } - if ((flags & DeviceListKind.DefaultPlayback) == DeviceListKind.DefaultPlayback) - { - All.Add(new DefaultPlaybackDeviceViewModel()); - } + foreach (var device in WindowsAudioFactory.Create(AudioDeviceKind.Playback).Devices.OrderBy(d => d.DisplayName)) + { + All.Add(new DeviceViewModel(device)); + } - foreach (var device in WindowsAudioFactory.Create(AudioDeviceKind.Playback).Devices.OrderBy(d => d.DisplayName)) + if (isRecording) + { + foreach (var device in WindowsAudioFactory.Create(AudioDeviceKind.Recording).Devices.OrderBy(d => d.DisplayName)) { All.Add(new DeviceViewModel(device)); } - - if (isRecording) - { - foreach (var device in WindowsAudioFactory.Create(AudioDeviceKind.Recording).Devices.OrderBy(d => d.DisplayName)) - { - All.Add(new DeviceViewModel(device)); - } - } } } } \ No newline at end of file diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceViewModel.cs index 1911e1fc4..f072a2a73 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceViewModel.cs @@ -2,28 +2,27 @@ using EarTrumpet.DataModel.WindowsAudio; using EarTrumpet.UI.Helpers; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +public class DeviceViewModel : DeviceViewModelBase, IAppIconSource { - public class DeviceViewModel : DeviceViewModelBase, IAppIconSource - { - public bool IsDesktopApp => true; - public string IconPath => _device.IconPath; - public string DeviceDescription => ((IAudioDeviceWindowsAudio)_device).DeviceDescription; - public string EnumeratorName => ((IAudioDeviceWindowsAudio)_device).EnumeratorName; - public string InterfaceName => ((IAudioDeviceWindowsAudio)_device).InterfaceName; + public bool IsDesktopApp => true; + public string IconPath => _device.IconPath; + public string DeviceDescription => ((IAudioDeviceWindowsAudio)_device).DeviceDescription; + public string EnumeratorName => ((IAudioDeviceWindowsAudio)_device).EnumeratorName; + public string InterfaceName => ((IAudioDeviceWindowsAudio)_device).InterfaceName; - private readonly IAudioDevice _device; + private readonly IAudioDevice _device; - public DeviceViewModel(IAudioDevice device) - { - _device = device; - Id = _device.Id; - DisplayName = _device.DisplayName; - Kind = _device.Parent.Kind; + public DeviceViewModel(IAudioDevice device) + { + _device = device; + Id = _device.Id; + DisplayName = _device.DisplayName; + Kind = _device.Parent.Kind; - GroupName = _device.Parent.Kind == AudioDeviceKind.Playback.ToString() ? - Properties.Resources.PlaybackDeviceGroupText : - Properties.Resources.RecordingDeviceGroupText; - } + GroupName = _device.Parent.Kind == AudioDeviceKind.Playback.ToString() ? + Properties.Resources.PlaybackDeviceGroupText : + Properties.Resources.RecordingDeviceGroupText; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceViewModelBase.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceViewModelBase.cs index 26bfdbb55..f92698c0c 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceViewModelBase.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceViewModelBase.cs @@ -1,11 +1,10 @@ -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +public class DeviceViewModelBase : BindableBase { - public class DeviceViewModelBase : BindableBase - { - public string DisplayName { get; set; } - public string GroupName { get; set; } - public string Id { get; set; } - public string Kind { get; set; } + public string DisplayName { get; set; } + public string GroupName { get; set; } + public string Id { get; set; } + public string Kind { get; set; } - } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EarTrumpetActionPageHeaderViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EarTrumpetActionPageHeaderViewModel.cs index f161f129f..9adf2b10f 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EarTrumpetActionPageHeaderViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EarTrumpetActionPageHeaderViewModel.cs @@ -1,25 +1,24 @@ using EarTrumpet.UI.ViewModels; using System.ComponentModel; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +public class EarTrumpetActionPageHeaderViewModel : SettingsPageHeaderViewModel { - public class EarTrumpetActionPageHeaderViewModel : SettingsPageHeaderViewModel - { - EarTrumpetActionViewModel _parent; + private EarTrumpetActionViewModel _parent; - public ToolbarItemViewModel[] Toolbar => _parent.Toolbar; - public string DisplayName { get => _parent.DisplayName; set => _parent.DisplayName = value; } - public bool IsEditClicked { get => _parent.IsEditClicked; set => _parent.IsEditClicked = value; } - public bool IsWorkSaved => _parent.IsWorkSaved; - public EarTrumpetActionPageHeaderViewModel(EarTrumpetActionViewModel parent) : base(parent) - { - _parent = parent; - ((INotifyPropertyChanged)_parent).PropertyChanged += EarTrumpetActionPageHeaderViewModel_PropertyChanged; - } + public ToolbarItemViewModel[] Toolbar => _parent.Toolbar; + public string DisplayName { get => _parent.DisplayName; set => _parent.DisplayName = value; } + public bool IsEditClicked { get => _parent.IsEditClicked; set => _parent.IsEditClicked = value; } + public bool IsWorkSaved => _parent.IsWorkSaved; + public EarTrumpetActionPageHeaderViewModel(EarTrumpetActionViewModel parent) : base(parent) + { + _parent = parent; + ((INotifyPropertyChanged)_parent).PropertyChanged += EarTrumpetActionPageHeaderViewModel_PropertyChanged; + } - private void EarTrumpetActionPageHeaderViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) - { - RaisePropertyChanged(e.PropertyName); - } + private void EarTrumpetActionPageHeaderViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + RaisePropertyChanged(e.PropertyName); } } \ No newline at end of file diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EarTrumpetActionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EarTrumpetActionViewModel.cs index e6f9fa94a..fb3ba91eb 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EarTrumpetActionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EarTrumpetActionViewModel.cs @@ -8,223 +8,220 @@ using System.Linq; using System.Windows.Input; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +public class EarTrumpetActionViewModel : SettingsPageViewModel { - public class EarTrumpetActionViewModel : SettingsPageViewModel - { - public ToolbarItemViewModel[] Toolbar { get; private set; } - public ICommand Delete => new RelayCommand(() => _parent.Delete(this)); - public Guid Id => _action.Id; + public ToolbarItemViewModel[] Toolbar { get; private set; } + public ICommand Delete => new RelayCommand(() => _parent.Delete(this)); + public Guid Id => _action.Id; - public string DisplayName + public string DisplayName + { + get => _action.DisplayName; + set { - get => _action.DisplayName; - set + if (DisplayName != value) { - if (DisplayName != value) - { - _action.DisplayName = value; - RaisePropertyChanged(nameof(DisplayName)); - Title = DisplayName; + _action.DisplayName = value; + RaisePropertyChanged(nameof(DisplayName)); + Title = DisplayName; - IsWorkSaved = false; - IsPersisted = true; - } + IsWorkSaved = false; + IsPersisted = true; } } + } - private bool _isEditClicked; - public bool IsEditClicked + private bool _isEditClicked; + public bool IsEditClicked + { + get => _isEditClicked; + set { - get => _isEditClicked; - set + if (_isEditClicked != value) { - if (_isEditClicked != value) - { - _isEditClicked = value; - RaisePropertyChanged(nameof(IsEditClicked)); + _isEditClicked = value; + RaisePropertyChanged(nameof(IsEditClicked)); - // Immediately unset the value so we can go again. - _isEditClicked = false; - RaisePropertyChanged(nameof(IsEditClicked)); - } + // Immediately unset the value so we can go again. + _isEditClicked = false; + RaisePropertyChanged(nameof(IsEditClicked)); } } + } - private bool _isWorkSaved; - public bool IsWorkSaved + private bool _isWorkSaved; + public bool IsWorkSaved + { + get => _isWorkSaved; + set { - get => _isWorkSaved; - set + if (_isWorkSaved != value) { - if (_isWorkSaved != value) - { - _isWorkSaved = value; - RaisePropertyChanged(nameof(IsWorkSaved)); - } + _isWorkSaved = value; + RaisePropertyChanged(nameof(IsWorkSaved)); } } + } - public List NewTriggers => PartViewModelFactory.Create().Select(t => MakeItem(t)).OrderBy(t => t.DisplayName).ToList(); - public List NewConditions => PartViewModelFactory.Create().Select(t => MakeItem(t)).OrderBy(t => t.DisplayName).ToList(); - public List NewActions => PartViewModelFactory.Create().Select(t => MakeItem(t)).OrderBy(t => t.DisplayName).ToList(); + public List NewTriggers => [.. PartViewModelFactory.Create().Select(t => MakeItem(t)).OrderBy(t => t.DisplayName)]; + public List NewConditions => [.. PartViewModelFactory.Create().Select(t => MakeItem(t)).OrderBy(t => t.DisplayName)]; + public List NewActions => [.. PartViewModelFactory.Create().Select(t => MakeItem(t)).OrderBy(t => t.DisplayName)]; - public ObservableCollection Triggers { get; private set; } - public ObservableCollection Conditions { get; private set; } - public ObservableCollection Actions { get; private set; } - public bool IsPersisted { get; set; } = true; + public ObservableCollection Triggers { get; private set; } + public ObservableCollection Conditions { get; private set; } + public ObservableCollection Actions { get; private set; } + public bool IsPersisted { get; set; } = true; - private EarTrumpetAction _action; - private ActionsCategoryViewModel _parent; + private EarTrumpetAction _action; + private ActionsCategoryViewModel _parent; - public EarTrumpetActionViewModel(ActionsCategoryViewModel parent, EarTrumpetAction action) : base("Saved Actions") + public EarTrumpetActionViewModel(ActionsCategoryViewModel parent, EarTrumpetAction action) : base("Saved Actions") + { + _parent = parent; + Reset(action); + Header = new EarTrumpetActionPageHeaderViewModel(this); + Toolbar = + [ + new() { + Command = new RelayCommand(() => + { + IsEditClicked = true; + }), + DisplayName = Properties.Resources.ToolbarEditText, + Glyph = "\xE70F", + GlyphFontSize = 15, + }, + new() { + Command = new RelayCommand(() => + { + IsPersisted = true; + _parent.Save(this); + }), + DisplayName = Properties.Resources.ToolbarSaveText, + Id = "Save", + Glyph = "\xE105", + GlyphFontSize = 15, + }, + ]; + + Glyph = "\xE1CE"; + Title = DisplayName; + } + + public void Reset(EarTrumpetAction action) + { + _action = action; + + Title = DisplayName; + Triggers = new ObservableCollection(action.Triggers.Select(t => CreatePartViewModel(t))); + Conditions = new ObservableCollection(action.Conditions.Select(t => CreatePartViewModel(t))); + Actions = new ObservableCollection(action.Actions.Select(t => CreatePartViewModel(t))); + + Triggers.CollectionChanged += Parts_CollectionChanged; + Conditions.CollectionChanged += Parts_CollectionChanged; + Actions.CollectionChanged += Parts_CollectionChanged; + + Parts_CollectionChanged(Triggers, null); + Parts_CollectionChanged(Conditions, null); + Parts_CollectionChanged(Actions, null); + RaisePropertyChanged(nameof(Triggers)); + RaisePropertyChanged(nameof(Conditions)); + RaisePropertyChanged(nameof(Actions)); + RaisePropertyChanged(nameof(DisplayName)); + IsWorkSaved = true; + } + + + public override bool NavigatingFrom(NavigationCookie cookie) + { + if (!IsWorkSaved && IsPersisted) { - _parent = parent; - Reset(action); - Header = new EarTrumpetActionPageHeaderViewModel(this); - Toolbar = new ToolbarItemViewModel[] + _parent.ShowDialog(Properties.Resources.LeavingPageDialogTitle, Properties.Resources.LeavingPageDialogText, Properties.Resources.LeavingPageDialogYesText, () => { - new ToolbarItemViewModel + _parent.CompleteNavigation(cookie); + + var existing = EarTrumpetActionsAddon.Current.Actions.FirstOrDefault(a => a.Id == Id); + if (existing == null) { - Command = new RelayCommand(() => - { - IsEditClicked = true; - }), - DisplayName = Properties.Resources.ToolbarEditText, - Glyph = "\xE70F", - GlyphFontSize = 15, - }, - new ToolbarItemViewModel + _parent.Delete(this, true); + } + else { - Command = new RelayCommand(() => - { - IsPersisted = true; - _parent.Save(this); - }), - DisplayName = Properties.Resources.ToolbarSaveText, - Id = "Save", - Glyph = "\xE105", - GlyphFontSize = 15, - }, - }; - - Glyph = "\xE1CE"; - Title = DisplayName; + Reset(existing); + } + + }, Properties.Resources.LeavingPageDialogNoText, () => { }); + return false; } + return base.NavigatingFrom(cookie); + } - public void Reset(EarTrumpetAction action) - { - _action = action; - - Title = DisplayName; - Triggers = new ObservableCollection(action.Triggers.Select(t => CreatePartViewModel(t))); - Conditions = new ObservableCollection(action.Conditions.Select(t => CreatePartViewModel(t))); - Actions = new ObservableCollection(action.Actions.Select(t => CreatePartViewModel(t))); - - Triggers.CollectionChanged += Parts_CollectionChanged; - Conditions.CollectionChanged += Parts_CollectionChanged; - Actions.CollectionChanged += Parts_CollectionChanged; - - Parts_CollectionChanged(Triggers, null); - Parts_CollectionChanged(Conditions, null); - Parts_CollectionChanged(Actions, null); - RaisePropertyChanged(nameof(Triggers)); - RaisePropertyChanged(nameof(Conditions)); - RaisePropertyChanged(nameof(Actions)); - RaisePropertyChanged(nameof(DisplayName)); - IsWorkSaved = true; - } + public EarTrumpetAction GetAction() + { + _action.DisplayName = DisplayName; + _action.Triggers = new ObservableCollection(Triggers.Select(t => (BaseTrigger)t.Part)); + _action.Conditions = new ObservableCollection(Conditions.Select(t => (BaseCondition)t.Part)); + _action.Actions = new ObservableCollection(Actions.Select(t => (BaseAction)t.Part)); + return _action; + } + private void Parts_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + var col = (ObservableCollection)sender; - public override bool NavigatingFrom(NavigationCookie cookie) + for (var i = 0; i < col.Count; i++) { - if (!IsWorkSaved && IsPersisted) - { - _parent.ShowDialog(Properties.Resources.LeavingPageDialogTitle, Properties.Resources.LeavingPageDialogText, Properties.Resources.LeavingPageDialogYesText, () => - { - _parent.CompleteNavigation(cookie); - - var existing = EarTrumpetActionsAddon.Current.Actions.FirstOrDefault(a => a.Id == Id); - if (existing == null) - { - _parent.Delete(this, true); - } - else - { - Reset(existing); - } - - }, Properties.Resources.LeavingPageDialogNoText, () => { }); - return false; - } - return base.NavigatingFrom(cookie); + col[i].IsShowingAdditionalText = i != 0; } + IsWorkSaved = false; + IsPersisted = true; + } - public EarTrumpetAction GetAction() + private ContextMenuItem MakeItem(PartViewModel part) + { + return new ContextMenuItem { - _action.DisplayName = DisplayName; - _action.Triggers = new ObservableCollection(Triggers.Select(t => (BaseTrigger)t.Part)); - _action.Conditions = new ObservableCollection(Conditions.Select(t => (BaseCondition)t.Part)); - _action.Actions = new ObservableCollection(Actions.Select(t => (BaseAction)t.Part)); - return _action; - } + DisplayName = part.AddText, + Command = new RelayCommand(() => + { + InitializeViewModel(part); + GetListFromPart(part).Add(part); + }), + }; + } - private void Parts_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) - { - var col = (ObservableCollection)sender; + private PartViewModel CreatePartViewModel(Part part) + { + var ret = PartViewModelFactory.Create(part); + InitializeViewModel(ret); + return ret; + } - for (var i = 0; i < col.Count; i++) - { - col[i].IsShowingAdditionalText = i != 0; - } - IsWorkSaved = false; - IsPersisted = true; - } + private void InitializeViewModel(PartViewModel part) + { + part.PropertyChanged += (_, __) => IsWorkSaved = false; + part.Remove = new RelayCommand(() => GetListFromPart(part).Remove(part)); + } - private ContextMenuItem MakeItem(PartViewModel part) + private ObservableCollection GetListFromPart(PartViewModel part) + { + if (part.Part is BaseTrigger) { - return new ContextMenuItem - { - DisplayName = part.AddText, - Command = new RelayCommand(() => - { - InitializeViewModel(part); - GetListFromPart(part).Add(part); - }), - }; + return Triggers; } - - private PartViewModel CreatePartViewModel(Part part) + else if (part.Part is BaseCondition) { - var ret = PartViewModelFactory.Create(part); - InitializeViewModel(ret); - return ret; + return Conditions; } - - private void InitializeViewModel(PartViewModel part) + else if (part.Part is BaseAction) { - part.PropertyChanged += (_, __) => IsWorkSaved = false; - part.Remove = new RelayCommand(() => GetListFromPart(part).Remove(part)); + return Actions; } - - private ObservableCollection GetListFromPart(PartViewModel part) + else { - if (part.Part is BaseTrigger) - { - return Triggers; - } - else if (part.Part is BaseCondition) - { - return Conditions; - } - else if (part.Part is BaseAction) - { - return Actions; - } - else - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EveryAppViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EveryAppViewModel.cs index dedea8825..36b0ab244 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EveryAppViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EveryAppViewModel.cs @@ -1,14 +1,13 @@ using EarTrumpet.UI.ViewModels; using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +internal class EveryAppViewModel : SettingsAppItemViewModel { - class EveryAppViewModel : SettingsAppItemViewModel + public EveryAppViewModel() { - public EveryAppViewModel() - { - DisplayName = Properties.Resources.EveryAppText; - Id = AppRef.EveryAppId; - } + DisplayName = Properties.Resources.EveryAppText; + Id = AppRef.EveryAppId; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ForegroundAppViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ForegroundAppViewModel.cs index e05d0ce73..0518ca308 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ForegroundAppViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ForegroundAppViewModel.cs @@ -1,14 +1,13 @@ using EarTrumpet.UI.ViewModels; using EarTrumpet.Actions.DataModel.Serialization; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +internal class ForegroundAppViewModel : SettingsAppItemViewModel { - class ForegroundAppViewModel : SettingsAppItemViewModel + public ForegroundAppViewModel() { - public ForegroundAppViewModel() - { - Id = AppRef.ForegroundAppId; - DisplayName = Properties.Resources.ForegroundAppText; - } + Id = AppRef.ForegroundAppId; + DisplayName = Properties.Resources.ForegroundAppText; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/HotkeyViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/HotkeyViewModel.cs index 3d830d1c9..19ed9cd55 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/HotkeyViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/HotkeyViewModel.cs @@ -1,45 +1,45 @@ using EarTrumpet.Actions.DataModel.Serialization; using System; +using System.Globalization; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +public class HotkeyViewModel : BindableBase { - public class HotkeyViewModel : BindableBase - { - public EarTrumpet.UI.ViewModels.HotkeyViewModel Hotkey { get; } + public EarTrumpet.UI.ViewModels.HotkeyViewModel Hotkey { get; } - private HotkeyTrigger _trigger; + private HotkeyTrigger _trigger; - public HotkeyViewModel(HotkeyTrigger trigger) + public HotkeyViewModel(HotkeyTrigger trigger) + { + _trigger = trigger; + Hotkey = new EarTrumpet.UI.ViewModels.HotkeyViewModel(_trigger.Option, (newHotkey) => { - _trigger = trigger; - Hotkey = new EarTrumpet.UI.ViewModels.HotkeyViewModel(_trigger.Option, (newHotkey) => - { - _trigger.Option = newHotkey; - RaisePropertyChanged(nameof(Hotkey)); - }); - } + _trigger.Option = newHotkey; + RaisePropertyChanged(nameof(Hotkey)); + }); + } - public override string ToString() + public override string ToString() + { + if (_trigger.Option.IsEmpty) + { + return ResolveResource("EmptyText"); + } + else { - if (_trigger.Option.IsEmpty) - { - return ResolveResource("EmptyText"); - } - else - { - return _trigger.Option.ToString(); - } + return _trigger.Option.ToString(); } + } - private string ResolveResource(string suffix) + private string ResolveResource(string suffix) + { + var res = $"{_trigger.GetType().Name}_{suffix}"; + var ret = Properties.Resources.ResourceManager.GetString(res, CultureInfo.CurrentCulture); + if (string.IsNullOrWhiteSpace(ret)) { - var res = $"{_trigger.GetType().Name}_{suffix}"; - var ret = Properties.Resources.ResourceManager.GetString(res); - if (string.IsNullOrWhiteSpace(ret)) - { - throw new NotImplementedException($"Missing resource: {res}"); - } - return ret; + throw new NotImplementedException($"Missing resource: {res}"); } + return ret; } } diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/IOptionViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/IOptionViewModel.cs index 7e84d1053..915c2bfb8 100644 --- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/IOptionViewModel.cs +++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/IOptionViewModel.cs @@ -1,10 +1,9 @@ using System.Collections.ObjectModel; -namespace EarTrumpet.Actions.ViewModel +namespace EarTrumpet.Actions.ViewModel; + +internal interface IOptionViewModel { - interface IOptionViewModel - { - ObservableCollection