Ejemplo n.º 1
0
 private void HandleWindowClosing(IWindowViewController viewController, CancelEventArgs args)
 {
     Log.Info($"Main window is closing(cancel: {args.Cancel}), {nameof(Visibility)}: {Visibility}, {nameof(MicSwitchConfig.MinimizeOnClose)}: {configProvider.ActualConfig.MinimizeOnClose}");
     if (MinimizeOnClose)
     {
         Log.Info("Cancelling main window closure (will be ignored during app shutdown)");
         args.Cancel = true;
         viewController.Hide();
     }
 }
Ejemplo n.º 2
0
        public MainWindowViewModel(
            IAppArguments appArguments,
            IApplicationAccessor applicationAccessor,
            IFactory <IStartupManager, StartupManagerArgs> startupManagerFactory,
            IMicSwitchOverlayViewModel overlay,
            IMicrophoneControllerViewModel microphoneControllerViewModel,
            IOverlayWindowController overlayWindowController,
            IWaveOutDeviceSelectorViewModel waveOutDeviceSelector,
            IAudioNotificationsManager audioNotificationsManager,
            IFactory <IAudioNotificationSelectorViewModel> audioSelectorFactory,
            IApplicationUpdaterViewModel appUpdater,
            [Dependency(WellKnownWindows.MainWindow)] IWindowTracker mainWindowTracker,
            IConfigProvider <MicSwitchConfig> configProvider,
            IConfigProvider <MicSwitchOverlayConfig> overlayConfigProvider,
            IImageProvider imageProvider,
            IErrorMonitorViewModel errorMonitor,
            IAudioNotificationsManager notificationsManager,
            IWindowViewController viewController,
            [Dependency(WellKnownSchedulers.UI)] IScheduler uiScheduler)
        {
            Title = $"{(appArguments.IsDebugMode ? "[D]" : "")} {appArguments.AppName} v{appArguments.Version}";

            this.appArguments          = appArguments;
            this.applicationAccessor   = applicationAccessor;
            this.MicrophoneController  = microphoneControllerViewModel.AddTo(Anchors);
            this.mainWindowTracker     = mainWindowTracker;
            this.configProvider        = configProvider;
            this.overlayConfigProvider = overlayConfigProvider;
            this.notificationsManager  = notificationsManager;
            this.viewController        = viewController;
            ApplicationUpdater         = appUpdater.AddTo(Anchors);
            WaveOutDeviceSelector      = waveOutDeviceSelector;
            ImageProvider            = imageProvider;
            ErrorMonitor             = errorMonitor;
            AudioSelectorWhenMuted   = audioSelectorFactory.Create().AddTo(Anchors);
            AudioSelectorWhenUnmuted = audioSelectorFactory.Create().AddTo(Anchors);
            WindowState = WindowState.Minimized;
            Overlay     = overlay.AddTo(Anchors);

            try
            {
                var startupManagerArgs = new StartupManagerArgs
                {
                    UniqueAppName   = $"{appArguments.AppName}{(appArguments.IsDebugMode ? "-debug" : string.Empty)}",
                    ExecutablePath  = appUpdater.LauncherExecutable.FullName,
                    CommandLineArgs = appArguments.StartupArgs,
                    AutostartFlag   = appArguments.AutostartFlag
                };
                this.startupManager     = startupManagerFactory.Create(startupManagerArgs);
                RunAtLoginToggleCommand = CommandWrapper.Create <bool>(RunAtLoginCommandExecuted, Observable.Return(startupManager?.IsReady ?? false));
            }
            catch (Exception e)
            {
                Log.Warn("Failed to initialize startup manager", e);
            }

            this.RaiseWhenSourceValue(x => x.IsActive, mainWindowTracker, x => x.IsActive, uiScheduler).AddTo(Anchors);
            this.RaiseWhenSourceValue(x => x.RunAtLogin, startupManager, x => x.IsRegistered, uiScheduler).AddTo(Anchors);
            this.RaiseWhenSourceValue(x => x.ShowOverlaySettings, Overlay, x => x.OverlayVisibilityMode).AddTo(Anchors);

            audioNotificationSource = Observable.Merge(
                AudioSelectorWhenMuted.ObservableForProperty(x => x.SelectedValue, skipInitial: true),
                AudioSelectorWhenUnmuted.ObservableForProperty(x => x.SelectedValue, skipInitial: true))
                                      .Select(x => new TwoStateNotification
            {
                On  = AudioSelectorWhenUnmuted.SelectedValue,
                Off = AudioSelectorWhenMuted.SelectedValue
            })
                                      .ToProperty(this, x => x.AudioNotification)
                                      .AddTo(Anchors);

            this.WhenAnyValue(x => x.AudioNotificationVolume)
            .Subscribe(x =>
            {
                AudioSelectorWhenUnmuted.Volume = AudioSelectorWhenMuted.Volume = x;
            })
            .AddTo(Anchors);

            MicrophoneController.ObservableForProperty(x => x.MicrophoneMuted, skipInitial: true)
            .DistinctUntilChanged()
            .Where(x => !MicrophoneController.MicrophoneLine.IsEmpty)
            .Select(isMuted => (isMuted.Value ? AudioNotification.Off : AudioNotification.On) ?? default(AudioNotificationType).ToString())
            .Where(notificationToPlay => !string.IsNullOrEmpty(notificationToPlay))
            .Select(notificationToPlay => Observable.FromAsync(async token =>
            {
                Log.Debug($"Playing notification {notificationToPlay}, volume: {audioNotificationVolume}");
                try
                {
                    await audioNotificationsManager.PlayNotification(notificationToPlay, audioNotificationVolume, waveOutDeviceSelector.SelectedItem, token);
                    Log.Debug($"Played notification {notificationToPlay}");
                }
                catch (Exception ex)
                {
                    Log.Debug($"Failed to play notification {notificationToPlay}", ex);
                }
            }))
            .Switch()
            .SubscribeToErrors(Log.HandleUiException)
            .AddTo(Anchors);

            this.WhenAnyValue(x => x.WindowState)
            .SubscribeSafe(x => ShowInTaskbar = x != WindowState.Minimized, Log.HandleUiException)
            .AddTo(Anchors);

            viewController
            .WhenClosing
            .SubscribeSafe(x => HandleWindowClosing(viewController, x), Log.HandleUiException)
            .AddTo(Anchors);

            ToggleOverlayLockCommand         = CommandWrapper.Create(ToggleOverlayCommandExecuted);
            ExitAppCommand                   = CommandWrapper.Create(ExitAppCommandExecuted);
            ShowAppCommand                   = CommandWrapper.Create(ShowAppCommandExecuted);
            OpenAppDataDirectoryCommand      = CommandWrapper.Create(OpenAppDataDirectory);
            ResetOverlayPositionCommand      = CommandWrapper.Create(ResetOverlayPositionCommandExecuted);
            RunAtLoginToggleCommand          = CommandWrapper.Create <bool>(RunAtLoginCommandExecuted, startupManager.WhenAnyValue(x => x.IsReady));
            SelectMicrophoneIconCommand      = CommandWrapper.Create(SelectMicrophoneIconCommandExecuted);
            SelectMutedMicrophoneIconCommand = CommandWrapper.Create(SelectMutedMicrophoneIconCommandExecuted);
            ResetMicrophoneIconsCommand      = CommandWrapper.Create(ResetMicrophoneIconsCommandExecuted);
            AddSoundCommand                  = CommandWrapper.Create(AddSoundCommandExecuted);

            Observable.Merge(configProvider.ListenTo(x => x.Notifications).ToUnit(), configProvider.ListenTo(x => x.NotificationVolume).ToUnit())
            .Select(_ => new { configProvider.ActualConfig.Notifications, configProvider.ActualConfig.NotificationVolume })
            .ObserveOn(uiScheduler)
            .SubscribeSafe(cfg =>
            {
                Log.Debug($"Applying new notification configuration: {cfg.DumpToTextRaw()} (current: {AudioNotification.DumpToTextRaw()}, volume: {AudioNotificationVolume})");
                AudioSelectorWhenMuted.SelectedValue   = cfg.Notifications.Off;
                AudioSelectorWhenUnmuted.SelectedValue = cfg.Notifications.On;
                AudioNotificationVolume = cfg.NotificationVolume;
            }, Log.HandleException)
            .AddTo(Anchors);

            configProvider.ListenTo(x => x.MinimizeOnClose)
            .ObserveOn(uiScheduler)
            .SubscribeSafe(x => MinimizeOnClose = x, Log.HandleException)
            .AddTo(Anchors);

            configProvider.ListenTo(x => x.OutputDeviceId)
            .ObserveOn(uiScheduler)
            .SubscribeSafe(x => WaveOutDeviceSelector.SelectById(x), Log.HandleException)
            .AddTo(Anchors);

            viewController
            .WhenLoaded
            .Take(1)
            .Select(_ => configProvider.ListenTo(y => y.StartMinimized))
            .Switch()
            .Take(1)
            .ObserveOn(uiScheduler)
            .SubscribeSafe(
                x =>
            {
                if (x)
                {
                    Log.Debug($"StartMinimized option is active - minimizing window, current state: {WindowState}");
                    StartMinimized = true;
                    viewController.Hide();
                }
                else
                {
                    Log.Debug($"StartMinimized option is not active - showing window as Normal, current state: {WindowState}");
                    StartMinimized = false;
                    viewController.Show();
                }
            }, Log.HandleUiException)
            .AddTo(Anchors);

            Observable.Merge(
                microphoneControllerViewModel.ObservableForProperty(x => x.MuteMode, skipInitial: true).ToUnit(),
                waveOutDeviceSelector.ObservableForProperty(x => x.SelectedItem, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.AudioNotification, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.MinimizeOnClose, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.Width, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.Height, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.Top, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.Left, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.AudioNotificationVolume, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.StartMinimized, skipInitial: true).ToUnit())
            .Throttle(ConfigThrottlingTimeout)
            .ObserveOn(uiScheduler)
            .SubscribeSafe(() =>
            {
                var config                = configProvider.ActualConfig.CloneJson();
                config.Notifications      = AudioNotification;
                config.NotificationVolume = AudioNotificationVolume;
                config.StartMinimized     = StartMinimized;
                config.MinimizeOnClose    = MinimizeOnClose;
                config.OutputDeviceId     = waveOutDeviceSelector.SelectedItem?.Id;
                config.MainWindowBounds   = new Rect(Left, Top, Width, Height);
                configProvider.Save(config);
            }, Log.HandleUiException)
            .AddTo(Anchors);

            viewController.WhenLoaded
            .SubscribeSafe(() =>
            {
                Log.Debug($"Main window loaded - loading overlay, current process({CurrentProcess.ProcessName} 0x{CurrentProcess.Id:x8}) main window: {CurrentProcess.MainWindowHandle} ({CurrentProcess.MainWindowTitle})");
                overlayWindowController.RegisterChild(Overlay).AddTo(Anchors);
                Log.Debug("Overlay loaded successfully");
            }, Log.HandleUiException)
            .AddTo(Anchors);

            configProvider.ListenTo(x => x.MainWindowBounds)
            .WithPrevious()
            .ObserveOn(uiScheduler)
            .SubscribeSafe(x =>
            {
                Log.Debug($"Main window config bounds updated: {x}");

                Rect bounds;
                if (x.Current == null)
                {
                    var monitorBounds = UnsafeNative.GetMonitorBounds(Rectangle.Empty).ScaleToWpf();
                    var monitorCenter = monitorBounds.Center();
                    bounds            = new Rect(
                        monitorCenter.X - DefaultSize.Width / 2f,
                        monitorCenter.Y - DefaultSize.Height / 2f,
                        DefaultSize.Width,
                        DefaultSize.Height);
                }
                else
                {
                    bounds = x.Current.Value;
                }

                Left   = bounds.Left;
                Top    = bounds.Top;
                Width  = bounds.Width;
                Height = bounds.Height;
            }, Log.HandleUiException)
            .AddTo(Anchors);

            var theme = Theme.Create(
                Theme.Light,
                primary: SwatchHelper.Lookup[(MaterialDesignColor)PrimaryColor.BlueGrey],
                accent: SwatchHelper.Lookup[(MaterialDesignColor)SecondaryColor.LightBlue]);
            var paletteHelper = new PaletteHelper();

            paletteHelper.SetTheme(theme);
        }
Ejemplo n.º 3
0
        public MainWindowViewModel(
            IAppArguments appArguments,
            IFactory <IStartupManager, StartupManagerArgs> startupManagerFactory,
            IMicSwitchOverlayViewModel overlay,
            IMicrophoneControllerViewModel microphoneControllerViewModel,
            IOverlayWindowController overlayWindowController,
            IAudioNotificationsManager audioNotificationsManager,
            IFactory <IAudioNotificationSelectorViewModel> audioSelectorFactory,
            IApplicationUpdaterViewModel appUpdater,
            [Dependency(WellKnownWindows.MainWindow)] IWindowTracker mainWindowTracker,
            IConfigProvider <MicSwitchConfig> configProvider,
            IConfigProvider <MicSwitchOverlayConfig> overlayConfigProvider,
            IImageProvider imageProvider,
            IAudioNotificationsManager notificationsManager,
            IWindowViewController viewController,
            [Dependency(WellKnownSchedulers.UI)] IScheduler uiScheduler)
        {
            Title = $"{(appArguments.IsDebugMode ? "[D]" : "")} {appArguments.AppName} v{appArguments.Version}";

            this.appArguments          = appArguments;
            this.MicrophoneController  = microphoneControllerViewModel.AddTo(Anchors);
            this.mainWindowTracker     = mainWindowTracker;
            this.configProvider        = configProvider;
            this.overlayConfigProvider = overlayConfigProvider;
            this.notificationsManager  = notificationsManager;
            this.viewController        = viewController;
            ApplicationUpdater         = appUpdater.AddTo(Anchors);
            ImageProvider            = imageProvider;
            AudioSelectorWhenMuted   = audioSelectorFactory.Create().AddTo(Anchors);
            AudioSelectorWhenUnmuted = audioSelectorFactory.Create().AddTo(Anchors);
            WindowState = WindowState.Minimized;
            Overlay     = overlay.AddTo(Anchors);

            var startupManagerArgs = new StartupManagerArgs
            {
                UniqueAppName   = $"{appArguments.AppName}{(appArguments.IsDebugMode ? "-debug" : string.Empty)}",
                ExecutablePath  = appUpdater.GetLatestExecutable().FullName,
                CommandLineArgs = appArguments.StartupArgs,
                AutostartFlag   = appArguments.AutostartFlag
            };

            this.startupManager = startupManagerFactory.Create(startupManagerArgs);

            this.RaiseWhenSourceValue(x => x.IsActive, mainWindowTracker, x => x.IsActive, uiScheduler).AddTo(Anchors);
            this.RaiseWhenSourceValue(x => x.RunAtLogin, startupManager, x => x.IsRegistered, uiScheduler).AddTo(Anchors);
            this.RaiseWhenSourceValue(x => x.ShowOverlaySettings, Overlay, x => x.OverlayVisibilityMode).AddTo(Anchors);

            audioNotificationSource = Observable.Merge(
                AudioSelectorWhenMuted.ObservableForProperty(x => x.SelectedValue, skipInitial: true),
                AudioSelectorWhenUnmuted.ObservableForProperty(x => x.SelectedValue, skipInitial: true))
                                      .Select(x => new TwoStateNotification
            {
                On  = AudioSelectorWhenUnmuted.SelectedValue,
                Off = AudioSelectorWhenMuted.SelectedValue
            })
                                      .ToPropertyHelper(this, x => x.AudioNotification)
                                      .AddTo(Anchors);

            this.WhenAnyValue(x => x.AudioNotificationVolume)
            .Subscribe(x =>
            {
                AudioSelectorWhenUnmuted.Volume = AudioSelectorWhenMuted.Volume = x;
            })
            .AddTo(Anchors);

            MicrophoneController.ObservableForProperty(x => x.MicrophoneMuted, skipInitial: true)
            .DistinctUntilChanged()
            .Where(x => !MicrophoneController.MicrophoneLine.IsEmpty)
            .SubscribeSafe(x =>
            {
                var notificationToPlay = (x.Value ? AudioNotification.On : AudioNotification.Off) ?? default(AudioNotificationType).ToString();
                Log.Debug($"Playing notification {notificationToPlay} (cfg: {AudioNotification.DumpToTextRaw()})");
                audioNotificationsManager.PlayNotification(notificationToPlay, audioNotificationVolume);
            }, Log.HandleUiException)
            .AddTo(Anchors);

            this.WhenAnyValue(x => x.WindowState)
            .SubscribeSafe(x => ShowInTaskbar = x != WindowState.Minimized, Log.HandleUiException)
            .AddTo(Anchors);

            viewController
            .WhenClosing
            .SubscribeSafe(x => HandleWindowClosing(viewController, x), Log.HandleUiException)
            .AddTo(Anchors);

            ToggleOverlayLockCommand         = CommandWrapper.Create(ToggleOverlayCommandExecuted);
            ExitAppCommand                   = CommandWrapper.Create(ExitAppCommandExecuted);
            ShowAppCommand                   = CommandWrapper.Create(ShowAppCommandExecuted);
            OpenAppDataDirectoryCommand      = CommandWrapper.Create(OpenAppDataDirectory);
            ResetOverlayPositionCommand      = CommandWrapper.Create(ResetOverlayPositionCommandExecuted);
            RunAtLoginToggleCommand          = CommandWrapper.Create <bool>(RunAtLoginCommandExecuted);
            SelectMicrophoneIconCommand      = CommandWrapper.Create(SelectMicrophoneIconCommandExecuted);
            SelectMutedMicrophoneIconCommand = CommandWrapper.Create(SelectMutedMicrophoneIconCommandExecuted);
            ResetMicrophoneIconsCommand      = CommandWrapper.Create(ResetMicrophoneIconsCommandExecuted);
            AddSoundCommand                  = CommandWrapper.Create(AddSoundCommandExecuted);

            Observable.Merge(configProvider.ListenTo(x => x.Notification).ToUnit(), configProvider.ListenTo(x => x.NotificationVolume).ToUnit())
            .Select(_ => new { configProvider.ActualConfig.Notification, configProvider.ActualConfig.NotificationVolume })
            .ObserveOn(uiScheduler)
            .SubscribeSafe(cfg =>
            {
                Log.Debug($"Applying new notification configuration: {cfg.DumpToTextRaw()} (current: {AudioNotification.DumpToTextRaw()}, volume: {AudioNotificationVolume})");
                AudioSelectorWhenMuted.SelectedValue   = cfg.Notification.Off;
                AudioSelectorWhenUnmuted.SelectedValue = cfg.Notification.On;
                AudioNotificationVolume = cfg.NotificationVolume;
            }, Log.HandleException)
            .AddTo(Anchors);

            configProvider.ListenTo(x => x.MinimizeOnClose)
            .ObserveOn(uiScheduler)
            .SubscribeSafe(x => MinimizeOnClose = x, Log.HandleException)
            .AddTo(Anchors);

            viewController
            .WhenLoaded
            .Take(1)
            .Select(_ => configProvider.ListenTo(y => y.StartMinimized))
            .Switch()
            .Take(1)
            .ObserveOn(uiScheduler)
            .SubscribeSafe(
                x =>
            {
                if (x)
                {
                    Log.Debug($"StartMinimized option is active - minimizing window, current state: {WindowState}");
                    StartMinimized = true;
                    viewController.Hide();
                }
                else
                {
                    Log.Debug($"StartMinimized option is not active - showing window as Normal, current state: {WindowState}");
                    StartMinimized = false;
                    viewController.Show();
                }
            }, Log.HandleUiException)
            .AddTo(Anchors);

            configProvider.WhenChanged
            .Subscribe()
            .AddTo(Anchors);

            Observable.Merge(
                microphoneControllerViewModel.ObservableForProperty(x => x.MuteMode, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.AudioNotification, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.MinimizeOnClose, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.AudioNotificationVolume, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.StartMinimized, skipInitial: true).ToUnit())
            .Throttle(ConfigThrottlingTimeout)
            .ObserveOn(uiScheduler)
            .SubscribeSafe(() =>
            {
                var config                = configProvider.ActualConfig.CloneJson();
                config.Notification       = AudioNotification;
                config.NotificationVolume = AudioNotificationVolume;
                config.StartMinimized     = StartMinimized;
                config.MinimizeOnClose    = MinimizeOnClose;
                configProvider.Save(config);
            }, Log.HandleUiException)
            .AddTo(Anchors);

            viewController.WhenLoaded
            .SubscribeSafe(() =>
            {
                Log.Debug($"Main window loaded - loading overlay, current process({CurrentProcess.ProcessName} 0x{CurrentProcess.Id:x8}) main window: {CurrentProcess.MainWindowHandle} ({CurrentProcess.MainWindowTitle})");
                overlayWindowController.RegisterChild(Overlay).AddTo(Anchors);
                Log.Debug("Overlay loaded successfully");
            }, Log.HandleUiException)
            .AddTo(Anchors);

            var theme = Theme.Create(
                Theme.Light,
                primary: SwatchHelper.Lookup[(MaterialDesignColor)PrimaryColor.BlueGrey],
                accent: SwatchHelper.Lookup[(MaterialDesignColor)SecondaryColor.LightBlue]);
            var paletteHelper = new PaletteHelper();

            paletteHelper.SetTheme(theme);
        }
Ejemplo n.º 4
0
        public MainWindowViewModel(
            [NotNull] IAppArguments appArguments,
            [NotNull] IFactory <IStartupManager, StartupManagerArgs> startupManagerFactory,
            [NotNull] IMicrophoneControllerEx microphoneController,
            [NotNull] IMicSwitchOverlayViewModel overlay,
            [NotNull] IOverlayWindowController overlayWindowController,
            [NotNull] IAudioNotificationsManager audioNotificationsManager,
            [NotNull] IFactory <IAudioNotificationSelectorViewModel> audioSelectorFactory,
            [NotNull] IApplicationUpdaterViewModel appUpdater,
            [NotNull][Dependency(WellKnownWindows.MainWindow)] IWindowTracker mainWindowTracker,
            [NotNull] IConfigProvider <MicSwitchConfig> configProvider,
            [NotNull] IComplexHotkeyTracker hotkeyTracker,
            [NotNull] IMicrophoneProvider microphoneProvider,
            [NotNull] IImageProvider imageProvider,
            [NotNull] IAudioNotificationsManager notificationsManager,
            [NotNull] IWindowViewController viewController,
            [NotNull][Dependency(WellKnownSchedulers.UI)] IScheduler uiScheduler)
        {
            var startupManagerArgs = new StartupManagerArgs
            {
                UniqueAppName   = $"{appArguments.AppName}{(appArguments.IsDebugMode ? "-debug" : string.Empty)}",
                ExecutablePath  = appUpdater.GetLatestExecutable().FullName,
                CommandLineArgs = appArguments.StartupArgs,
                AutostartFlag   = appArguments.AutostartFlag
            };

            this.startupManager = startupManagerFactory.Create(startupManagerArgs);

            this.appArguments         = appArguments;
            this.microphoneController = microphoneController;

            ApplicationUpdater        = appUpdater;
            this.mainWindowTracker    = mainWindowTracker;
            this.configProvider       = configProvider;
            this.notificationsManager = notificationsManager;
            this.RaiseWhenSourceValue(x => x.IsActive, mainWindowTracker, x => x.IsActive).AddTo(Anchors);

            AudioSelectorWhenMuted   = audioSelectorFactory.Create();
            AudioSelectorWhenUnmuted = audioSelectorFactory.Create();

            Observable.Merge(
                AudioSelectorWhenMuted.ObservableForProperty(x => x.SelectedValue, skipInitial: true),
                AudioSelectorWhenUnmuted.ObservableForProperty(x => x.SelectedValue, skipInitial: true))
            .Subscribe(() => this.RaisePropertyChanged(nameof(AudioNotification)), Log.HandleException)
            .AddTo(Anchors);

            configProvider.WhenChanged
            .Subscribe()
            .AddTo(Anchors);

            configProvider.ListenTo(x => x.Notification)
            .ObserveOn(uiScheduler)
            .Subscribe(cfg =>
            {
                Log.Debug($"Applying new notification configuration: {cfg.DumpToTextRaw()} (current: {AudioNotification.DumpToTextRaw()})");
                AudioNotification = cfg;
            }, Log.HandleException)
            .AddTo(Anchors);

            configProvider.ListenTo(x => x.MuteMode)
            .ObserveOn(uiScheduler)
            .Subscribe(x =>
            {
                Log.Debug($"Mute mode loaded from config: {x}");
                MuteMode = x;
            }, Log.HandleException)
            .AddTo(Anchors);

            this.WhenAnyValue(x => x.MuteMode)
            .Subscribe(x =>
            {
                if (x == MuteMode.PushToTalk)
                {
                    Log.Debug($"{nameof(MuteMode.PushToTalk)} mute mode is enabled, un-muting microphone");
                    MuteMicrophoneCommand.Execute(true);
                }
                else if (x == MuteMode.PushToMute)
                {
                    MuteMicrophoneCommand.Execute(false);
                    Log.Debug($"{nameof(MuteMode.PushToMute)} mute mode is enabled, muting microphone");
                }
            })
            .AddTo(Anchors);

            configProvider.ListenTo(x => x.IsPushToTalkMode)
            .Where(x => x == true)
            .ObserveOn(uiScheduler)
            .Subscribe(x =>
            {
                //FIXME This whole block is for backward-compatibility reasons and should be removed when possible
                MuteMode = MuteMode.PushToTalk;
            }, Log.HandleException)
            .AddTo(Anchors);

            configProvider.ListenTo(x => x.SuppressHotkey)
            .ObserveOn(uiScheduler)
            .Subscribe(x => SuppressHotkey = x, Log.HandleException)
            .AddTo(Anchors);

            configProvider.ListenTo(x => x.MinimizeOnClose)
            .ObserveOn(uiScheduler)
            .Subscribe(x => MinimizeOnClose = x, Log.HandleException)
            .AddTo(Anchors);

            configProvider.ListenTo(x => x.VolumeControlEnabled)
            .ObserveOn(uiScheduler)
            .Subscribe(x => MicrophoneVolumeControlEnabled = x, Log.HandleException)
            .AddTo(Anchors);

            Observable.Merge(configProvider.ListenTo(x => x.MicrophoneHotkey), configProvider.ListenTo(x => x.MicrophoneHotkeyAlt))
            .Select(x => new
            {
                Hotkey    = (HotkeyGesture) new HotkeyConverter().ConvertFrom(configProvider.ActualConfig.MicrophoneHotkey ?? string.Empty),
                HotkeyAlt = (HotkeyGesture) new HotkeyConverter().ConvertFrom(configProvider.ActualConfig.MicrophoneHotkeyAlt ?? string.Empty),
            })
            .ObserveOn(uiScheduler)
            .Subscribe(cfg =>
            {
                Log.Debug($"Setting new hotkeys configuration: {cfg.DumpToTextRaw()} (current: {hotkey}, alt: {hotkeyAlt})");
                Hotkey    = cfg.Hotkey;
                HotkeyAlt = cfg.HotkeyAlt;
            }, Log.HandleException)
            .AddTo(Anchors);

            Overlay = overlay;

            this.RaiseWhenSourceValue(x => x.RunAtLogin, startupManager, x => x.IsRegistered, uiScheduler).AddTo(Anchors);
            this.RaiseWhenSourceValue(x => x.MicrophoneVolume, microphoneController, x => x.VolumePercent, uiScheduler).AddTo(Anchors);
            this.RaiseWhenSourceValue(x => x.MicrophoneMuted, microphoneController, x => x.Mute, uiScheduler).AddTo(Anchors);
            ImageProvider = imageProvider;

            microphoneProvider.Microphones
            .ToObservableChangeSet()
            .ObserveOn(uiScheduler)
            .Bind(out var microphones)
            .Subscribe()
            .AddTo(Anchors);
            Microphones = microphones;

            this.ObservableForProperty(x => x.MicrophoneMuted, skipInitial: true)
            .DistinctUntilChanged()
            .Where(x => !MicrophoneLine.IsEmpty)
            .Skip(1)     // skip initial setup
            .Subscribe(x =>
            {
                var cfg = configProvider.ActualConfig.Notification;
                var notificationToPlay = x.Value ? cfg.On : cfg.Off;
                Log.Debug($"Playing notification {notificationToPlay} (cfg: {cfg.DumpToTextRaw()})");
                audioNotificationsManager.PlayNotification(notificationToPlay);
            }, Log.HandleUiException)
            .AddTo(Anchors);

            this.WhenAnyValue(x => x.MicrophoneLine)
            .DistinctUntilChanged()
            .Subscribe(x => microphoneController.LineId = x, Log.HandleUiException)
            .AddTo(Anchors);

            Observable.Merge(
                configProvider.ListenTo(x => x.MicrophoneLineId).ToUnit(),
                Microphones.ToObservableChangeSet().ToUnit())
            .Select(_ => configProvider.ActualConfig.MicrophoneLineId)
            .ObserveOn(uiScheduler)
            .Subscribe(configLineId =>
            {
                Log.Debug($"Microphone line configuration changed, lineId: {configLineId}, known lines: {Microphones.DumpToTextRaw()}");

                var micLine = Microphones.FirstOrDefault(line => line.Equals(configLineId));
                if (micLine.IsEmpty)
                {
                    Log.Debug($"Selecting first one of available microphone lines, known lines: {Microphones.DumpToTextRaw()}");
                    micLine = Microphones.FirstOrDefault();
                }
                MicrophoneLine = micLine;
                MuteMicrophoneCommand.ResetError();
            }, Log.HandleUiException)
            .AddTo(Anchors);

            hotkeyTracker
            .WhenAnyValue(x => x.IsActive)
            .Skip(1)
            .ObserveOn(uiScheduler)
            .Subscribe(async isActive =>
            {
                Log.Debug($"Handling hotkey press (isActive: {isActive}), mute mode: {muteMode}");
                switch (muteMode)
                {
                case MuteMode.PushToTalk:
                    await MuteMicrophoneCommandExecuted(!isActive);
                    break;

                case MuteMode.PushToMute:
                    await MuteMicrophoneCommandExecuted(isActive);
                    break;

                case MuteMode.ToggleMute:
                    await MuteMicrophoneCommandExecuted(!MicrophoneMuted);
                    break;

                default:
                    throw new ArgumentOutOfRangeException(nameof(muteMode), muteMode, @"Unsupported mute mode");
                }
            }, Log.HandleUiException)
            .AddTo(Anchors);

            ToggleOverlayLockCommand = CommandWrapper.Create(
                () =>
            {
                if (overlay.IsLocked && overlay.UnlockWindowCommand.CanExecute(null))
                {
                    overlay.UnlockWindowCommand.Execute(null);
                }
                else if (!overlay.IsLocked && overlay.LockWindowCommand.CanExecute(null))
                {
                    overlay.LockWindowCommand.Execute(null);
                }
            });

            ExitAppCommand = CommandWrapper.Create(
                () =>
            {
                Log.Debug("Closing application");
                configProvider.Save(configProvider.ActualConfig);
                Application.Current.Shutdown();
            });

            this.WhenAnyValue(x => x.WindowState)
            .Subscribe(x => ShowInTaskbar = x != WindowState.Minimized, Log.HandleUiException)
            .AddTo(Anchors);

            ShowAppCommand = CommandWrapper.Create(
                () =>
            {
                if (Visibility != Visibility.Visible)
                {
                    Log.Debug($"Showing application, currents state: {Visibility}");
                    viewController.Show();
                }
                else
                {
                    Log.Debug($"Hiding application, currents state: {Visibility}");
                    viewController.Hide();
                }
            });

            OpenAppDataDirectoryCommand = CommandWrapper.Create(OpenAppDataDirectory);

            ResetOverlayPositionCommand = CommandWrapper.Create(ResetOverlayPositionCommandExecuted);

            RunAtLoginToggleCommand          = CommandWrapper.Create <bool>(RunAtLoginCommandExecuted);
            MuteMicrophoneCommand            = CommandWrapper.Create <bool>(MuteMicrophoneCommandExecuted);
            SelectMicrophoneIconCommand      = CommandWrapper.Create(SelectMicrophoneIconCommandExecuted);
            SelectMutedMicrophoneIconCommand = CommandWrapper.Create(SelectMutedMicrophoneIconCommandExecuted);
            ResetMicrophoneIconsCommand      = CommandWrapper.Create(ResetMicrophoneIconsCommandExecuted);
            AddSoundCommand = CommandWrapper.Create(AddSoundCommandExecuted);

            var executingAssemblyName = Assembly.GetExecutingAssembly().GetName();

            Title = $"{(appArguments.IsDebugMode ? "[D]" : "")} {executingAssemblyName.Name} v{executingAssemblyName.Version}";

            WindowState = WindowState.Minimized;
            viewController
            .WhenLoaded
            .Take(1)
            .Select(() => configProvider.ListenTo(y => y.StartMinimized))
            .Switch()
            .Take(1)
            .ObserveOn(uiScheduler)
            .Subscribe(
                x =>
            {
                if (x)
                {
                    Log.Debug($"StartMinimized option is active - minimizing window, current state: {WindowState}");
                    StartMinimized = true;
                    viewController.Hide();
                }
                else
                {
                    Log.Debug($"StartMinimized option is not active - showing window as Normal, current state: {WindowState}");
                    StartMinimized = false;
                    viewController.Show();
                }
            }, Log.HandleUiException)
            .AddTo(Anchors);

            viewController
            .WhenClosing
            .Subscribe(x => HandleWindowClosing(viewController, x))
            .AddTo(Anchors);

            // config processing
            Observable.Merge(
                this.ObservableForProperty(x => x.MicrophoneLine, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.MuteMode, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.AudioNotification, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.HotkeyAlt, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.Hotkey, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.SuppressHotkey, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.MinimizeOnClose, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.MicrophoneVolumeControlEnabled, skipInitial: true).ToUnit(),
                this.ObservableForProperty(x => x.StartMinimized, skipInitial: true).ToUnit())
            .Throttle(ConfigThrottlingTimeout)
            .ObserveOn(uiScheduler)
            .Subscribe(() =>
            {
                var config = configProvider.ActualConfig.CloneJson();
                config.IsPushToTalkMode    = null;
                config.MuteMode            = muteMode;
                config.MicrophoneHotkey    = (Hotkey ?? new HotkeyGesture()).ToString();
                config.MicrophoneHotkeyAlt = (HotkeyAlt ?? new HotkeyGesture()).ToString();
                config.MicrophoneLineId    = MicrophoneLine;
                config.Notification        = AudioNotification;
                config.SuppressHotkey      = SuppressHotkey;
                config.StartMinimized      = StartMinimized;
                config.MinimizeOnClose     = MinimizeOnClose;
                configProvider.Save(config);
            }, Log.HandleUiException)
            .AddTo(Anchors);

            viewController.WhenLoaded
            .Subscribe(() =>
            {
                Log.Debug($"Main window loaded - loading overlay, current process({CurrentProcess.ProcessName} 0x{CurrentProcess.Id:x8}) main window: {CurrentProcess.MainWindowHandle} ({CurrentProcess.MainWindowTitle})");
                overlayWindowController.RegisterChild(overlay);
                Log.Debug("Overlay loaded successfully");
            }, Log.HandleUiException)
            .AddTo(Anchors);
        }