diff --git a/AvaloniaMisterDoctor/App.axaml b/AvaloniaMisterDoctor/App.axaml index bb3f88f..74c7828 100644 --- a/AvaloniaMisterDoctor/App.axaml +++ b/AvaloniaMisterDoctor/App.axaml @@ -1,11 +1,10 @@ - - + diff --git a/AvaloniaMisterDoctor/App.axaml.cs b/AvaloniaMisterDoctor/App.axaml.cs index 0e35eeb..be346e2 100644 --- a/AvaloniaMisterDoctor/App.axaml.cs +++ b/AvaloniaMisterDoctor/App.axaml.cs @@ -15,7 +15,7 @@ public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainWindow(); + desktop.MainWindow = new Views.MainWindow(); } base.OnFrameworkInitializationCompleted(); diff --git a/AvaloniaMisterDoctor/AvaloniaMisterDoctor.csproj b/AvaloniaMisterDoctor/AvaloniaMisterDoctor.csproj index a2f8357..db310e6 100644 --- a/AvaloniaMisterDoctor/AvaloniaMisterDoctor.csproj +++ b/AvaloniaMisterDoctor/AvaloniaMisterDoctor.csproj @@ -19,16 +19,23 @@ - + - - - + + + + + + + MainWindow.axaml + Code + + diff --git a/AvaloniaMisterDoctor/Classes/FACommand.cs b/AvaloniaMisterDoctor/Classes/FACommand.cs new file mode 100644 index 0000000..0495d77 --- /dev/null +++ b/AvaloniaMisterDoctor/Classes/FACommand.cs @@ -0,0 +1,23 @@ +using System; +using System.Windows.Input; + +namespace AvaloniaMisterDoctor.Classes; + +public class FACommand: ICommand +{ + public FACommand(Action executeMethod) + { + _executeMethod = executeMethod; + } + + public event EventHandler CanExecuteChanged; + + public bool CanExecute(object parameter) => true; + + public void Execute(object parameter) + { + _executeMethod.Invoke(parameter); + } + + private Action _executeMethod; +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/MainWindow.axaml b/AvaloniaMisterDoctor/MainWindow.axaml deleted file mode 100644 index 3a4de1d..0000000 --- a/AvaloniaMisterDoctor/MainWindow.axaml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - Main - - - - - - - - - - - Plugins - - - - - - - - - - Options - - - - - - - - - - diff --git a/AvaloniaMisterDoctor/MainWindow.axaml.cs b/AvaloniaMisterDoctor/MainWindow.axaml.cs deleted file mode 100644 index 11981dc..0000000 --- a/AvaloniaMisterDoctor/MainWindow.axaml.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Interactivity; -using EngineDoctor; -using EngineDoctor.Helpers; - -namespace AvaloniaMisterDoctor -{ - public partial class MainWindow : Window - { - public MainWindow() - { - InitializeComponent(); - var appSettings = DbHelper.ReadConnectionSettings(); - Engine.Setup(appSettings); - } - - private void BtnBotStart_OnClick(object? sender, RoutedEventArgs e) - { - // Button Bot Start Click event handler - - Engine.Connect(); - } - } -} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/Services/NavigationService.cs b/AvaloniaMisterDoctor/Services/NavigationService.cs new file mode 100644 index 0000000..c67f75a --- /dev/null +++ b/AvaloniaMisterDoctor/Services/NavigationService.cs @@ -0,0 +1,43 @@ +using System; +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; + +namespace AvaloniaMisterDoctor.Services; + +public class NavigationService +{ + public static NavigationService Instance { get; } = new NavigationService(); + + public void SetFrame(Frame f) + { + _frame = f; + } + + public void SetOverlayHost(Panel p) + { + _overlayHost = p; + } + + public void Navigate(Type t) + { + _frame.Navigate(t); + } + + public void ShowControlDefinitionOverlay(Type targetType) + { + if (_overlayHost != null) + { + // (_overlayHost.Children[0] as ControlDefinitionOverlay).TargetType = targetType; + // (_overlayHost.Children[0] as ControlDefinitionOverlay).Show(); + } + } + + public void ClearOverlay() + { + _overlayHost?.Children.Clear(); + + } + + private Frame _frame; + private Panel _overlayHost; +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/ViewModels/CredentialsViewViewModel.cs b/AvaloniaMisterDoctor/ViewModels/CredentialsViewViewModel.cs new file mode 100644 index 0000000..adfaa83 --- /dev/null +++ b/AvaloniaMisterDoctor/ViewModels/CredentialsViewViewModel.cs @@ -0,0 +1,105 @@ +using System; +using EngineDoctor; +using FluentAvalonia.UI.Controls; + +namespace AvaloniaMisterDoctor.ViewModels; + +public class CredentialsViewViewModel : ViewModelBase +{ + private readonly ContentDialog dialog; + + public CredentialsViewViewModel(ContentDialog dialog) + { + if (dialog is null) + { + throw new ArgumentNullException(nameof(dialog)); + } + + this.dialog = dialog; + dialog.Closed += DialogOnClosed; + } + private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args) + { + dialog.Closed -= DialogOnClosed; + + if (!Engine.IsConnected) return; + + var resultHint = new ContentDialog + { + Content = $"Successfully connected the Twitch Bot", + Title = "Connected", + PrimaryButtonText = "Awesome" + }; + + _ = resultHint.ShowAsync(); + + } + + private string _username; + + /// + /// Gets or sets the user input to check + /// + public string Username + { + get => _username; + set + { + if (RaiseAndSetIfChanged(ref _username, value)) + { + //HandleUserInput(); + } + } + } + + private string _oauthToken; + + /// + /// Gets or sets the user input to check + /// + public string OAuthToken + { + get => _oauthToken; + set + { + if (RaiseAndSetIfChanged(ref _oauthToken, value)) + { + //HandleUserInput(); + } + } + } + + private string _channelName; + + /// + /// Gets or sets the user input to check + /// + public string ChannelName + { + get => _channelName; + set + { + if (RaiseAndSetIfChanged(ref _channelName, value)) + { + //HandleUserInput(); + } + } + } + + private string _botClientId; + + /// + /// Gets or sets the user input to check + /// + public string BotClientId + { + get => _botClientId; + set + { + if (RaiseAndSetIfChanged(ref _botClientId, value)) + { + //HandleUserInput(); + } + } + } +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/ViewModels/MainViewViewModel.cs b/AvaloniaMisterDoctor/ViewModels/MainViewViewModel.cs new file mode 100644 index 0000000..3a997a3 --- /dev/null +++ b/AvaloniaMisterDoctor/ViewModels/MainViewViewModel.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using Avalonia; +using Avalonia.Media; +using FluentAvalonia.Styling; + +namespace AvaloniaMisterDoctor.ViewModels; + +public class MainViewViewModel: ViewModelBase + { + public MainViewViewModel() + { + var faTheme = AvaloniaLocator.Current.GetService(); + faTheme.RequestedThemeChanged += OnAppThemeChanged; + + _currentAppTheme = faTheme.RequestedTheme; + + if (faTheme.TryGetResource("SystemAccentColor", out var value)) + { + _customAccentColor = (Color)value; + _listBoxColor = _customAccentColor; + } + + GetPredefColors(); + + GetSearchTerms(); + } + + + public string[] AppThemes { get; } = + new[] { FluentAvaloniaTheme.LightModeString, FluentAvaloniaTheme.DarkModeString, FluentAvaloniaTheme.HighContrastModeString }; + + public string CurrentAppTheme + { + get => _currentAppTheme; + set + { + if (RaiseAndSetIfChanged(ref _currentAppTheme, value)) + { + var faTheme = AvaloniaLocator.Current.GetService(); + faTheme.RequestedTheme = value; + } + } + } + + public bool UseCustomAccent + { + get => _useCustomAccentColor; + set + { + if (RaiseAndSetIfChanged(ref _useCustomAccentColor, value)) + { + if (value) + { + var faTheme = AvaloniaLocator.Current.GetService(); + if (faTheme.TryGetResource("SystemAccentColor", out var curColor)) + { + _customAccentColor = (Color)curColor; + _listBoxColor = _customAccentColor; + + RaisePropertyChanged(nameof(CustomAccentColor)); + RaisePropertyChanged(nameof(ListBoxColor)); + } + + AvaloniaLocator.Current.GetService().CustomAccentColor = CustomAccentColor; + } + else + { + CustomAccentColor = default; + AvaloniaLocator.Current.GetService().CustomAccentColor = null; + } + } + } + } + + public Color ListBoxColor + { + get => _listBoxColor; + set + { + RaiseAndSetIfChanged(ref _listBoxColor, value); + + if (!_ignoreSetListBoxColor) + CustomAccentColor = value; + } + } + + public Color CustomAccentColor + { + get => _customAccentColor; + set + { + if (RaiseAndSetIfChanged(ref _customAccentColor, value)) + { + AvaloniaLocator.Current.GetService().CustomAccentColor = value; + + _ignoreSetListBoxColor = true; + ListBoxColor = value; + _ignoreSetListBoxColor = false; + } + } + } + + public List PredefinedColors { get; private set; } + + public string CurrentVersion + { + get + { + return typeof(FluentAvalonia.UI.Controls.CoreWindow).Assembly.GetName().Version?.ToString(); + } + } + + public string CurrentAvaloniaVersion + { + get + { + return typeof(Avalonia.Application).Assembly.GetName().Version?.ToString(); + } + } + + public List MainSearchItems { get; private set; } + + private void OnAppThemeChanged(FluentAvaloniaTheme sender, RequestedThemeChangedEventArgs args) + { + if (_currentAppTheme != args.NewTheme) + { + _currentAppTheme = args.NewTheme; + RaisePropertyChanged(nameof(CurrentAppTheme)); + } + } + + private void GetPredefColors() + { + PredefinedColors = new List + { + Color.FromRgb(255,185,0), + Color.FromRgb(255,140,0), + Color.FromRgb(247,99,12), + Color.FromRgb(202,80,16), + Color.FromRgb(218,59,1), + Color.FromRgb(239,105,80), + Color.FromRgb(209,52,56), + Color.FromRgb(255,67,67), + Color.FromRgb(231,72,86), + Color.FromRgb(232,17,35), + Color.FromRgb(234,0,94), + Color.FromRgb(195,0,82), + Color.FromRgb(227,0,140), + Color.FromRgb(191,0,119), + Color.FromRgb(194,57,179), + Color.FromRgb(154,0,137), + Color.FromRgb(0,120,212), + Color.FromRgb(0,99,177), + Color.FromRgb(142,140,216), + Color.FromRgb(107,105,214), + Color.FromRgb(135,100,184), + Color.FromRgb(116,77,169), + Color.FromRgb(177,70,194), + Color.FromRgb(136,23,152), + Color.FromRgb(0,153,188), + Color.FromRgb(45,125,154), + Color.FromRgb(0,183,195), + Color.FromRgb(3,131,135), + Color.FromRgb(0,178,148), + Color.FromRgb(1,133,116), + Color.FromRgb(0,204,106), + Color.FromRgb(16,137,62), + Color.FromRgb(122,117,116), + Color.FromRgb(93,90,88), + Color.FromRgb(104,118,138), + Color.FromRgb(81,92,107), + Color.FromRgb(86,124,115), + Color.FromRgb(72,104,96), + Color.FromRgb(73,130,5), + Color.FromRgb(16,124,16), + Color.FromRgb(118,118,118), + Color.FromRgb(76,74,72), + Color.FromRgb(105,121,126), + Color.FromRgb(74,84,89), + Color.FromRgb(100,124,100), + Color.FromRgb(82,94,84), + Color.FromRgb(132,117,69), + Color.FromRgb(126,115,95) + }; + } + + private void GetSearchTerms() + { + MainSearchItems = new List(); + + Type TryResolveAvaloniaType(string type) + { + return Type.GetType($"Avalonia.Controls.{type}") ?? + Type.GetType($"Avalonia.Controls.Primitives.{type}"); + } + + var tabs = new List + { + new TabItem + { + Header = "Channel", + PageType = "ChannelViewModel", + Description = "View the channel and its chat" + }, + new TabItem + { + Header = "Plugins", + PageType = "PluginsViewModel", + Description = "Configure Plugins" + }, + new TabItem + { + Header = "Options", + PageType = "OptionsViewModel", + Description = "Configure Twitch Bot" + }, + }; + + // Get all FluentAvalonia pages + foreach(var tab in tabs) + { + // Differentiate between the Avalonia MenuFlyout and my own + if (tab.Header == "MenuFlyout") + { + MainSearchItems.Add(new MainAppSearchItem($"{tab.Header}", Type.GetType($"AvaloniaMisterDoctor.Views.{tab.PageType}"))); + } + else + { + MainSearchItems.Add(new MainAppSearchItem(tab.Header, Type.GetType($"AvaloniaMisterDoctor.Views.{tab.PageType}"))); + } + } + + } + + + private bool _useCustomAccentColor; + private Color _customAccentColor = Colors.SlateBlue; + private string _currentAppTheme; + private Color _listBoxColor; + private bool _ignoreSetListBoxColor = false; + + public class MainAppSearchItem + { + public MainAppSearchItem(string pageHeader, Type pageType) + { + Header = pageHeader; + PageType = pageType; + } + + public string Header { get; set; } + + public Type PageType { get; set; } + } + } \ No newline at end of file diff --git a/AvaloniaMisterDoctor/ViewModels/PluginsViewModel.cs b/AvaloniaMisterDoctor/ViewModels/PluginsViewModel.cs new file mode 100644 index 0000000..8deb822 --- /dev/null +++ b/AvaloniaMisterDoctor/ViewModels/PluginsViewModel.cs @@ -0,0 +1,29 @@ +using System; +using AvaloniaMisterDoctor.Classes; +using AvaloniaMisterDoctor.Services; + +namespace AvaloniaMisterDoctor.ViewModels; + +public class PluginsViewModel : ViewModelBase +{ + public PluginsViewModel() + { + InvokeCommand = new FACommand(OnInvokeCommandExecute); + } + + public string Header { get; init; } + + public string Description { get; init; } + + public bool Navigates { get; init; } + + public string PageType { get; init; } + + public FACommand InvokeCommand { get; } + + private void OnInvokeCommandExecute(object parameter) + { + var type = Type.GetType($"MisterDocterNET.Views.{PageType}"); + NavigationService.Instance.Navigate(type); + } +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/ViewModels/TabItem.cs b/AvaloniaMisterDoctor/ViewModels/TabItem.cs new file mode 100644 index 0000000..bc94f94 --- /dev/null +++ b/AvaloniaMisterDoctor/ViewModels/TabItem.cs @@ -0,0 +1,29 @@ +using System; +using AvaloniaMisterDoctor.Classes; +using AvaloniaMisterDoctor.Services; + +namespace AvaloniaMisterDoctor.ViewModels; + +public class TabItem : ViewModelBase +{ + public TabItem() + { + InvokeCommand = new FACommand(OnInvokeCommandExecute); + } + + public string Header { get; set; } + + public string Description { get; set; } + + public string PreviewImageSource { get; set; } + + public string PageType { get; init; } + + public FACommand InvokeCommand { get; } + + private void OnInvokeCommandExecute(object parameter) + { + var type = Type.GetType($"MisterDocterNET.Views.{PageType}"); + NavigationService.Instance.Navigate(type); + } +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/ViewModels/ViewModelBase.cs b/AvaloniaMisterDoctor/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..6e3bcd4 --- /dev/null +++ b/AvaloniaMisterDoctor/ViewModels/ViewModelBase.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Runtime.CompilerServices; +using Avalonia; +using Avalonia.Platform; + +namespace AvaloniaMisterDoctor.ViewModels; + +public class ViewModelBase: INotifyPropertyChanged +{ + public event PropertyChangedEventHandler PropertyChanged; + + protected string GetAssemblyResource(string name) + { + var assets = AvaloniaLocator.Current.GetService(); + using (var stream = assets.Open(new Uri(name))) + using (StreamReader reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + + protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = "") + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } + return false; + } + + protected void RaisePropertyChanged(string propName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName)); + } + +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/Views/ChannelView.axaml b/AvaloniaMisterDoctor/Views/ChannelView.axaml new file mode 100644 index 0000000..7b6279f --- /dev/null +++ b/AvaloniaMisterDoctor/Views/ChannelView.axaml @@ -0,0 +1,12 @@ + + + + + + diff --git a/AvaloniaMisterDoctor/Views/ChannelView.axaml.cs b/AvaloniaMisterDoctor/Views/ChannelView.axaml.cs new file mode 100644 index 0000000..08f3e8e --- /dev/null +++ b/AvaloniaMisterDoctor/Views/ChannelView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace AvaloniaMisterDoctor.Views; + +public partial class ChannelView : UserControl +{ + public ChannelView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/Views/ConfigureCredentialsView.axaml b/AvaloniaMisterDoctor/Views/ConfigureCredentialsView.axaml new file mode 100644 index 0000000..5f45f21 --- /dev/null +++ b/AvaloniaMisterDoctor/Views/ConfigureCredentialsView.axaml @@ -0,0 +1,50 @@ + + + + Channel Name + + + + Username + + + + OAuthToken + + + + Bot Client ID + + + + + diff --git a/AvaloniaMisterDoctor/Views/ConfigureCredentialsView.axaml.cs b/AvaloniaMisterDoctor/Views/ConfigureCredentialsView.axaml.cs new file mode 100644 index 0000000..366a83d --- /dev/null +++ b/AvaloniaMisterDoctor/Views/ConfigureCredentialsView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace AvaloniaMisterDoctor.Views; + +public partial class ConfigureCredentialsView : UserControl +{ + public ConfigureCredentialsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/Views/HomeView.axaml b/AvaloniaMisterDoctor/Views/HomeView.axaml new file mode 100644 index 0000000..1ac1d28 --- /dev/null +++ b/AvaloniaMisterDoctor/Views/HomeView.axaml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/AvaloniaMisterDoctor/Views/HomeView.axaml.cs b/AvaloniaMisterDoctor/Views/HomeView.axaml.cs new file mode 100644 index 0000000..3df4ee9 --- /dev/null +++ b/AvaloniaMisterDoctor/Views/HomeView.axaml.cs @@ -0,0 +1,162 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using AvaloniaMisterDoctor.ViewModels; +using EngineDoctor; +using EngineDoctor.Helpers; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Input; +using MessageBox.Avalonia.Enums; + +namespace AvaloniaMisterDoctor.Views; + +public partial class HomeView : UserControl +{ + public HomeView() + { + InitializeComponent(); + + var appSettings = DbHelper.ReadConnectionSettings(); + Engine.Setup(appSettings); + Engine.ErrorMessages.ListChanged += ErrorMessagesOnListChanged; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void BtnBotStart_OnClick(object? sender, RoutedEventArgs e) + { + // Button Bot Start Click event handler + + Engine.Connect(); + } + + private void ErrorMessagesOnListChanged(object? sender, ListChangedEventArgs e) + { + if (sender is not BindingList errorList) return; + if (e.ListChangedType == ListChangedType.ItemAdded) + { + // If message is added show a message box + var errorMessage = errorList[e.NewIndex]; + var messageBoxStandardWindow = MessageBox.Avalonia.MessageBoxManager + .GetMessageBoxStandardWindow("Error", errorMessage, ButtonEnum.Ok, MessageBox.Avalonia.Enums.Icon.Error); + messageBoxStandardWindow.Show(); + } + } + + private async void BtnConfigureCredentials_OnClick(object? sender, RoutedEventArgs e) + { + // Throw login box + var loginDialog = new ContentDialog + { + Title = "Configure your Twitch Credentials", + PrimaryButtonText = "Save", + CloseButtonText = "Cancel", + }; + + var viewModel = new CredentialsViewViewModel(loginDialog); + var connectionSettings = DbHelper.ReadConnectionSettings(); + + viewModel.ChannelName = connectionSettings.ChannelName; + viewModel.Username = connectionSettings.BotUsername; + viewModel.OAuthToken = connectionSettings.BotOAuthKey; + viewModel.BotClientId = connectionSettings.BotClientId; + + loginDialog.Content = new ConfigureCredentialsView + { + DataContext = viewModel + }; + + loginDialog.PrimaryButtonClick += ConfigureConnection; + + var result = await loginDialog.ShowAsync(); + + loginDialog.Closed += (dialog, args) => loginDialog.PrimaryButtonClick -= ConfigureConnection; + } + + private async void ConfigureConnection(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + var def = args.GetDeferral(); + //Engine.Setup(); + if (sender.Content is not ConfigureCredentialsView credentialsView) return; + if (credentialsView.DataContext is not CredentialsViewViewModel viewModel) + { + return; + } + var channel = viewModel.ChannelName; + if (string.IsNullOrEmpty(channel)) + { + ShowError("Channel Name can not be blank"); + args.Cancel = true; + return; + } + + var userName = viewModel.Username; + if (string.IsNullOrEmpty(userName)) + { + ShowError("No Username Set"); + args.Cancel = true; + return; + } + + var oAuth = viewModel.OAuthToken; + if (string.IsNullOrEmpty(oAuth)) + { + ShowError("No OAuth Set"); + args.Cancel = true; + return; + } + + if (!oAuth.StartsWith("oauth:", StringComparison.CurrentCultureIgnoreCase)) + { + ShowError("OAuth Key must start with 'oauth:'"); + args.Cancel = true; + return; + } + + var clientId = viewModel.BotClientId; + if (string.IsNullOrEmpty(clientId)) + { + ShowError("No Client ID"); + args.Cancel = true; + + return; + } + + var connectionSettings = DbHelper.ReadConnectionSettings(); + connectionSettings.BotUsername = userName; + connectionSettings.ChannelName = channel; + connectionSettings.BotOAuthKey = oAuth; + connectionSettings.BotClientId = clientId; + + Engine.UpdateSettings(connectionSettings); + Engine.Connect(); + var errors = Engine.ErrorMessages; + if (errors.Any()) + { + ShowError(errors.First()); + args.Cancel = true; + return; + } + + def.Complete(); + } + + private void ShowError(string message) + { + var errorDialog = new ContentDialog() + { + Content = $"\"{message}\"", + Title = "Error", + PrimaryButtonText = "Ok" + }; + + _ = errorDialog.ShowAsync(); + } +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/Views/MainView.axaml b/AvaloniaMisterDoctor/Views/MainView.axaml new file mode 100644 index 0000000..8a36166 --- /dev/null +++ b/AvaloniaMisterDoctor/Views/MainView.axaml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AvaloniaMisterDoctor/Views/MainView.axaml.cs b/AvaloniaMisterDoctor/Views/MainView.axaml.cs new file mode 100644 index 0000000..ad9efd3 --- /dev/null +++ b/AvaloniaMisterDoctor/Views/MainView.axaml.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; +using AvaloniaMisterDoctor.Services; +using AvaloniaMisterDoctor.ViewModels; +using FluentAvalonia.Core; +using FluentAvalonia.Core.ApplicationModel; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Media.Animation; +using FluentAvalonia.UI.Navigation; +using MessageBox.Avalonia.Enums; + +namespace AvaloniaMisterDoctor.Views; + +public partial class MainView : UserControl +{ + private NavigationViewItem _lastItem; + + + public MainView() + { + InitializeComponent(); + DataContext = new MainViewViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + // Changed for SplashScreens: + // -- If using a SplashScreen, the window will be available when this is attached + // and we can just call OnParentWindowOpened + // -- If not using a SplashScreen (like before), the window won't be initialized + // yet and setting our custom titlebar won't work... so wait for the + // WindowOpened event first + if (e.Root is Window b) + { + if (!b.IsActive) + b.Opened += OnParentWindowOpened; + else + OnParentWindowOpened(b, null); + } + + _windowIconControl = this.FindControl("WindowIcon"); + _frameView = this.FindControl("FrameView"); + _navView = this.FindControl("NavView"); + _navView.MenuItems = GetNavigationViewItems(); + _navView.FooterMenuItems = GetFooterNavigationViewItems(); + + _frameView.Navigated += OnFrameViewNavigated; + _navView.ItemInvoked += OnNavigationViewItemInvoked; + _navView.BackRequested += OnNavigationViewBackRequested; + + _frameView.Navigate(typeof(HomeView)); + + NavigationService.Instance.SetFrame(_frameView); + NavigationService.Instance.SetOverlayHost(this.FindControl("OverlayHost")); + } + + private void FrameView_OnNavigationFailed(object sender, NavigationFailedEventArgs e) + { + // Error for navigation failed + var errorMessage = e.Exception.Message; + var messageBoxStandardWindow = MessageBox.Avalonia.MessageBoxManager + .GetMessageBoxStandardWindow("Navigation Failed", errorMessage, ButtonEnum.Ok, + MessageBox.Avalonia.Enums.Icon.Error); + messageBoxStandardWindow.Show(); + } + + private void NavView_OnItemInvoked(object? sender, NavigationViewItemInvokedEventArgs e) + { + // Nav item is invoked/clicked + var item = e.InvokedItemContainer as NavigationViewItem; + if (item == null || item == _lastItem) return; + + var viewName = item.Tag as string ?? ""; + + if (!NavigateToView(viewName)) return; + + _lastItem = item; + } + + private bool NavigateToView(string clickedView) + { + Type view; + switch (clickedView) + { + case "HomeView": + case "Home": + { + view = typeof(HomeView); + break; + } + + case "PluginsView": + case "Plugins": + { + view = typeof(PluginsView); + break; + } + + case "SettingsView": + case "Settings": + { + view = typeof(SettingsView); + break; + } + + default: + { + return false; + } + } + + FrameView.Navigate(view, null, new EntranceNavigationTransitionInfo()); + + return true; + } + + private void NavView_OnSelectionChanged(object? sender, NavigationViewSelectionChangedEventArgs e) + { + // Nav view selection changed + } + + private void NavView_OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + foreach (NavigationViewItemBase item in NavView.MenuItems) + { + if (item.Tag != null && item is NavigationViewItem && item.Tag is "HomeView") + { + item.IsSelected = true; + } + } + + FrameView.Navigate(typeof(HomeView)); + } + + private List GetNavigationViewItems() + { + return new List + { + new NavigationViewItem + { + Content = "Home", + Tag = typeof(HomeView), + Icon = new FluentAvalonia.UI.Controls.IconElement{ }, + Classes = + { + "SampleAppNav" + } + }, + new NavigationViewItem + { + Content = "Channel", + Tag = typeof(ChannelView), + Icon = new IconSourceElement { IconSource = (IconSource)this.FindResource("ResourcesIcon") }, + Classes = + { + "SampleAppNav" + } + }, + new NavigationViewItem + { + Content = "Plugins", + Tag = typeof(PluginsView), + Icon = new IconSourceElement { IconSource = (IconSource)this.FindResource("CoreCtrlsIcon") }, + Classes = + { + "SampleAppNav" + } + }, + new NavigationViewItem + { + Content = "Settings", + Tag = typeof(SettingsView), + Icon = new IconSourceElement { IconSource = (IconSource)this.FindResource("CtrlsIcon") }, + Classes = + { + "SampleAppNav" + } + }, + + }; + } + + private List GetFooterNavigationViewItems() + { + return new List + { + new NavigationViewItem + { + Content = "Settings", + Tag = typeof(SettingsView), + Icon = new IconSourceElement { IconSource = (IconSource)this.FindResource("SettingsIcon") }, + Classes = + { + "SampleAppNav" + } + } + }; + } + + private void OnParentWindowOpened(object sender, EventArgs e) + { + if (e != null) + (sender as Window).Opened -= OnParentWindowOpened; + + if (sender is CoreWindow cw) + { + var titleBar = cw.TitleBar; + if (titleBar != null) + { + titleBar.ExtendViewIntoTitleBar = true; + + titleBar.LayoutMetricsChanged += OnApplicationTitleBarLayoutMetricsChanged; + + if (this.FindControl("TitleBarHost") is Grid g) + { + cw.SetTitleBar(g); + g.Margin = new Thickness(0, 0, titleBar.SystemOverlayRightInset, 0); + } + } + } + } + + private void OnApplicationTitleBarLayoutMetricsChanged(CoreApplicationViewTitleBar sender, object args) + { + if (this.FindControl("TitleBarHost") is Grid g) + { + g.Margin = new Thickness(0, 0, sender.SystemOverlayRightInset, 0); + } + } + + private void OnNavigationViewBackRequested(object sender, NavigationViewBackRequestedEventArgs e) + { + _frameView.GoBack(); + } + + private void OnNavigationViewItemInvoked(object sender, NavigationViewItemInvokedEventArgs e) + { + // Change the current selected item back to normal + SetNVIIcon(_navView.SelectedItem as NavigationViewItem, false); + + if (e.InvokedItemContainer is NavigationViewItem nvi && nvi.Tag is Type typ) + { + _frameView.Navigate(typ, null, e.RecommendedNavigationTransitionInfo); + } + } + + private void SetNVIIcon(NavigationViewItem item, bool selected) + { + // Technically, yes you could set up binding and converters and whatnot to let the icon change + // between filled and unfilled based on selection, but this is so much simpler + + if (item == null) + return; + + Type t = item.Tag as Type; + + if (t == typeof(HomeView)) + { + (item.Icon as IconSourceElement).IconSource = this.TryFindResource(selected ? "HomeIconFilled" : "HomeIcon", out var value) ? + (IconSource)value : null; + } + else if (t == typeof(ChannelView)) + { + (item.Icon as IconSourceElement).IconSource = this.TryFindResource(selected ? "CoreCtrlsIconFilled" : "CoreCtrlsIcon", out var value) ? + (IconSource)value : null; + } + // Skip NewControlsPage as its icon is the same for both + else if (t == typeof(PluginsView)) + { + (item.Icon as IconSourceElement).IconSource = this.TryFindResource(selected ? "ResourcesIconFilled" : "ResourcesIcon", out var value) ? + (IconSource)value : null; + } + else if (t == typeof(SettingsView)) + { + (item.Icon as IconSourceElement).IconSource = this.TryFindResource(selected ? "SettingsIconFilled" : "SettingsIcon", out var value) ? + (IconSource)value : null; + } + } + + private void OnFrameViewNavigated(object sender, NavigationEventArgs e) + { + bool found = false; + foreach (NavigationViewItem nvi in _navView.MenuItems) + { + if (nvi.Tag is Type tag && tag == e.SourcePageType) + { + found = true; + _navView.SelectedItem = nvi; + SetNVIIcon(nvi, true); + break; + } + } + + if (!found) + { + if (e.SourcePageType == typeof(SettingsView)) + { + _navView.SelectedItem = _navView.FooterMenuItems.ElementAt(0); + } + else + { + // only remaining page type is core controls pages + _navView.SelectedItem = _navView.MenuItems.ElementAt(1); + } + } + + if (_frameView.BackStackDepth > 0 && !_navView.IsBackButtonVisible) + { + AnimateContentForBackButton(true); + } + else if (_frameView.BackStackDepth == 0 && _navView.IsBackButtonVisible) + { + AnimateContentForBackButton(false); + } + } + + private async void AnimateContentForBackButton(bool show) + { + if (show) + { + var ani = new Animation + { + Duration = TimeSpan.FromMilliseconds(250), + FillMode = FillMode.Forward, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(MarginProperty, new Thickness(12, 4, 12, 4)) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + KeySpline = new KeySpline(0,0,0,1), + Setters = + { + new Setter(MarginProperty, new Thickness(48,4,12,4)) + } + } + } + }; + + await ani.RunAsync((Animatable)_windowIconControl, null); + + _navView.IsBackButtonVisible = true; + } + else + { + _navView.IsBackButtonVisible = false; + + var ani = new Animation + { + Duration = TimeSpan.FromMilliseconds(250), + FillMode = FillMode.Forward, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(MarginProperty, new Thickness(48, 4, 12, 4)) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + KeySpline = new KeySpline(0,0,0,1), + Setters = + { + new Setter(MarginProperty, new Thickness(12,4,12,4)) + } + } + } + }; + + await ani.RunAsync((Animatable)_windowIconControl, null); + } + } + + private IControl _windowIconControl; + private Frame _frameView; + private NavigationView _navView; +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/Views/MainWindow.axaml b/AvaloniaMisterDoctor/Views/MainWindow.axaml new file mode 100644 index 0000000..b07b72d --- /dev/null +++ b/AvaloniaMisterDoctor/Views/MainWindow.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AvaloniaMisterDoctor/Views/MainWindow.axaml.cs b/AvaloniaMisterDoctor/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..6664e6d --- /dev/null +++ b/AvaloniaMisterDoctor/Views/MainWindow.axaml.cs @@ -0,0 +1,105 @@ +using System; +using System.Reflection; +using Avalonia; +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Media.Animation; +using FluentAvalonia.UI.Navigation; +using MessageBox.Avalonia.Enums; + + +namespace AvaloniaMisterDoctor.Views +{ + public partial class MainWindow : CoreWindow + { + private NavigationViewItem _lastItem; + public MainWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + + } + + private void ContentFrame_OnNavigationFailed(object sender, NavigationFailedEventArgs e) + { + // Error for navigation failed + var errorMessage = e.Exception.Message; + var messageBoxStandardWindow = MessageBox.Avalonia.MessageBoxManager + .GetMessageBoxStandardWindow("Navigation Failed", errorMessage, ButtonEnum.Ok, MessageBox.Avalonia.Enums.Icon.Error); + messageBoxStandardWindow.Show(); + } + + private void NavView_OnItemInvoked(object? sender, NavigationViewItemInvokedEventArgs e) + { + // Nav item is invoked/clicked + var item = e.InvokedItemContainer as NavigationViewItem; + if (item == null || item == _lastItem) return; + + var viewName = item.Tag as string ?? ""; + + if (!NavigateToView(viewName)) return; + + _lastItem = item; + } + + private bool NavigateToView(string clickedView) + { + Type view; + switch (clickedView) + { + case "HomeView": + case "Home": + { + view = typeof(HomeView); + break; + } + case "PluginsView": + case "Plugins": + { + view = typeof(PluginsView); + break; + } + case "ChannelView": + case "Channel": + { + view = typeof(ChannelView); + break; + } + case "SettingsView": + case "Settings": + { + view = typeof(SettingsView); + break; + } + default: + { + return false; + } + } + + ContentFrame.Navigate(view, null, new EntranceNavigationTransitionInfo()); + + return true; + } + + private void NavView_OnSelectionChanged(object? sender, NavigationViewSelectionChangedEventArgs e) + { + // Nav view selection changed + } + + private void NavView_OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + foreach (NavigationViewItemBase item in NavView.MenuItems) + { + if (item.Tag != null && item is NavigationViewItem && item.Tag is "HomeView") + { + item.IsSelected = true; + } + } + + ContentFrame.Navigate(typeof(HomeView)); + } + } +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/Views/PluginsView.axaml b/AvaloniaMisterDoctor/Views/PluginsView.axaml new file mode 100644 index 0000000..63453b8 --- /dev/null +++ b/AvaloniaMisterDoctor/Views/PluginsView.axaml @@ -0,0 +1,18 @@ + + + + + + {Bind} + + + + diff --git a/AvaloniaMisterDoctor/Views/PluginsView.axaml.cs b/AvaloniaMisterDoctor/Views/PluginsView.axaml.cs new file mode 100644 index 0000000..cea09e5 --- /dev/null +++ b/AvaloniaMisterDoctor/Views/PluginsView.axaml.cs @@ -0,0 +1,29 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using EngineDoctor; + +namespace AvaloniaMisterDoctor.Views; + +public partial class PluginsView : UserControl +{ + public PluginsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + + + private void PluginsList_OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + // Bind list of plugins + if (sender is not ItemsRepeater repeatingList) return; + var plugins = Engine.LoadedPlugins; + repeatingList.Items = plugins; + } +} \ No newline at end of file diff --git a/AvaloniaMisterDoctor/Views/SettingsView.axaml b/AvaloniaMisterDoctor/Views/SettingsView.axaml new file mode 100644 index 0000000..cfb3cbd --- /dev/null +++ b/AvaloniaMisterDoctor/Views/SettingsView.axaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/AvaloniaMisterDoctor/Views/SettingsView.axaml.cs b/AvaloniaMisterDoctor/Views/SettingsView.axaml.cs new file mode 100644 index 0000000..edacf9d --- /dev/null +++ b/AvaloniaMisterDoctor/Views/SettingsView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace AvaloniaMisterDoctor.Views; + +public partial class SettingsView : UserControl +{ + public SettingsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/EngineDoctor/Engine.cs b/EngineDoctor/Engine.cs index 9705d32..f5d6353 100644 --- a/EngineDoctor/Engine.cs +++ b/EngineDoctor/Engine.cs @@ -1,12 +1,10 @@ -using System.Diagnostics; -using System.Drawing; +using System.ComponentModel; +using System.Diagnostics; using System.Net; -using System.Net.Mime; using EngineDoctor.Classes; using EngineDoctor.Extensions; using EngineDoctor.Helpers; using EngineDoctor.Managers; -using Microsoft.VisualBasic; using MisterDoctor.Plugins; using MisterDoctor.Plugins.Classes; using MisterDoctor.Plugins.Enums; @@ -40,7 +38,27 @@ public static class Engine public static string BotUsername { get; private set; } public static string ChannelName { get; private set; } - public static List Messages { get; } = new(); + public static BindingList Messages { get; } = new(); + public static BindingList ErrorMessages { get; } = new(); + + public static List LoadedPlugins { get; } = PluginManager.LoadedPlugins; + + public static bool IsConnected + { + get + { + var connected = false; + lock (_threadlock) + { + if (_twitchClient != null && _twitchClient.IsConnected) + { + connected = true; + } + } + + return connected; + } + } public static void Setup(ConnectionSettings connectionSettings) { @@ -63,10 +81,17 @@ public static void UpdateSettings(ConnectionSettings connectionSettings) BotClientId = connectionSettings.BotClientId; BotUsername = connectionSettings.BotUsername; BotOAuthKey = connectionSettings.BotOAuthKey; + + DbHelper.SaveConnectionSettings(connectionSettings); } public static void Connect() { + if (_twitchClient != null && _twitchClient.IsConnected) + { + Disconnect(); + } + var channel = ChannelName; if (string.IsNullOrEmpty(channel)) { @@ -455,6 +480,6 @@ private static void ShowError(Exception ex) private static void ShowError(string message) { - Messages.Add(message); + ErrorMessages.Add(message); } } \ No newline at end of file diff --git a/Plugin.BetterButtsbot/Plugin.BetterButtsbot.csproj b/Plugin.BetterButtsbot/Plugin.BetterButtsbot.csproj index 7d1fbcf..be8258a 100644 --- a/Plugin.BetterButtsbot/Plugin.BetterButtsbot.csproj +++ b/Plugin.BetterButtsbot/Plugin.BetterButtsbot.csproj @@ -9,7 +9,7 @@ - + diff --git a/Plugin.Commands/Plugin.Commands.csproj b/Plugin.Commands/Plugin.Commands.csproj index 38cc959..757f3ba 100644 --- a/Plugin.Commands/Plugin.Commands.csproj +++ b/Plugin.Commands/Plugin.Commands.csproj @@ -5,7 +5,7 @@ - + diff --git a/Plugin.Goodbot/Plugin.Goodbot.csproj b/Plugin.Goodbot/Plugin.Goodbot.csproj index f4a1bdb..55bc8c7 100644 --- a/Plugin.Goodbot/Plugin.Goodbot.csproj +++ b/Plugin.Goodbot/Plugin.Goodbot.csproj @@ -9,7 +9,7 @@ - + diff --git a/Plugin.Markov/Plugin.Markov.csproj b/Plugin.Markov/Plugin.Markov.csproj index 38cc959..757f3ba 100644 --- a/Plugin.Markov/Plugin.Markov.csproj +++ b/Plugin.Markov/Plugin.Markov.csproj @@ -5,7 +5,7 @@ - + diff --git a/Plugin.PhraseDetect/Plugin.PhraseDetect.csproj b/Plugin.PhraseDetect/Plugin.PhraseDetect.csproj index 38cc959..757f3ba 100644 --- a/Plugin.PhraseDetect/Plugin.PhraseDetect.csproj +++ b/Plugin.PhraseDetect/Plugin.PhraseDetect.csproj @@ -5,7 +5,7 @@ - + diff --git a/Plugin.ReplaceWord/Plugin.ReplaceWord.csproj b/Plugin.ReplaceWord/Plugin.ReplaceWord.csproj index 7d1fbcf..be8258a 100644 --- a/Plugin.ReplaceWord/Plugin.ReplaceWord.csproj +++ b/Plugin.ReplaceWord/Plugin.ReplaceWord.csproj @@ -9,7 +9,7 @@ - + diff --git a/Plugin.Roulette/Plugin.Roulette.csproj b/Plugin.Roulette/Plugin.Roulette.csproj index 7d1fbcf..be8258a 100644 --- a/Plugin.Roulette/Plugin.Roulette.csproj +++ b/Plugin.Roulette/Plugin.Roulette.csproj @@ -9,7 +9,7 @@ - + diff --git a/Plugin.Welcome/Plugin.Welcome.csproj b/Plugin.Welcome/Plugin.Welcome.csproj index f4a1bdb..55bc8c7 100644 --- a/Plugin.Welcome/Plugin.Welcome.csproj +++ b/Plugin.Welcome/Plugin.Welcome.csproj @@ -9,7 +9,7 @@ - +