Lots of code cleanup, moved Twitch stuff to services, further improvements on UI

This commit is contained in:
Enrico Ludwig 2024-09-04 02:27:55 +02:00
parent 8a10573fbf
commit 814cb58742
11 changed files with 307 additions and 155 deletions

View File

@ -12,7 +12,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace BetterRaid; namespace BetterRaid;
public partial class App : Application public class App : Application
{ {
private static readonly ServiceCollection Services = []; private static readonly ServiceCollection Services = [];
private static ServiceProvider? _serviceProvider; private static ServiceProvider? _serviceProvider;

View File

@ -2,7 +2,7 @@ using System;
namespace BetterRaid.Attributes; namespace BetterRaid.Attributes;
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = true)] [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
public class PubSubAttribute : Attribute public class PubSubAttribute : Attribute
{ {
public PubSubType Type { get; } public PubSubType Type { get; }

View File

@ -7,5 +7,7 @@ public enum PubSubType
Subscriptions, Subscriptions,
ChannelPoints, ChannelPoints,
Bits, Bits,
Raids Raids,
StreamUp,
StreamDown
} }

View File

@ -20,7 +20,7 @@ public static class Tools
if (_oauthListener == null) if (_oauthListener == null)
{ {
_oauthListener = new HttpListener(); _oauthListener = new HttpListener();
_oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl); _oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/");
_oauthListener.Start(); _oauthListener.Start();
Task.Run(() => WaitForCallback(callback, token), token); Task.Run(() => WaitForCallback(callback, token), token);
@ -110,7 +110,7 @@ public static class Tools
var dataService = App.ServiceProvider?.GetService(typeof(ITwitchDataService)); var dataService = App.ServiceProvider?.GetService(typeof(ITwitchDataService));
if (dataService is ITwitchDataService twitchDataService) if (dataService is ITwitchDataService twitchDataService)
{ {
twitchDataService.ConnectApi(Constants.TwitchClientId, accessToken!); twitchDataService.ConnectApiAsync(Constants.TwitchClientId, accessToken!);
} }
res.StatusCode = 200; res.StatusCode = 200;

View File

@ -45,6 +45,8 @@ public class TwitchChannel : INotifyPropertyChanged
} }
} }
[PubSub(PubSubType.StreamUp, nameof(BroadcasterId))]
[PubSub(PubSubType.StreamDown, nameof(BroadcasterId))]
public bool IsLive public bool IsLive
{ {
get => _isLive; get => _isLive;

View File

@ -1,4 +1,5 @@
using System.ComponentModel; using System.ComponentModel;
using System.Threading.Tasks;
using BetterRaid.Models; using BetterRaid.Models;
using TwitchLib.Api; using TwitchLib.Api;
@ -11,11 +12,12 @@ public interface ITwitchDataService
public TwitchAPI TwitchApi { get; } public TwitchAPI TwitchApi { get; }
public bool IsRaidStarted { get; set; } public bool IsRaidStarted { get; set; }
public void ConnectApi(string clientId, string accessToken); public Task ConnectApiAsync(string clientId, string accessToken);
public void SaveAccessToken(string token); public void SaveAccessToken(string token);
public bool TryGetUserChannel(out TwitchChannel? userChannel); public bool TryGetUserChannel(out TwitchChannel? userChannel);
public string GetOAuthUrl(); public string GetOAuthUrl();
public void StartRaid(string from, string to); public void StartRaid(string from, string to);
public bool CanStartRaidCommand(object? arg);
public void StartRaidCommand(object? arg); public void StartRaidCommand(object? arg);
public void StopRaid(); public void StopRaid();
public void StopRaidCommand(); public void StopRaidCommand();

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BetterRaid.Misc; using BetterRaid.Misc;
using BetterRaid.Models; using BetterRaid.Models;
using TwitchLib.Api; using TwitchLib.Api;
@ -45,7 +46,7 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INo
if (TryLoadAccessToken(out var token)) if (TryLoadAccessToken(out var token))
{ {
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Found access token."); Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Found access token.");
ConnectApi(Constants.TwitchClientId, token); Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token));
} }
else else
{ {
@ -53,7 +54,7 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INo
} }
} }
public void ConnectApi(string clientId, string accessToken) public async Task ConnectApiAsync(string clientId, string accessToken)
{ {
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Connecting to Twitch API ..."); Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Connecting to Twitch API ...");
@ -74,6 +75,8 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INo
Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] Could not get user channel."); Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] Could not get user channel.");
Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] Failed to connect to Twitch API."); Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] Failed to connect to Twitch API.");
} }
await Task.CompletedTask;
} }
private bool TryLoadAccessToken(out string token) private bool TryLoadAccessToken(out string token)
@ -129,6 +132,11 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INo
IsRaidStarted = true; IsRaidStarted = true;
} }
public bool CanStartRaidCommand(object? arg)
{
return UserChannel?.IsLive == true && IsRaidStarted == false;
}
public void StartRaidCommand(object? arg) public void StartRaidCommand(object? arg)
{ {
if (arg == null || UserChannel?.BroadcasterId == null) if (arg == null || UserChannel?.BroadcasterId == null)

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using BetterRaid.Attributes; using BetterRaid.Attributes;
using BetterRaid.Extensions; using BetterRaid.Extensions;
using BetterRaid.Models; using BetterRaid.Models;
@ -13,45 +14,127 @@ namespace BetterRaid.Services.Implementations;
public class TwitchPubSubService : ITwitchPubSubService public class TwitchPubSubService : ITwitchPubSubService
{ {
private readonly Dictionary<PubSubType, List<PubSubListener>> _targets = new(); private readonly Dictionary<PubSubType, List<PubSubListener>> _targets = new();
private readonly TwitchPubSub _sub;
private readonly ITwitchDataService _dataService; private readonly ITwitchDataService _dataService;
private TwitchPubSub? _sub;
public TwitchPubSubService(ITwitchDataService dataService) public TwitchPubSubService(ITwitchDataService dataService)
{ {
_dataService = dataService; _dataService = dataService;
Task.Run(InitializePubSubAsync);
}
private async Task InitializePubSubAsync()
{
while (_dataService.UserChannel == null)
{
await Task.Delay(100);
}
_sub = new TwitchPubSub(); _sub = new TwitchPubSub();
_sub.OnPubSubServiceConnected += OnSubOnOnPubSubServiceConnected; _sub.OnPubSubServiceConnected += OnSubOnOnPubSubServiceConnected;
_sub.OnPubSubServiceError += OnSubOnOnPubSubServiceError; _sub.OnPubSubServiceError += OnSubOnOnPubSubServiceError;
_sub.OnPubSubServiceClosed += OnSubOnOnPubSubServiceClosed; _sub.OnPubSubServiceClosed += OnSubOnOnPubSubServiceClosed;
_sub.OnListenResponse += OnSubOnOnListenResponse; _sub.OnListenResponse += OnSubOnOnListenResponse;
_sub.OnViewCount += OnSubOnOnViewCount; _sub.OnViewCount += OnSubOnOnViewCount;
_sub.OnStreamUp += OnStreamUp;
_sub.OnStreamDown += OnStreamDown;
_sub.Connect(); _sub.Connect();
if (_dataService.UserChannel != null) if (_dataService.UserChannel != null)
{ {
RegisterReceiver(_dataService.UserChannel); RegisterReceiver(_dataService.UserChannel);
} }
_dataService.PropertyChanging += (_, args) => _dataService.PropertyChanging += (_, args) =>
{ {
if (args.PropertyName != nameof(_dataService.UserChannel)) if (args.PropertyName != nameof(_dataService.UserChannel))
return; return;
if (_dataService.UserChannel != null) if (_dataService.UserChannel != null)
UnregisterReceiver(_dataService.UserChannel); UnregisterReceiver(_dataService.UserChannel);
}; };
_dataService.PropertyChanged += (_, args) => _dataService.PropertyChanged += (_, args) =>
{ {
if (args.PropertyName != nameof(_dataService.UserChannel)) if (args.PropertyName != nameof(_dataService.UserChannel))
return; return;
if (_dataService.UserChannel != null) if (_dataService.UserChannel != null)
RegisterReceiver(_dataService.UserChannel); RegisterReceiver(_dataService.UserChannel);
}; };
await Task.CompletedTask;
}
private void OnStreamDown(object? sender, OnStreamDownArgs args)
{
var listeners = _targets
.Where(x => x.Key == PubSubType.StreamDown)
.SelectMany(x => x.Value)
.Where(x => x.ChannelId == args.ChannelId)
.ToList();
foreach (var listener in listeners)
{
if (listener.Listener == null || listener.Instance == null)
continue;
try
{
if (listener.Listener.SetValue(listener.Instance, false) == false)
{
Console.WriteLine(
$"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to true");
}
else
{
Console.WriteLine(
$"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to true");
}
}
catch (Exception e)
{
Console.WriteLine(
$"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}");
}
}
}
private void OnStreamUp(object? sender, OnStreamUpArgs args)
{
var listeners = _targets
.Where(x => x.Key == PubSubType.StreamUp)
.SelectMany(x => x.Value)
.Where(x => x.ChannelId == args.ChannelId)
.ToList();
foreach (var listener in listeners)
{
if (listener.Listener == null || listener.Instance == null)
continue;
try
{
if (listener.Listener.SetValue(listener.Instance, true) == false)
{
Console.WriteLine(
$"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to true");
}
else
{
Console.WriteLine(
$"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to true");
}
}
catch (Exception e)
{
Console.WriteLine(
$"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}");
}
}
} }
private void OnSubOnOnViewCount(object? sender, OnViewCountArgs args) private void OnSubOnOnViewCount(object? sender, OnViewCountArgs args)
@ -61,26 +144,29 @@ public class TwitchPubSubService : ITwitchPubSubService
.SelectMany(x => x.Value) .SelectMany(x => x.Value)
.Where(x => x.ChannelId == args.ChannelId) .Where(x => x.ChannelId == args.ChannelId)
.ToList(); .ToList();
foreach (var listener in listeners) foreach (var listener in listeners)
{ {
if (listener.Listener == null || listener.Instance == null) if (listener.Listener == null || listener.Instance == null)
continue; continue;
try try
{ {
if (listener.Listener.SetValue(listener.Instance, args.Viewers) == false) if (listener.Listener.SetValue(listener.Instance, args.Viewers) == false)
{ {
Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}"); Console.WriteLine(
$"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}");
} }
else else
{ {
Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}"); Console.WriteLine(
$"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}");
} }
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}"); Console.WriteLine(
$"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}");
} }
} }
} }
@ -109,8 +195,14 @@ public class TwitchPubSubService : ITwitchPubSubService
{ {
ArgumentNullException.ThrowIfNull(receiver, nameof(receiver)); ArgumentNullException.ThrowIfNull(receiver, nameof(receiver));
if (_sub == null)
{
Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] PubSub is not initialized");
return;
}
Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {receiver.GetType().Name}"); Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {receiver.GetType().Name}");
var type = typeof(T); var type = typeof(T);
var publicTargets = type var publicTargets = type
.GetProperties() .GetProperties()
@ -120,93 +212,79 @@ public class TwitchPubSubService : ITwitchPubSubService
foreach (var target in publicTargets) foreach (var target in publicTargets)
{ {
if (target.GetCustomAttribute<PubSubAttribute>() is not { } attr) if (target.GetCustomAttributes<PubSubAttribute>() is not { } attrs)
{ {
continue; continue;
} }
var channelId = foreach (var attr in attrs)
type.GetProperty(attr.ChannelIdField)?.GetValue(receiver)?.ToString() ??
type.GetField(attr.ChannelIdField)?.GetValue(receiver)?.ToString();
if (string.IsNullOrEmpty(channelId))
{ {
Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] {target.Name} is missing ChannelIdField named {attr.ChannelIdField}"); var channelId =
continue; type.GetProperty(attr.ChannelIdField)?.GetValue(receiver)?.ToString() ??
} type.GetField(attr.ChannelIdField)?.GetValue(receiver)?.ToString();
switch (attr.Type) if (channelId == null)
{ {
case PubSubType.Bits: Console.WriteLine(
break; $"[ERROR][{nameof(TwitchPubSubService)}] {target.Name} is missing ChannelIdField named {attr.ChannelIdField}");
case PubSubType.ChannelPoints: continue;
break; }
case PubSubType.Follows:
break; if (string.IsNullOrWhiteSpace(channelId))
case PubSubType.Raids: {
break; Console.WriteLine(
case PubSubType.Subscriptions: $"[ERROR][{nameof(TwitchPubSubService)}] {target.Name} ChannelIdField named {attr.ChannelIdField} is empty");
break; continue;
case PubSubType.VideoPlayback: }
Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {target.Name} for {attr.Type}");
if (_targets.TryGetValue(PubSubType.VideoPlayback, out var value)) Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {target.Name} for {attr.Type}");
if (_targets.TryGetValue(attr.Type, out var listeners))
{
listeners.Add(new PubSubListener
{ {
value.Add(new PubSubListener ChannelId = channelId,
Instance = receiver,
Listener = target
});
}
else
{
_targets.Add(attr.Type, [
new PubSubListener
{ {
ChannelId = channelId, ChannelId = channelId,
Instance = receiver, Instance = receiver,
Listener = target Listener = target
}); }
} ]);
else }
{
_targets.Add(PubSubType.VideoPlayback, [ _sub.ListenToVideoPlayback(channelId);
new PubSubListener _sub.SendTopics(_dataService.AccessToken, true);
{
ChannelId = channelId,
Instance = receiver,
Listener = target
}
]);
}
_sub.ListenToVideoPlayback(channelId);
_sub.SendTopics(_dataService.AccessToken);
break;
} }
} }
} }
public void UnregisterReceiver<T>(T receiver) where T : class public void UnregisterReceiver<T>(T receiver) where T : class
{ {
ArgumentNullException.ThrowIfNull(receiver, nameof(receiver)); ArgumentNullException.ThrowIfNull(receiver, nameof(receiver));
if (_sub == null)
{
Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] PubSub is not initialized");
return;
}
foreach (var (topic, listeners) in _targets) foreach (var (topic, listeners) in _targets)
{ {
var listener = listeners.Where(x => x.Instance == receiver).ToList(); var listener = listeners.Where(x => x.Instance == receiver).ToList();
foreach (var l in listener) foreach (var l in listener)
{ {
switch (topic) _sub.ListenToVideoPlayback(l.ChannelId);
{ _sub.SendTopics(_dataService.AccessToken, true);
case PubSubType.Bits:
break;
case PubSubType.ChannelPoints:
break;
case PubSubType.Follows:
break;
case PubSubType.Raids:
break;
case PubSubType.Subscriptions:
break;
case PubSubType.VideoPlayback:
_sub.ListenToVideoPlayback(l.ChannelId);
_sub.SendTopics(_dataService.AccessToken, true);
break;
}
} }
_targets[topic].RemoveAll(x => x.Instance == receiver); _targets[topic].RemoveAll(x => x.Instance == receiver);
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,17 +13,17 @@ using BetterRaid.Views;
namespace BetterRaid.ViewModels; namespace BetterRaid.ViewModels;
public partial class MainWindowViewModel : ViewModelBase public class MainWindowViewModel : ViewModelBase
{ {
private string? _filter; private string? _filter;
private ObservableCollection<TwitchChannel> _channels = []; private ObservableCollection<TwitchChannel> _channels = [];
private BetterRaidDatabase? _db; private readonly BetterRaidDatabase? _db;
private readonly ITwitchPubSubService _pubSub; private readonly ITwitchPubSubService _pubSub;
public BetterRaidDatabase? Database public BetterRaidDatabase? Database
{ {
get => _db; get => _db;
set private init
{ {
if (SetProperty(ref _db, value) && _db != null) if (SetProperty(ref _db, value) && _db != null)
{ {
@ -37,6 +38,8 @@ public partial class MainWindowViewModel : ViewModelBase
set => SetProperty(ref _channels, value); set => SetProperty(ref _channels, value);
} }
public ObservableCollection<TwitchChannel> FilteredChannels => GetFilteredChannels();
public ITwitchDataService DataService { get; } public ITwitchDataService DataService { get; }
public string? Filter public string? Filter
@ -51,10 +54,12 @@ public partial class MainWindowViewModel : ViewModelBase
{ {
_pubSub = pubSub; _pubSub = pubSub;
DataService = dataService; DataService = dataService;
DataService.PropertyChanged += OnDataServicePropertyChanged;
Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath); Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath);
Database.PropertyChanged += OnDatabasePropertyChanged;
} }
public void ExitApplication() public void ExitApplication()
{ {
//TODO polish later //TODO polish later
@ -107,4 +112,40 @@ public partial class MainWindowViewModel : ViewModelBase
Channels.Add(channel); Channels.Add(channel);
} }
} }
private ObservableCollection<TwitchChannel> GetFilteredChannels()
{
var filteredChannels = Channels
.Where(channel => Database?.OnlyOnline == false || channel.IsLive)
.Where(channel => string.IsNullOrWhiteSpace(Filter) || channel.Name?.Contains(Filter, StringComparison.OrdinalIgnoreCase) == true)
.ToList();
return new ObservableCollection<TwitchChannel>(filteredChannels);
}
private void OnDataServicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(DataService.UserChannel))
return;
OnPropertyChanged(nameof(IsLoggedIn));
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.PropertyName == nameof(Filter))
{
OnPropertyChanged(nameof(FilteredChannels));
}
}
private void OnDatabasePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(BetterRaidDatabase.OnlyOnline))
return;
OnPropertyChanged(nameof(FilteredChannels));
}
} }

View File

@ -22,83 +22,85 @@
</Window.Resources> </Window.Resources>
<Grid HorizontalAlignment="Stretch" <Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch"
ColumnDefinitions="Auto,*"
RowDefinitions="50,*">
<Grid.ColumnDefinitions> <StackPanel Grid.Column="0"
<ColumnDefinition Width="*" /> Grid.Row="0"
<ColumnDefinition Width="Auto" /> Orientation="Horizontal">
</Grid.ColumnDefinitions> <ai:AdvancedImage CornerRadius="20"
Width="40"
Height="40"
Margin="5"
Source="{Binding DataService.UserChannel.ThumbnailUrl,
FallbackValue={x:Static misc:Constants.ChannelPlaceholderImageUrl},
TargetNullValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}}" />
<TextBlock VerticalAlignment="Center"
Margin="5, 0, 0, 0"
FontWeight="Bold">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} ({1})">
<Binding Path="DataService.UserChannel.DisplayName"
FallbackValue="-" />
<Binding Path="DataService.UserChannel.ViewerCount"
FallbackValue="Offline"
TargetNullValue="Offline" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Menu Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="0">
<MenuItem Header="File">
<MenuItem Header="About"
CommandParameter="{Binding $parent[Window]}"
Command="{Binding ShowAboutWindow}" />
<Separator />
<MenuItem Header="Exit"
Command="{Binding ExitApplication}" />
</MenuItem>
</Menu>
<StackPanel Grid.Column="1" <StackPanel Grid.Column="1"
Grid.Row="0" Grid.Row="0"
Orientation="Horizontal" Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="5"> Spacing="5">
<Button Command="{Binding LoginWithTwitch}"
IsVisible="{Binding !IsLoggedIn, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
<Button.Styles>
<Style Selector="Button">
<Setter Property="Background" Value="#6441a5" />
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#b9a3e3" />
</Style>
</Button.Styles>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Login"
Foreground="#f1f1f1"
FontSize="14" />
<Image Source="avares://BetterRaid/Assets/glitch_flat_white.png"
Width="16"
Height="16"
Margin="5, 0, 0, 0" />
</StackPanel>
</Button>
<CheckBox Content="Only Online" <CheckBox Content="Only Online"
IsChecked="{Binding Database.OnlyOnline, Mode=TwoWay}" /> IsChecked="{Binding Database.OnlyOnline, Mode=TwoWay, FallbackValue=False}" />
<TextBox Grid.Column="1" <TextBox Width="200"
Grid.Row="0" Margin="5, 10, 5, 10"
Width="200"
Margin="2"
Watermark="Filter Channels" Watermark="Filter Channels"
Text="{Binding Filter, Mode=TwoWay}" Text="{Binding Filter, Mode=TwoWay}"
HorizontalAlignment="Right" /> HorizontalAlignment="Right" />
</StackPanel> </StackPanel>
<Button Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="200"
Height="50"
Command="{Binding LoginWithTwitch}"
IsVisible="{Binding !IsLoggedIn, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
<Button.Styles>
<Style Selector="Button">
<Setter Property="Background" Value="#6441a5" />
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#6C4CA5" />
</Style>
</Button.Styles>
<TextBlock Text="Please login first!" <StackPanel Orientation="Horizontal">
Grid.Column="0" <TextBlock Text="Login with Twitch"
Grid.ColumnSpan="2" Foreground="#f1f1f1"
Grid.Row="1" FontSize="18"
IsVisible="{Binding !IsLoggedIn, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" />
HorizontalAlignment="Center" <Image Source="avares://BetterRaid/Assets/glitch_flat_white.png"
VerticalAlignment="Center" Width="24"
TextAlignment="Center" Height="24"
FontSize="24" VerticalAlignment="Center"
Foreground="#f1f1f1" /> Margin="5, 0, 0, 0" />
</StackPanel>
</Button>
<ScrollViewer Grid.Column="0" <ScrollViewer Grid.Column="0"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
@ -113,12 +115,21 @@
IsScrollInertiaEnabled="True" /> IsScrollInertiaEnabled="True" />
</ScrollViewer.GestureRecognizers> </ScrollViewer.GestureRecognizers>
<ListBox ItemsSource="{Binding Channels}"> <ListBox ItemsSource="{Binding FilteredChannels, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Single"
SelectionChanged="SelectingItemsControl_OnSelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid ColumnDefinitions="100, *, 120" <Grid ColumnDefinitions="100, *, 120"
RowDefinitions="100"> RowDefinitions="100">
<ai:AdvancedImage Grid.Column="0" <ai:AdvancedImage Grid.Column="0"
Grid.Row="0" Grid.Row="0"
Source="{Binding ThumbnailUrl, TargetNullValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}}" /> Source="{Binding ThumbnailUrl, TargetNullValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}}" />

View File

@ -8,4 +8,12 @@ public partial class MainWindow : Window
{ {
InitializeComponent(); InitializeComponent();
} }
private void SelectingItemsControl_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (sender is ListBox listBox)
{
listBox.SelectedItems?.Clear();
}
}
} }