Implemented user control for raid button, connected to api and more UI improvements

This commit is contained in:
Enrico Ludwig 2024-08-20 19:42:23 +02:00
parent 0aa931275b
commit 1d0b109fcf
9 changed files with 466 additions and 92 deletions

View File

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
@ -17,6 +18,8 @@ namespace BetterRaid;
public partial class App : Application public partial class App : Application
{ {
public static int AutoUpdateDelay = 10_000;
public static string TwitchBroadcasterId = "";
public static string TwitchChannelName = ""; public static string TwitchChannelName = "";
public static string TokenClientId = ""; public static string TokenClientId = "";
public static string TokenClientSecret = ""; public static string TokenClientSecret = "";
@ -54,7 +57,6 @@ public partial class App : Application
TwitchClient = new TwitchClient(customClient); TwitchClient = new TwitchClient(customClient);
TwitchClient.Initialize(creds, TwitchChannelName); TwitchClient.Initialize(creds, TwitchChannelName);
TwitchClient.OnMessageReceived += OnMessageReceived;
TwitchClient.OnConnected += OnConnected; TwitchClient.OnConnected += OnConnected;
TwitchClient.OnConnectionError += OnConnectionError; TwitchClient.OnConnectionError += OnConnectionError;
@ -64,6 +66,19 @@ public partial class App : Application
TwitchAPI.Settings.ClientId = TokenClientId; TwitchAPI.Settings.ClientId = TokenClientId;
TwitchAPI.Settings.AccessToken = TokenClientAccess; 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); AvaloniaXamlLoader.Load(this);
} }
@ -77,11 +92,6 @@ public partial class App : Application
Console.WriteLine("[INFO] Twitch Client connected!"); 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() public override void OnFrameworkInitializationCompleted()
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)

View File

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.5.002.0 VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -11,10 +11,10 @@ Global
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{050F930D-FE73-4FA8-A05B-E659A4A0CEEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7AB75970-30DB-496E-960C-535708A65EC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{050F930D-FE73-4FA8-A05B-E659A4A0CEEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {7AB75970-30DB-496E-960C-535708A65EC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{050F930D-FE73-4FA8-A05B-E659A4A0CEEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {7AB75970-30DB-496E-960C-535708A65EC6}.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}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

52
Controls/RaidButton.axaml Normal file
View File

@ -0,0 +1,52 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ai="using:AsyncImageLoader"
xmlns:vm="using:BetterRaid.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="BetterRaid.RaidButton"
x:DataType="vm:RaidButtonViewModel"
Margin="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Design.DataContext>
<vm:RaidButtonViewModel />
</Design.DataContext>
<Button HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Command="{Binding RaidChannel}">
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="75*" />
<RowDefinition Height="25*" />
</Grid.RowDefinitions>
<ai:AdvancedImage Grid.Column="0"
Grid.Row="0"
Source="{Binding Channel.ThumbnailUrl}" />
<StackPanel Grid.Column="0"
Grid.Row="1"
Orientation="Vertical">
<Label HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="{Binding Channel.Name}" />
<Label HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Foreground="{Binding ViewerCountColor}"
Content="{Binding Channel.ViewerCount}" />
</StackPanel>
</Grid>
</Button>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace BetterRaid;
public partial class RaidButton : UserControl
{
public RaidButton()
{
InitializeComponent();
}
}

View File

@ -1,14 +1,94 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Threading;
namespace BetterRaid.Models; namespace BetterRaid.Models;
public class TwitchChannel public class TwitchChannel : INotifyPropertyChanged
{ {
public string? BroadcasterId { get; set; } private string? viewerCount;
public string Name { get; set; } private bool isLive;
public bool IsLive { get; set; } private string? name;
public int ViewerCount { get; set; } 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) public TwitchChannel(string channelName)
{ {
Name = channelName; Name = channelName;
} }
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
} }

View File

@ -1,6 +1,28 @@
namespace BetterRaid.ViewModels; using System;
using Avalonia;
namespace BetterRaid.ViewModels;
public partial class MainWindowViewModel : ViewModelBase 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);
}
} }

View File

@ -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<bool> 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<Channel?> 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<Stream?> 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));
}
}

View File

@ -15,8 +15,52 @@
<vm:MainWindowViewModel/> <vm:MainWindowViewModel/>
</Design.DataContext> </Design.DataContext>
<Grid <Grid HorizontalAlignment="Stretch"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Menu Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="0">
<MenuItem Header="File">
<MenuItem Header="Add Channel" />
<MenuItem Header="Remove Channel" />
<Separator />
<MenuItem Header="Exit" Command="{Binding ExitApplication}" />
</MenuItem>
</Menu>
<TextBox Grid.Column="1"
Grid.Row="0"
Width="200"
Margin="2"
Watermark="Filter Channels"
Text="{Binding Filter, Mode=TwoWay}"
HorizontalAlignment="Right" />
<ScrollViewer Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="1"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ScrollViewer.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="False"
CanVerticallyScroll="True"
IsScrollInertiaEnabled="True" />
</ScrollViewer.GestureRecognizers>
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
x:Name="raidGrid"> x:Name="raidGrid">
@ -27,5 +71,8 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
</Grid> </Grid>
</ScrollViewer>
</Grid>
</Window> </Window>

View File

@ -1,21 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.ComponentModel;
using System.Linq; using System.Threading.Tasks;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading; using Avalonia.Threading;
using BetterRaid.Models;
using BetterRaid.ViewModels; using BetterRaid.ViewModels;
using TwitchLib.Client.Events;
namespace BetterRaid.Views; namespace BetterRaid.Views;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private BackgroundWorker _autoUpdater;
private string[] _channelNames = [ private string[] _channelNames = [
"Cedricun", // Ehrenbruder "Cedricun", // Ehrenbruder
"ZanTal", // Ehrenschwester "ZanTal", // Ehrenschwester
@ -31,33 +26,80 @@ public partial class MainWindow : Window
public MainWindow() public MainWindow()
{ {
_autoUpdater = new BackgroundWorker();
InitializeComponent(); InitializeComponent();
PrepareRaidGrid(); GenerateRaidGrid();
ConnectToTwitch();
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); var rows = (int)Math.Ceiling(_channelNames.Length / 3.0);
for (var i = 0; i < rows; i++) 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 colIndex = 0;
var rowIndex = 0; var rowIndex = 0;
foreach (var channel in _channelNames) foreach (var channel in _channelNames)
{ {
var btn = new Button if (string.IsNullOrEmpty(channel))
continue;
var btn = new RaidButton
{ {
Content = channel, DataContext = new RaidButtonViewModel
DataContext = new TwitchChannel(channel), {
Margin = Thickness.Parse("5"), ChannelName = channel
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, }
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch,
HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center
}; };
Grid.SetColumn(btn, colIndex); Grid.SetColumn(btn, colIndex);
@ -71,64 +113,41 @@ public partial class MainWindow : Window
colIndex = 0; colIndex = 0;
rowIndex++; rowIndex++;
} }
if (btn.DataContext is RaidButtonViewModel vm)
{
Dispatcher.UIThread.InvokeAsync(vm.GetOrUpdateChannelAsync);
} }
} }
private void ConnectToTwitch() _autoUpdater.DoWork += UpdateAllTiles;
{ _autoUpdater.RunWorkerAsync();
if (App.TwitchClient != null && App.TwitchAPI != null)
{
foreach (var c in raidGrid.Children)
{
if (c is Button btn)
{
var channel = (btn.DataContext as TwitchChannel)?.Name;
if (string.IsNullOrEmpty(channel) == false)
{
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<string>([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; public void UpdateAllTiles(object? sender, DoWorkEventArgs e)
}
else
{ {
btn.Foreground = new SolidColorBrush(new Color(byte.MaxValue, byte.MaxValue, 0, 0)); while (e.Cancel == false)
btn.Content = $"{exactChannel.DisplayName} (Offline)"; {
} Task.Delay(App.AutoUpdateDelay).Wait();
}
} if (raidGrid == null || raidGrid.Children.Count == 0)
}); {
return;
}
foreach (var children in raidGrid.Children)
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
if (children.DataContext is RaidButtonViewModel vm)
{
await vm.GetOrUpdateChannelAsync();
} }
} }
);
} }
Console.WriteLine("Data Update");
} }
} }
} }