Further work on ui-rework, also implemented services, re-implemented raiding functionality and PubSub

This commit is contained in:
Enrico Ludwig 2024-09-03 20:30:29 +02:00
parent c2309599f2
commit a8a481d4f9
19 changed files with 587 additions and 108 deletions

View File

@ -5,11 +5,14 @@ using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using BetterRaid.Extensions;
using BetterRaid.Services; using BetterRaid.Services;
using BetterRaid.Services.Implementations;
using BetterRaid.ViewModels; using BetterRaid.ViewModels;
using BetterRaid.Views; using BetterRaid.Views;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using TwitchLib.Api; using TwitchLib.Api;
using TwitchLib.PubSub;
namespace BetterRaid; namespace BetterRaid;
@ -43,6 +46,7 @@ public partial class App : Application
public static TwitchAPI? TwitchApi => _twitchApi; public static TwitchAPI? TwitchApi => _twitchApi;
public static bool HasUserZnSubbed => _hasUserZnSubbed; public static bool HasUserZnSubbed => _hasUserZnSubbed;
public static string BetterRaidDataPath => _betterRaidDataPath;
public IServiceProvider? Provider => _provider; public IServiceProvider? Provider => _provider;
public static string? TwitchBroadcasterId => _twitchBroadcasterId; public static string? TwitchBroadcasterId => _twitchBroadcasterId;
@ -59,8 +63,8 @@ public partial class App : Application
public override void Initialize() public override void Initialize()
{ {
InitializeServices();
LoadTwitchToken(); LoadTwitchToken();
InitializeServices();
AvaloniaXamlLoader.Load(_provider, this); AvaloniaXamlLoader.Load(_provider, this);
} }
@ -98,8 +102,8 @@ public partial class App : Application
private void InitializeServices() private void InitializeServices()
{ {
_services.AddSingleton<ITwitchDataService, TwitchDataService>(); _services.AddSingleton<ITwitchDataService, TwitchDataService>();
_services.AddSingleton<ITwitchPubSubService, TwitchPubSubService>();
_services.AddTransient<MainWindowViewModel>(); _services.AddTransient<MainWindowViewModel>();
_services.AddTransient<AboutWindowViewModel>();
_provider = _services.BuildServiceProvider(); _provider = _services.BuildServiceProvider();
} }
@ -154,7 +158,6 @@ public partial class App : Application
} }
_twitchBroadcasterId = channel.Id; _twitchBroadcasterId = channel.Id;
Console.WriteLine(_twitchBroadcasterId);
Console.WriteLine("[INFO] Connected to Twitch API as '{0}'!", user.DisplayName); Console.WriteLine("[INFO] Connected to Twitch API as '{0}'!", user.DisplayName);
@ -188,25 +191,18 @@ public partial class App : Application
{ {
BindingPlugins.DataValidators.RemoveAt(0); BindingPlugins.DataValidators.RemoveAt(0);
var vm = _provider?.GetRequiredService<MainWindowViewModel>();
switch (ApplicationLifetime) switch (ApplicationLifetime)
{ {
case IClassicDesktopStyleApplicationLifetime desktop: case IClassicDesktopStyleApplicationLifetime desktop:
// Line below is needed to remove Avalonia data validation. desktop.MainWindow = new MainWindow();
// Without this line you will get duplicate validations from both Avalonia and CT desktop.MainWindow.InjectDataContext<MainWindowViewModel>();
desktop.MainWindow = new MainWindow
{
DataContext = vm
};
break; break;
case ISingleViewApplicationLifetime singleViewPlatform: case ISingleViewApplicationLifetime singleViewPlatform:
singleViewPlatform.MainView = new MainWindow singleViewPlatform.MainView = new MainWindow();
{ singleViewPlatform.MainView.InjectDataContext<MainWindowViewModel>();
DataContext = vm
};
break; break;
} }

View File

@ -0,0 +1,17 @@
using System;
namespace BetterRaid.Attributes;
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = true)]
public class PubSubAttribute : Attribute
{
public PubSubType Type { get; }
public string ChannelIdField { get; set; }
public PubSubAttribute(PubSubType type, string channelIdField)
{
Type = type;
ChannelIdField = channelIdField;
}
}

11
Attributes/PubSubType.cs Normal file
View File

@ -0,0 +1,11 @@
namespace BetterRaid.Attributes;
public enum PubSubType
{
VideoPlayback,
Follows,
Subscriptions,
ChannelPoints,
Bits,
Raids
}

View File

@ -14,7 +14,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" /> <AvaloniaResource Include="Assets\**" />
</ItemGroup> </ItemGroup>

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", "{49E459C8-9DCF-4D6D-95FC-75303243F248}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{C23C5237-3D18-424A-ACF2-62215BE5D557}"
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
{49E459C8-9DCF-4D6D-95FC-75303243F248}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C23C5237-3D18-424A-ACF2-62215BE5D557}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{49E459C8-9DCF-4D6D-95FC-75303243F248}.Debug|Any CPU.Build.0 = Debug|Any CPU {C23C5237-3D18-424A-ACF2-62215BE5D557}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49E459C8-9DCF-4D6D-95FC-75303243F248}.Release|Any CPU.ActiveCfg = Release|Any CPU {C23C5237-3D18-424A-ACF2-62215BE5D557}.Release|Any CPU.ActiveCfg = Release|Any CPU
{49E459C8-9DCF-4D6D-95FC-75303243F248}.Release|Any CPU.Build.0 = Release|Any CPU {C23C5237-3D18-424A-ACF2-62215BE5D557}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -0,0 +1,29 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace BetterRaid.Converters;
public class ChannelOnlineColorConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool isOnline)
{
return isOnline ? new SolidColorBrush(Colors.GreenYellow) : new SolidColorBrush(Colors.OrangeRed);
}
return new SolidColorBrush(Colors.OrangeRed);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is SolidColorBrush brush)
{
return brush.Color == Colors.GreenYellow;
}
return false;
}
}

View File

@ -0,0 +1,55 @@
using System.Reflection;
namespace BetterRaid.Extensions;
public static class MemberInfoExtensions
{
public static bool SetValue<T>(this MemberInfo member, object instance, T value)
{
var targetType = member switch
{
PropertyInfo p => p.PropertyType,
FieldInfo f => f.FieldType,
_ => null
};
if (targetType == null)
return false;
if (member is PropertyInfo property)
{
if (targetType == typeof(T) || targetType.IsAssignableFrom(typeof(T)))
{
property.SetValue(instance, value);
return true;
}
if (targetType == typeof(string))
{
property.SetValue(instance, value?.ToString());
return true;
}
return false;
}
if (member is FieldInfo field)
{
if (targetType == typeof(T) || targetType.IsAssignableFrom(typeof(T)))
{
field.SetValue(instance, value);
return true;
}
if (targetType == typeof(string))
{
field.SetValue(instance, value?.ToString());
return true;
}
return false;
}
return false;
}
}

View File

@ -15,44 +15,18 @@ public static class Tools
private static Task? _oauthWaiterTask; private static Task? _oauthWaiterTask;
// Source: https://stackoverflow.com/a/43232486 // Source: https://stackoverflow.com/a/43232486
public static void StartOAuthLogin(string url, Action? callback = null, CancellationToken? token = null) public static void StartOAuthLogin(string url, Action? callback = null, CancellationToken token = default)
{ {
if (_oauthListener != null) if (_oauthListener == null)
return;
var _token = token ?? CancellationToken.None;
_oauthListener = new HttpListener();
_oauthListener.Prefixes.Add("http://localhost:9900/");
_oauthListener.Start();
_oauthWaiterTask = WaitForCallback(callback, _token);
try
{ {
Process.Start(url); _oauthListener = new HttpListener();
} _oauthListener.Prefixes.Add("http://localhost:9900/");
catch _oauthListener.Start();
{
// hack because of this: https://github.com/dotnet/corefx/issues/10361 _oauthWaiterTask = WaitForCallback(callback, token);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
url = url.Replace("&", "^&");
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Process.Start("xdg-open", url);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Process.Start("open", url);
}
else
{
throw;
}
} }
OpenUrl(url);
} }
private static async Task WaitForCallback(Action? callback, CancellationToken token) private static async Task WaitForCallback(Action? callback, CancellationToken token)
@ -147,6 +121,35 @@ public static class Tools
} }
} }
public static void OpenUrl(string url)
{
try
{
Process.Start(url);
}
catch
{
// hack because of this: https://github.com/dotnet/corefx/issues/10361
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
url = url.Replace("&", "^&");
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Process.Start("xdg-open", url);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Process.Start("open", url);
}
else
{
throw;
}
}
}
private const string OAUTH_CLIENT_DOCUMENT = private const string OAUTH_CLIENT_DOCUMENT =
@" @"
<!DOCTYPE html> <!DOCTYPE html>

10
Models/PubSubListener.cs Normal file
View File

@ -0,0 +1,10 @@
using System.Reflection;
namespace BetterRaid.Models;
public class PubSubListener
{
public string ChannelId { get; set; }
public object? Instance { get; set; }
public MemberInfo? Listener { get; set; }
}

View File

@ -1,11 +1,15 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using BetterRaid.Attributes;
namespace BetterRaid.Models; namespace BetterRaid.Models;
public class TwitchChannel : INotifyPropertyChanged public class TwitchChannel : INotifyPropertyChanged
{ {
private string? _broadcasterId;
private string? _viewerCount; private string? _viewerCount;
private bool _isLive; private bool _isLive;
private string? _name; private string? _name;
@ -17,9 +21,17 @@ public class TwitchChannel : INotifyPropertyChanged
public string? BroadcasterId public string? BroadcasterId
{ {
get; get => _broadcasterId;
set; set
{
if (value == _broadcasterId)
return;
_broadcasterId = value;
OnPropertyChanged();
}
} }
public string? Name public string? Name
{ {
get => _name; get => _name;
@ -32,6 +44,7 @@ public class TwitchChannel : INotifyPropertyChanged
OnPropertyChanged(); OnPropertyChanged();
} }
} }
public bool IsLive public bool IsLive
{ {
get => _isLive; get => _isLive;
@ -44,6 +57,8 @@ public class TwitchChannel : INotifyPropertyChanged
OnPropertyChanged(); OnPropertyChanged();
} }
} }
[PubSub(PubSubType.VideoPlayback, nameof(BroadcasterId))]
public string? ViewerCount public string? ViewerCount
{ {
get => _viewerCount; get => _viewerCount;
@ -127,6 +142,28 @@ public class TwitchChannel : INotifyPropertyChanged
Name = channelName; Name = channelName;
} }
public void InitChannel()
{
var channel = App.TwitchApi?.Helix.Search.SearchChannelsAsync(Name).Result.Channels
.FirstOrDefault(c => c.BroadcasterLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase));
if (channel == null)
return;
var stream = App.TwitchApi?.Helix.Streams.GetStreamsAsync(userLogins: [ Name ]).Result.Streams
.FirstOrDefault(s => s.UserLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase));
BroadcasterId = channel.Id;
DisplayName = channel.DisplayName;
ThumbnailUrl = channel.ThumbnailUrl;
Category = channel.GameName;
Title = channel.Title;
IsLive = channel.IsLive;
ViewerCount = stream?.ViewerCount == null
? null
: $"{stream.ViewerCount}";
}
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) private void OnPropertyChanged([CallerMemberName] string? propertyName = null)

View File

@ -1,2 +1 @@
# BetterRaid # BetterRaid

View File

@ -2,5 +2,11 @@ namespace BetterRaid.Services;
public interface ITwitchDataService public interface ITwitchDataService
{ {
public bool IsRaidStarted { get; set; }
public void StartRaid(string from, string to);
public void StartRaidCommand(object? arg);
public void StopRaid();
public void StopRaidCommand();
public void OpenChannelCommand(object? arg);
} }

View File

@ -0,0 +1,7 @@
namespace BetterRaid.Services;
public interface ITwitchPubSubService
{
void RegisterReceiver<T>(T receiver) where T : class;
void UnregisterReceiver<T>(T receiver) where T : class;
}

View File

@ -1,6 +1,78 @@
namespace BetterRaid.Services; using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using BetterRaid.Misc;
public class TwitchDataService : ITwitchDataService namespace BetterRaid.Services.Implementations;
public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged
{ {
private bool _isRaidStarted;
public bool IsRaidStarted
{
get => _isRaidStarted;
set => SetField(ref _isRaidStarted, value);
}
public void StartRaid(string from, string to)
{
// TODO: Also check, if the logged in user is live
App.TwitchApi?.Helix.Raids.StartRaidAsync(from, to);
IsRaidStarted = true;
}
public void StartRaidCommand(object? arg)
{
if (arg == null || App.TwitchBroadcasterId == null)
{
return;
}
var from = App.TwitchBroadcasterId;
var to = arg.ToString()!;
StartRaid(from, to);
}
public void StopRaid()
{
if (IsRaidStarted == false)
return;
App.TwitchApi?.Helix.Raids.CancelRaidAsync(App.TwitchBroadcasterId);
IsRaidStarted = false;
}
public void StopRaidCommand()
{
StopRaid();
}
public void OpenChannelCommand(object? arg)
{
var channelName = arg?.ToString();
if (string.IsNullOrEmpty(channelName))
return;
var url = $"https://twitch.tv/{channelName}";
Tools.OpenUrl(url);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
} }

View File

@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BetterRaid.Attributes;
using BetterRaid.Extensions;
using BetterRaid.Models;
using TwitchLib.PubSub;
using TwitchLib.PubSub.Events;
namespace BetterRaid.Services.Implementations;
public class TwitchPubSubService : ITwitchPubSubService
{
private readonly Dictionary<PubSubType, List<PubSubListener>> _targets = new();
private readonly TwitchPubSub _sub;
public TwitchPubSubService()
{
_sub = new TwitchPubSub();
_sub.OnPubSubServiceConnected += OnSubOnOnPubSubServiceConnected;
_sub.OnPubSubServiceError += OnSubOnOnPubSubServiceError;
_sub.OnPubSubServiceClosed += OnSubOnOnPubSubServiceClosed;
_sub.OnListenResponse += OnSubOnOnListenResponse;
_sub.OnViewCount += OnSubOnOnViewCount;
_sub.Connect();
}
private void OnSubOnOnViewCount(object? sender, OnViewCountArgs args)
{
var listeners = _targets
.Where(x => x.Key == PubSubType.VideoPlayback)
.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, args.Viewers) == false)
{
Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}");
}
else
{
Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}");
}
}
catch (Exception e)
{
Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}");
}
}
}
private void OnSubOnOnListenResponse(object? sender, OnListenResponseArgs args)
{
Console.WriteLine($"Listen Response: {args.Topic}");
}
private void OnSubOnOnPubSubServiceClosed(object? sender, EventArgs args)
{
Console.WriteLine("PubSub Closed");
}
private void OnSubOnOnPubSubServiceError(object? sender, OnPubSubServiceErrorArgs args)
{
Console.WriteLine($"PubSub Error: {args.Exception.Message}");
}
private void OnSubOnOnPubSubServiceConnected(object? sender, EventArgs args)
{
Console.WriteLine("Connected to PubSub");
}
public void RegisterReceiver<T>(T receiver) where T : class
{
ArgumentNullException.ThrowIfNull(receiver, nameof(receiver));
Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {receiver.GetType().Name}");
var type = typeof(T);
var publicTargets = type
.GetProperties()
.Concat(
type.GetFields() as MemberInfo[]
);
foreach (var target in publicTargets)
{
if (target.GetCustomAttribute<PubSubAttribute>() is not { } attr)
{
continue;
}
var channelId =
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}");
continue;
}
switch (attr.Type)
{
case PubSubType.Bits:
break;
case PubSubType.ChannelPoints:
break;
case PubSubType.Follows:
break;
case PubSubType.Raids:
break;
case PubSubType.Subscriptions:
break;
case PubSubType.VideoPlayback:
Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {target.Name} for {attr.Type}");
if (_targets.TryGetValue(PubSubType.VideoPlayback, out var value))
{
value.Add(new PubSubListener
{
ChannelId = channelId,
Instance = receiver,
Listener = target
});
}
else
{
_targets.Add(PubSubType.VideoPlayback, [
new PubSubListener
{
ChannelId = channelId,
Instance = receiver,
Listener = target
}
]);
}
_sub.ListenToVideoPlayback(channelId);
_sub.SendTopics(App.TwitchOAuthAccessToken);
break;
}
}
}
public void UnregisterReceiver<T>(T receiver) where T : class
{
ArgumentNullException.ThrowIfNull(receiver, nameof(receiver));
foreach (var target in _targets)
{
var topic = target.Key;
var listener = target.Value.Where(x => x.Instance == receiver).ToList();
foreach (var l in listener)
{
switch (topic)
{
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(App.TwitchOAuthAccessToken, true);
break;
}
}
_targets[topic].RemoveAll(x => x.Instance == receiver);
}
}
}

View File

@ -1,13 +0,0 @@
using System;
using BetterRaid.Services;
namespace BetterRaid.ViewModels;
public class AboutWindowViewModel : ViewModelBase
{
public AboutWindowViewModel(ITwitchDataService s)
{
Console.WriteLine(s);
Console.WriteLine("[DEBUG] AboutWindowViewModel created");
}
}

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using BetterRaid.Extensions; using BetterRaid.Extensions;
using BetterRaid.Misc; using BetterRaid.Misc;
@ -16,6 +18,7 @@ public partial class MainWindowViewModel : ViewModelBase
private string? _filter; private string? _filter;
private ObservableCollection<TwitchChannel> _channels = []; private ObservableCollection<TwitchChannel> _channels = [];
private BetterRaidDatabase? _db; private BetterRaidDatabase? _db;
private readonly ITwitchPubSubService _pubSub;
public BetterRaidDatabase? Database public BetterRaidDatabase? Database
{ {
@ -35,6 +38,8 @@ public partial class MainWindowViewModel : ViewModelBase
set => SetProperty(ref _channels, value); set => SetProperty(ref _channels, value);
} }
public ITwitchDataService DataService { get; }
public string? Filter public string? Filter
{ {
get => _filter; get => _filter;
@ -43,10 +48,12 @@ public partial class MainWindowViewModel : ViewModelBase
public bool IsLoggedIn => App.TwitchApi != null; public bool IsLoggedIn => App.TwitchApi != null;
public MainWindowViewModel(ITwitchDataService t) public MainWindowViewModel(ITwitchPubSubService pubSub, ITwitchDataService dataService)
{ {
Console.WriteLine(t); _pubSub = pubSub;
Console.WriteLine("[DEBUG] MainWindowViewModel created"); DataService = dataService;
Database = BetterRaidDatabase.LoadFromFile(Path.Combine(App.BetterRaidDataPath, "db.json"));
} }
public void ExitApplication() public void ExitApplication()
@ -58,7 +65,6 @@ public partial class MainWindowViewModel : ViewModelBase
public void ShowAboutWindow(Window owner) public void ShowAboutWindow(Window owner)
{ {
var about = new AboutWindow(); var about = new AboutWindow();
about.InjectDataContext<AboutWindowViewModel>();
about.ShowDialog(owner); about.ShowDialog(owner);
about.CenterToOwner(); about.CenterToOwner();
} }
@ -80,15 +86,26 @@ public partial class MainWindowViewModel : ViewModelBase
return; return;
} }
foreach (var channel in Channels)
{
_pubSub.UnregisterReceiver(channel);
}
Channels.Clear(); Channels.Clear();
var channels = _db.Channels var channels = _db.Channels
.Select(channelName => new TwitchChannel(channelName)) .Select(channelName => new TwitchChannel(channelName))
.ToList(); .ToList();
foreach (var c in channels) foreach (var channel in channels)
{ {
Channels.Add(c); Task.Run(() =>
{
channel.InitChannel();
_pubSub.RegisterReceiver(channel);
});
Channels.Add(channel);
} }
} }
} }

View File

@ -5,7 +5,6 @@
xmlns:vm="clr-namespace:BetterRaid.ViewModels" xmlns:vm="clr-namespace:BetterRaid.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="BetterRaid.Views.AboutWindow" x:Class="BetterRaid.Views.AboutWindow"
x:DataType="vm:AboutWindowViewModel"
Title="About" Title="About"
MaxWidth="300" MaxWidth="300"
MinWidth="300" MinWidth="300"

View File

@ -2,10 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:BetterRaid.ViewModels" xmlns:vm="using:BetterRaid.ViewModels"
xmlns:br="using:BetterRaid" xmlns:br="using:BetterRaid"
xmlns:con="using:BetterRaid.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ai="using:AsyncImageLoader" xmlns:ai="using:AsyncImageLoader"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="800" xmlns:misc="clr-namespace:BetterRaid.Misc"
mc:Ignorable="d"
d:DesignWidth="600"
d:DesignHeight="800"
Width="600" Width="600"
Height="800" Height="800"
x:Class="BetterRaid.Views.MainWindow" x:Class="BetterRaid.Views.MainWindow"
@ -14,9 +18,9 @@
Title="BetterRaid" Title="BetterRaid"
Background="DarkSlateGray"> Background="DarkSlateGray">
<Design.DataContext> <Window.Resources>
<vm:MainWindowViewModel/> <con:ChannelOnlineColorConverter x:Key="ChannelOnlineColorConverter" />
</Design.DataContext> </Window.Resources>
<Grid HorizontalAlignment="Stretch" <Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
@ -113,8 +117,8 @@
<ListBox ItemsSource="{Binding Channels}"> <ListBox ItemsSource="{Binding Channels}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid ColumnDefinitions="200,*" <Grid ColumnDefinitions="100, *, 120"
RowDefinitions="200"> RowDefinitions="100">
<ai:AdvancedImage Grid.Column="0" <ai:AdvancedImage Grid.Column="0"
Grid.Row="0" Grid.Row="0"
@ -124,25 +128,33 @@
Grid.Row="0" Grid.Row="0"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Height="20" Height="25"
MinWidth="20" MinWidth="25"
CornerRadius="10" CornerRadius="12.5"
Background="#FFFFFF" Background="{Binding IsLive, Converter={StaticResource ChannelOnlineColorConverter}}"
Padding="3" Padding="0"
Margin="0, 0, 10, 10"> Margin="0, 0, 5, 5">
<TextBlock Text="{Binding ViewerCount, TargetNullValue='-'}"/>
<TextBlock Text="{Binding ViewerCount, TargetNullValue='-', Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
FontSize="12"
TextAlignment="Center"
FontWeight="SemiBold"
Padding="0"
Margin="5"
Foreground="Black"/>
</Border> </Border>
<Grid Grid.Column="1" <Grid Grid.Column="1"
Grid.Row="0" Grid.Row="0"
ColumnDefinitions="30*, 70*" Margin="10, 0, 0, 0"
RowDefinitions="40, 40, 40, 40, 40"> ColumnDefinitions="100, *"
RowDefinitions="20, 20, 40, 20">
<TextBlock Grid.Column="0" <TextBlock Grid.Column="0"
Grid.Row="0" Grid.Row="0"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
FontWeight="Bold" FontWeight="Bold"
TextDecorations="Underline" TextDecorations="Underline"
Text="{Binding DisplayName, TargetNullValue='???'}" /> Text="{Binding Name, TargetNullValue='???'}" />
<TextBlock Grid.Column="0" <TextBlock Grid.Column="0"
Grid.Row="1" Grid.Row="1"
@ -159,27 +171,62 @@
Text="Last Raided:" Text="Last Raided:"
FontWeight="SemiBold" /> FontWeight="SemiBold" />
<TextBlock Grid.Column="0"
Grid.Row="4"
Text=""
FontWeight="SemiBold" />
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
Grid.Row="1" Grid.Row="1"
Text="{Binding Category, TargetNullValue='-'}" /> Text="{Binding Category, TargetNullValue='-'}" />
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
Grid.Row="2" Grid.Row="2"
TextWrapping="Wrap"
Text="{Binding Title, TargetNullValue='-'}" /> Text="{Binding Title, TargetNullValue='-'}" />
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
Grid.Row="3" Grid.Row="3"
Text="{Binding LastRaided, TargetNullValue='Never Raided'}" /> Text="{Binding LastRaided, TargetNullValue='Never Raided'}" />
<TextBlock Grid.Column="1"
Grid.Row="4"
Text="" />
</Grid> </Grid>
<StackPanel Grid.Column="2"
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Button Content="Start Raid"
Height="50"
Margin="0"
CornerRadius="0"
Background="ForestGreen"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
IsEnabled="{Binding IsLive}"
IsVisible="{Binding !$parent[Window].((vm:MainWindowViewModel)DataContext).DataService.IsRaidStarted}"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.StartRaidCommand}"
CommandParameter="{Binding BroadcasterId}" />
<Button Content="Cancel Raid"
Height="50"
Margin="0"
CornerRadius="0"
Background="DarkRed"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
IsVisible="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.IsRaidStarted}"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.StopRaidCommand}" />
<Button Content="View Channel"
Height="50"
CornerRadius="0"
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.OpenChannelCommand}"
CommandParameter="{Binding Name}" />
</StackPanel>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>