Merge branch 'ui-rework' into dispatcher-service
This commit is contained in:
commit
b2252557a6
@ -7,7 +7,6 @@ using Avalonia.Markup.Xaml;
|
|||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using BetterRaid.Extensions;
|
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;
|
||||||
@ -27,8 +26,7 @@ public class App : Application
|
|||||||
private ServiceProvider InitializeServices()
|
private ServiceProvider InitializeServices()
|
||||||
{
|
{
|
||||||
var Services = new ServiceCollection();
|
var Services = new ServiceCollection();
|
||||||
Services.AddSingleton<ITwitchDataService, TwitchDataService>();
|
Services.AddSingleton<ITwitchService, TwitchService>();
|
||||||
Services.AddSingleton<ITwitchPubSubService, TwitchPubSubService>();
|
|
||||||
Services.AddSingleton<ISynchronizaionService, DispatcherService>(serviceProvider => new DispatcherService(Dispatcher.UIThread));
|
Services.AddSingleton<ISynchronizaionService, DispatcherService>(serviceProvider => new DispatcherService(Dispatcher.UIThread));
|
||||||
Services.AddTransient<IMainViewModelFactory, MainWindowViewModelFactory>();
|
Services.AddTransient<IMainViewModelFactory, MainWindowViewModelFactory>();
|
||||||
|
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace BetterRaid.Attributes;
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
|
|
||||||
public class PubSubAttribute : Attribute
|
|
||||||
{
|
|
||||||
public PubSubType Type { get; }
|
|
||||||
public string ChannelIdField { get; set; }
|
|
||||||
|
|
||||||
public PubSubAttribute(PubSubType type, string channelIdField)
|
|
||||||
{
|
|
||||||
Type = type;
|
|
||||||
ChannelIdField = channelIdField;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace BetterRaid.Attributes;
|
|
||||||
|
|
||||||
public enum PubSubType
|
|
||||||
{
|
|
||||||
VideoPlayback,
|
|
||||||
Follows,
|
|
||||||
Subscriptions,
|
|
||||||
ChannelPoints,
|
|
||||||
Bits,
|
|
||||||
Raids,
|
|
||||||
StreamUp,
|
|
||||||
StreamDown
|
|
||||||
}
|
|
@ -15,7 +15,7 @@ public static class Tools
|
|||||||
private static HttpListener? _oauthListener;
|
private static HttpListener? _oauthListener;
|
||||||
|
|
||||||
// Source: https://stackoverflow.com/a/43232486
|
// Source: https://stackoverflow.com/a/43232486
|
||||||
public static void StartOAuthLogin(string url, ITwitchDataService twitchDataService, Action? callback = null, CancellationToken token = default)
|
public static void StartOAuthLogin(ITwitchService twitchService, Action? callback = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (_oauthListener == null)
|
if (_oauthListener == null)
|
||||||
{
|
{
|
||||||
@ -23,13 +23,13 @@ public static class Tools
|
|||||||
_oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/");
|
_oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/");
|
||||||
_oauthListener.Start();
|
_oauthListener.Start();
|
||||||
|
|
||||||
Task.Run(() => WaitForCallback(callback, token, twitchDataService), token);
|
Task.Run(() => WaitForCallback(callback, token, twitchService), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
OpenUrl(url);
|
OpenUrl(twitchService.GetOAuthUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task WaitForCallback(Action? callback, CancellationToken token, ITwitchDataService twitchDataService)
|
private static async Task WaitForCallback(Action? callback, CancellationToken token, ITwitchService twitchService)
|
||||||
{
|
{
|
||||||
if (_oauthListener == null)
|
if (_oauthListener == null)
|
||||||
return;
|
return;
|
||||||
@ -107,7 +107,7 @@ public static class Tools
|
|||||||
|
|
||||||
var accessToken = jsonData["access_token"]?.ToString();
|
var accessToken = jsonData["access_token"]?.ToString();
|
||||||
|
|
||||||
twitchDataService.ConnectApiAsync(Constants.TwitchClientId, accessToken!);
|
twitchService.ConnectApiAsync(Constants.TwitchClientId, accessToken!);
|
||||||
|
|
||||||
res.StatusCode = 200;
|
res.StatusCode = 200;
|
||||||
res.Close();
|
res.Close();
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace BetterRaid.Models;
|
|
||||||
|
|
||||||
public class PubSubListener
|
|
||||||
{
|
|
||||||
public string ChannelId { get; set; }
|
|
||||||
public object? Instance { get; set; }
|
|
||||||
public MemberInfo? Listener { get; set; }
|
|
||||||
}
|
|
@ -2,8 +2,8 @@ using System;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using BetterRaid.Attributes;
|
|
||||||
using BetterRaid.Services;
|
using BetterRaid.Services;
|
||||||
|
using TwitchLib.PubSub.Events;
|
||||||
|
|
||||||
namespace BetterRaid.Models;
|
namespace BetterRaid.Models;
|
||||||
|
|
||||||
@ -45,8 +45,6 @@ public class TwitchChannel : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[PubSub(PubSubType.StreamUp, nameof(BroadcasterId))]
|
|
||||||
[PubSub(PubSubType.StreamDown, nameof(BroadcasterId))]
|
|
||||||
public bool IsLive
|
public bool IsLive
|
||||||
{
|
{
|
||||||
get => _isLive;
|
get => _isLive;
|
||||||
@ -60,7 +58,6 @@ public class TwitchChannel : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[PubSub(PubSubType.VideoPlayback, nameof(BroadcasterId))]
|
|
||||||
public string? ViewerCount
|
public string? ViewerCount
|
||||||
{
|
{
|
||||||
get => _viewerCount;
|
get => _viewerCount;
|
||||||
@ -144,15 +141,15 @@ public class TwitchChannel : INotifyPropertyChanged
|
|||||||
Name = channelName;
|
Name = channelName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateChannelData(ITwitchDataService dataService)
|
public void UpdateChannelData(ITwitchService service)
|
||||||
{
|
{
|
||||||
var channel = dataService.TwitchApi.Helix.Search.SearchChannelsAsync(Name).Result.Channels
|
var channel = service.TwitchApi.Helix.Search.SearchChannelsAsync(Name).Result.Channels
|
||||||
.FirstOrDefault(c => c.BroadcasterLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase));
|
.FirstOrDefault(c => c.BroadcasterLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase));
|
||||||
|
|
||||||
if (channel == null)
|
if (channel == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var stream = dataService.TwitchApi.Helix.Streams.GetStreamsAsync(userLogins: [ Name ]).Result.Streams
|
var stream = service.TwitchApi.Helix.Streams.GetStreamsAsync(userLogins: [ Name ]).Result.Streams
|
||||||
.FirstOrDefault(s => s.UserLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase));
|
.FirstOrDefault(s => s.UserLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase));
|
||||||
|
|
||||||
BroadcasterId = channel.Id;
|
BroadcasterId = channel.Id;
|
||||||
@ -166,6 +163,21 @@ public class TwitchChannel : INotifyPropertyChanged
|
|||||||
: $"{stream.ViewerCount}";
|
: $"{stream.ViewerCount}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnStreamUp(object? sender, OnStreamUpArgs args)
|
||||||
|
{
|
||||||
|
IsLive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnStreamDown(object? sender, OnStreamDownArgs e)
|
||||||
|
{
|
||||||
|
IsLive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnViewCount(object? sender, OnViewCountArgs e)
|
||||||
|
{
|
||||||
|
ViewerCount = $"{e.Viewers}";
|
||||||
|
}
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BetterRaid.Models;
|
|
||||||
using TwitchLib.Api;
|
|
||||||
|
|
||||||
namespace BetterRaid.Services;
|
|
||||||
|
|
||||||
public interface ITwitchDataService
|
|
||||||
{
|
|
||||||
public string? AccessToken { get; set; }
|
|
||||||
public TwitchChannel? UserChannel { get; set; }
|
|
||||||
public TwitchAPI TwitchApi { get; }
|
|
||||||
public bool IsRaidStarted { get; set; }
|
|
||||||
|
|
||||||
public Task ConnectApiAsync(string clientId, string accessToken);
|
|
||||||
public void SaveAccessToken(string token);
|
|
||||||
public bool TryGetUserChannel(out TwitchChannel? userChannel);
|
|
||||||
public string GetOAuthUrl();
|
|
||||||
public void StartRaid(string from, string to);
|
|
||||||
public bool CanStartRaidCommand(object? arg);
|
|
||||||
public void StartRaidCommand(object? arg);
|
|
||||||
public void StopRaid();
|
|
||||||
public void StopRaidCommand();
|
|
||||||
public void OpenChannelCommand(object? arg);
|
|
||||||
|
|
||||||
public event PropertyChangingEventHandler? PropertyChanging;
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
namespace BetterRaid.Services;
|
|
||||||
|
|
||||||
public interface ITwitchPubSubService
|
|
||||||
{
|
|
||||||
void RegisterReceiver<T>(T receiver) where T : class;
|
|
||||||
void UnregisterReceiver<T>(T receiver) where T : class;
|
|
||||||
}
|
|
@ -1,206 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BetterRaid.Misc;
|
|
||||||
using BetterRaid.Models;
|
|
||||||
using TwitchLib.Api;
|
|
||||||
|
|
||||||
namespace BetterRaid.Services.Implementations;
|
|
||||||
|
|
||||||
public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INotifyPropertyChanging
|
|
||||||
{
|
|
||||||
private bool _isRaidStarted;
|
|
||||||
private TwitchChannel? _userChannel;
|
|
||||||
|
|
||||||
public string AccessToken { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public bool IsRaidStarted
|
|
||||||
{
|
|
||||||
get => _isRaidStarted;
|
|
||||||
set => SetField(ref _isRaidStarted, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TwitchChannel? UserChannel
|
|
||||||
{
|
|
||||||
get => _userChannel;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_userChannel != null && _userChannel.Name?.Equals(value?.Name) == true)
|
|
||||||
return;
|
|
||||||
|
|
||||||
SetField(ref _userChannel, value);
|
|
||||||
|
|
||||||
_userChannel?.UpdateChannelData(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public TwitchAPI TwitchApi { get; }
|
|
||||||
|
|
||||||
public TwitchDataService()
|
|
||||||
{
|
|
||||||
TwitchApi = new TwitchAPI();
|
|
||||||
|
|
||||||
if (TryLoadAccessToken(out var token))
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Found access token.");
|
|
||||||
Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] No access token found.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ConnectApiAsync(string clientId, string accessToken)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Connecting to Twitch API ...");
|
|
||||||
|
|
||||||
AccessToken = accessToken;
|
|
||||||
|
|
||||||
TwitchApi.Settings.ClientId = clientId;
|
|
||||||
TwitchApi.Settings.AccessToken = accessToken;
|
|
||||||
|
|
||||||
if (TryGetUserChannel(out var channel))
|
|
||||||
{
|
|
||||||
UserChannel = channel;
|
|
||||||
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Connected to Twitch API as {channel?.Name}.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UserChannel = null;
|
|
||||||
|
|
||||||
Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] Could not get user channel.");
|
|
||||||
Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] Failed to connect to Twitch API.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryLoadAccessToken(out string token)
|
|
||||||
{
|
|
||||||
token = string.Empty;
|
|
||||||
|
|
||||||
if (!File.Exists(Constants.TwitchOAuthAccessTokenFilePath))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
token = File.ReadAllText(Constants.TwitchOAuthAccessTokenFilePath);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SaveAccessToken(string token)
|
|
||||||
{
|
|
||||||
File.WriteAllText(Constants.TwitchOAuthAccessTokenFilePath, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetUserChannel(out TwitchChannel? userChannel)
|
|
||||||
{
|
|
||||||
userChannel = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var user = TwitchApi.Helix.Users.GetUsersAsync().Result.Users[0];
|
|
||||||
userChannel = new TwitchChannel(user.Login);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] {e.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetOAuthUrl()
|
|
||||||
{
|
|
||||||
var scopes = string.Join("+", Constants.TwitchOAuthScopes);
|
|
||||||
|
|
||||||
return $"https://id.twitch.tv/oauth2/authorize"
|
|
||||||
+ $"?client_id={Constants.TwitchClientId}"
|
|
||||||
+ $"&redirect_uri={Constants.TwitchOAuthRedirectUrl}"
|
|
||||||
+ $"&response_type={Constants.TwitchOAuthResponseType}"
|
|
||||||
+ $"&scope={scopes}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartRaid(string from, string to)
|
|
||||||
{
|
|
||||||
// TODO: Also check, if the logged in user is live
|
|
||||||
|
|
||||||
TwitchApi.Helix.Raids.StartRaidAsync(from, to);
|
|
||||||
IsRaidStarted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanStartRaidCommand(object? arg)
|
|
||||||
{
|
|
||||||
return UserChannel?.IsLive == true && IsRaidStarted == false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartRaidCommand(object? arg)
|
|
||||||
{
|
|
||||||
if (arg == null || UserChannel?.BroadcasterId == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var from = UserChannel.BroadcasterId!;
|
|
||||||
var to = arg.ToString()!;
|
|
||||||
|
|
||||||
StartRaid(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopRaid()
|
|
||||||
{
|
|
||||||
if (UserChannel?.BroadcasterId == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (IsRaidStarted == false)
|
|
||||||
return;
|
|
||||||
|
|
||||||
TwitchApi.Helix.Raids.CancelRaidAsync(UserChannel.BroadcasterId);
|
|
||||||
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 PropertyChangingEventHandler? PropertyChanging;
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null)
|
|
||||||
{
|
|
||||||
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
|
||||||
{
|
|
||||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
OnPropertyChanging(propertyName);
|
|
||||||
field = value;
|
|
||||||
OnPropertyChanged(propertyName);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,291 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
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 ITwitchDataService _dataService;
|
|
||||||
private TwitchPubSub? _sub;
|
|
||||||
|
|
||||||
public TwitchPubSubService(ITwitchDataService dataService)
|
|
||||||
{
|
|
||||||
_dataService = dataService;
|
|
||||||
|
|
||||||
Task.Run(InitializePubSubAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InitializePubSubAsync()
|
|
||||||
{
|
|
||||||
while (_dataService.UserChannel == null)
|
|
||||||
{
|
|
||||||
await Task.Delay(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sub = new TwitchPubSub();
|
|
||||||
|
|
||||||
_sub.OnPubSubServiceConnected += OnSubOnOnPubSubServiceConnected;
|
|
||||||
_sub.OnPubSubServiceError += OnSubOnOnPubSubServiceError;
|
|
||||||
_sub.OnPubSubServiceClosed += OnSubOnOnPubSubServiceClosed;
|
|
||||||
_sub.OnListenResponse += OnSubOnOnListenResponse;
|
|
||||||
_sub.OnViewCount += OnSubOnOnViewCount;
|
|
||||||
_sub.OnStreamUp += OnStreamUp;
|
|
||||||
_sub.OnStreamDown += OnStreamDown;
|
|
||||||
|
|
||||||
_sub.Connect();
|
|
||||||
|
|
||||||
if (_dataService.UserChannel != null)
|
|
||||||
{
|
|
||||||
RegisterReceiver(_dataService.UserChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
_dataService.PropertyChanging += (_, args) =>
|
|
||||||
{
|
|
||||||
if (args.PropertyName != nameof(_dataService.UserChannel))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_dataService.UserChannel != null)
|
|
||||||
UnregisterReceiver(_dataService.UserChannel);
|
|
||||||
};
|
|
||||||
|
|
||||||
_dataService.PropertyChanged += (_, args) =>
|
|
||||||
{
|
|
||||||
if (args.PropertyName != nameof(_dataService.UserChannel))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_dataService.UserChannel != null)
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
|
|
||||||
if (_sub == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] PubSub is not initialized");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.GetCustomAttributes<PubSubAttribute>() is not { } attrs)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var attr in attrs)
|
|
||||||
{
|
|
||||||
var channelId =
|
|
||||||
type.GetProperty(attr.ChannelIdField)?.GetValue(receiver)?.ToString() ??
|
|
||||||
type.GetField(attr.ChannelIdField)?.GetValue(receiver)?.ToString();
|
|
||||||
|
|
||||||
if (channelId == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine(
|
|
||||||
$"[ERROR][{nameof(TwitchPubSubService)}] {target.Name} is missing ChannelIdField named {attr.ChannelIdField}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(channelId))
|
|
||||||
{
|
|
||||||
Console.WriteLine(
|
|
||||||
$"[ERROR][{nameof(TwitchPubSubService)}] {target.Name} ChannelIdField named {attr.ChannelIdField} is empty");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {target.Name} for {attr.Type}");
|
|
||||||
if (_targets.TryGetValue(attr.Type, out var listeners))
|
|
||||||
{
|
|
||||||
listeners.Add(new PubSubListener
|
|
||||||
{
|
|
||||||
ChannelId = channelId,
|
|
||||||
Instance = receiver,
|
|
||||||
Listener = target
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_targets.Add(attr.Type, [
|
|
||||||
new PubSubListener
|
|
||||||
{
|
|
||||||
ChannelId = channelId,
|
|
||||||
Instance = receiver,
|
|
||||||
Listener = target
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sub.ListenToVideoPlayback(channelId);
|
|
||||||
_sub.SendTopics(_dataService.AccessToken, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnregisterReceiver<T>(T receiver) where T : class
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(receiver, nameof(receiver));
|
|
||||||
|
|
||||||
if (_sub == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] PubSub is not initialized");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (topic, listeners) in _targets)
|
|
||||||
{
|
|
||||||
var listener = listeners.Where(x => x.Instance == receiver).ToList();
|
|
||||||
|
|
||||||
foreach (var l in listener)
|
|
||||||
{
|
|
||||||
_sub.ListenToVideoPlayback(l.ChannelId);
|
|
||||||
_sub.SendTopics(_dataService.AccessToken, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
_targets[topic].RemoveAll(x => x.Instance == receiver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
349
Services/TwitchService.cs
Normal file
349
Services/TwitchService.cs
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BetterRaid.Misc;
|
||||||
|
using BetterRaid.Models;
|
||||||
|
using TwitchLib.Api;
|
||||||
|
using TwitchLib.Api.Helix.Models.Users.GetUsers;
|
||||||
|
using TwitchLib.PubSub;
|
||||||
|
using TwitchLib.PubSub.Events;
|
||||||
|
using OnEmoteOnlyArgs = TwitchLib.PubSub.Events.OnEmoteOnlyArgs;
|
||||||
|
using OnLogArgs = TwitchLib.PubSub.Events.OnLogArgs;
|
||||||
|
|
||||||
|
namespace BetterRaid.Services;
|
||||||
|
|
||||||
|
public interface ITwitchService
|
||||||
|
{
|
||||||
|
public string? AccessToken { get; }
|
||||||
|
public TwitchChannel? UserChannel { get; set; }
|
||||||
|
public TwitchAPI TwitchApi { get; }
|
||||||
|
public bool IsRaidStarted { get; set; }
|
||||||
|
|
||||||
|
public Task ConnectApiAsync(string clientId, string accessToken);
|
||||||
|
public string GetOAuthUrl();
|
||||||
|
public void StartRaid(string from, string to);
|
||||||
|
public bool CanStartRaidCommand(object? arg);
|
||||||
|
public void StartRaidCommand(object? arg);
|
||||||
|
public void StopRaid();
|
||||||
|
public void StopRaidCommand();
|
||||||
|
public void OpenChannelCommand(object? arg);
|
||||||
|
public void RegisterForEvents(TwitchChannel channel);
|
||||||
|
public void UnregisterFromEvents(TwitchChannel channel);
|
||||||
|
|
||||||
|
public event PropertyChangingEventHandler? PropertyChanging;
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INotifyPropertyChanging
|
||||||
|
{
|
||||||
|
private bool _isRaidStarted;
|
||||||
|
private TwitchChannel? _userChannel;
|
||||||
|
private readonly List<TwitchChannel> _registeredChannels;
|
||||||
|
private User? _user;
|
||||||
|
|
||||||
|
public string AccessToken { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool IsRaidStarted
|
||||||
|
{
|
||||||
|
get => _isRaidStarted;
|
||||||
|
set => SetField(ref _isRaidStarted, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public User? User
|
||||||
|
{
|
||||||
|
get => _user;
|
||||||
|
set => SetField(ref _user, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TwitchChannel? UserChannel
|
||||||
|
{
|
||||||
|
get => _userChannel;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_userChannel != null && _userChannel.Name?.Equals(value?.Name) == true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SetField(ref _userChannel, value);
|
||||||
|
|
||||||
|
_userChannel?.UpdateChannelData(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TwitchAPI TwitchApi { get; }
|
||||||
|
public TwitchPubSub TwitchEvents { get; }
|
||||||
|
|
||||||
|
public TwitchService()
|
||||||
|
{
|
||||||
|
_registeredChannels = [];
|
||||||
|
|
||||||
|
TwitchApi = new TwitchAPI();
|
||||||
|
TwitchEvents = new TwitchPubSub();
|
||||||
|
|
||||||
|
if (TryLoadAccessToken(out var token))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Found access token.");
|
||||||
|
Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token))
|
||||||
|
.ContinueWith(_ => ConnectTwitchEvents(token));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[INFO][{nameof(TwitchService)}] No access token found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConnectTwitchEvents(string token)
|
||||||
|
{
|
||||||
|
if (UserChannel == null || User == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connecting to Twitch Events ...");
|
||||||
|
|
||||||
|
TwitchEvents.OnRaidGo += OnUserRaidGo;
|
||||||
|
TwitchEvents.OnRaidUpdateV2 += OnUserRaidUpdate;
|
||||||
|
TwitchEvents.OnStreamUp += OnUserStreamUp;
|
||||||
|
TwitchEvents.OnStreamDown += OnUserStreamDown;
|
||||||
|
|
||||||
|
TwitchEvents.ListenToRaid(UserChannel.BroadcasterId);
|
||||||
|
TwitchEvents.ListenToVideoPlayback(UserChannel.BroadcasterId);
|
||||||
|
|
||||||
|
TwitchEvents.SendTopics(token);
|
||||||
|
TwitchEvents.Connect();
|
||||||
|
|
||||||
|
RegisterForEvents(UserChannel);
|
||||||
|
|
||||||
|
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connected to Twitch Events.");
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ConnectApiAsync(string clientId, string accessToken)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connecting to Twitch API ...");
|
||||||
|
|
||||||
|
AccessToken = accessToken;
|
||||||
|
|
||||||
|
TwitchApi.Settings.ClientId = clientId;
|
||||||
|
TwitchApi.Settings.AccessToken = accessToken;
|
||||||
|
|
||||||
|
if (TryGetUser(out var user))
|
||||||
|
{
|
||||||
|
User = user;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
User = null;
|
||||||
|
|
||||||
|
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not get user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetUserChannel(out var channel))
|
||||||
|
{
|
||||||
|
UserChannel = channel;
|
||||||
|
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connected to Twitch API as {channel?.Name}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UserChannel = null;
|
||||||
|
|
||||||
|
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not get user channel.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (User == null || UserChannel == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not connect to Twitch API.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryLoadAccessToken(out string token)
|
||||||
|
{
|
||||||
|
token = string.Empty;
|
||||||
|
|
||||||
|
if (!File.Exists(Constants.TwitchOAuthAccessTokenFilePath))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
token = File.ReadAllText(Constants.TwitchOAuthAccessTokenFilePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveAccessToken(string token)
|
||||||
|
{
|
||||||
|
File.WriteAllText(Constants.TwitchOAuthAccessTokenFilePath, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetUser(out User? user)
|
||||||
|
{
|
||||||
|
user = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userResult = TwitchApi.Helix.Users.GetUsersAsync().Result.Users[0];
|
||||||
|
|
||||||
|
if (userResult == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
user = userResult;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] {e.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetUserChannel(out TwitchChannel? channel)
|
||||||
|
{
|
||||||
|
channel = null;
|
||||||
|
|
||||||
|
if (User == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
channel = new TwitchChannel(User.Login);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterForEvents(TwitchChannel channel)
|
||||||
|
{
|
||||||
|
TwitchEvents.OnStreamUp += channel.OnStreamUp;
|
||||||
|
TwitchEvents.OnStreamDown += channel.OnStreamDown;
|
||||||
|
TwitchEvents.OnViewCount += channel.OnViewCount;
|
||||||
|
|
||||||
|
TwitchEvents.ListenToVideoPlayback(channel.BroadcasterId);
|
||||||
|
|
||||||
|
TwitchEvents.SendTopics(AccessToken);
|
||||||
|
|
||||||
|
_registeredChannels.Add(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnregisterFromEvents(TwitchChannel channel)
|
||||||
|
{
|
||||||
|
TwitchEvents.OnStreamUp -= channel.OnStreamUp;
|
||||||
|
TwitchEvents.OnStreamDown -= channel.OnStreamDown;
|
||||||
|
TwitchEvents.OnViewCount -= channel.OnViewCount;
|
||||||
|
|
||||||
|
TwitchEvents.ListenToVideoPlayback(channel.BroadcasterId);
|
||||||
|
|
||||||
|
TwitchEvents.SendTopics(AccessToken, true);
|
||||||
|
|
||||||
|
_registeredChannels.Remove(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetOAuthUrl()
|
||||||
|
{
|
||||||
|
var scopes = string.Join("+", Constants.TwitchOAuthScopes);
|
||||||
|
|
||||||
|
return $"https://id.twitch.tv/oauth2/authorize"
|
||||||
|
+ $"?client_id={Constants.TwitchClientId}"
|
||||||
|
+ $"&redirect_uri={Constants.TwitchOAuthRedirectUrl}"
|
||||||
|
+ $"&response_type={Constants.TwitchOAuthResponseType}"
|
||||||
|
+ $"&scope={scopes}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartRaid(string from, string to)
|
||||||
|
{
|
||||||
|
// TODO: Also check, if the logged in user is live
|
||||||
|
|
||||||
|
TwitchApi.Helix.Raids.StartRaidAsync(from, to);
|
||||||
|
IsRaidStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanStartRaidCommand(object? arg)
|
||||||
|
{
|
||||||
|
return UserChannel?.IsLive == true && IsRaidStarted == false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartRaidCommand(object? arg)
|
||||||
|
{
|
||||||
|
if (arg == null || UserChannel?.BroadcasterId == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var from = UserChannel.BroadcasterId!;
|
||||||
|
var to = arg.ToString()!;
|
||||||
|
|
||||||
|
StartRaid(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopRaid()
|
||||||
|
{
|
||||||
|
if (UserChannel?.BroadcasterId == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsRaidStarted == false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TwitchApi.Helix.Raids.CancelRaidAsync(UserChannel.BroadcasterId);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserRaidUpdate(object? sender, OnRaidUpdateV2Args e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserRaidGo(object? sender, OnRaidGoArgs e)
|
||||||
|
{
|
||||||
|
IsRaidStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserStreamDown(object? sender, OnStreamDownArgs e)
|
||||||
|
{
|
||||||
|
IsRaidStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserStreamUp(object? sender, OnStreamUpArgs e)
|
||||||
|
{
|
||||||
|
IsRaidStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangingEventHandler? PropertyChanging;
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPropertyChanging([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
OnPropertyChanging(propertyName);
|
||||||
|
field = value;
|
||||||
|
OnPropertyChanged(propertyName);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -18,7 +18,6 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
private string? _filter;
|
private string? _filter;
|
||||||
private ObservableCollection<TwitchChannel> _channels = [];
|
private ObservableCollection<TwitchChannel> _channels = [];
|
||||||
private readonly BetterRaidDatabase? _db;
|
private readonly BetterRaidDatabase? _db;
|
||||||
private readonly ITwitchPubSubService _pubSub;
|
|
||||||
private readonly ISynchronizaionService _synchronizaionService;
|
private readonly ISynchronizaionService _synchronizaionService;
|
||||||
|
|
||||||
public BetterRaidDatabase? Database
|
public BetterRaidDatabase? Database
|
||||||
@ -41,7 +40,7 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
public ObservableCollection<TwitchChannel> FilteredChannels => GetFilteredChannels();
|
public ObservableCollection<TwitchChannel> FilteredChannels => GetFilteredChannels();
|
||||||
|
|
||||||
public ITwitchDataService DataService { get; }
|
public ITwitchService Twitch { get; }
|
||||||
|
|
||||||
public string? Filter
|
public string? Filter
|
||||||
{
|
{
|
||||||
@ -49,14 +48,14 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
set => SetProperty(ref _filter, value);
|
set => SetProperty(ref _filter, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsLoggedIn => DataService.UserChannel != null;
|
public bool IsLoggedIn => Twitch.UserChannel != null;
|
||||||
|
|
||||||
public MainWindowViewModel(ITwitchPubSubService pubSub, ITwitchDataService dataService, ISynchronizaionService synchronizaionService)
|
public MainWindowViewModel(ITwitchService twitch, ISynchronizaionService synchronizaionService)
|
||||||
{
|
{
|
||||||
_pubSub = pubSub;
|
|
||||||
DataService = dataService;
|
|
||||||
_synchronizaionService = synchronizaionService;
|
_synchronizaionService = synchronizaionService;
|
||||||
DataService.PropertyChanged += OnDataServicePropertyChanged;
|
|
||||||
|
Twitch = twitch;
|
||||||
|
Twitch.PropertyChanged += OnTwitchPropertyChanged;
|
||||||
|
|
||||||
Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath);
|
Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath);
|
||||||
Database.PropertyChanged += OnDatabasePropertyChanged;
|
Database.PropertyChanged += OnDatabasePropertyChanged;
|
||||||
@ -77,7 +76,7 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
public void LoginWithTwitch()
|
public void LoginWithTwitch()
|
||||||
{
|
{
|
||||||
Tools.StartOAuthLogin(DataService.GetOAuthUrl(), DataService, OnTwitchLoginCallback, CancellationToken.None);
|
Tools.StartOAuthLogin(Twitch, OnTwitchLoginCallback, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTwitchLoginCallback()
|
private void OnTwitchLoginCallback()
|
||||||
@ -94,7 +93,7 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
foreach (var channel in Channels)
|
foreach (var channel in Channels)
|
||||||
{
|
{
|
||||||
_pubSub.UnregisterReceiver(channel);
|
Twitch.UnregisterFromEvents(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
Channels.Clear();
|
Channels.Clear();
|
||||||
@ -107,8 +106,8 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
channel.UpdateChannelData(DataService);
|
channel.UpdateChannelData(Twitch);
|
||||||
_pubSub.RegisterReceiver(channel);
|
Twitch.RegisterForEvents(channel);
|
||||||
});
|
});
|
||||||
|
|
||||||
Channels.Add(channel);
|
Channels.Add(channel);
|
||||||
@ -125,9 +124,9 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
return new ObservableCollection<TwitchChannel>(filteredChannels);
|
return new ObservableCollection<TwitchChannel>(filteredChannels);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDataServicePropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void OnTwitchPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.PropertyName != nameof(DataService.UserChannel))
|
if (e.PropertyName != nameof(Twitch.UserChannel))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
OnPropertyChanged(nameof(IsLoggedIn));
|
OnPropertyChanged(nameof(IsLoggedIn));
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
Width="40"
|
Width="40"
|
||||||
Height="40"
|
Height="40"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Source="{Binding DataService.UserChannel.ThumbnailUrl,
|
Source="{Binding Twitch.UserChannel.ThumbnailUrl,
|
||||||
FallbackValue={x:Static misc:Constants.ChannelPlaceholderImageUrl},
|
FallbackValue={x:Static misc:Constants.ChannelPlaceholderImageUrl},
|
||||||
TargetNullValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}}" />
|
TargetNullValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}}" />
|
||||||
|
|
||||||
@ -42,9 +42,9 @@
|
|||||||
FontWeight="Bold">
|
FontWeight="Bold">
|
||||||
<TextBlock.Text>
|
<TextBlock.Text>
|
||||||
<MultiBinding StringFormat="{}{0} ({1})">
|
<MultiBinding StringFormat="{}{0} ({1})">
|
||||||
<Binding Path="DataService.UserChannel.DisplayName"
|
<Binding Path="Twitch.UserChannel.DisplayName"
|
||||||
FallbackValue="-" />
|
FallbackValue="-" />
|
||||||
<Binding Path="DataService.UserChannel.ViewerCount"
|
<Binding Path="Twitch.UserChannel.ViewerCount"
|
||||||
FallbackValue="Offline"
|
FallbackValue="Offline"
|
||||||
TargetNullValue="Offline" />
|
TargetNullValue="Offline" />
|
||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
@ -210,8 +210,8 @@
|
|||||||
HorizontalContentAlignment="Center"
|
HorizontalContentAlignment="Center"
|
||||||
VerticalContentAlignment="Center"
|
VerticalContentAlignment="Center"
|
||||||
IsEnabled="{Binding IsLive}"
|
IsEnabled="{Binding IsLive}"
|
||||||
IsVisible="{Binding !$parent[Window].((vm:MainWindowViewModel)DataContext).DataService.IsRaidStarted}"
|
IsVisible="{Binding !$parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.IsRaidStarted}"
|
||||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.StartRaidCommand}"
|
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.StartRaidCommand}"
|
||||||
CommandParameter="{Binding BroadcasterId}" />
|
CommandParameter="{Binding BroadcasterId}" />
|
||||||
|
|
||||||
<Button Content="Cancel Raid"
|
<Button Content="Cancel Raid"
|
||||||
@ -223,8 +223,8 @@
|
|||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Center"
|
HorizontalContentAlignment="Center"
|
||||||
VerticalContentAlignment="Center"
|
VerticalContentAlignment="Center"
|
||||||
IsVisible="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.IsRaidStarted}"
|
IsVisible="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.IsRaidStarted}"
|
||||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.StopRaidCommand}" />
|
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.StopRaidCommand}" />
|
||||||
|
|
||||||
<Button Content="View Channel"
|
<Button Content="View Channel"
|
||||||
Height="50"
|
Height="50"
|
||||||
@ -234,7 +234,7 @@
|
|||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Center"
|
HorizontalContentAlignment="Center"
|
||||||
VerticalContentAlignment="Center"
|
VerticalContentAlignment="Center"
|
||||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.OpenChannelCommand}"
|
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.OpenChannelCommand}"
|
||||||
CommandParameter="{Binding Name}" />
|
CommandParameter="{Binding Name}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
Reference in New Issue
Block a user