A 'little' code cleanup

This commit is contained in:
Enrico Ludwig 2024-09-04 00:10:18 +02:00
parent a8a481d4f9
commit 8a10573fbf
16 changed files with 232 additions and 511 deletions

View File

@ -1,6 +1,4 @@
using System; using System;
using System.IO;
using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
@ -11,180 +9,30 @@ 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.PubSub;
namespace BetterRaid; namespace BetterRaid;
public partial class App : Application public partial class App : Application
{ {
private readonly ServiceCollection _services = []; private static readonly ServiceCollection Services = [];
private ServiceProvider? _provider; private static ServiceProvider? _serviceProvider;
private static TwitchAPI? _twitchApi; public static IServiceProvider? ServiceProvider => _serviceProvider;
private static bool _hasUserZnSubbed;
private static string _betterRaidDataPath = "";
private static string _twitchBroadcasterId = "";
private static string _twitchOAuthAccessToken = "";
private static string _twitchOAuthAccessTokenFilePath = "";
private const string TokenClientId = "kkxu4jorjrrc5jch1ito5i61hbev2o";
private const string TwitchOAuthRedirectUrl = "http://localhost:9900";
private const string TwitchOAuthResponseType = "token";
private static readonly string[] TwitchOAuthScopes = [
"channel:manage:raids",
"user:read:subscriptions"
];
internal static readonly string TwitchOAuthUrl = $"https://id.twitch.tv/oauth2/authorize"
+ $"?client_id={TokenClientId}"
+ $"&redirect_uri={TwitchOAuthRedirectUrl}"
+ $"&response_type={TwitchOAuthResponseType}"
+ $"&scope={string.Join("+", TwitchOAuthScopes)}";
public const string ChannelPlaceholderImageUrl = "https://cdn.pixabay.com/photo/2018/11/13/22/01/avatar-3814081_1280.png";
public static TwitchAPI? TwitchApi => _twitchApi;
public static bool HasUserZnSubbed => _hasUserZnSubbed;
public static string BetterRaidDataPath => _betterRaidDataPath;
public IServiceProvider? Provider => _provider;
public static string? TwitchBroadcasterId => _twitchBroadcasterId;
public static string TwitchOAuthAccessToken
{
get => _twitchOAuthAccessToken;
set
{
_twitchOAuthAccessToken = value;
InitTwitchClient(true);
}
}
public override void Initialize() public override void Initialize()
{ {
LoadTwitchToken();
InitializeServices(); InitializeServices();
AvaloniaXamlLoader.Load(_provider, this); AvaloniaXamlLoader.Load(_serviceProvider, this);
}
private void LoadTwitchToken()
{
var userHomeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
_betterRaidDataPath = Environment.OSVersion.Platform switch
{
PlatformID.Win32NT => Path.Combine(userHomeDir, "AppData", "Roaming", "BetterRaid"),
PlatformID.Unix => Path.Combine(userHomeDir, ".config", "BetterRaid"),
PlatformID.MacOSX => Path.Combine(userHomeDir, "Library", "Application Support", "BetterRaid"),
_ => throw new PlatformNotSupportedException($"Your platform '{Environment.OSVersion.Platform}' is not supported. Please report this issue here: https://www.github.com/zion-networks/BetterRaid/issues")
};
if (!Directory.Exists(_betterRaidDataPath))
{
var di = Directory.CreateDirectory(_betterRaidDataPath);
if (di.Exists == false)
{
throw new Exception($"Failed to create directory '{_betterRaidDataPath}'");
}
}
_twitchOAuthAccessTokenFilePath = Path.Combine(_betterRaidDataPath, ".access_token");
if (!File.Exists(_twitchOAuthAccessTokenFilePath))
return;
_twitchOAuthAccessToken = File.ReadAllText(_twitchOAuthAccessTokenFilePath);
InitTwitchClient();
} }
private void InitializeServices() private void InitializeServices()
{ {
_services.AddSingleton<ITwitchDataService, TwitchDataService>(); Services.AddSingleton<ITwitchDataService, TwitchDataService>();
_services.AddSingleton<ITwitchPubSubService, TwitchPubSubService>(); Services.AddSingleton<ITwitchPubSubService, TwitchPubSubService>();
_services.AddTransient<MainWindowViewModel>(); Services.AddTransient<MainWindowViewModel>();
_provider = _services.BuildServiceProvider(); _serviceProvider = Services.BuildServiceProvider();
}
public static void InitTwitchClient(bool overrideToken = false)
{
Console.WriteLine("[INFO] Initializing Twitch Client...");
if (string.IsNullOrEmpty(_twitchOAuthAccessToken))
{
Console.WriteLine("[ERROR] Failed to initialize Twitch Client: Access Token is empty!");
return;
}
_twitchApi = new TwitchAPI
{
Settings =
{
ClientId = TokenClientId,
AccessToken = _twitchOAuthAccessToken
}
};
Console.WriteLine("[INFO] Testing Twitch API connection...");
var user = _twitchApi.Helix.Users.GetUsersAsync().Result.Users.FirstOrDefault();
if (user == null)
{
_twitchApi = null;
Console.WriteLine("[ERROR] Failed to connect to Twitch API!");
return;
}
var channel = _twitchApi.Helix.Search
.SearchChannelsAsync(user.Login).Result.Channels
.FirstOrDefault(c => c.BroadcasterLogin == user.Login);
var userSubs = _twitchApi.Helix.Subscriptions.CheckUserSubscriptionAsync(
userId: user.Id,
broadcasterId: "1120558409"
).Result.Data;
if (userSubs is { Length: > 0 } && userSubs.Any(s => s.BroadcasterId == "1120558409"))
{
_hasUserZnSubbed = true;
}
if (channel == null)
{
Console.WriteLine($"[ERROR] Failed to get channel information for '{user.Login}'!");
return;
}
_twitchBroadcasterId = channel.Id;
Console.WriteLine("[INFO] Connected to Twitch API as '{0}'!", user.DisplayName);
if (!overrideToken)
return;
File.WriteAllText(_twitchOAuthAccessTokenFilePath, _twitchOAuthAccessToken);
switch (Environment.OSVersion.Platform)
{
case PlatformID.Win32NT:
File.SetAttributes(_twitchOAuthAccessTokenFilePath, File.GetAttributes(_twitchOAuthAccessTokenFilePath) | FileAttributes.Hidden);
break;
case PlatformID.Unix:
#pragma warning disable CA1416 // Validate platform compatibility
File.SetUnixFileMode(_twitchOAuthAccessTokenFilePath, UnixFileMode.UserRead);
#pragma warning restore CA1416 // Validate platform compatibility
break;
case PlatformID.MacOSX:
File.SetAttributes(_twitchOAuthAccessTokenFilePath, File.GetAttributes(_twitchOAuthAccessTokenFilePath) | FileAttributes.Hidden);
break;
default:
throw new PlatformNotSupportedException($"Your platform '{Environment.OSVersion.Platform}' is not supported. Please report this issue here: https://www.github.com/zion-networks/BetterRaid/issues");
}
} }
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()

View File

@ -1,95 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ai="using:AsyncImageLoader"
xmlns:vm="using:BetterRaid.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="BetterRaid.RaidButton"
x:DataType="vm:RaidButtonViewModel"
Margin="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Design.DataContext>
<vm:RaidButtonViewModel />
</Design.DataContext>
<Button HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Command="{Binding RaidChannel}">
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="75*" />
<RowDefinition Height="25*" />
</Grid.RowDefinitions>
<ai:AdvancedImage Grid.Column="0"
Grid.Row="0"
Source="{Binding Channel.ThumbnailUrl}" />
<Border IsVisible="{Binding IsAd}"
BorderThickness="1"
BorderBrush="DarkGoldenrod"
CornerRadius="4"
Width="24"
Height="16"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="5">
<TextBlock Text="Ad"
Margin="2"
FontSize="12"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextAlignment="Center"
Foreground="DarkGoldenrod" />
</Border>
<Button Width="32"
Height="32"
Background="DarkRed"
CornerRadius="16"
Padding="0"
BorderThickness="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="5"
IsVisible="{Binding !HideDeleteButton}"
Command="{Binding RemoveChannel}">
<Image Source="avares://BetterRaid/Assets/icons8-close-32.png"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Button>
<StackPanel Grid.Column="0"
Grid.Row="1"
Orientation="Vertical">
<Label HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="{Binding Channel.DisplayName, FallbackValue=...}" />
<Label HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="{Binding Channel.Category, TargetNullValue=-, FallbackValue=...}" />
<Label HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Foreground="{Binding ViewerCountColor}"
Content="{Binding Channel.ViewerCount, TargetNullValue=(Offline), FallbackValue=...}" />
<Label HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="{Binding LastRaided, TargetNullValue=Never Raided, FallbackValue=...}" />
</StackPanel>
</Grid>
</Button>
</UserControl>

View File

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

View File

@ -1,5 +1,4 @@
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace BetterRaid.Extensions; namespace BetterRaid.Extensions;
@ -13,10 +12,10 @@ public static class DataContextExtensions
public static void InjectDataContext<T>(this StyledElement e) where T : class public static void InjectDataContext<T>(this StyledElement e) where T : class
{ {
if (Application.Current is not App { Provider: not null } app) if (App.ServiceProvider == null)
return; return;
var vm = app.Provider.GetRequiredService<T>(); var vm = App.ServiceProvider.GetRequiredService<T>();
e.DataContext = vm; e.DataContext = vm;
} }
} }

30
Misc/Constants.cs Normal file
View File

@ -0,0 +1,30 @@
using System;
using System.IO;
namespace BetterRaid.Misc;
public class Constants
{
// General
public const string ChannelPlaceholderImageUrl = "https://cdn.pixabay.com/photo/2018/11/13/22/01/avatar-3814081_1280.png";
// Paths
public static string BetterRaidDataPath => Environment.OSVersion.Platform switch
{
PlatformID.Win32NT => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "Roaming", "BetterRaid"),
PlatformID.Unix => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "BetterRaid"),
PlatformID.MacOSX => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "BetterRaid"),
_ => throw new PlatformNotSupportedException($"Your platform '{Environment.OSVersion.Platform}' is not supported. Please report this issue here: https://www.github.com/zion-networks/BetterRaid/issues")
};
public static string TwitchOAuthAccessTokenFilePath => Path.Combine(BetterRaidDataPath, ".access_token");
public static string DatabaseFilePath => Path.Combine(BetterRaidDataPath, "db.json");
// Twitch API
public const string TwitchClientId = "kkxu4jorjrrc5jch1ito5i61hbev2o";
public const string TwitchOAuthRedirectUrl = "http://localhost:9900";
public const string TwitchOAuthResponseType = "token";
public static readonly string[] TwitchOAuthScopes = [
"channel:manage:raids", // Allows the application to start and cancel raids on the broadcaster's channel
"user:read:subscriptions" // Allows the application to check, if the user has subscribed to the developer's channel
];
}

View File

@ -6,13 +6,13 @@ using System.Text;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BetterRaid.Services;
namespace BetterRaid.Misc; namespace BetterRaid.Misc;
public static class Tools public static class Tools
{ {
private static HttpListener? _oauthListener; private static HttpListener? _oauthListener;
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 = default) public static void StartOAuthLogin(string url, Action? callback = null, CancellationToken token = default)
@ -20,10 +20,10 @@ public static class Tools
if (_oauthListener == null) if (_oauthListener == null)
{ {
_oauthListener = new HttpListener(); _oauthListener = new HttpListener();
_oauthListener.Prefixes.Add("http://localhost:9900/"); _oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl);
_oauthListener.Start(); _oauthListener.Start();
_oauthWaiterTask = WaitForCallback(callback, token); Task.Run(() => WaitForCallback(callback, token), token);
} }
OpenUrl(url); OpenUrl(url);
@ -84,7 +84,7 @@ public static class Tools
req.InputStream.Close(); req.InputStream.Close();
var json = data.ToString(); var json = data.ToString();
var jsonData = JsonObject.Parse(json); var jsonData = JsonNode.Parse(json);
if (jsonData == null) if (jsonData == null)
{ {
@ -106,7 +106,12 @@ public static class Tools
} }
var accessToken = jsonData["access_token"]?.ToString(); var accessToken = jsonData["access_token"]?.ToString();
App.TwitchOAuthAccessToken = accessToken!;
var dataService = App.ServiceProvider?.GetService(typeof(ITwitchDataService));
if (dataService is ITwitchDataService twitchDataService)
{
twitchDataService.ConnectApi(Constants.TwitchClientId, accessToken!);
}
res.StatusCode = 200; res.StatusCode = 200;
res.Close(); res.Close();

View File

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic;
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.Attributes;
using BetterRaid.Services;
namespace BetterRaid.Models; namespace BetterRaid.Models;
@ -142,15 +142,15 @@ public class TwitchChannel : INotifyPropertyChanged
Name = channelName; Name = channelName;
} }
public void InitChannel() public void UpdateChannelData(ITwitchDataService dataService)
{ {
var channel = App.TwitchApi?.Helix.Search.SearchChannelsAsync(Name).Result.Channels var channel = dataService.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 = App.TwitchApi?.Helix.Streams.GetStreamsAsync(userLogins: [ Name ]).Result.Streams var stream = dataService.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;

View File

@ -1,12 +1,26 @@
using System.ComponentModel;
using BetterRaid.Models;
using TwitchLib.Api;
namespace BetterRaid.Services; namespace BetterRaid.Services;
public interface ITwitchDataService public interface ITwitchDataService
{ {
public string? AccessToken { get; set; }
public TwitchChannel? UserChannel { get; set; }
public TwitchAPI TwitchApi { get; }
public bool IsRaidStarted { get; set; } public bool IsRaidStarted { get; set; }
public void ConnectApi(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 void StartRaid(string from, string to);
public void StartRaidCommand(object? arg); public void StartRaidCommand(object? arg);
public void StopRaid(); public void StopRaid();
public void StopRaidCommand(); public void StopRaidCommand();
public void OpenChannelCommand(object? arg); public void OpenChannelCommand(object? arg);
public event PropertyChangingEventHandler? PropertyChanging;
public event PropertyChangedEventHandler? PropertyChanged;
} }

View File

@ -1,36 +1,142 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using BetterRaid.Misc; using BetterRaid.Misc;
using BetterRaid.Models;
using TwitchLib.Api;
namespace BetterRaid.Services.Implementations; namespace BetterRaid.Services.Implementations;
public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INotifyPropertyChanging
{ {
private bool _isRaidStarted; private bool _isRaidStarted;
private TwitchChannel? _userChannel;
public string AccessToken { get; set; } = string.Empty;
public bool IsRaidStarted public bool IsRaidStarted
{ {
get => _isRaidStarted; get => _isRaidStarted;
set => SetField(ref _isRaidStarted, value); 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.");
ConnectApi(Constants.TwitchClientId, token);
}
else
{
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] No access token found.");
}
}
public void ConnectApi(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.");
}
}
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) public void StartRaid(string from, string to)
{ {
// TODO: Also check, if the logged in user is live // TODO: Also check, if the logged in user is live
App.TwitchApi?.Helix.Raids.StartRaidAsync(from, to); TwitchApi.Helix.Raids.StartRaidAsync(from, to);
IsRaidStarted = true; IsRaidStarted = true;
} }
public void StartRaidCommand(object? arg) public void StartRaidCommand(object? arg)
{ {
if (arg == null || App.TwitchBroadcasterId == null) if (arg == null || UserChannel?.BroadcasterId == null)
{ {
return; return;
} }
var from = App.TwitchBroadcasterId; var from = UserChannel.BroadcasterId!;
var to = arg.ToString()!; var to = arg.ToString()!;
StartRaid(from, to); StartRaid(from, to);
@ -38,10 +144,13 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged
public void StopRaid() public void StopRaid()
{ {
if (UserChannel?.BroadcasterId == null)
return;
if (IsRaidStarted == false) if (IsRaidStarted == false)
return; return;
App.TwitchApi?.Helix.Raids.CancelRaidAsync(App.TwitchBroadcasterId); TwitchApi.Helix.Raids.CancelRaidAsync(UserChannel.BroadcasterId);
IsRaidStarted = false; IsRaidStarted = false;
} }
@ -61,18 +170,29 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged
Tools.OpenUrl(url); Tools.OpenUrl(url);
} }
public event PropertyChangingEventHandler? PropertyChanging;
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 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) protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{ {
if (EqualityComparer<T>.Default.Equals(field, value)) return false; if (EqualityComparer<T>.Default.Equals(field, value))
return false;
OnPropertyChanging(propertyName);
field = value; field = value;
OnPropertyChanged(propertyName); OnPropertyChanged(propertyName);
return true; return true;
} }
} }

View File

@ -14,9 +14,12 @@ 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 TwitchPubSub _sub;
private readonly ITwitchDataService _dataService;
public TwitchPubSubService() public TwitchPubSubService(ITwitchDataService dataService)
{ {
_dataService = dataService;
_sub = new TwitchPubSub(); _sub = new TwitchPubSub();
_sub.OnPubSubServiceConnected += OnSubOnOnPubSubServiceConnected; _sub.OnPubSubServiceConnected += OnSubOnOnPubSubServiceConnected;
@ -26,6 +29,29 @@ public class TwitchPubSubService : ITwitchPubSubService
_sub.OnViewCount += OnSubOnOnViewCount; _sub.OnViewCount += OnSubOnOnViewCount;
_sub.Connect(); _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);
};
} }
private void OnSubOnOnViewCount(object? sender, OnViewCountArgs args) private void OnSubOnOnViewCount(object? sender, OnViewCountArgs args)
@ -145,7 +171,7 @@ public class TwitchPubSubService : ITwitchPubSubService
} }
_sub.ListenToVideoPlayback(channelId); _sub.ListenToVideoPlayback(channelId);
_sub.SendTopics(App.TwitchOAuthAccessToken); _sub.SendTopics(_dataService.AccessToken);
break; break;
} }
@ -156,10 +182,9 @@ public class TwitchPubSubService : ITwitchPubSubService
{ {
ArgumentNullException.ThrowIfNull(receiver, nameof(receiver)); ArgumentNullException.ThrowIfNull(receiver, nameof(receiver));
foreach (var target in _targets) foreach (var (topic, listeners) in _targets)
{ {
var topic = target.Key; var listener = listeners.Where(x => x.Instance == receiver).ToList();
var listener = target.Value.Where(x => x.Instance == receiver).ToList();
foreach (var l in listener) foreach (var l in listener)
{ {
@ -177,7 +202,7 @@ public class TwitchPubSubService : ITwitchPubSubService
break; break;
case PubSubType.VideoPlayback: case PubSubType.VideoPlayback:
_sub.ListenToVideoPlayback(l.ChannelId); _sub.ListenToVideoPlayback(l.ChannelId);
_sub.SendTopics(App.TwitchOAuthAccessToken, true); _sub.SendTopics(_dataService.AccessToken, true);
break; break;
} }
} }

View File

@ -1,6 +1,5 @@
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 System.Threading.Tasks;
@ -46,14 +45,14 @@ public partial class MainWindowViewModel : ViewModelBase
set => SetProperty(ref _filter, value); set => SetProperty(ref _filter, value);
} }
public bool IsLoggedIn => App.TwitchApi != null; public bool IsLoggedIn => DataService.UserChannel != null;
public MainWindowViewModel(ITwitchPubSubService pubSub, ITwitchDataService dataService) public MainWindowViewModel(ITwitchPubSubService pubSub, ITwitchDataService dataService)
{ {
_pubSub = pubSub; _pubSub = pubSub;
DataService = dataService; DataService = dataService;
Database = BetterRaidDatabase.LoadFromFile(Path.Combine(App.BetterRaidDataPath, "db.json")); Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath);
} }
public void ExitApplication() public void ExitApplication()
@ -71,7 +70,7 @@ public partial class MainWindowViewModel : ViewModelBase
public void LoginWithTwitch() public void LoginWithTwitch()
{ {
Tools.StartOAuthLogin(App.TwitchOAuthUrl, OnTwitchLoginCallback, CancellationToken.None); Tools.StartOAuthLogin(DataService.GetOAuthUrl(), OnTwitchLoginCallback, CancellationToken.None);
} }
private void OnTwitchLoginCallback() private void OnTwitchLoginCallback()
@ -101,7 +100,7 @@ public partial class MainWindowViewModel : ViewModelBase
{ {
Task.Run(() => Task.Run(() =>
{ {
channel.InitChannel(); channel.UpdateChannelData(DataService);
_pubSub.RegisterReceiver(channel); _pubSub.RegisterReceiver(channel);
}); });

View File

@ -1,195 +0,0 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.Threading;
using BetterRaid.Events;
using BetterRaid.Models;
using TwitchLib.Api.Helix.Models.Raids.StartRaid;
using TwitchLib.Api.Helix.Models.Search;
using TwitchLib.Api.Helix.Models.Streams.GetStreams;
namespace BetterRaid.ViewModels;
public class RaidButtonViewModel : ViewModelBase
{
private TwitchChannel? _channel;
private SolidColorBrush _viewerCountColor = new SolidColorBrush(Color.FromRgb(byte.MaxValue, byte.MaxValue, byte.MaxValue));
private bool _hideDeleteButton;
private bool _isAd;
public string ChannelName
{
get;
set;
}
public bool HideDeleteButton
{
get => _hideDeleteButton;
set => SetProperty(ref _hideDeleteButton, value);
}
public bool IsAd
{
get => _isAd;
set => SetProperty(ref _isAd, value);
}
public TwitchChannel? Channel => _channel ?? new TwitchChannel(ChannelName);
public SolidColorBrush ViewerCountColor
{
get => _viewerCountColor;
set => SetProperty(ref _viewerCountColor, value);
}
public MainWindowViewModel? MainVm { get; set; }
public DateTime? LastRaided => MainVm?.Database?.GetLastRaided(ChannelName);
public event EventHandler<ChannelDataChangedEventArgs>? ChannelDataChanged;
public RaidButtonViewModel(string channelName)
{
ChannelName = channelName;
}
public async Task<bool> GetOrUpdateChannelAsync()
{
Console.WriteLine("[DEBUG] Updating channel '{0}' ...", ChannelName);
var currentChannelData = await GetChannelAsync(ChannelName);
if (currentChannelData == null)
return false;
var currentStreamData = await GetStreamAsync(currentChannelData);
var swapChannel = new TwitchChannel(ChannelName)
{
BroadcasterId = currentChannelData.Id,
Name = ChannelName,
DisplayName = currentChannelData.DisplayName,
IsLive = currentChannelData.IsLive,
ThumbnailUrl = currentChannelData.ThumbnailUrl,
ViewerCount = currentStreamData?.ViewerCount == null
? "(Offline)"
: $"{currentStreamData?.ViewerCount} Viewers",
Category = currentStreamData?.GameName
};
if (_channel != null)
{
_channel.PropertyChanged -= OnChannelDataChanged;
}
Dispatcher.UIThread.Invoke(() => {
ViewerCountColor = new SolidColorBrush(Color.FromRgb(
r: swapChannel.IsLive ? (byte) 0 : byte.MaxValue,
g: swapChannel.IsLive ? byte.MaxValue : (byte) 0,
b: 0)
);
_channel = swapChannel;
OnPropertyChanged(nameof(Channel));
});
if (_channel != null)
{
_channel.PropertyChanged += OnChannelDataChanged;
}
Console.WriteLine("[DEBUG] DONE Updating channel '{0}'", ChannelName);
return true;
}
private async Task<Channel?> GetChannelAsync(string channelName)
{
if (App.TwitchApi == null)
return null;
if (string.IsNullOrEmpty(channelName))
return null;
var channels = await App.TwitchApi.Helix.Search.SearchChannelsAsync(channelName);
var exactChannel = channels.Channels.FirstOrDefault(c => c.BroadcasterLogin.Equals(channelName, StringComparison.CurrentCultureIgnoreCase));
return exactChannel;
}
private async Task<Stream?> GetStreamAsync(Channel currentChannelData)
{
if (App.TwitchApi == null)
return null;
if (currentChannelData == null)
return null;
var streams = await App.TwitchApi.Helix.Streams.GetStreamsAsync(userLogins: [currentChannelData.BroadcasterLogin]);
var exactStream = streams.Streams.FirstOrDefault(s => s.UserLogin == currentChannelData.BroadcasterLogin);
return exactStream;
}
public async Task RaidChannel()
{
if (App.TwitchApi == null)
return;
if (Channel == null)
return;
if (string.IsNullOrWhiteSpace(App.TwitchBroadcasterId))
return;
if (App.TwitchBroadcasterId == Channel.BroadcasterId)
return;
try
{
await App.TwitchApi.Helix.Raids.StartRaidAsync(App.TwitchBroadcasterId, Channel.BroadcasterId);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Console.WriteLine(e.StackTrace);
return;
}
if (MainVm?.Database != null)
{
MainVm.Database.SetRaided(ChannelName, DateTime.Now);
}
}
public void RemoveChannel()
{
if (MainVm?.Database == null)
return;
MainVm.Database.RemoveChannel(ChannelName);
}
private void OnChannelDataChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "IsLive":
OnChannelDataChanged(ChannelDataChangedEventArgs.FromIsLive(false, true));
break;
case "ViewerCount":
OnChannelDataChanged(ChannelDataChangedEventArgs.FromViewerCount(0, 10));
break;
}
}
private void OnChannelDataChanged(ChannelDataChangedEventArgs args)
{
ChannelDataChanged?.Invoke(this, args);
}
}

View File

@ -1,6 +1,4 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace BetterRaid.Views; namespace BetterRaid.Views;

View File

@ -1,6 +1,4 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace BetterRaid.Views; namespace BetterRaid.Views;

View File

@ -1,12 +1,11 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
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:con="using:BetterRaid.Converters" 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"
xmlns:misc="clr-namespace:BetterRaid.Misc" xmlns:misc="using:BetterRaid.Misc"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="600" d:DesignWidth="600"
d:DesignHeight="800" d:DesignHeight="800"
@ -122,7 +121,7 @@
<ai:AdvancedImage Grid.Column="0" <ai:AdvancedImage Grid.Column="0"
Grid.Row="0" Grid.Row="0"
Source="{Binding ThumbnailUrl, TargetNullValue={x:Static br:App.ChannelPlaceholderImageUrl}}" /> Source="{Binding ThumbnailUrl, TargetNullValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}}" />
<Border Grid.Column="0" <Border Grid.Column="0"
Grid.Row="0" Grid.Row="0"

View File

@ -1,15 +1,4 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Threading;
using BetterRaid.Extensions;
using BetterRaid.Models;
using BetterRaid.ViewModels;
namespace BetterRaid.Views; namespace BetterRaid.Views;