From 1d0b109fcfa5167ae4620333297833a74ca0055e Mon Sep 17 00:00:00 2001 From: Enrico Ludwig Date: Tue, 20 Aug 2024 19:42:23 +0200 Subject: [PATCH] Implemented user control for raid button, connected to api and more UI improvements --- App.axaml.cs | 22 +++-- BetterRaid.generated.sln | 10 +- Controls/RaidButton.axaml | 52 ++++++++++ Controls/RaidButton.axaml.cs | 13 +++ Models/TwitchChannel.cs | 90 ++++++++++++++++- ViewModels/MainWindowViewModel.cs | 26 ++++- ViewModels/RaidButtonViewModel.cs | 131 +++++++++++++++++++++++++ Views/MainWindow.axaml | 59 ++++++++++-- Views/MainWindow.axaml.cs | 155 +++++++++++++++++------------- 9 files changed, 466 insertions(+), 92 deletions(-) create mode 100644 Controls/RaidButton.axaml create mode 100644 Controls/RaidButton.axaml.cs create mode 100644 ViewModels/RaidButtonViewModel.cs diff --git a/App.axaml.cs b/App.axaml.cs index e136622..b454913 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; @@ -17,6 +18,8 @@ namespace BetterRaid; public partial class App : Application { + public static int AutoUpdateDelay = 10_000; + public static string TwitchBroadcasterId = ""; public static string TwitchChannelName = ""; public static string TokenClientId = ""; public static string TokenClientSecret = ""; @@ -54,7 +57,6 @@ public partial class App : Application TwitchClient = new TwitchClient(customClient); TwitchClient.Initialize(creds, TwitchChannelName); - TwitchClient.OnMessageReceived += OnMessageReceived; TwitchClient.OnConnected += OnConnected; TwitchClient.OnConnectionError += OnConnectionError; @@ -64,6 +66,19 @@ public partial class App : Application TwitchAPI.Settings.ClientId = TokenClientId; TwitchAPI.Settings.AccessToken = TokenClientAccess; + var channels = TwitchAPI.Helix.Search.SearchChannelsAsync(TwitchChannelName).Result; + var exactChannel = channels.Channels.FirstOrDefault(c => c.BroadcasterLogin == TwitchChannelName); + + if (exactChannel != null) + { + TwitchBroadcasterId = exactChannel.Id; + Console.WriteLine($"TWITCH BROADCASTER ID SET TO {TwitchBroadcasterId}"); + } + else + { + Console.WriteLine("FAILED TO SET BROADCASTER ID!"); + } + AvaloniaXamlLoader.Load(this); } @@ -77,11 +92,6 @@ public partial class App : Application Console.WriteLine("[INFO] Twitch Client connected!"); } - private void OnMessageReceived(object? sender, OnMessageReceivedArgs e) - { - Console.WriteLine($"{e.ChatMessage.DisplayName}: {e.ChatMessage.Message}"); - } - public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) diff --git a/BetterRaid.generated.sln b/BetterRaid.generated.sln index e1c46b5..4942695 100644 --- a/BetterRaid.generated.sln +++ b/BetterRaid.generated.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.002.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{050F930D-FE73-4FA8-A05B-E659A4A0CEEA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{7AB75970-30DB-496E-960C-535708A65EC6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +11,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {050F930D-FE73-4FA8-A05B-E659A4A0CEEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {050F930D-FE73-4FA8-A05B-E659A4A0CEEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {050F930D-FE73-4FA8-A05B-E659A4A0CEEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {050F930D-FE73-4FA8-A05B-E659A4A0CEEA}.Release|Any CPU.Build.0 = Release|Any CPU + {7AB75970-30DB-496E-960C-535708A65EC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AB75970-30DB-496E-960C-535708A65EC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AB75970-30DB-496E-960C-535708A65EC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AB75970-30DB-496E-960C-535708A65EC6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Controls/RaidButton.axaml b/Controls/RaidButton.axaml new file mode 100644 index 0000000..2249a06 --- /dev/null +++ b/Controls/RaidButton.axaml @@ -0,0 +1,52 @@ + + + + + + + + diff --git a/Controls/RaidButton.axaml.cs b/Controls/RaidButton.axaml.cs new file mode 100644 index 0000000..023c2c6 --- /dev/null +++ b/Controls/RaidButton.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace BetterRaid; + +public partial class RaidButton : UserControl +{ + public RaidButton() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Models/TwitchChannel.cs b/Models/TwitchChannel.cs index 20bdff9..a534f26 100644 --- a/Models/TwitchChannel.cs +++ b/Models/TwitchChannel.cs @@ -1,14 +1,94 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Avalonia.Threading; + namespace BetterRaid.Models; -public class TwitchChannel +public class TwitchChannel : INotifyPropertyChanged { - public string? BroadcasterId { get; set; } - public string Name { get; set; } - public bool IsLive { get; set; } - public int ViewerCount { get; set; } + private string? viewerCount; + private bool isLive; + private string? name; + private string? displayName; + private string? thumbnailUrl; + + public string? BroadcasterId + { + get; + set; + } + public string? Name + { + get => name; + set + { + if (value == name) + return; + + name = value; + OnPropertyChanged(); + } + } + public bool IsLive + { + get => isLive; + set + { + if (value == isLive) + return; + + isLive = value; + OnPropertyChanged(); + } + } + public string? ViewerCount + { + get => viewerCount; + set + { + if (value == viewerCount) + return; + + viewerCount = value; + OnPropertyChanged(); + } + } + + public string? ThumbnailUrl + { + get => thumbnailUrl; + set + { + if (value == thumbnailUrl) + return; + + thumbnailUrl = value; + OnPropertyChanged(); + } + } + + public string? DisplayName + { + get => displayName; + set + { + if (value == displayName) + return; + + displayName = value; + OnPropertyChanged(); + } + } public TwitchChannel(string channelName) { Name = channelName; } + + public event PropertyChangedEventHandler? PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } \ No newline at end of file diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 58589dc..aea25a2 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -1,6 +1,28 @@ -namespace BetterRaid.ViewModels; +using System; +using Avalonia; + +namespace BetterRaid.ViewModels; public partial class MainWindowViewModel : ViewModelBase { - + private string? _filter; + + public string? Filter + { + get => _filter; + set + { + if (value == _filter) + return; + + _filter = value; + OnPropertyChanged(); + } + } + + public void ExitApplication() + { + //TODO polish later + Environment.Exit(0); + } } diff --git a/ViewModels/RaidButtonViewModel.cs b/ViewModels/RaidButtonViewModel.cs new file mode 100644 index 0000000..d1da0bf --- /dev/null +++ b/ViewModels/RaidButtonViewModel.cs @@ -0,0 +1,131 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Media; +using BetterRaid.Models; +using TwitchLib.Api.Helix.Models.Raids.StartRaid; +using TwitchLib.Api.Helix.Models.Search; +using TwitchLib.Api.Helix.Models.Streams.GetStreams; + +namespace BetterRaid.ViewModels; + +public class RaidButtonViewModel : ViewModelBase +{ + private TwitchChannel? _channel; + private SolidColorBrush _viewerCountColor = new SolidColorBrush(Color.FromRgb(byte.MaxValue, byte.MaxValue, byte.MaxValue)); + + public required string ChannelName + { + get; + set; + } + + public TwitchChannel Channel => _channel ?? new TwitchChannel(ChannelName); + + public SolidColorBrush ViewerCountColor + { + get => _viewerCountColor; + set + { + if (value == _viewerCountColor) + return; + + _viewerCountColor = value; + OnPropertyChanged(); + } + } + + public async Task GetOrUpdateChannelAsync() + { + if (_channel == null) + { + _channel = new TwitchChannel(ChannelName); + _channel.PropertyChanged += OnChannelDataChanged; + } + + var currentChannelData = await GetChannelAsync(ChannelName); + + if (currentChannelData == null) + return false; + + var currentStreamData = await GetStreamAsync(currentChannelData); + + _channel.BroadcasterId = currentChannelData.Id; + _channel.Name = ChannelName; + _channel.DisplayName = currentChannelData.DisplayName; + _channel.IsLive = currentChannelData.IsLive; + _channel.ThumbnailUrl = currentChannelData.ThumbnailUrl; + _channel.ViewerCount = currentStreamData?.ViewerCount.ToString() ?? "(Offline)"; + + if (_channel.IsLive) + { + ViewerCountColor = new SolidColorBrush(Color.FromRgb(0, byte.MaxValue, 0)); + } + else + { + ViewerCountColor = new SolidColorBrush(Color.FromRgb(byte.MaxValue, 0, 0)); + } + + return true; + } + + private async Task GetChannelAsync(string channelName) + { + if (App.TwitchAPI == null) + return null; + + if (string.IsNullOrEmpty(channelName)) + return null; + + var channels = await App.TwitchAPI.Helix.Search.SearchChannelsAsync(channelName); + var exactChannel = channels.Channels.FirstOrDefault(c => c.BroadcasterLogin.Equals(channelName, StringComparison.CurrentCultureIgnoreCase)); + + return exactChannel; + } + + private async Task GetStreamAsync(Channel currentChannelData) + { + if (App.TwitchAPI == null) + return null; + + if (currentChannelData == null) + return null; + + var streams = await App.TwitchAPI.Helix.Streams.GetStreamsAsync(userLogins: [currentChannelData.BroadcasterLogin]); + var exactStream = streams.Streams.FirstOrDefault(s => s.UserLogin == currentChannelData.BroadcasterLogin); + + return exactStream; + } + + public async Task RaidChannel() + { + if (App.TwitchAPI == null) + return; + + StartRaidResponse? raid = null; + + try + { + raid = await App.TwitchAPI.Helix.Raids.StartRaidAsync(App.TwitchBroadcasterId, Channel.BroadcasterId); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + Console.WriteLine(e.StackTrace); + + return; + } + + if (raid.Data.Length > 0) + { + var createdAt = raid.Data[0].CreatedAt; + var isMature = raid.Data[0].IsMature; + } + } + + private void OnChannelDataChanged(object? sender, PropertyChangedEventArgs e) + { + OnPropertyChanged(nameof(Channel)); + } +} \ No newline at end of file diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index d7ae223..ba339df 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -15,17 +15,64 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/MainWindow.axaml.cs b/Views/MainWindow.axaml.cs index 6fc9b5b..3abc772 100644 --- a/Views/MainWindow.axaml.cs +++ b/Views/MainWindow.axaml.cs @@ -1,21 +1,16 @@ using System; -using System.Collections.Generic; -using System.Linq; -using AsyncImageLoader; -using Avalonia; +using System.ComponentModel; +using System.Threading.Tasks; using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Media; -using Avalonia.Media.Imaging; using Avalonia.Threading; -using BetterRaid.Models; using BetterRaid.ViewModels; -using TwitchLib.Client.Events; namespace BetterRaid.Views; public partial class MainWindow : Window { + private BackgroundWorker _autoUpdater; + private string[] _channelNames = [ "Cedricun", // Ehrenbruder "ZanTal", // Ehrenschwester @@ -31,33 +26,80 @@ public partial class MainWindow : Window public MainWindow() { + _autoUpdater = new BackgroundWorker(); + InitializeComponent(); - PrepareRaidGrid(); - ConnectToTwitch(); + GenerateRaidGrid(); + + DataContextChanged += OnDataContextChanged; } - private void PrepareRaidGrid() + private void OnDataContextChanged(object? sender, EventArgs e) + { + if (DataContext is MainWindowViewModel vm) + { + vm.PropertyChanged += OnViewModelChanged; + } + } + + private void OnViewModelChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(MainWindowViewModel.Filter)) + { + if (DataContext is MainWindowViewModel mainWindowVm) + { + if (string.IsNullOrEmpty(mainWindowVm.Filter)) + { + foreach (var child in raidGrid.Children) + { + child.IsVisible = true; + } + + return; + } + + foreach (var child in raidGrid.Children) + { + if (child.DataContext is RaidButtonViewModel vm) + { + if (string.IsNullOrEmpty(vm.Channel?.DisplayName)) + continue; + + if (string.IsNullOrEmpty(mainWindowVm.Filter)) + continue; + + if (vm.Channel.DisplayName.Contains(mainWindowVm.Filter, StringComparison.OrdinalIgnoreCase) == false) + { + child.IsVisible = false; + } + } + } + } + } + } + + private void GenerateRaidGrid() { var rows = (int)Math.Ceiling(_channelNames.Length / 3.0); for (var i = 0; i < rows; i++) { - raidGrid.RowDefinitions.Add(new RowDefinition(GridLength.Parse("200"))); + raidGrid.RowDefinitions.Add(new RowDefinition(GridLength.Parse("*"))); } var colIndex = 0; var rowIndex = 0; foreach (var channel in _channelNames) { - var btn = new Button + if (string.IsNullOrEmpty(channel)) + continue; + + var btn = new RaidButton { - Content = channel, - DataContext = new TwitchChannel(channel), - Margin = Thickness.Parse("5"), - HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch, - HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center, - VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center + DataContext = new RaidButtonViewModel + { + ChannelName = channel + } }; Grid.SetColumn(btn, colIndex); @@ -71,64 +113,41 @@ public partial class MainWindow : Window colIndex = 0; rowIndex++; } + + if (btn.DataContext is RaidButtonViewModel vm) + { + Dispatcher.UIThread.InvokeAsync(vm.GetOrUpdateChannelAsync); + } } + + _autoUpdater.DoWork += UpdateAllTiles; + _autoUpdater.RunWorkerAsync(); } - private void ConnectToTwitch() + public void UpdateAllTiles(object? sender, DoWorkEventArgs e) { - if (App.TwitchClient != null && App.TwitchAPI != null) + while (e.Cancel == false) { - foreach (var c in raidGrid.Children) + Task.Delay(App.AutoUpdateDelay).Wait(); + + if (raidGrid == null || raidGrid.Children.Count == 0) { - if (c is Button btn) + return; + } + + foreach (var children in raidGrid.Children) + { + Dispatcher.UIThread.InvokeAsync(async () => { - var channel = (btn.DataContext as TwitchChannel)?.Name; - - if (string.IsNullOrEmpty(channel) == false) + if (children.DataContext is RaidButtonViewModel vm) { - var channels = App.TwitchAPI.Helix.Search.SearchChannelsAsync(channel).Result; - var exactChannel = channels.Channels.FirstOrDefault(c => c.BroadcasterLogin.ToLower() == channel.ToLower()); - - Dispatcher.UIThread.Invoke(() => - { - if (exactChannel != null) - { - if (btn.DataContext is TwitchChannel ctx) - { - ctx.BroadcasterId = exactChannel.Id; - var ib = new ImageBrush(); - ImageBrushLoader.SetSource(ib, exactChannel.ThumbnailUrl); - btn.Background = ib; - - var streamInfo = App.TwitchAPI.Helix.Streams.GetStreamsAsync(userLogins: new List([channel])).Result; - var exactStreamInfo = streamInfo.Streams.FirstOrDefault(s => s.UserLogin.ToLower() == channel.ToLower()); - - if (exactStreamInfo != null) - { - if (exactChannel.IsLive) - { - btn.Foreground = new SolidColorBrush(new Color(byte.MaxValue, 0, byte.MaxValue, 0)); - btn.Content = $"{exactChannel.DisplayName} ({exactStreamInfo.ViewerCount})"; - } - else - { - btn.Foreground = new SolidColorBrush(new Color(byte.MaxValue, byte.MaxValue, 0, 0)); - btn.Content = $"{exactChannel.DisplayName} (Offline)"; - } - - ctx.ViewerCount = exactStreamInfo.ViewerCount; - } - else - { - btn.Foreground = new SolidColorBrush(new Color(byte.MaxValue, byte.MaxValue, 0, 0)); - btn.Content = $"{exactChannel.DisplayName} (Offline)"; - } - } - } - }); + await vm.GetOrUpdateChannelAsync(); } } + ); } + + Console.WriteLine("Data Update"); } } } \ No newline at end of file