A 'little' code cleanup
This commit is contained in:
parent
a8a481d4f9
commit
8a10573fbf
168
App.axaml.cs
168
App.axaml.cs
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
@ -11,180 +9,30 @@ using BetterRaid.Services.Implementations;
|
||||
using BetterRaid.ViewModels;
|
||||
using BetterRaid.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TwitchLib.Api;
|
||||
using TwitchLib.PubSub;
|
||||
|
||||
namespace BetterRaid;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private readonly ServiceCollection _services = [];
|
||||
private ServiceProvider? _provider;
|
||||
private static readonly ServiceCollection Services = [];
|
||||
private static ServiceProvider? _serviceProvider;
|
||||
|
||||
private static TwitchAPI? _twitchApi;
|
||||
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 static IServiceProvider? ServiceProvider => _serviceProvider;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
LoadTwitchToken();
|
||||
InitializeServices();
|
||||
|
||||
AvaloniaXamlLoader.Load(_provider, 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();
|
||||
AvaloniaXamlLoader.Load(_serviceProvider, this);
|
||||
}
|
||||
|
||||
private void InitializeServices()
|
||||
{
|
||||
_services.AddSingleton<ITwitchDataService, TwitchDataService>();
|
||||
_services.AddSingleton<ITwitchPubSubService, TwitchPubSubService>();
|
||||
_services.AddTransient<MainWindowViewModel>();
|
||||
Services.AddSingleton<ITwitchDataService, TwitchDataService>();
|
||||
Services.AddSingleton<ITwitchPubSubService, TwitchPubSubService>();
|
||||
Services.AddTransient<MainWindowViewModel>();
|
||||
|
||||
_provider = _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");
|
||||
}
|
||||
_serviceProvider = Services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
|
@ -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>
|
@ -1,13 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace BetterRaid;
|
||||
|
||||
public partial class RaidButton : UserControl
|
||||
{
|
||||
public RaidButton()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BetterRaid.Extensions;
|
||||
@ -13,10 +12,10 @@ public static class DataContextExtensions
|
||||
|
||||
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;
|
||||
|
||||
var vm = app.Provider.GetRequiredService<T>();
|
||||
var vm = App.ServiceProvider.GetRequiredService<T>();
|
||||
e.DataContext = vm;
|
||||
}
|
||||
}
|
30
Misc/Constants.cs
Normal file
30
Misc/Constants.cs
Normal 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
|
||||
];
|
||||
}
|
@ -6,13 +6,13 @@ using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BetterRaid.Services;
|
||||
|
||||
namespace BetterRaid.Misc;
|
||||
|
||||
public static class Tools
|
||||
{
|
||||
private static HttpListener? _oauthListener;
|
||||
private static Task? _oauthWaiterTask;
|
||||
|
||||
// Source: https://stackoverflow.com/a/43232486
|
||||
public static void StartOAuthLogin(string url, Action? callback = null, CancellationToken token = default)
|
||||
@ -20,10 +20,10 @@ public static class Tools
|
||||
if (_oauthListener == null)
|
||||
{
|
||||
_oauthListener = new HttpListener();
|
||||
_oauthListener.Prefixes.Add("http://localhost:9900/");
|
||||
_oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl);
|
||||
_oauthListener.Start();
|
||||
|
||||
_oauthWaiterTask = WaitForCallback(callback, token);
|
||||
Task.Run(() => WaitForCallback(callback, token), token);
|
||||
}
|
||||
|
||||
OpenUrl(url);
|
||||
@ -84,7 +84,7 @@ public static class Tools
|
||||
req.InputStream.Close();
|
||||
|
||||
var json = data.ToString();
|
||||
var jsonData = JsonObject.Parse(json);
|
||||
var jsonData = JsonNode.Parse(json);
|
||||
|
||||
if (jsonData == null)
|
||||
{
|
||||
@ -106,7 +106,12 @@ public static class Tools
|
||||
}
|
||||
|
||||
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.Close();
|
||||
|
@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BetterRaid.Attributes;
|
||||
using BetterRaid.Services;
|
||||
|
||||
namespace BetterRaid.Models;
|
||||
|
||||
@ -142,15 +142,15 @@ public class TwitchChannel : INotifyPropertyChanged
|
||||
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));
|
||||
|
||||
if (channel == null)
|
||||
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));
|
||||
|
||||
BroadcasterId = channel.Id;
|
||||
|
@ -1,12 +1,26 @@
|
||||
using System.ComponentModel;
|
||||
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 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 StartRaidCommand(object? arg);
|
||||
public void StopRaid();
|
||||
public void StopRaidCommand();
|
||||
public void OpenChannelCommand(object? arg);
|
||||
|
||||
public event PropertyChangingEventHandler? PropertyChanging;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
}
|
@ -1,36 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BetterRaid.Misc;
|
||||
using BetterRaid.Models;
|
||||
using TwitchLib.Api;
|
||||
|
||||
namespace BetterRaid.Services.Implementations;
|
||||
|
||||
public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged
|
||||
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.");
|
||||
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)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
public void StartRaidCommand(object? arg)
|
||||
{
|
||||
if (arg == null || App.TwitchBroadcasterId == null)
|
||||
if (arg == null || UserChannel?.BroadcasterId == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var from = App.TwitchBroadcasterId;
|
||||
var from = UserChannel.BroadcasterId!;
|
||||
var to = arg.ToString()!;
|
||||
|
||||
StartRaid(from, to);
|
||||
@ -38,10 +144,13 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged
|
||||
|
||||
public void StopRaid()
|
||||
{
|
||||
if (UserChannel?.BroadcasterId == null)
|
||||
return;
|
||||
|
||||
if (IsRaidStarted == false)
|
||||
return;
|
||||
|
||||
App.TwitchApi?.Helix.Raids.CancelRaidAsync(App.TwitchBroadcasterId);
|
||||
TwitchApi.Helix.Raids.CancelRaidAsync(UserChannel.BroadcasterId);
|
||||
IsRaidStarted = false;
|
||||
}
|
||||
|
||||
@ -61,18 +170,29 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged
|
||||
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;
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
return false;
|
||||
|
||||
OnPropertyChanging(propertyName);
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -14,9 +14,12 @@ public class TwitchPubSubService : ITwitchPubSubService
|
||||
{
|
||||
private readonly Dictionary<PubSubType, List<PubSubListener>> _targets = new();
|
||||
private readonly TwitchPubSub _sub;
|
||||
private readonly ITwitchDataService _dataService;
|
||||
|
||||
public TwitchPubSubService()
|
||||
public TwitchPubSubService(ITwitchDataService dataService)
|
||||
{
|
||||
_dataService = dataService;
|
||||
|
||||
_sub = new TwitchPubSub();
|
||||
|
||||
_sub.OnPubSubServiceConnected += OnSubOnOnPubSubServiceConnected;
|
||||
@ -26,6 +29,29 @@ public class TwitchPubSubService : ITwitchPubSubService
|
||||
_sub.OnViewCount += OnSubOnOnViewCount;
|
||||
|
||||
_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)
|
||||
@ -145,7 +171,7 @@ public class TwitchPubSubService : ITwitchPubSubService
|
||||
}
|
||||
|
||||
_sub.ListenToVideoPlayback(channelId);
|
||||
_sub.SendTopics(App.TwitchOAuthAccessToken);
|
||||
_sub.SendTopics(_dataService.AccessToken);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -156,10 +182,9 @@ public class TwitchPubSubService : ITwitchPubSubService
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(receiver, nameof(receiver));
|
||||
|
||||
foreach (var target in _targets)
|
||||
foreach (var (topic, listeners) in _targets)
|
||||
{
|
||||
var topic = target.Key;
|
||||
var listener = target.Value.Where(x => x.Instance == receiver).ToList();
|
||||
var listener = listeners.Where(x => x.Instance == receiver).ToList();
|
||||
|
||||
foreach (var l in listener)
|
||||
{
|
||||
@ -177,7 +202,7 @@ public class TwitchPubSubService : ITwitchPubSubService
|
||||
break;
|
||||
case PubSubType.VideoPlayback:
|
||||
_sub.ListenToVideoPlayback(l.ChannelId);
|
||||
_sub.SendTopics(App.TwitchOAuthAccessToken, true);
|
||||
_sub.SendTopics(_dataService.AccessToken, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -46,14 +45,14 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
set => SetProperty(ref _filter, value);
|
||||
}
|
||||
|
||||
public bool IsLoggedIn => App.TwitchApi != null;
|
||||
public bool IsLoggedIn => DataService.UserChannel != null;
|
||||
|
||||
public MainWindowViewModel(ITwitchPubSubService pubSub, ITwitchDataService dataService)
|
||||
{
|
||||
_pubSub = pubSub;
|
||||
DataService = dataService;
|
||||
|
||||
Database = BetterRaidDatabase.LoadFromFile(Path.Combine(App.BetterRaidDataPath, "db.json"));
|
||||
Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath);
|
||||
}
|
||||
|
||||
public void ExitApplication()
|
||||
@ -71,7 +70,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
public void LoginWithTwitch()
|
||||
{
|
||||
Tools.StartOAuthLogin(App.TwitchOAuthUrl, OnTwitchLoginCallback, CancellationToken.None);
|
||||
Tools.StartOAuthLogin(DataService.GetOAuthUrl(), OnTwitchLoginCallback, CancellationToken.None);
|
||||
}
|
||||
|
||||
private void OnTwitchLoginCallback()
|
||||
@ -101,7 +100,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
channel.InitChannel();
|
||||
channel.UpdateChannelData(DataService);
|
||||
_pubSub.RegisterReceiver(channel);
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace BetterRaid.Views;
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace BetterRaid.Views;
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:BetterRaid.ViewModels"
|
||||
xmlns:br="using:BetterRaid"
|
||||
xmlns:con="using:BetterRaid.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ai="using:AsyncImageLoader"
|
||||
xmlns:misc="clr-namespace:BetterRaid.Misc"
|
||||
xmlns:misc="using:BetterRaid.Misc"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="600"
|
||||
d:DesignHeight="800"
|
||||
@ -122,7 +121,7 @@
|
||||
|
||||
<ai:AdvancedImage Grid.Column="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"
|
||||
Grid.Row="0"
|
||||
|
@ -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.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using BetterRaid.Extensions;
|
||||
using BetterRaid.Models;
|
||||
using BetterRaid.ViewModels;
|
||||
|
||||
namespace BetterRaid.Views;
|
||||
|
||||
|
Reference in New Issue
Block a user