Repository: File-New-Project/EarTrumpet
Branch: master
Commit: b6ad9a786d28
Files: 424
Total size: 2.0 MB
Directory structure:
gitextract_y35wddg7/
├── .azure-pipelines.yml
├── .chocolatey/
│ ├── eartrumpet.nuspec
│ └── tools/
│ ├── LICENSE.txt
│ ├── VERIFICATION.txt
│ ├── chocolateybeforemodify.ps1
│ ├── chocolateyinstall.ps1
│ └── chocolateyuninstall.ps1
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── config.yml
│ └── workflows/
│ ├── main.yml
│ ├── sponsors.yml
│ └── translators.yml
├── .gitignore
├── CHANGELOG.md
├── COMPILING.md
├── CONTRIBUTING.md
├── EarTrumpet/
│ ├── Addons/
│ │ └── EarTrumpet.Actions/
│ │ ├── AddonResources.xaml
│ │ ├── Controls/
│ │ │ ├── LinkedTextBlock.cs
│ │ │ └── MenuButton.cs
│ │ ├── DataModel/
│ │ │ ├── Enum/
│ │ │ │ ├── AudioAppEventKind.cs
│ │ │ │ ├── AudioDeviceEventKind.cs
│ │ │ │ ├── BoolValue.cs
│ │ │ │ ├── ComparisonBoolKind.cs
│ │ │ │ ├── EarTrumpetEventKind.cs
│ │ │ │ ├── MuteKind.cs
│ │ │ │ ├── ProcessEventKind.cs
│ │ │ │ ├── ProcessStateKind.cs
│ │ │ │ └── SetVolumeKind.cs
│ │ │ ├── IPartWithApp.cs
│ │ │ ├── IPartWithDevice.cs
│ │ │ ├── IPartWithText.cs
│ │ │ ├── IPartWithVolume.cs
│ │ │ ├── LocalVariablesContainer.cs
│ │ │ ├── Part.cs
│ │ │ ├── ProcessWatcher.cs
│ │ │ ├── Processing/
│ │ │ │ ├── ActionProcessor.cs
│ │ │ │ ├── AudioTriggerManager.cs
│ │ │ │ ├── ConditionProcessor.cs
│ │ │ │ └── TriggerManager.cs
│ │ │ └── Serialization/
│ │ │ ├── Actions.cs
│ │ │ ├── App.cs
│ │ │ ├── Conditions.cs
│ │ │ ├── Device.cs
│ │ │ ├── EarTrumpetAction.cs
│ │ │ └── Triggers.cs
│ │ ├── EarTrumpetActionsAddon.cs
│ │ ├── Interop/
│ │ │ ├── Helpers/
│ │ │ │ └── WindowWatcher.cs
│ │ │ └── User32.cs
│ │ └── ViewModel/
│ │ ├── Actions/
│ │ │ ├── SetAppMuteActionViewModel.cs
│ │ │ ├── SetAppVolumeActionViewModel.cs
│ │ │ ├── SetDefaultDeviceActionViewModel.cs
│ │ │ ├── SetDeviceMuteActionViewModel.cs
│ │ │ ├── SetDeviceVolumeActionViewModel.cs
│ │ │ └── SetVariableActionViewModel.cs
│ │ ├── ActionsCategoryViewModel.cs
│ │ ├── AppListViewModel.cs
│ │ ├── Conditions/
│ │ │ ├── DefaultDeviceConditionViewModel.cs
│ │ │ ├── ProcessConditionViewModel.cs
│ │ │ └── VariableConditionViewModel.cs
│ │ ├── DefaultPlaybackDeviceViewModel.cs
│ │ ├── DeviceListViewModel.cs
│ │ ├── DeviceViewModel.cs
│ │ ├── DeviceViewModelBase.cs
│ │ ├── EarTrumpetActionPageHeaderViewModel.cs
│ │ ├── EarTrumpetActionViewModel.cs
│ │ ├── EveryAppViewModel.cs
│ │ ├── ForegroundAppViewModel.cs
│ │ ├── HotkeyViewModel.cs
│ │ ├── IOptionViewModel.cs
│ │ ├── ImportExportPageViewModel.cs
│ │ ├── Option.cs
│ │ ├── OptionViewModel.cs
│ │ ├── PartViewModel.cs
│ │ ├── PartViewModelFactory.cs
│ │ ├── TextViewModel.cs
│ │ ├── Triggers/
│ │ │ ├── AppEventTriggerViewModel.cs
│ │ │ ├── ContextMenuTriggerViewModel.cs
│ │ │ ├── DeviceEventTriggerViewModel.cs
│ │ │ ├── EventTriggerViewModel.cs
│ │ │ ├── HotkeyTriggerViewModel.cs
│ │ │ └── ProcessTriggerViewModel.cs
│ │ └── VolumeViewModel.cs
│ ├── App.config
│ ├── App.manifest
│ ├── App.xaml
│ ├── App.xaml.cs
│ ├── AppSettings.cs
│ ├── BindableBase.cs
│ ├── DataModel/
│ │ ├── AppInformation/
│ │ │ ├── AppInformationFactory.cs
│ │ │ ├── IAppInfo.cs
│ │ │ └── Internal/
│ │ │ ├── DesktopAppInfo.cs
│ │ │ ├── ModernAppInfo.cs
│ │ │ ├── SystemSoundsAppInfo.cs
│ │ │ └── ZombieProcessException.cs
│ │ ├── Audio/
│ │ │ ├── IAudioDevice.cs
│ │ │ ├── IAudioDeviceManager.cs
│ │ │ ├── IAudioDeviceSession.cs
│ │ │ ├── IAudioDeviceSessionComparer.cs
│ │ │ ├── IStreamWithVolumeControl.cs
│ │ │ ├── Mocks/
│ │ │ │ ├── AudioDevice.cs
│ │ │ │ └── AudioDeviceSession.cs
│ │ │ └── SessionState.cs
│ │ ├── FilteredCollectionChain.cs
│ │ ├── ProcessWatcherService.cs
│ │ ├── Storage/
│ │ │ ├── ISettingsBag.cs
│ │ │ ├── Internal/
│ │ │ │ ├── NamespacedSettingsBag.cs
│ │ │ │ ├── RegistrySettingsBag.cs
│ │ │ │ └── WindowsStorageSettingsBag.cs
│ │ │ ├── Serializer.cs
│ │ │ └── StorageFactory.cs
│ │ ├── SystemSettings.cs
│ │ └── WindowsAudio/
│ │ ├── AudioDeviceKind.cs
│ │ ├── IAudioDeviceChannel.cs
│ │ ├── IAudioDeviceManagerWindowsAudio.cs
│ │ ├── IAudioDeviceSessionChannel.cs
│ │ ├── IAudioDeviceWindowsAudio.cs
│ │ ├── Internal/
│ │ │ ├── AudioDevice.cs
│ │ │ ├── AudioDeviceChannel.cs
│ │ │ ├── AudioDeviceChannelCollection.cs
│ │ │ ├── AudioDeviceManager.cs
│ │ │ ├── AudioDeviceSession.cs
│ │ │ ├── AudioDeviceSessionChannel.cs
│ │ │ ├── AudioDeviceSessionChannelCollection.cs
│ │ │ ├── AudioDeviceSessionChannelMultiplexer.cs
│ │ │ ├── AudioDeviceSessionCollection.cs
│ │ │ ├── AudioDeviceSessionGroup.cs
│ │ │ ├── AudioPolicyConfigService.cs
│ │ │ ├── Helpers.cs
│ │ │ ├── IAudioDeviceInternal.cs
│ │ │ └── IAudioDeviceSessionInternal.cs
│ │ └── WindowsAudioFactory.cs
│ ├── DebugHelpers.cs
│ ├── Diagnosis/
│ │ ├── CircularBufferTraceListener.cs
│ │ ├── ErrorReporter.cs
│ │ ├── LocalDataExporter.cs
│ │ └── SnapshotData.cs
│ ├── EarTrumpet.csproj
│ ├── Extensibility/
│ │ ├── EarTrumpetAddon.cs
│ │ ├── EarTrumpetAddonManifest.cs
│ │ ├── Hosting/
│ │ │ ├── AddonHost.cs
│ │ │ ├── AddonManager.cs
│ │ │ └── AddonResolver.cs
│ │ ├── IEarTrumpetAddonAppContent.cs
│ │ ├── IEarTrumpetAddonDeviceContent.cs
│ │ ├── IEarTrumpetAddonEvents.cs
│ │ ├── IEarTrumpetAddonNotificationAreaContextMenu.cs
│ │ ├── IEarTrumpetAddonSettingsPage.cs
│ │ └── Shared/
│ │ ├── PlaybackDataModelHost.cs
│ │ ├── ResourceLoader.cs
│ │ └── ServiceBus.cs
│ ├── Extensions/
│ │ ├── CollectionExtensions.cs
│ │ ├── ColorExtensions.cs
│ │ ├── DependencyObjectExtensions.cs
│ │ ├── EarTrumpetAddonExtensions.cs
│ │ ├── EventBinding/
│ │ │ ├── EventBindingExtension.cs
│ │ │ └── HandledEventBindingExtension.cs
│ │ ├── ExceptionExtensions.cs
│ │ ├── FloatExtensions.cs
│ │ ├── FrameworkElementExtensions.cs
│ │ ├── IEnumerableExtensions.cs
│ │ ├── IPropertyStoreExtensions.cs
│ │ ├── IconExtensions.cs
│ │ ├── ListExtensions.cs
│ │ ├── OperatingSystemExtensions.cs
│ │ ├── RegistryKeyExtensions.cs
│ │ ├── VisualExtensions.cs
│ │ └── WindowExtensions.cs
│ ├── Features.cs
│ ├── Interop/
│ │ ├── AppBarData.cs
│ │ ├── AppBarEdge.cs
│ │ ├── AppBarMessage.cs
│ │ ├── ApplicationResolver.cs
│ │ ├── CLSCTX.cs
│ │ ├── Combase.cs
│ │ ├── DwmApi.cs
│ │ ├── FolderIds.cs
│ │ ├── GPS.cs
│ │ ├── Gdi32.cs
│ │ ├── HRESULT.cs
│ │ ├── Helpers/
│ │ │ ├── AccentPolicyLibrary.cs
│ │ │ ├── AudioPolicyConfigFactory.cs
│ │ │ ├── AudioPolicyConfigFactoryImplFor21H2.cs
│ │ │ ├── AudioPolicyConfigFactoryImplForDownlevel.cs
│ │ │ ├── HotkeyData.cs
│ │ │ ├── HotkeyManager.cs
│ │ │ ├── IconHelper.cs
│ │ │ ├── ImmersiveSystemColors.cs
│ │ │ ├── InputHelper.cs
│ │ │ ├── Kernel32Helper.cs
│ │ │ ├── LegacyControlPanelHelper.cs
│ │ │ ├── MouseHook.cs
│ │ │ ├── PackageHelper.cs
│ │ │ ├── ProcessHelper.cs
│ │ │ ├── SettingsPageHelper.cs
│ │ │ ├── SingleInstanceAppMutex.cs
│ │ │ ├── Win32Window.cs
│ │ │ ├── WindowSizeHelper.cs
│ │ │ └── WindowsTaskbar.cs
│ │ ├── IPropertyStore.cs
│ │ ├── IShellItem.cs
│ │ ├── IShellItem2.cs
│ │ ├── IShellItemImageFactory.cs
│ │ ├── Kernel32.cs
│ │ ├── MMDeviceAPI/
│ │ │ ├── AUDIO_VOLUME_NOTIFICATION_DATA.cs
│ │ │ ├── AudioSessionDisconnectReason.cs
│ │ │ ├── AudioSessionState.cs
│ │ │ ├── DeviceState.cs
│ │ │ ├── EDataFlow.cs
│ │ │ ├── ERole.cs
│ │ │ ├── IAudioEndpointVolume.cs
│ │ │ ├── IAudioEndpointVolumeCallback.cs
│ │ │ ├── IAudioMeterInformation.cs
│ │ │ ├── IAudioPolicyConfigFactory.cs
│ │ │ ├── IAudioPolicyConfigFactoryVariantFor21H2.cs
│ │ │ ├── IAudioPolicyConfigFactoryVariantForDownlevel.cs
│ │ │ ├── IAudioSessionControl.cs
│ │ │ ├── IAudioSessionControl2.cs
│ │ │ ├── IAudioSessionEnumerator.cs
│ │ │ ├── IAudioSessionEvents.cs
│ │ │ ├── IAudioSessionManager.cs
│ │ │ ├── IAudioSessionManager2.cs
│ │ │ ├── IAudioSessionNotification.cs
│ │ │ ├── IAudioVolumeDuckNotification.cs
│ │ │ ├── IChannelAudioVolume.cs
│ │ │ ├── IDeviceTopology.cs
│ │ │ ├── IMMDevice.cs
│ │ │ ├── IMMDeviceCollection.cs
│ │ │ ├── IMMDeviceEnumerator.cs
│ │ │ ├── IMMEndpoint.cs
│ │ │ ├── IMMNotificationClient.cs
│ │ │ ├── IPolicyConfig.cs
│ │ │ ├── ISimpleAudioVolume.cs
│ │ │ ├── MMDeviceEnumerator.cs
│ │ │ └── PolicyConfigClient.cs
│ │ ├── NotifyIconData.cs
│ │ ├── Ntdll.cs
│ │ ├── Ole32.cs
│ │ ├── PropVariant.cs
│ │ ├── PropVariantUnion.cs
│ │ ├── PropertyKeys.cs
│ │ ├── RECT.cs
│ │ ├── SFGAO.cs
│ │ ├── SICHINT.cs
│ │ ├── SIGDN.cs
│ │ ├── SIZE.cs
│ │ ├── STGM.cs
│ │ ├── SafeHandles/
│ │ │ └── HMODULE.cs
│ │ ├── Shell32.cs
│ │ ├── SndVolSSO.cs
│ │ ├── User32.cs
│ │ ├── Uxtheme.cs
│ │ └── shlwapi.cs
│ ├── Properties/
│ │ ├── AssemblyInfo.cs
│ │ ├── Resources.Designer.cs
│ │ ├── Resources.af-ZA.resx
│ │ ├── Resources.ar-SA.resx
│ │ ├── Resources.bs-latn-ba.resx
│ │ ├── Resources.ca-ES.resx
│ │ ├── Resources.cs-CZ.resx
│ │ ├── Resources.da-DK.resx
│ │ ├── Resources.de-DE.resx
│ │ ├── Resources.el-GR.resx
│ │ ├── Resources.es-ES.resx
│ │ ├── Resources.fi-FI.resx
│ │ ├── Resources.fr-FR.resx
│ │ ├── Resources.he-IL.resx
│ │ ├── Resources.hr-HR.resx
│ │ ├── Resources.hu-HU.resx
│ │ ├── Resources.it-IT.resx
│ │ ├── Resources.ja-JP.resx
│ │ ├── Resources.ko-KR.resx
│ │ ├── Resources.nl-NL.resx
│ │ ├── Resources.no-NO.resx
│ │ ├── Resources.pl-PL.resx
│ │ ├── Resources.pt-BR.resx
│ │ ├── Resources.pt-PT.resx
│ │ ├── Resources.resx
│ │ ├── Resources.ro-RO.resx
│ │ ├── Resources.ru-RU.resx
│ │ ├── Resources.sl-SI.resx
│ │ ├── Resources.sv-SE.resx
│ │ ├── Resources.ta-IN.resx
│ │ ├── Resources.th-TH.resx
│ │ ├── Resources.tr-TR.resx
│ │ ├── Resources.uk-UA.resx
│ │ ├── Resources.vi-VN.resx
│ │ ├── Resources.zh-CN.resx
│ │ └── Resources.zh-TW.resx
│ ├── README.md
│ ├── UI/
│ │ ├── Behaviors/
│ │ │ ├── ButtonEx.cs
│ │ │ ├── ComboBoxEx.cs
│ │ │ ├── FrameworkElementEx.cs
│ │ │ ├── ScrollViewerEx.cs
│ │ │ └── TextBoxEx.cs
│ │ ├── Controls/
│ │ │ ├── AppPopup.cs
│ │ │ ├── ImageEx.cs
│ │ │ ├── ListView.cs
│ │ │ ├── ListViewItem.cs
│ │ │ ├── MenuItemTemplateSelector.cs
│ │ │ ├── SearchBox.xaml
│ │ │ └── VolumeSlider.cs
│ │ ├── Helpers/
│ │ │ ├── FlyoutViewState.cs
│ │ │ ├── IAppIconSource.cs
│ │ │ ├── IShellNotifyIconSource.cs
│ │ │ ├── NavigationCookie.cs
│ │ │ ├── RelayCommand.cs
│ │ │ ├── ShellNotifyIcon.cs
│ │ │ ├── SystemSoundsHelper.cs
│ │ │ ├── TaskbarIconSource.cs
│ │ │ ├── WindowAnimationLibrary.cs
│ │ │ ├── WindowHolder.cs
│ │ │ └── WindowViewState.cs
│ │ ├── Mutable.xaml
│ │ ├── Themes/
│ │ │ ├── AcrylicBrush.cs
│ │ │ ├── Brush.cs
│ │ │ ├── BrushValueParser.cs
│ │ │ ├── Manager.cs
│ │ │ ├── OS.cs
│ │ │ ├── Options.cs
│ │ │ ├── Ref.cs
│ │ │ ├── Rule.cs
│ │ │ └── ThemeBindingInfo.cs
│ │ ├── ViewModels/
│ │ │ ├── AddonAboutPageViewModel.cs
│ │ │ ├── AdvertisedCategorySettingsViewModel.cs
│ │ │ ├── AppItemViewModel.cs
│ │ │ ├── AudioSessionViewModel.cs
│ │ │ ├── BackstackViewModel.cs
│ │ │ ├── ContextMenuItem.cs
│ │ │ ├── DeviceCollectionViewModel.cs
│ │ │ ├── DeviceViewModel.cs
│ │ │ ├── EarTrumpetAboutPageViewModel.cs
│ │ │ ├── EarTrumpetCommunitySettingsPageViewModel.cs
│ │ │ ├── EarTrumpetLegacySettingsPageViewModel.cs
│ │ │ ├── EarTrumpetMouseSettingsPageViewModel.cs
│ │ │ ├── EarTrumpetShortcutsPageViewModel.cs
│ │ │ ├── FlyoutViewModel.cs
│ │ │ ├── FocusedAppItemViewModel.cs
│ │ │ ├── FocusedDeviceViewModel.cs
│ │ │ ├── FullWindowViewModel.cs
│ │ │ ├── HotkeyViewModel.cs
│ │ │ ├── IAppItemViewModel.cs
│ │ │ ├── IDeviceViewModel.cs
│ │ │ ├── IFlyoutViewModel.cs
│ │ │ ├── IFocusedViewModel.cs
│ │ │ ├── IPopupHostViewModel.cs
│ │ │ ├── ISettingsViewModel.cs
│ │ │ ├── ModalDialogViewModel.cs
│ │ │ ├── SettingsAppItemViewModel.cs
│ │ │ ├── SettingsCategoryViewModel.cs
│ │ │ ├── SettingsDialogViewModel.cs
│ │ │ ├── SettingsPageHeaderViewModel.cs
│ │ │ ├── SettingsPageViewModel.cs
│ │ │ ├── SettingsSearchItemViewModel.cs
│ │ │ ├── SettingsViewModel.cs
│ │ │ ├── TemporaryAppItemViewModel.cs
│ │ │ ├── ToolbarItemViewModel.cs
│ │ │ └── WelcomeViewModel.cs
│ │ └── Views/
│ │ ├── AppItemView.xaml
│ │ ├── AppItemView.xaml.cs
│ │ ├── DeviceView.xaml
│ │ ├── DeviceView.xaml.cs
│ │ ├── DialogWindow.xaml
│ │ ├── DialogWindow.xaml.cs
│ │ ├── FlyoutWindow.xaml
│ │ ├── FlyoutWindow.xaml.cs
│ │ ├── FullWindow.xaml
│ │ ├── FullWindow.xaml.cs
│ │ ├── SettingsWindow.xaml
│ │ └── SettingsWindow.xaml.cs
│ ├── packages.config
│ └── prebuild.ps1
├── EarTrumpet.ColorTool/
│ ├── App.config
│ ├── App.xaml
│ ├── App.xaml.cs
│ ├── ColorItemViewModel.cs
│ ├── ColorViewModel.cs
│ ├── EarTrumpet.ColorTool.csproj
│ ├── MainWindow.xaml
│ ├── MainWindow.xaml.cs
│ └── Properties/
│ └── AssemblyInfo.cs
├── EarTrumpet.Package/
│ ├── EarTrumpet.Package.wapproj
│ ├── Package.StoreAssociation.xml
│ ├── Package.appxmanifest
│ └── Package.xml
├── EarTrumpet.vs15.sln
├── GitVersion.yml
├── LICENSE
├── PRIVACY.md
├── Packaging/
│ └── MicrosoftStore/
│ ├── PDPs/
│ │ ├── ar-SA/
│ │ │ └── pdp.xml
│ │ ├── bs-latn-ba/
│ │ │ └── pdp.xml
│ │ ├── ca-ES/
│ │ │ └── pdp.xml
│ │ ├── cs-CZ/
│ │ │ └── pdp.xml
│ │ ├── da-DK/
│ │ │ └── pdp.xml
│ │ ├── de-DE/
│ │ │ └── pdp.xml
│ │ ├── el-GR/
│ │ │ └── pdp.xml
│ │ ├── en-us/
│ │ │ └── pdp.xml
│ │ ├── es-ES/
│ │ │ └── pdp.xml
│ │ ├── fr-FR/
│ │ │ └── pdp.xml
│ │ ├── he-IL/
│ │ │ └── pdp.xml
│ │ ├── hr-HR/
│ │ │ └── pdp.xml
│ │ ├── hu-HU/
│ │ │ └── pdp.xml
│ │ ├── it-IT/
│ │ │ └── pdp.xml
│ │ ├── ja-JP/
│ │ │ └── pdp.xml
│ │ ├── ko-KR/
│ │ │ └── pdp.xml
│ │ ├── nl-NL/
│ │ │ └── pdp.xml
│ │ ├── no-NO/
│ │ │ └── pdp.xml
│ │ ├── pl-PL/
│ │ │ └── pdp.xml
│ │ ├── pt-BR/
│ │ │ └── pdp.xml
│ │ ├── pt-PT/
│ │ │ └── pdp.xml
│ │ ├── ro-RO/
│ │ │ └── pdp.xml
│ │ ├── ru-RU/
│ │ │ └── pdp.xml
│ │ ├── sl-SI/
│ │ │ └── pdp.xml
│ │ ├── sv-SE/
│ │ │ └── pdp.xml
│ │ ├── th-TH/
│ │ │ └── pdp.xml
│ │ ├── tr-TR/
│ │ │ └── pdp.xml
│ │ ├── uk-UA/
│ │ │ └── pdp.xml
│ │ ├── vi-VN/
│ │ │ └── pdp.xml
│ │ ├── zh-CN/
│ │ │ └── pdp.xml
│ │ └── zh-TW/
│ │ └── pdp.xml
│ └── sbconfig.json
├── README.md
└── crowdin.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: .azure-pipelines.yml
================================================
trigger:
branches:
include:
- master
- review/*
- experiment/*
- dev
paths:
exclude:
- '**/*.md'
- '.github/**/*'
pr:
branches:
include:
- dev
paths:
exclude:
- '**/*.md'
- '.github/**/*'
variables:
- group: 'Bugsnag'
- name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE
value: true
- name: Solution
value: 'EarTrumpet.vs15.sln'
- name: BuildConfiguration
value: 'Release'
- name: BuildPlatform
value: 'x86'
jobs:
- job: Build
continueOnError: 'false'
pool:
vmImage: vs2017-win2016
strategy:
matrix:
AppInstaller:
Channel: AppInstaller
Publisher: 'CN=File-New-Project, O=File-New-Project, L=Purcellville, S=Virginia, C=US'
Store:
Channel: Store
Publisher: 'CN=6099D0EF-9374-47ED-BDFE-A82136831235'
maxParallel: 2
steps:
- task: GitVersion@4
displayName: 'Initialize Git Version'
inputs:
updateAssemblyInfo: false
- task: PowerShell@2
displayName: 'Set build number'
inputs:
targetType: inline
script: |
Write-Host "##vso[build.updatebuildnumber]$(GitVersion.SemVer)"
- task: PowerShell@2
displayName: 'Generate versioning metadata'
inputs:
targetType: inline
script: |
New-Item -ItemType Directory "$(Build.ArtifactStagingDirectory)\Metadata" -ErrorAction Ignore
Set-Content "$(Build.ArtifactStagingDirectory)\Metadata\semver.txt" "$(GitVersion.SemVer)"
Set-Content "$(Build.ArtifactStagingDirectory)\Metadata\branch.txt" "$(GitVersion.BranchName)"
Set-Content "$(Build.ArtifactStagingDirectory)\Metadata\commits.txt" "$(GitVersion.CommitsSinceVersionSource)"
if("$(Channel)" -eq "Store") {
$Version = "$(GitVersion.MajorMinorPatch).0"
} else {
$Version = "$(GitVersion.AssemblySemVer)"
}
Set-Content "$(Build.ArtifactStagingDirectory)\Metadata\$(Channel).version.txt" $Version
- task: NuGetToolInstaller@0
displayName: 'Install NuGet Tooling'
- task: NuGetCommand@2
displayName: 'Restore NuGet Packages'
inputs:
restoreSolution: '$(solution)'
- task: PowerShell@2
displayName: 'Set Bugsnag API Key'
inputs:
targetType: inline
script: |
$cfg = Get-Content ".\EarTrumpet\app.config"
$cfg | ForEach-Object { $_.Replace("{bugsnag.apikey}", "$(bugsnag.apikey)") } | Set-Content ".\EarTrumpet\app.config"
- task: PowerShell@2
displayName: 'Adjust manifest and store association'
inputs:
targetType: inline
script: |
$manifestPath = ".\EarTrumpet.Package\Package.appxmanifest"
$storeAssociationPath = ".\EarTrumpet.Package\Package.StoreAssociation.xml"
$manifest = [xml](Get-Content $manifestPath)
$manifest.Package.Identity.Publisher = "$(Publisher)"
if("$(Channel)" -eq "AppInstaller") {
$manifest.Package.Properties.DisplayName = "EarTrumpet ($(GitVersion.BranchName))"
$manifest.Package.Applications.Application.VisualElements.DisplayName = "EarTrumpet ($(GitVersion.BranchName))"
}
$manifest.Save($manifestPath)
$storeAssociation = [xml](Get-Content $storeAssociationPath)
$storeAssociation.StoreAssociation.Publisher = "$(Publisher)"
if("$(Channel)" -eq "AppInstaller") {
$storeAssociation.StoreAssociation.ProductReservedInfo.ReservedNames.ReservedName = "EarTrumpet ($(GitVersion.BranchName))"
}
$storeAssociation.Save($storeAssociationPath)
- task: MSBuild@1
displayName: 'Build EarTrumpet appxupload package'
inputs:
solution: 'EarTrumpet.Package/EarTrumpet.Package.wapproj'
platform: 'x86'
configuration: '$(buildConfiguration)'
msbuildArguments: '/p:AppxBundle=Always /p:Channel=$(Channel) /p:AppxPackageDir="$(Build.ArtifactStagingDirectory)/AppxUpload" /p:AppxPackageSigningEnabled=false /p:UapAppxPackageBuildMode=CI'
maximumCpuCount: true
condition: and(succeeded(), eq(variables['Channel'], 'Store'))
- task: PublishBuildArtifacts@1
displayName: 'Publish appxupload artifacts'
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)/AppxUpload'
artifactName: 'AppxUpload'
condition: and(succeeded(), eq(variables['Channel'], 'Store'))
- task: MSBuild@1
displayName: 'Build EarTrumpet appinstaller/sideload package'
inputs:
solution: 'EarTrumpet.Package/EarTrumpet.Package.wapproj'
platform: 'x86'
configuration: '$(buildConfiguration)'
msbuildArguments: '/p:AppxBundle=Always /p:Channel=$(Channel) /p:AppxPackageDir="$(Build.ArtifactStagingDirectory)/Sideload" /p:AppxPackageSigningEnabled=false /p:UapAppxPackageBuildMode=SideloadOnly /p:GenerateAppInstallerFile=true /p:AppxPackageTestDir="$(Build.ArtifactStagingDirectory)/Sideload/" /p:AppInstallerUri=https://install.eartrumpet.app'
maximumCpuCount: true
condition: and(succeeded(), eq(variables['Channel'], 'AppInstaller'))
- task: PowerShell@2
displayName: 'Adjust appinstaller manifest'
inputs:
targetType: inline
script: |
$manifestPath = "$(Build.ArtifactStagingDirectory)/Sideload/EarTrumpet.Package.appinstaller"
$manifest = [xml](Get-Content $manifestPath)
$manifest.AppInstaller.Uri = "https://install.eartrumpet.app/$(GitVersion.BranchName)/EarTrumpet.Package.appinstaller"
$manifest.AppInstaller.MainBundle.Uri = "https://install.eartrumpet.app/$(GitVersion.BranchName)/EarTrumpet.Package_$(GitVersion.MajorMinorPatch).$(GitVersion.CommitsSinceVersionSource)_x86.appxbundle"
$manifest.AppInstaller.MainBundle.Publisher = "$(Publisher)"
$manifest.Save($manifestPath)
pwsh: true
condition: and(succeeded(), eq(variables['Channel'], 'AppInstaller'))
- task: PublishBuildArtifacts@1
displayName: 'Publish appinstaller/sideload package artifacts'
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)/Sideload'
artifactName: 'Sideload'
condition: and(succeeded(), eq(variables['Channel'], 'AppInstaller'))
- task: CopyFiles@2
displayName: 'Stage packaging metadata'
inputs:
SourceFolder: 'Packaging'
Contents: '**'
TargetFolder: '$(Build.ArtifactStagingDirectory)/Metadata/Packaging'
condition: and(succeeded(), eq(variables['Channel'], 'Store'))
- task: PublishBuildArtifacts@1
displayName: 'Publish metadata artifacts'
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)/Metadata'
artifactName: 'Metadata'
================================================
FILE: .chocolatey/eartrumpet.nuspec
================================================
eartrumpet
2.1.7.0
https://github.com/File-New-Project/EarTrumpet/tree/master/.chocolatey
File-New-Project
EarTrumpet
EarTrumpet Contributors
https://github.com/File-New-Project/EarTrumpet
https://raw.githubusercontent.com/File-New-Project/EarTrumpet/master/.chocolatey/logo.png
2015 File-New-Project
https://github.com/File-New-Project/EarTrumpet/blob/master/LICENSE
false
https://github.com/File-New-Project/EarTrumpet
https://github.com/File-New-Project/EarTrumpet/issues
EarTrumpet
EarTrumpet is a powerful volume control app for Windows
================================================
FILE: .chocolatey/tools/LICENSE.txt
================================================
From: https://github.com/File-New-Project/EarTrumpet/blob/master/LICENSE
LICENSE
The MIT License (MIT)
Copyright (c) 2015
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: .chocolatey/tools/VERIFICATION.txt
================================================
VERIFICATION
Verification is intended to assist the Chocolatey moderators and community
in verifying that this package's contents are trustworthy.
This package is published directly by File-New-Project, the team that owns EarTrumpet.
================================================
FILE: .chocolatey/tools/chocolateybeforemodify.ps1
================================================
$process = Get-Process -Name EarTrumpet -ErrorAction SilentlyContinue
if ($process) {
$process | Stop-Process -Force
}
================================================
FILE: .chocolatey/tools/chocolateyinstall.ps1
================================================
$ErrorActionPreference = 'Stop'
$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$installPath = Join-Path $toolsDir 'EarTrumpet'
$exePath = Join-Path $toolsDir 'EarTrumpet\EarTrumpet.exe'
$zipPath = Join-Path $toolsDir 'Release.zip'
Install-ChocolateyZipPackage -PackageName "$packageName" `
-Url "$zipPath" `
-UnzipLocation "$installPath"
Write-Output "Adding shortcut to Start Menu"
Install-ChocolateyShortcut -ShortcutFilePath "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\EarTrumpet.lnk" -TargetPath $exePath
Write-Output "Adding shortcut to Startup"
New-ItemProperty -LiteralPath 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'EarTrumpet' -Value $exePath -PropertyType String -Force -ea SilentlyContinue | Out-Null
================================================
FILE: .chocolatey/tools/chocolateyuninstall.ps1
================================================
Remove-Item "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\EarTrumpet.lnk" -ErrorAction Continue
Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name "EarTrumpet"
================================================
FILE: .gitattributes
================================================
[core]
* text eol=crlf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.mov binary
*.mp4 binary
*.mp3 binary
*.flv binary
*.fla binary
*.swf binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.eot binary
*.woff binary
*.pyc binary
*.pdf binary
*.ez binary
*.bz2 binary
*.swp binary
*.pfx binary
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
## Supported
github: File-New-Project
## Currently unsupported
patreon:
open_collective:
ko_fi:
tidelift:
community_bridge:
liberapay:
issuehunt:
otechie:
lfx_crowdfunding:
custom:
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug report
description: Report a problem with EarTrumpet or related add-on
body:
- type: textarea
id: summary
attributes:
label: Summary
description: A clear explaination of what the problem is.
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
validations:
required: true
- type: input
id: eartrumpet_version
attributes:
label: EarTrumpet version
description: Right-click the EarTrumpet icon, then click Settings > General > About.
validations:
required: true
- type: input
id: windows_version
attributes:
label: Windows version
description: Open the Command Prompt and observe the version shown on the first line.
placeholder: 10.0.22000.1024
validations:
required: true
- type: textarea
id: additional_info
attributes:
label: Additional information
description: Screenshots, logs, and other supplemental data go here.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
- name: Request a feature
url: https://github.com/File-New-Project/EarTrumpet/discussions/new?category=ideas
about: Share an idea or suggest a change to EarTrumpet or related add-on
- name: Ask a question
url: https://github.com/File-New-Project/EarTrumpet/discussions/new?category=Q-A
about: Ask a question about EarTrumpet or anything related
================================================
FILE: .github/workflows/main.yml
================================================
name: EarTrumpet-CI
on:
push:
branches:
- master
- dev
- rafael/*
- dave/*
- david/*
paths-ignore:
- "**/*.md"
- ".github/ISSUE_TEMPLATE/*"
- ".github/workflows/sponsors.yml"
- ".github/workflows/translators.yml"
- "Graphics/*"
pull_request:
branches:
- dev
paths-ignore:
- "**/*.md"
- crowdin.yml
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
BUILD_CONFIGURATION: Release
BUILD_PLATFORM: x86
ARTIFACTS_BASE: '${{ github.workspace }}\artifacts'
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
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Fetch all history for all tags and branches
run: git fetch --prune --unshallow
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v0.9.15
with:
versionSpec: "5.x"
includePrerelease: false
- name: Use GitVersion
id: gitversion
uses: gittools/actions/gitversion/execute@v0.9.15
- name: Create artifact layout
shell: powershell
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
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 }}"
}
Set-Content "$env:ARTIFACTS_BASE\metadata\${{ matrix.channel }}.version.txt" $Version
- name: Install NuGet
uses: NuGet/setup-nuget@v1
with:
nuget-version: latest
- name: Restore NuGet Packages
run: nuget restore EarTrumpet.vs15.sln
- 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"
- 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)
- name: Set up MSBuild
uses: microsoft/setup-msbuild@v1
- 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
with:
name: appxupload
path: artifacts/appxupload
- 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
with:
name: loose
path: artifacts/loose
- 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\
/p:AppxPackageSigningEnabled=false
/p:UapAppxPackageBuildMode=SideloadOnly
/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
- name: Fix up PDPs
if: matrix.channel == 'Store' && github.event_name != 'pull_request'
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"))
}
- 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: Upload metadata artifacts
uses: actions/upload-artifact@v3
with:
name: metadata
path: artifacts/metadata
- 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\"
- 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 }}
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
path: artifacts
- name: Install NuGet
uses: NuGet/setup-nuget@v1
with:
nuget-version: latest
- name: Install Build Tools
run: nuget install Microsoft.Windows.SDK.BuildTools
- name: Install Azure Codesigning
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
- name: Sign and repackage Store artifacts
if: matrix.channel == 'Store'
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'
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"
$SideloadPath = "$env:ARTIFACTS_BASE\sideload"
$SignedPath = "$env:ARTIFACTS_BASE\sideload\signed"
### 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"
### Place symbols next to executable image
Copy-Item "$ExtractedPath\Symbols\EarTrumpet.pdb" "$ExtractedPath\Package\EarTrumpet\"
### 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"
New-Item -ItemType Directory "$SignedPath"
New-Item -ItemType Directory "$SignedPath\Package"
New-Item -ItemType Directory "$SignedPath\Bundle"
### 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\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
### 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"
### 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"
Copy-Item "$SideloadPath\*.appinstaller" "$SignedPath\Bundle"
- name: Sign and repackage Chocolatey artifacts
if: matrix.channel == 'Chocolatey'
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"
### 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"
### Package for release
Compress-Archive -Path "$LooseFilesPath\*" -DestinationPath "$env:ARTIFACTS_BASE\chocolatey\tools\release.zip" -CompressionLevel Optimal
- name: Adjust nuspec
if: matrix.channel == 'Chocolatey'
shell: pwsh
run: |
$MetadataPath = "$env:ARTIFACTS_BASE\metadata"
$Version = [Version](Get-Content "$MetadataPath\Chocolatey.version.txt")
$NuspecPath = "$env:ARTIFACTS_BASE\chocolatey\eartrumpet.nuspec"
$nuspec = [xml](Get-Content -Path $NuspecPath)
$nuspec.package.metadata.version = $Version
$nuspec.Save($NuspecPath)
- name: Create chocolatey package
if: matrix.channel == 'Chocolatey'
shell: powershell
run: |
choco pack "$env:ARTIFACTS_BASE\chocolatey\eartrumpet.nuspec" --out "$env:ARTIFACTS_BASE\chocolatey"
- name: Upload chocolatey artifact
if: matrix.channel == 'Chocolatey'
uses: actions/upload-artifact@v3
with:
name: chocolatey-package
path: artifacts/chocolatey/*.nupkg
- name: Install OpenSSH FOD
if: matrix.channel == 'AppInstaller' || matrix.channel == 'Store'
shell: powershell
run: |
Set-Service -Name wuauserv -StartupType Manual
Start-Service -Name wuauserv
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
- 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
- name: Stage AppInstaller artifacts via SCP
if: matrix.channel == 'AppInstaller'
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'
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
================================================
FILE: .github/workflows/sponsors.yml
================================================
name: Generate Sponsors
on:
workflow_dispatch:
schedule:
- cron: 0 12 1-31 * *
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Generate Sponsors
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_PAT }}
file: 'README.md'
organization: true
template: '
'
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: master
folder: '.'
================================================
FILE: .github/workflows/translators.yml
================================================
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
"
"
}) -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
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
# Visual Studo 2015 cache/options directory
.vs/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding addin-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
#*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
#!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
*.[Cc]ache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio databases
*.VC.db
*.VC.VC.opendb
*.vs15.VC.db
*.vs15.VC.VC.opendb
*.lastcodeanalysissucceeded
*.CodeAnalysisLog.xml
*.filters
# Auto-generated files
BundleArtifacts/
_pkginfo.txt
# Choco
.chocolatey/*.nupkg
.chocolatey/tools/Release.zip
/artifacts/sideload
/Tools
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## 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!)
- Added new community settings area
- Added new community setting to turn on/off use of a logarithmic volume scale (thanks @yonatan-mitmit!)
- Added legacy shortcuts to the context menu pointing to [App volume and device preferences] / [Volume mixer]
- Added ability to use the Windows key in shortcuts (thanks @iamevn!)
- Added linguistic display name sorting for audio devices (thanks @Tester798!)
- Added a workaround for Windows Search (CortanaUI) showing a default asset (X) icon
- Fixed an issue where installation of EarTrumpet via AppInstaller would fail if the Visual C++ libs package was not installed
- Fixed an issue where EarTrumpet tooltips were not updating live while scrolling the mouse wheel on Windows 10 (thanks @krlvm!)
- Forced EarTrumpet to render in software-only mode to keep it off power hungry GPUs.
- Improved the flyout animation (thanks @krlvm!)
## 2.2.2.0
- Fixed an issue with the volume changing when scrolling in certain scenarios (e.g. virtual reality)
- Updated Japanese translations
- Cleaned up old language resources
## 2.2.1.0
- Fixed touch context menu behavior on Windows 11 machines with future "ShyTaskbar" enabled
- Fixed appearance of flyout on Windows 10 with light mode enabled (thanks @xmha97)
- Fixed flyout animation not respecting correct system settings on Windows 10 and Windows 11
- Upgraded GIF playback library to reduce memory usage (thanks @rocksdanister)
- Reduced use of a workaround for Acrylic slowdowns for most builds of Windows (thanks @krlvm)
- Updated translations from Crowdin contributors
- Updated Microsoft Store Product Detail Page metadata to correct localization issues
## 2.2.0.0
- Added hotkeys to control volume of multiple devices at once (thanks @Taknok!)
- Adjusted how application data is stored to increase reliability
- Fixed crash that could occur when the flyout Acrylic was toggled on/off in certain scenarios
- Fixed an issue with the expanded flyout becoming too tall on Windows 11
- Updated Japanese, Greek, Croatian, and Russian translations (thank you Crowdin contributors!)
## 2.1.10.0
- Flyout now remembers if it was expanded/collapsed between launches (thanks @Tester798!)
- Fixed an issue with the flyout open animation behaving erratically with some mice
- Fixed an issue where devices without certain characteristics would interfere with mute and other actions
- Fixed an issue with device names not appearing correctly if they contained underscores
- Fixed an issue with the flyout opening outside the working area in additional cases
- Adjusted the flyout position from the edge of the screen to match Windows 11 look and feel
- Removed solid color plating behind application icons
- Updated Finnish, German, and other translations
## 2.1.9.0
- Added basic support for Windows 11
- Added/updated Italian, Hungarian, Spanish, Portuguese, Turkish, Chinese, Norwegian, Arabic, Czech, Polish, Swedish, Romanian, and Russian translations
- Fixed an issue with the flyout opening outside the working area
- Fixed an issue with slow window movement when dragging
## 2.1.8.0
- Added Hungarian, Swedish, Korean, and Tamil translations
- Updated Japanese translations
- Added a fluent icon
- Fixed an issue with missing Czech and Afrikaans translations
- Fixed an issue with icons not appearing for packaged desktop applications (e.g. Microsoft Flight Simulator)
- Fixed an issue with the [Windows Legacy > Sound settings] menu item opening the wrong panel
- Fixed an issue with notification area icon handling
- Fixed an issue that prevented the use of system keys in keyboard shortcuts
## 2.1.7.0
- Fixed crash when retrieving region info for GDPR compliance on some machines
## 2.1.6.0
- Added ability to turn on/off crash reporting
- Added missing translations
- Added privacy policy link
- Fixed an icon display issue with libmpv-based apps (e.g. Plex)
- Fixed an issue that made volume sliders difficult to manipulate with a mouse at high DPI
- Restored pre-2.1.2.0 tray icon behavior until we can address icon duplication issues
## 2.1.5.0
- Fixed an issue with line in icon not appearing
- Fixed an issue with context menu submenus disappearing unexpectedly
## 2.1.4.0
- Fixed various bugs with the search textbox in Settings
- Fixed tray icon disappearing when the Windows Shell crashes/restarts
- Fixed master volume slider dimming on mute
- Fixed flyout track color in light theme
- Fixed mute icon when at zero volume
- Added mixer window width constraints when a high number of audio devices are present
- Added Slovenian language support
- Added hover states to buttons
- Additional crash bugfixes
## 2.1.3.0
- Fixed a crash causing EarTrumpet disappear on startup
- Fixed various potential leaks
- Added a help dialog to assist when EarTrumpet can't start due to broken fonts
## 2.1.2.0
- Fixed icon handle leak that caused a crash
- Fixed hotkeys not being properly unregistered
- Fixed arrow keys changing the default device volume
- Fixed High Contrast theme colors
- Fixed settings window covering the Taskbar when maximized
- Tray icon should remain in place after updates going forward
- Tray and app icons will now scale correctly
- Tray icon supports scrolling without opening the flyout
- Removed unwanted metadata from telemetry
## 2.1.1.0
- Fixed a crash when parsing numbers on non-English systems
## 2.1.0.0
- Added new settings experience
- Added support for Windows light mode
- Added keyboard shortcuts for opening the mixer and settings windows
- Reduced clutter in the context menu
- Fixed various display issues when using RTL writing systems
- Changed app naming behavior to align with Windows
- Added mute text to the notification area icon
- Added 'Open sound settings' link to context menu
- 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
- Additional bugfixes
## 2.0.8.0
- Changed grouping behavior to key off app install path vs. executable name
- Disabled flyout window blur when not visible to ensure it doesn't appear in task switcher
- Fixed an issue where the Enhancements tab was missing in playback devices dialog
- Fixed an issue where the flyout was too tall when the taskbar is configured to auto-hide
- Fixed an issue where disabled or unplugged devices would unexpectedly appear
- Fixed a crash when no default audio endpoint was present
- Fixed a crash when right-clicking an audio session after moving it
## 2.0.7.0
- Added additional support for high contrast themes
- Added per-monitor DPI support
- Added internal diagnostic logging buffer limit
- Disabled Alt+Space on the flyout window
- Fixed a rendering issue when the DPI was greater than 100% and there were more devices than would fit in the flyout without a scrollbar
- Fixed a rendering issue where the Notification Area icon becomes blurry at DPIs geater than 100%
- Fixed the icon and name of recording devices in 'Listen' mode
- Fixed Notification Area icon scaling
- Fixed overflow flyout at greater than 100% DPI
## 2.0.6.0
- Fixed an issue that affected localization on non-English systems
## 2.0.5.0
- Fixed System Sounds icon on ARM64
- High Contrast colors updated
- Added collection of debug information when device enumeration fails
## 2.0.4.0
- Scoped the mouse wheel scrolling to only over the Notification Area icon.
- Added support for RS3.
- Fixed a bug that caused the flyout to show 'It doesn't look like you have any playback devices.' when removing the default device.
- Fixed a crash during adding/removing devices.
- Removed non-fatal errors from telemetry.
## 2.0.3.0
- Fixed an issue with certain apps (e.g. Sea of Thieves) not appearing correctly
- Added additional languages (Arabic, Spanish, Hungarian, Korean, Turkish, and Ukrainian)
## 2.0.2.0
- Changes to telemetry
## 2.0.1.0
- Fixed a crash when a device is quickly added/removed
- Fixed a crash with multi-process audio sessions
- Fixed a crash launching web links
- Fixed a crash when closing EarTrumpet windows
- Fixed a crash when expanding/collapsing the main window
- Fixed a crash when Taskbar auto-hide is in use
- Fixed a crash when the registry has invalid personalization data
- Fixed a crash when calling application data storage APIs
## 2.0.0.0
- Added middle click on notification area icon to mute
- Added ability to use mouse wheel to change device volume when window is open
- Added multi-channel peak metering
- Added ability to move apps between devices
- Added ability to view multiple devices
- Added volume mixer window
- Enhanced app session grouping
- Enhanced keyboarding
- Added keyboard shortcut to open flyout
- Added support for Windows light/dark mode
- Added Sounds, Recording, etc. links to context menu
- Enhanced animations and detail work
- Additional fixes for RTL, Accessibility, and apps without icons
## 1.5.3.0
- Improved slider performance
- Fixed Acrylic Blur compatibility for Windows 10 1803
## 1.5.2.0
## 1.5.1.0
## 1.5.0.0
## 1.4.4.0
## 1.4.3.0
## 1.4.2.0
## 1.4.1.0
## 1.4.0.0
## 1.3.2.0
- Fixed changing audio devices in Windows 10 (RS1)
## 1.3.1.0
- Fixed DWM scaling issue where window appeared in the wrong position
- Fixed UI issues when no audio devices found
- Fixed changing audio devices in Windows 10 (TH1) and Windows 10 November Update (TH2)
- Fixed multiple sessions not appearing for some applications
- Added company metadata for Task Manager
## 1.3.0.0
- Fixed Speech Runtime display
- Fixed positioning when Taskbar auto-hide is enabled
- Installer/uninstaller now checks if the app is running
- Added ability to change default audio device (right-click the tray icon)
- Added ability to mute apps/audio device
- Added default audio device master volume slider
## 1.2.0.0
- Fixed issue with a number of apps not appearing in Ear Trumpet when using background audio services (e.g. iHeartRadio)
- Fixed issue with a number of apps not appearing in Ear Trumpet when playing protected media (e.g. Netflix)
- Fixed issue with apps not showing due to unexpected logo/icon paths (e.g. Skype Translator)
- Added base localization to Ear Trumpet (defaults to English for now - feel free to provide translations as pull requests)
## 1.1.1.0
## 1.1.0.0
- Fixed DPI scaling issue
- Fixed Ear Trumpet not displaying correctly when the Taskbar was in a different location or not on the primary monitor
- Initial fix for modern app missing extracted logo
- Ear Trumpet will now only allow one open instance
- GitHub readme updated with details and minimum versions
- Installer no longer allows installs on Windows versions before Windows 10
- Fixed issue with Windows 10 tablet mode
- Fixed Ear Trumpet window not having the correct border and drop shadow
## 1.0.0.0
- Initial release
================================================
FILE: COMPILING.md
================================================
# Compiling EarTrumpet
## Requirements
* [Visual Studio 2017](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)
## 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.
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.
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.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to EarTrumpet
Thanks for your interest in contributing to EarTrumpet!
You can contribute to EarTrumpet with issues and pull requests (PRs). Simply filing issues for problems you encounter is a great way to contribute. Contributing code via the below workflow is greatly appreciated.
## Copyright
EarTrumpet copyright is held by "Rafael Rivera, David Golden, David "Dave" Amenta, and Contributors".
## Contribution Workflow
Before contributing code, we require the following workflow:
1. Create an issue for your work or reuse an existing issue on the topic, if there is one.
2. Get agreement from the team that your proposed change is OK. (You can alternatively email the `team@eartrumpet.app`.)
3. Clearly state that you are going to take on the bug/enhancement work and we will assign the task to you.
4. Create a fork of the repository on GitHub (if you don't already have one).
5. Create a branch from **dev** (`git checkout -b mybranch dev`).
6. Name the branch so that it clearly communicates your intentions, such as issue-123 or feature-456.
7. Build the repository with your changes. Make sure that the builds are clean in all configurations (i.e. `Debug`, `Release`, and `VSDebug`).
8. Commit and push your changes to your fork.
9. Create a pull request (PR) against our **dev** branch.
ℹ It is OK for your PR to include a large number of commits. We will squash them on merge.
ℹ It is also OK to create your PR as "[WIP]" before the implementation is done. This can be useful if you'd like to start the feedback process while you finish your implementation. State that this is the case in the initial PR comment.
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/AddonResources.xaml
================================================
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/Controls/LinkedTextBlock.cs
================================================
using EarTrumpet.Extensions;
using EarTrumpet.UI.Helpers;
using EarTrumpet.UI.ViewModels;
using EarTrumpet.Actions.ViewModel;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
namespace EarTrumpet.Actions.Controls
{
public class LinkedTextBlock : TextBlock
{
public Popup Popup
{
get { return (Popup)this.GetValue(PopupProperty); }
set { this.SetValue(PopupProperty, value); }
}
public static readonly DependencyProperty PopupProperty = DependencyProperty.Register(
"Popup", typeof(Popup), typeof(LinkedTextBlock), new PropertyMetadata(null));
// We avoid ContextMenu because something external connects it to right-click behavior that (seemingly) can't be prevented.
public ContextMenu ContextMenu2
{
get { return (ContextMenu)this.GetValue(ContextMenuProperty2); }
set { this.SetValue(ContextMenuProperty2, value); }
}
public static readonly DependencyProperty ContextMenuProperty2 = DependencyProperty.Register(
"ContextMenu2", typeof(ContextMenu), typeof(LinkedTextBlock), new PropertyMetadata(null));
public object DataItem
{
get { return (object)this.GetValue(DataItemProperty); }
set { this.SetValue(DataItemProperty, value); }
}
public static readonly DependencyProperty DataItemProperty = DependencyProperty.Register(
"DataItem", typeof(object), typeof(LinkedTextBlock), new PropertyMetadata(null, new PropertyChangedCallback(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)));
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 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 void DataItemChanged()
{
((INotifyPropertyChanged)DataItem).PropertyChanged += (s, e) => PropertiesChanged();
}
private void PropertiesChanged()
{
this.Inlines.Clear();
if (ContextMenu != null && Popup != null)
{
ContextMenu.Loaded += ContextMenu_Loaded;
Popup.Loaded += Popup_Loaded;
}
ReadLinksAndText(FormatText, (text, isLink) =>
{
text = text.Trim();
if (!isLink)
{
var run = new Run(text);
run.Style = RunStyle;
this.Inlines.Add(run);
}
else
{
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;
link.RequestNavigate += (s, e) =>
{
// 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 = 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) =>
{
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(" "));
});
}
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 List GetContextMenuFromOptionViewModel(IOptionViewModel options)
{
return options.All.Select(item => new ContextMenuItem
{
DisplayName = item.DisplayName,
IsChecked = (item == options.Selected),
Command = new RelayCommand(() => options.Selected = item),
}).ToList();
}
private void ReadLinksAndText(string text, Action callback)
{
int ptr = 0;
for (int i = 0; i < text.Length; i++)
{
if (text[i] == '{')
{
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;
}
}
if (ptr < text.Length - 1)
{
callback(text.Substring(ptr, text.Length - ptr), false);
}
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/Controls/MenuButton.cs
================================================
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace EarTrumpet.Actions.Controls
{
public class MenuButton : Button
{
public MenuButton()
{
Click += Button_Click;
}
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.PlacementTarget = btn;
btn.ContextMenu.Placement = PlacementMode.Bottom;
btn.ContextMenu.IsOpen = true;
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/AudioAppEventKind.cs
================================================
namespace EarTrumpet.Actions.DataModel.Enum
{
public enum AudioAppEventKind
{
Added,
Removed,
PlayingSound,
NotPlayingSound,
Muted,
Unmuted,
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/AudioDeviceEventKind.cs
================================================
namespace EarTrumpet.Actions.DataModel.Enum
{
public enum AudioDeviceEventKind
{
Added,
Removed,
BecomingDefault,
LeavingDefault,
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/BoolValue.cs
================================================
namespace EarTrumpet.Actions.DataModel.Enum
{
public enum BoolValue
{
True,
False,
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ComparisonBoolKind.cs
================================================
namespace EarTrumpet.Actions.DataModel.Enum
{
public enum ComparisonBoolKind
{
Is,
IsNot,
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/EarTrumpetEventKind.cs
================================================
namespace EarTrumpet.Actions.DataModel.Enum
{
public enum EarTrumpetEventKind
{
Startup,
Shutdown,
};
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/MuteKind.cs
================================================
namespace EarTrumpet.Actions.DataModel.Enum
{
public enum MuteKind
{
Mute,
Unmute,
ToggleMute,
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ProcessEventKind.cs
================================================
namespace EarTrumpet.Actions.DataModel.Enum
{
public enum ProcessEventKind
{
Start,
Stop,
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/ProcessStateKind.cs
================================================
namespace EarTrumpet.Actions.DataModel.Enum
{
public enum ProcessStateKind
{
Running,
NotRunning
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Enum/SetVolumeKind.cs
================================================
namespace EarTrumpet.Actions.DataModel.Enum
{
public enum SetVolumeKind
{
Set,
Increment,
Decrement,
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithApp.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.DataModel
{
interface IPartWithApp
{
AppRef App { get; set; }
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithDevice.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.DataModel
{
public interface IPartWithDevice
{
Device Device { get; set; }
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithText.cs
================================================
namespace EarTrumpet.Actions.DataModel
{
interface IPartWithText
{
string Text { get; set; }
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/IPartWithVolume.cs
================================================
namespace EarTrumpet.Actions.DataModel
{
public interface IPartWithVolume
{
double Volume { get; set; }
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/LocalVariablesContainer.cs
================================================
using EarTrumpet.DataModel.Storage;
namespace EarTrumpet.Actions.DataModel
{
public class LocalVariablesContainer
{
public bool this[string key]
{
get => _settings.Get($"{s_localVariablePrefix}{key}", false);
set => _settings.Set($"{s_localVariablePrefix}{key}", value);
}
private const string s_localVariablePrefix = "LocalVariable.";
private readonly ISettingsBag _settings;
public LocalVariablesContainer(ISettingsBag settings)
{
_settings = settings;
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Part.cs
================================================
namespace EarTrumpet.Actions.DataModel
{
public abstract class Part { }
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/ProcessWatcher.cs
================================================
using EarTrumpet.Interop;
using EarTrumpet.Actions.Interop.Helpers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace EarTrumpet.Actions.DataModel
{
public class ProcessWatcher
{
class ProcessInfo
{
public List Windows = new List();
}
class WatcherInfo
{
public List StartCallbacks = new List();
public List StopCallbacks = new List();
public Dictionary RunningProcesses = new Dictionary();
}
public static ProcessWatcher Current { get; } = new ProcessWatcher();
WindowWatcher _watcher = new WindowWatcher();
Dictionary _info = new Dictionary();
public ProcessWatcher()
{
_watcher.WindowCreated += OnWindowCreated;
}
// Used only by the condition processor, so we use realtime data only.
public bool IsRunning(string procName)
{
try
{
return Process.GetProcessesByName(procName).Any();
}
catch (Exception ex)
{
Trace.WriteLine(ex);
}
return false;
}
private void OnWindowCreated(IntPtr hwnd)
{
try
{
User32.GetWindowThreadProcessId(hwnd, out uint pid);
using (var proc = Process.GetProcessById((int)pid))
{
if (_info.ContainsKey(proc.ProcessName.ToLower()))
{
FoundNewRelevantProcess(proc);
}
}
}
catch (Exception ex)
{
Trace.WriteLine(ex);
}
}
bool FoundNewRelevantProcess(Process proc)
{
var info = _info[proc.ProcessName.ToLower()];
if (!info.RunningProcesses.ContainsKey(proc.Id))
{
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;
}
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);
try
{
var runningProcs = Process.GetProcessesByName(text);
foreach (var proc in runningProcs)
{
FoundNewRelevantProcess(proc);
}
}
catch (Exception ex)
{
Trace.WriteLine(ex);
}
}
public void RegisterStart(string text, Action callback)
{
Trace.WriteLine($"ProcessWatcher RegisterStart {text}");
text = text.ToLower();
WatcherInfo info = _info.ContainsKey(text) ? _info[text] : new WatcherInfo();
info.StartCallbacks.Add(callback);
try
{
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();
}
}
catch (Exception ex)
{
Trace.WriteLine(ex);
}
}
public void Clear()
{
Trace.WriteLine("ProcessWatcher Clear");
_info = new Dictionary();
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ActionProcessor.cs
================================================
using EarTrumpet.Interop;
using EarTrumpet.Actions.DataModel.Serialization;
using EarTrumpet.Actions.DataModel.Enum;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using EarTrumpet.DataModel.Audio;
using EarTrumpet.DataModel.WindowsAudio;
using EarTrumpet.DataModel.AppInformation;
namespace EarTrumpet.Actions.DataModel.Processing
{
class ActionProcessor
{
public static void Invoke(BaseAction a)
{
Trace.WriteLine($"ActionProcessor Invoke: {a.GetType().Name}");
if (a is SetVariableAction)
{
EarTrumpetActionsAddon.Current.LocalVariables[((SetVariableAction)a).Text] = (((SetVariableAction)a).Value == BoolValue.True);
}
else if (a is SetDefaultDeviceAction)
{
var mgr = WindowsAudioFactory.Create((AudioDeviceKind)System.Enum.Parse(typeof(AudioDeviceKind), ((SetDefaultDeviceAction)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 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 (action.App.Id == AppRef.ForegroundAppId)
{
var app = FindForegroundApp(device.Groups);
if (app != null)
{
DoAudioAction(action.Option, app, action);
}
}
else
{
foreach (var app in device.Groups.Where(app => action.App.Id == AppRef.EveryAppId || app.AppId == action.App.Id))
{
DoAudioAction(action.Option, app, action);
}
}
}
}
else if (a is SetAppMuteAction)
{
var action = (SetAppMuteAction)a;
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)
{
if (action.App.Id == AppRef.ForegroundAppId)
{
var app = FindForegroundApp(device.Groups);
if (app != null)
{
DoAudioAction(action.Option, app);
}
}
else
{
foreach (var app in device.Groups.Where(app => action.App.Id == AppRef.EveryAppId || app.AppId == action.App.Id))
{
DoAudioAction(action.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));
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 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);
}
}
else
{
throw new NotImplementedException();
}
}
private static IAudioDeviceSession FindForegroundApp(ObservableCollection groups)
{
var hWnd = User32.GetForegroundWindow();
var foregroundClassName = new StringBuilder(User32.MAX_CLASSNAME_LENGTH);
User32.GetClassName(hWnd, foregroundClassName, foregroundClassName.Capacity);
if (hWnd == IntPtr.Zero)
{
Trace.WriteLine($"ActionProcessor FindForegroundApp: No Window (1)");
return null;
}
// 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);
}
if (hWnd == IntPtr.Zero)
{
Trace.WriteLine($"ActionProcessor FindForegroundApp: No Window (2)");
return null;
}
User32.GetWindowThreadProcessId(hWnd, out uint processId);
try
{
var appInfo = AppInformationFactory.CreateForProcess((int)processId);
foreach(var group in groups)
{
if (group.AppId == appInfo.PackageInstallPath)
{
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;
}
private static void DoAudioAction(MuteKind action, IStreamWithVolumeControl stream)
{
switch (action)
{
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)
{
var vol = (float)(part.Volume / 100f);
switch (action)
{
case SetVolumeKind.Set:
stream.Volume = vol;
break;
case SetVolumeKind.Increment:
stream.Volume += vol;
break;
case SetVolumeKind.Decrement:
stream.Volume -= vol;
break;
}
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/AudioTriggerManager.cs
================================================
using EarTrumpet.Actions.DataModel.Enum;
using EarTrumpet.Actions.DataModel.Serialization;
using System;
using System.Collections.Generic;
using EarTrumpet.DataModel.Audio;
using EarTrumpet.DataModel.WindowsAudio;
using EarTrumpet.Extensibility.Shared;
namespace EarTrumpet.Actions.DataModel.Processing
{
class AudioTriggerManager
{
public event Action Triggered;
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 void Register(BaseTrigger trigger)
{
if (trigger is DeviceEventTrigger)
{
_deviceTriggers.Add((DeviceEventTrigger)trigger);
}
else if (trigger is AppEventTrigger)
{
_appTriggers.Add((AppEventTrigger)trigger);
}
else throw new NotImplementedException();
}
public void Clear()
{
_appTriggers.Clear();
_deviceTriggers.Clear();
}
private void PlaybackDeviceManager_DefaultChanged(object sender, EarTrumpet.DataModel.Audio.IAudioDevice newDefault)
{
if (newDefault == null) return;
ProcessDefaultChanged(newDefault);
_defaultPlaybackDevice = newDefault;
}
private void RecordingMgr_DefaultChanged(object sender, IAudioDevice newDefault)
{
if (newDefault == null) return;
ProcessDefaultChanged(newDefault);
_defaultRecordingDevice = newDefault;
}
private void ProcessDefaultChanged(IAudioDevice newDefault)
{
foreach (var trigger in _deviceTriggers)
{
if (trigger.Device.Id == _defaultPlaybackDevice?.Id &&
trigger.Option == AudioDeviceEventKind.LeavingDefault)
{
Triggered?.Invoke(trigger);
}
if (trigger.Device.Id == newDefault.Id &&
trigger.Option == AudioDeviceEventKind.BecomingDefault)
{
Triggered?.Invoke(trigger);
}
}
}
private void OnDeviceAddOrRemove(IAudioDevice device, AudioDeviceEventKind option)
{
foreach (var trigger in _deviceTriggers)
{
if (trigger.Option == option)
{
// Default device: not supported
if (trigger.Device.Id == device.Id)
{
Triggered?.Invoke(trigger);
}
}
}
}
private void OnAppAddOrRemove(IAudioDeviceSession app, AudioAppEventKind option)
{
foreach (var trigger in _appTriggers)
{
if (trigger.Option == option)
{
var device = app.Parent;
if ((trigger.Device?.Id == null && device == _playbackManager.DeviceManager.Default) ||
trigger.Device?.Id == device.Id)
{
if (trigger.App.Id == app.AppId)
{
Triggered?.Invoke(trigger);
}
}
}
}
}
private void OnAppPropertyChanged(IAudioDeviceSession app, string propertyName)
{
foreach (var trigger in _appTriggers)
{
var device = app.Parent;
if ((trigger.Device?.Id == null && device == _playbackManager.DeviceManager.Default) || trigger.Device?.Id == device.Id)
{
if (trigger.App.Id == app.AppId)
{
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;
}
}
}
}
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/ConditionProcessor.cs
================================================
using EarTrumpet.Actions.DataModel.Enum;
using EarTrumpet.Actions.DataModel.Serialization;
using System;
using EarTrumpet.DataModel.WindowsAudio;
namespace EarTrumpet.Actions.DataModel.Processing
{
class ConditionProcessor
{
public static bool IsMet(BaseCondition condition)
{
if (condition is ProcessCondition)
{
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();
}
}
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)
{
return (EarTrumpetActionsAddon.Current.LocalVariables[((VariableCondition)condition).Text] == (((VariableCondition)condition).Value == BoolValue.True));
}
throw new NotImplementedException();
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Processing/TriggerManager.cs
================================================
using EarTrumpet.Extensibility;
using EarTrumpet.Interop.Helpers;
using EarTrumpet.Actions.DataModel.Enum;
using EarTrumpet.Actions.DataModel.Serialization;
using System;
using System.Collections.Generic;
namespace EarTrumpet.Actions.DataModel.Processing
{
class TriggerManager
{
public event Action Triggered;
private List _eventTriggers = new List();
private AudioTriggerManager _audioManager;
public TriggerManager()
{
_audioManager = new AudioTriggerManager();
_audioManager.Triggered += (t) => Triggered?.Invoke(t);
}
public void Clear()
{
ProcessWatcher.Current.Clear();
_eventTriggers.Clear();
_audioManager.Clear();
}
public void OnEvent(AddonEventKind evt)
{
foreach (var trigger in _eventTriggers)
{
if ((trigger.Option == EarTrumpetEventKind.Startup && evt == AddonEventKind.InitializeAddon) ||
(trigger.Option == EarTrumpetEventKind.Shutdown && evt == AddonEventKind.AppShuttingDown))
{
Triggered?.Invoke(trigger);
}
}
}
public void Register(BaseTrigger trig)
{
if (trig is ProcessTrigger)
{
var trigger = (ProcessTrigger)trig;
if (!string.IsNullOrWhiteSpace(trigger.Text))
{
if (trigger.Option == ProcessEventKind.Start)
{
ProcessWatcher.Current.RegisterStart(trigger.Text, () => Triggered?.Invoke(trig));
}
else
{
ProcessWatcher.Current.RegisterStop(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) =>
{
if (data.Equals(trigger.Option))
{
Triggered?.Invoke(trig);
}
};
}
else if (trig is ContextMenuTrigger)
{
// Nothing to do.
}
else throw new NotImplementedException();
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Actions.cs
================================================
using EarTrumpet.Actions.DataModel.Enum;
using System.Xml.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 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; }
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/App.cs
================================================
namespace EarTrumpet.Actions.DataModel.Serialization
{
public class AppRef
{
public static readonly string EveryAppId = "EarTrumpet.EveryApp";
public static readonly string ForegroundAppId = "EarTrumpet.ForegroundApp";
public string Id { get; set; }
public override int GetHashCode()
{
return Id == null ? 0 : Id.GetHashCode();
}
public bool Equals(AppRef other)
{
return other.Id == Id;
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Conditions.cs
================================================
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 { }
public class DefaultDeviceCondition : BaseCondition, IPartWithDevice
{
public Device Device { get; set; }
public ComparisonBoolKind Option { 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; }
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Device.cs
================================================
namespace EarTrumpet.Actions.DataModel.Serialization
{
public class Device
{
public string Id { get; set; }
public string Kind { get; set; }
public override int GetHashCode()
{
return Id == null ? 0 : Id.GetHashCode();
}
public bool Equals(Device other)
{
return other.Id == Id;
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/EarTrumpetAction.cs
================================================
using System;
using System.Collections.ObjectModel;
namespace EarTrumpet.Actions.DataModel.Serialization
{
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();
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/DataModel/Serialization/Triggers.cs
================================================
using EarTrumpet.Interop.Helpers;
using EarTrumpet.Actions.DataModel.Enum;
using System.Xml.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
{
public string Text { get; set; }
public ProcessEventKind Option { get; set; }
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/EarTrumpetActionsAddon.cs
================================================
using EarTrumpet.Actions.DataModel;
using EarTrumpet.Actions.DataModel.Processing;
using EarTrumpet.Actions.DataModel.Serialization;
using EarTrumpet.Actions.ViewModel;
using EarTrumpet.DataModel.Storage;
using EarTrumpet.Extensibility;
using EarTrumpet.UI.Helpers;
using EarTrumpet.UI.ViewModels;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
namespace EarTrumpet.Actions
{
[Export(typeof(EarTrumpetAddon))]
public class EarTrumpetActionsAddon : EarTrumpetAddon, IEarTrumpetAddonEvents, IEarTrumpetAddonSettingsPage, IEarTrumpetAddonNotificationAreaContextMenu
{
public static EarTrumpetActionsAddon Current { get; private set; }
public LocalVariablesContainer LocalVariables { get; private set; }
public EarTrumpetActionsAddon() : base()
{
DisplayName = Properties.Resources.MyActionsText;
}
public EarTrumpetAction[] Actions
{
get => _actions;
set
{
Settings.Set(c_actionsSettingKey, value);
LoadAndRegister();
}
}
private readonly string c_actionsSettingKey = "ActionsData";
private EarTrumpetAction[] _actions = new EarTrumpetAction[] { };
private TriggerManager _triggerManager = new TriggerManager();
public void OnAddonEvent(AddonEventKind evt)
{
if (evt == AddonEventKind.AddonsInitialized)
{
Current = this;
LocalVariables = new LocalVariablesContainer(Settings);
_triggerManager.Triggered += OnTriggered;
LoadAndRegister();
_triggerManager.OnEvent(AddonEventKind.InitializeAddon);
}
else if (evt == AddonEventKind.AppShuttingDown)
{
_triggerManager.OnEvent(AddonEventKind.AppShuttingDown);
}
}
public SettingsCategoryViewModel GetSettingsCategory()
{
LoadAddonResources();
return new ActionsCategoryViewModel();
}
public IEnumerable NotificationAreaContextMenuItems
{
get
{
var ret = new List();
if (EarTrumpetActionsAddon.Current == null)
{
return ret;
}
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))
});
}
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)
{
imp.Id = Guid.NewGuid();
}
imported.AddRange(Actions);
Actions = imported.ToArray();
}
public string Export()
{
return Settings.Get(c_actionsSettingKey, "");
}
private void OnTriggered(BaseTrigger trigger)
{
var action = Actions.FirstOrDefault(a => a.Triggers.Contains(trigger));
if (action != null && action.Conditions.All(c => ConditionProcessor.IsMet(c)))
{
TriggerAction(action);
}
}
public void TriggerAction(EarTrumpetAction action)
{
action.Actions.ToList().ForEach(a => ActionProcessor.Invoke(a));
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/Interop/Helpers/WindowWatcher.cs
================================================
using EarTrumpet.Interop.Helpers;
using System;
using System.Diagnostics;
using System.Windows.Forms;
namespace EarTrumpet.Actions.Interop.Helpers
{
class WindowWatcher
{
public event Action WindowCreated;
public event Action WindowDestroyed;
readonly Win32Window _window;
readonly uint _ShellNotifyMsg;
public WindowWatcher()
{
_window = new Win32Window();
_window.Initialize(WndProc);
_ShellNotifyMsg = User32.RegisterWindowMessageW(User32.SHELLHOOK);
if (!User32.RegisterShellHookWindow(_window.Handle))
{
Trace.WriteLine("Failed to register shell hook window");
}
}
void WndProc(Message m)
{
if (m.Msg == _ShellNotifyMsg)
{
if (m.WParam.ToInt32() == User32.HSHELL_WINDOWCREATED)
{
WindowCreated?.Invoke(m.LParam);
}
else if (m.WParam.ToInt32() == User32.HSHELL_WINDOWDESTROYED)
{
WindowDestroyed?.Invoke(m.LParam);
}
}
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/Interop/User32.cs
================================================
using System;
using System.Runtime.InteropServices;
namespace EarTrumpet.Actions.Interop
{
class User32
{
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)]
public static extern uint RegisterWindowMessageW([MarshalAs(UnmanagedType.LPWStr)] string msg);
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetAppMuteActionViewModel.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.ViewModel.Actions
{
class SetAppMuteActionViewModel : PartViewModel
{
public OptionViewModel Option { get; }
public DeviceListViewModel Device { get; }
public AppListViewModel App { get; }
private SetAppMuteAction _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);
Attach(Option);
Attach(App);
Attach(Device);
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetAppVolumeActionViewModel.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
using EarTrumpet.Actions.DataModel.Enum;
namespace EarTrumpet.Actions.ViewModel.Actions
{
class SetAppVolumeActionViewModel : PartViewModel
{
public OptionViewModel Option { get; }
public DeviceListViewModel Device { get; }
public AppListViewModel App { get; }
public VolumeViewModel Volume { get; }
private SetAppVolumeAction _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);
Attach(Option);
Attach(App);
Attach(Device);
Attach(Volume);
}
public override string LinkText
{
get
{
if (_action.Option == SetVolumeKind.Set)
{
return base.LinkText;
}
else
{
return Properties.Resources.SetAppVolumeAction_LinkTextIncrement;
}
}
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDefaultDeviceActionViewModel.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.ViewModel.Actions
{
class SetDefaultDeviceActionViewModel : PartViewModel
{
public DeviceListViewModel Device { get; }
public SetDefaultDeviceActionViewModel(SetDefaultDeviceAction action) : base(action)
{
Device = new DeviceListViewModel(action, DeviceListViewModel.DeviceListKind.Recording);
Attach(Device);
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDeviceMuteActionViewModel.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.ViewModel.Actions
{
class SetDeviceMuteActionViewModel : PartViewModel
{
public OptionViewModel Option { get; }
public DeviceListViewModel Device { get; }
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);
Attach(Option);
Attach(Device);
}
public override string LinkText
{
get
{
if (_action.Option == DataModel.Enum.MuteKind.ToggleMute)
{
return Properties.Resources.SetDeviceMuteAction_LinkTextToggle;
}
else
{
return base.LinkText;
}
}
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetDeviceVolumeActionViewModel.cs
================================================
using EarTrumpet.Actions.DataModel.Enum;
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.ViewModel.Actions
{
class SetDeviceVolumeActionViewModel : PartViewModel
{
public OptionViewModel Option { get; }
public DeviceListViewModel Device { get; }
public VolumeViewModel Volume { get; }
private SetDeviceVolumeAction _action;
public SetDeviceVolumeActionViewModel(SetDeviceVolumeAction action) : base(action)
{
_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);
}
public override string LinkText
{
get
{
if (_action.Option == SetVolumeKind.Set)
{
return base.LinkText;
}
else
{
return Properties.Resources.SetDeviceVolumeAction_LinkTextIncrement;
}
}
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Actions/SetVariableActionViewModel.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.ViewModel.Actions
{
class SetVariableActionViewModel : PartViewModel
{
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);
Attach(Option);
Attach(Text);
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ActionsCategoryViewModel.cs
================================================
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
{
public class ActionsCategoryViewModel : SettingsCategoryViewModel
{
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;
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;
vm.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(vm.IsSelected) &&
vm.IsSelected && !Pages.Contains(vm))
{
Pages.Insert(0, vm);
}
};
Selected = vm;
}),
DisplayName = Properties.Resources.NewActionText,
Glyph = "\xE948",
GlyphFontSize = 15,
} };
if (Pages.Count == 2)
{
Toolbar[0].Command.Execute(null);
}
}
internal void ReloadSavedPages()
{
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];
}
public void Delete(EarTrumpetActionViewModel earTrumpetActionViewModel, bool promptOverride = false)
{
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();
}
}
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.ToArray();
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);
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/AppListViewModel.cs
================================================
using EarTrumpet.Extensions;
using EarTrumpet.UI.ViewModels;
using EarTrumpet.Actions.DataModel;
using EarTrumpet.Actions.DataModel.Serialization;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls.Primitives;
using EarTrumpet.DataModel.WindowsAudio;
using EarTrumpet.DataModel.Audio;
namespace EarTrumpet.Actions.ViewModel
{
class AppListViewModel : BindableBase
{
[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 = new ObservableCollection();
GetApps(flags);
if (part.App?.Id == null)
{
_part.App = new AppRef { Id = All[0].Id };
}
}
public void OnInvoked(object sender, IAppItemViewModel vivewModel)
{
_part.App = new AppRef { Id = vivewModel.Id };
RaisePropertyChanged(""); // Signal change so ToString will be called.
var popup = ((DependencyObject)sender).FindVisualParent();
popup.IsOpen = false;
}
public override string ToString()
{
var existing = All.FirstOrDefault(d => d.Id == _part.App?.Id);
if (existing != null)
{
return existing.DisplayName;
}
return _part.App?.Id;
}
public void GetApps(AppKind flags)
{
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));
}
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/DefaultDeviceConditionViewModel.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.ViewModel.Conditions
{
class DefaultDeviceConditionViewModel : PartViewModel
{
public DeviceListViewModel Device { 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);
Attach(Option);
Attach(Device);
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/ProcessConditionViewModel.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.ViewModel.Conditions
{
class ProcessConditionViewModel : PartViewModel
{
public OptionViewModel Option { get; }
public TextViewModel Text { get; }
public ProcessConditionViewModel(ProcessCondition condition) : base(condition)
{
Option = new OptionViewModel(condition, nameof(condition.Option));
Text = new TextViewModel(condition);
Attach(Option);
Attach(Text);
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/Conditions/VariableConditionViewModel.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.ViewModel.Conditions
{
class VariableConditionViewModel : PartViewModel
{
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);
Attach(Option);
Attach(Text);
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DefaultPlaybackDeviceViewModel.cs
================================================
using EarTrumpet.DataModel.WindowsAudio;
namespace EarTrumpet.Actions.ViewModel
{
class DefaultPlaybackDeviceViewModel : DeviceViewModelBase
{
public DefaultPlaybackDeviceViewModel()
{
DisplayName = Properties.Resources.DefaultPlaybackDeviceText;
Kind = AudioDeviceKind.Playback.ToString();
GroupName = Properties.Resources.PlaybackDeviceGroupText;
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceListViewModel.cs
================================================
using EarTrumpet.Extensions;
using EarTrumpet.Actions.DataModel;
using EarTrumpet.Actions.DataModel.Serialization;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls.Primitives;
using EarTrumpet.DataModel.WindowsAudio;
namespace EarTrumpet.Actions.ViewModel
{
public class DeviceListViewModel : BindableBase
{
[Flags]
public enum DeviceListKind
{
Playback = 0,
Recording = 1,
DefaultPlayback = 2
}
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.
var popup = ((DependencyObject)sender).FindVisualParent();
popup.IsOpen = false;
}
private IPartWithDevice _part;
public DeviceListViewModel(IPartWithDevice part, DeviceListKind flags)
{
_part = part;
All = new ObservableCollection();
GetDevices(flags);
if (_part.Device == null)
{
_part.Device = new Device { Id = All[0].Id, Kind = All[0].Kind };
}
}
public override string ToString()
{
var existing = All.FirstOrDefault(d => d.Id == _part.Device?.Id);
if (existing != null)
{
return existing.DisplayName;
}
return _part.Device?.Id;
}
void GetDevices(DeviceListKind flags)
{
bool isRecording = (flags & DeviceListKind.Recording) == DeviceListKind.Recording;
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));
}
if (isRecording)
{
foreach (var device in WindowsAudioFactory.Create(AudioDeviceKind.Recording).Devices.OrderBy(d => d.DisplayName))
{
All.Add(new DeviceViewModel(device));
}
}
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceViewModel.cs
================================================
using EarTrumpet.DataModel.Audio;
using EarTrumpet.DataModel.WindowsAudio;
using EarTrumpet.UI.Helpers;
namespace EarTrumpet.Actions.ViewModel
{
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;
private readonly IAudioDevice _device;
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;
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/DeviceViewModelBase.cs
================================================
namespace EarTrumpet.Actions.ViewModel
{
public class DeviceViewModelBase : BindableBase
{
public string DisplayName { get; set; }
public string GroupName { get; set; }
public string Id { get; set; }
public string Kind { get; set; }
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EarTrumpetActionPageHeaderViewModel.cs
================================================
using EarTrumpet.UI.ViewModels;
using System.ComponentModel;
namespace EarTrumpet.Actions.ViewModel
{
public class EarTrumpetActionPageHeaderViewModel : SettingsPageHeaderViewModel
{
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;
}
private void EarTrumpetActionPageHeaderViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
RaisePropertyChanged(e.PropertyName);
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EarTrumpetActionViewModel.cs
================================================
using EarTrumpet.UI.Helpers;
using EarTrumpet.UI.ViewModels;
using EarTrumpet.Actions.DataModel;
using EarTrumpet.Actions.DataModel.Serialization;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace EarTrumpet.Actions.ViewModel
{
public class EarTrumpetActionViewModel : SettingsPageViewModel
{
public ToolbarItemViewModel[] Toolbar { get; private set; }
public ICommand Delete => new RelayCommand(() => _parent.Delete(this));
public Guid Id => _action.Id;
public string DisplayName
{
get => _action.DisplayName;
set
{
if (DisplayName != value)
{
_action.DisplayName = value;
RaisePropertyChanged(nameof(DisplayName));
Title = DisplayName;
IsWorkSaved = false;
IsPersisted = true;
}
}
}
private bool _isEditClicked;
public bool IsEditClicked
{
get => _isEditClicked;
set
{
if (_isEditClicked != value)
{
_isEditClicked = value;
RaisePropertyChanged(nameof(IsEditClicked));
// Immediately unset the value so we can go again.
_isEditClicked = false;
RaisePropertyChanged(nameof(IsEditClicked));
}
}
}
private bool _isWorkSaved;
public bool IsWorkSaved
{
get => _isWorkSaved;
set
{
if (_isWorkSaved != value)
{
_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 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;
public EarTrumpetActionViewModel(ActionsCategoryViewModel parent, EarTrumpetAction action) : base("Saved Actions")
{
_parent = parent;
Reset(action);
Header = new EarTrumpetActionPageHeaderViewModel(this);
Toolbar = new ToolbarItemViewModel[]
{
new ToolbarItemViewModel
{
Command = new RelayCommand(() =>
{
IsEditClicked = true;
}),
DisplayName = Properties.Resources.ToolbarEditText,
Glyph = "\xE70F",
GlyphFontSize = 15,
},
new ToolbarItemViewModel
{
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.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);
}
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;
for (var i = 0; i < col.Count; i++)
{
col[i].IsShowingAdditionalText = i != 0;
}
IsWorkSaved = false;
IsPersisted = true;
}
private ContextMenuItem MakeItem(PartViewModel part)
{
return new ContextMenuItem
{
DisplayName = part.AddText,
Command = new RelayCommand(() =>
{
InitializeViewModel(part);
GetListFromPart(part).Add(part);
}),
};
}
private PartViewModel CreatePartViewModel(Part part)
{
var ret = PartViewModelFactory.Create(part);
InitializeViewModel(ret);
return ret;
}
private void InitializeViewModel(PartViewModel part)
{
part.PropertyChanged += (_, __) => IsWorkSaved = false;
part.Remove = new RelayCommand(() => GetListFromPart(part).Remove(part));
}
private ObservableCollection GetListFromPart(PartViewModel part)
{
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();
}
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/EveryAppViewModel.cs
================================================
using EarTrumpet.UI.ViewModels;
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.ViewModel
{
class EveryAppViewModel : SettingsAppItemViewModel
{
public EveryAppViewModel()
{
DisplayName = Properties.Resources.EveryAppText;
Id = AppRef.EveryAppId;
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ForegroundAppViewModel.cs
================================================
using EarTrumpet.UI.ViewModels;
using EarTrumpet.Actions.DataModel.Serialization;
namespace EarTrumpet.Actions.ViewModel
{
class ForegroundAppViewModel : SettingsAppItemViewModel
{
public ForegroundAppViewModel()
{
Id = AppRef.ForegroundAppId;
DisplayName = Properties.Resources.ForegroundAppText;
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/HotkeyViewModel.cs
================================================
using EarTrumpet.Actions.DataModel.Serialization;
using System;
namespace EarTrumpet.Actions.ViewModel
{
public class HotkeyViewModel : BindableBase
{
public EarTrumpet.UI.ViewModels.HotkeyViewModel Hotkey { get; }
private HotkeyTrigger _trigger;
public HotkeyViewModel(HotkeyTrigger trigger)
{
_trigger = trigger;
Hotkey = new EarTrumpet.UI.ViewModels.HotkeyViewModel(_trigger.Option, (newHotkey) =>
{
_trigger.Option = newHotkey;
RaisePropertyChanged(nameof(Hotkey));
});
}
public override string ToString()
{
if (_trigger.Option.IsEmpty)
{
return ResolveResource("EmptyText");
}
else
{
return _trigger.Option.ToString();
}
}
private string ResolveResource(string suffix)
{
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;
}
}
}
================================================
FILE: EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/IOptionViewModel.cs
================================================
using System.Collections.ObjectModel;
namespace EarTrumpet.Actions.ViewModel
{
interface IOptionViewModel
{
ObservableCollection