Compare commits
No commits in common. "reactive-ui" and "main" have entirely different histories.
reactive-u
...
main
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,7 +20,6 @@
|
|||||||
mono_crash.*
|
mono_crash.*
|
||||||
|
|
||||||
# Build results
|
# Build results
|
||||||
[Bb]iuld/
|
|
||||||
[Dd]ebug/
|
[Dd]ebug/
|
||||||
[Dd]ebugPublic/
|
[Dd]ebugPublic/
|
||||||
[Rr]elease/
|
[Rr]elease/
|
||||||
|
189
App.axaml.cs
189
App.axaml.cs
@ -1,123 +1,146 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
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;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Threading;
|
|
||||||
using BetterRaid.Services;
|
|
||||||
using BetterRaid.Services.Implementations;
|
|
||||||
using BetterRaid.ViewModels;
|
using BetterRaid.ViewModels;
|
||||||
using BetterRaid.Views;
|
using BetterRaid.Views;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using TwitchLib.Api;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace BetterRaid;
|
namespace BetterRaid;
|
||||||
|
|
||||||
public class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
private ServiceProvider? _serviceProvider;
|
internal static TwitchAPI? TwitchApi = null;
|
||||||
private ILogger<App>? _logger;
|
internal static int AutoUpdateDelay = 10_000;
|
||||||
|
internal static bool HasUserZnSubbed = false;
|
||||||
|
internal static string BetterRaidDataPath = "";
|
||||||
|
internal static string TwitchBroadcasterId = "";
|
||||||
|
internal static string TwitchOAuthAccessToken = "";
|
||||||
|
internal static string TwitchOAuthAccessTokenFilePath = "";
|
||||||
|
internal static string TokenClientId = "kkxu4jorjrrc5jch1ito5i61hbev2o";
|
||||||
|
internal static readonly string TwitchOAuthRedirectUrl = "http://localhost:9900";
|
||||||
|
internal static readonly string TwitchOAuthResponseType = "token";
|
||||||
|
internal 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=http://localhost:9900"
|
||||||
|
+ $"&response_type={TwitchOAuthResponseType}"
|
||||||
|
+ $"&scope={string.Join("+", TwitchOAuthScopes)}";
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
_serviceProvider = InitializeServices();
|
var userHomeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
_logger = _serviceProvider.GetRequiredService<ILogger<App>>();
|
|
||||||
|
|
||||||
if (TryLoadDatabase() == false)
|
switch (Environment.OSVersion.Platform)
|
||||||
{
|
{
|
||||||
_logger?.LogError("Failed to load or initialize database");
|
case PlatformID.Win32NT:
|
||||||
|
BetterRaidDataPath = Path.Combine(userHomeDir, "AppData", "Roaming", "BetterRaid");
|
||||||
Environment.Exit(1);
|
break;
|
||||||
|
case PlatformID.Unix:
|
||||||
|
BetterRaidDataPath = Path.Combine(userHomeDir, ".config", "BetterRaid");
|
||||||
|
break;
|
||||||
|
case PlatformID.MacOSX:
|
||||||
|
BetterRaidDataPath = Path.Combine(userHomeDir, "Library", "Application Support", "BetterRaid");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
AvaloniaXamlLoader.Load(_serviceProvider, this);
|
if (!Directory.Exists(BetterRaidDataPath))
|
||||||
|
Directory.CreateDirectory(BetterRaidDataPath);
|
||||||
|
|
||||||
|
TwitchOAuthAccessTokenFilePath = Path.Combine(BetterRaidDataPath, ".access_token");
|
||||||
|
|
||||||
|
if (File.Exists(TwitchOAuthAccessTokenFilePath))
|
||||||
|
{
|
||||||
|
TwitchOAuthAccessToken = File.ReadAllText(TwitchOAuthAccessTokenFilePath);
|
||||||
|
InitTwitchClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryLoadDatabase()
|
AvaloniaXamlLoader.Load(this);
|
||||||
{
|
|
||||||
if (_serviceProvider == null)
|
|
||||||
{
|
|
||||||
throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var db = _serviceProvider.GetRequiredService<IDatabaseService>();
|
public static void InitTwitchClient(bool overrideToken = false)
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
db.LoadOrCreate();
|
Console.WriteLine("[INFO] Initializing Twitch Client...");
|
||||||
Task.Run(db.UpdateLoadedChannels);
|
|
||||||
|
|
||||||
return true;
|
TwitchApi = new TwitchAPI();
|
||||||
}
|
TwitchApi.Settings.ClientId = TokenClientId;
|
||||||
catch (Exception e)
|
TwitchApi.Settings.AccessToken = TwitchOAuthAccessToken;
|
||||||
|
|
||||||
|
Console.WriteLine("[INFO] Testing Twitch API connection...");
|
||||||
|
|
||||||
|
var user = TwitchApi.Helix.Users.GetUsersAsync().Result.Users.FirstOrDefault();
|
||||||
|
if (user == null)
|
||||||
{
|
{
|
||||||
_logger?.LogError(e, "Failed to load database");
|
TwitchApi = null;
|
||||||
return false;
|
Console.WriteLine("[ERROR] Failed to connect to Twitch API!");
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServiceProvider InitializeServices()
|
var channel = TwitchApi.Helix.Search
|
||||||
{
|
.SearchChannelsAsync(user.Login).Result.Channels
|
||||||
var services = new ServiceCollection();
|
.FirstOrDefault(c => c.BroadcasterLogin == user.Login);
|
||||||
services.AddLogging(logging =>
|
|
||||||
{
|
|
||||||
logging.SetMinimumLevel(LogLevel.Debug);
|
|
||||||
logging.AddConsole();
|
|
||||||
});
|
|
||||||
services.AddSingleton<ITwitchService, TwitchService>();
|
|
||||||
services.AddSingleton<IWebToolsService, WebToolsService>();
|
|
||||||
services.AddSingleton<IDatabaseService, DatabaseService>();
|
|
||||||
services.AddSingleton<ISynchronizaionService, DispatcherService>(_ => new DispatcherService(Dispatcher.UIThread));
|
|
||||||
services.AddTransient<MainWindowViewModel>();
|
|
||||||
|
|
||||||
return services.BuildServiceProvider();
|
var userSubs = TwitchApi.Helix.Subscriptions.CheckUserSubscriptionAsync(
|
||||||
|
userId: user.Id,
|
||||||
|
broadcasterId: "1120558409"
|
||||||
|
).Result.Data;
|
||||||
|
|
||||||
|
if (userSubs.Length > 0 && userSubs.Any(s => s.BroadcasterId == "1120558409"))
|
||||||
|
{
|
||||||
|
HasUserZnSubbed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[ERROR] User channel could not be found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TwitchBroadcasterId = channel.Id;
|
||||||
|
System.Console.WriteLine(TwitchBroadcasterId);
|
||||||
|
|
||||||
|
Console.WriteLine("[INFO] Connected to Twitch API as '{0}'!", user.DisplayName);
|
||||||
|
|
||||||
|
if (overrideToken)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
// Line below is needed to remove Avalonia data validation.
|
||||||
|
// Without this line you will get duplicate validations from both Avalonia and CT
|
||||||
BindingPlugins.DataValidators.RemoveAt(0);
|
BindingPlugins.DataValidators.RemoveAt(0);
|
||||||
|
desktop.MainWindow = new MainWindow
|
||||||
if(_serviceProvider == null)
|
|
||||||
{
|
{
|
||||||
throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null");
|
DataContext = new MainWindowViewModel(),
|
||||||
}
|
|
||||||
|
|
||||||
var mainWindow = new MainWindow
|
|
||||||
{
|
|
||||||
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (ApplicationLifetime)
|
|
||||||
{
|
|
||||||
case IClassicDesktopStyleApplicationLifetime desktop:
|
|
||||||
desktop.MainWindow = mainWindow;
|
|
||||||
desktop.Exit += OnDesktopOnExit;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ISingleViewApplicationLifetime singleViewPlatform:
|
|
||||||
singleViewPlatform.MainView = mainWindow;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDesktopOnExit(object? o, ControlledApplicationLifetimeExitEventArgs controlledApplicationLifetimeExitEventArgs)
|
|
||||||
{
|
|
||||||
if (_serviceProvider == null)
|
|
||||||
{
|
|
||||||
throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _serviceProvider.GetRequiredService<IDatabaseService>();
|
|
||||||
db.Save();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger?.LogError(e, "Failed to save database");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -14,23 +14,19 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Models\" />
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0" />
|
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0" />
|
||||||
<PackageReference Include="Avalonia" Version="11.1.3" />
|
<PackageReference Include="Avalonia" Version="11.1.0" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.1.3" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.1.0" />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.1.3" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.0" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.0" />
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3" />
|
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
|
||||||
<PackageReference Include="TwitchLib" Version="3.5.3" />
|
<PackageReference Include="TwitchLib" Version="3.5.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.5.002.0
|
VisualStudioVersion = 17.5.002.0
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{5E0DA55A-6B6B-4906-ACB9-401AB203D537}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{77D4100D-424A-4E36-BFF2-14A40F217605}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -11,10 +11,10 @@ Global
|
|||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{5E0DA55A-6B6B-4906-ACB9-401AB203D537}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{77D4100D-424A-4E36-BFF2-14A40F217605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{5E0DA55A-6B6B-4906-ACB9-401AB203D537}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{77D4100D-424A-4E36-BFF2-14A40F217605}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{5E0DA55A-6B6B-4906-ACB9-401AB203D537}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{77D4100D-424A-4E36-BFF2-14A40F217605}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{5E0DA55A-6B6B-4906-ACB9-401AB203D537}.Release|Any CPU.Build.0 = Release|Any CPU
|
{77D4100D-424A-4E36-BFF2-14A40F217605}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
BIN
Build/BetterRaid-0.0.1-alpha.exe
Executable file
BIN
Build/BetterRaid-0.0.1-alpha.exe
Executable file
Binary file not shown.
BIN
Build/BetterRaid-0.0.1-alpha.x86_64
Executable file
BIN
Build/BetterRaid-0.0.1-alpha.x86_64
Executable file
Binary file not shown.
95
Controls/RaidButton.axaml
Normal file
95
Controls/RaidButton.axaml
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<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>
|
13
Controls/RaidButton.axaml.cs
Normal file
13
Controls/RaidButton.axaml.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace BetterRaid;
|
||||||
|
|
||||||
|
public partial class RaidButton : UserControl
|
||||||
|
{
|
||||||
|
public RaidButton()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using Avalonia.Data.Converters;
|
|
||||||
using Avalonia.Media;
|
|
||||||
|
|
||||||
namespace BetterRaid.Converters;
|
|
||||||
|
|
||||||
public class ChannelOnlineColorConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
if (value is bool isOnline)
|
|
||||||
{
|
|
||||||
return isOnline ? new SolidColorBrush(Colors.GreenYellow) : new SolidColorBrush(Colors.OrangeRed);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SolidColorBrush(Colors.OrangeRed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
if (value is SolidColorBrush brush)
|
|
||||||
{
|
|
||||||
return brush.Color == Colors.GreenYellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
11
Extensions/DataContextExtensions.cs
Normal file
11
Extensions/DataContextExtensions.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace BetterRaid.Extensions;
|
||||||
|
|
||||||
|
public static class DataContextExtensions
|
||||||
|
{
|
||||||
|
public static T? GetDataContextAs<T>(this T obj) where T : Window
|
||||||
|
{
|
||||||
|
return obj.DataContext as T;
|
||||||
|
}
|
||||||
|
}
|
@ -1,55 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace BetterRaid.Extensions;
|
|
||||||
|
|
||||||
public static class MemberInfoExtensions
|
|
||||||
{
|
|
||||||
public static bool SetValue<T>(this MemberInfo member, object instance, T value)
|
|
||||||
{
|
|
||||||
var targetType = member switch
|
|
||||||
{
|
|
||||||
PropertyInfo p => p.PropertyType,
|
|
||||||
FieldInfo f => f.FieldType,
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (targetType == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (member is PropertyInfo property)
|
|
||||||
{
|
|
||||||
if (targetType == typeof(T) || targetType.IsAssignableFrom(typeof(T)))
|
|
||||||
{
|
|
||||||
property.SetValue(instance, value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == typeof(string))
|
|
||||||
{
|
|
||||||
property.SetValue(instance, value?.ToString());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (member is FieldInfo field)
|
|
||||||
{
|
|
||||||
if (targetType == typeof(T) || targetType.IsAssignableFrom(typeof(T)))
|
|
||||||
{
|
|
||||||
field.SetValue(instance, value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == typeof(string))
|
|
||||||
{
|
|
||||||
field.SetValue(instance, value?.ToString());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace BetterRaid.Misc;
|
|
||||||
|
|
||||||
public static 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, "brdb.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,137 +6,28 @@ 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.Misc;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace BetterRaid.Services;
|
namespace BetterRaid.Misc;
|
||||||
|
|
||||||
public interface IWebToolsService
|
public static class Tools
|
||||||
{
|
{
|
||||||
void StartOAuthLogin(ITwitchService twitch, Action? callback = null, CancellationToken token = default);
|
private static HttpListener? _oauthListener;
|
||||||
void OpenUrl(string url);
|
private static Task? _oauthWaiterTask;
|
||||||
}
|
|
||||||
|
|
||||||
public class WebToolsService : IWebToolsService
|
|
||||||
{
|
|
||||||
private HttpListener? _oauthListener;
|
|
||||||
private readonly ILogger<WebToolsService> _logger;
|
|
||||||
|
|
||||||
public WebToolsService(ILogger<WebToolsService> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source: https://stackoverflow.com/a/43232486
|
// Source: https://stackoverflow.com/a/43232486
|
||||||
public void StartOAuthLogin(ITwitchService twitch, Action? callback = null, CancellationToken token = default)
|
public static void StartOAuthLogin(string url, Action? callback = null, CancellationToken? token = null)
|
||||||
{
|
|
||||||
if (_oauthListener == null)
|
|
||||||
{
|
{
|
||||||
|
if (_oauthListener != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var _token = token ?? CancellationToken.None;
|
||||||
|
|
||||||
_oauthListener = new HttpListener();
|
_oauthListener = new HttpListener();
|
||||||
_oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/");
|
_oauthListener.Prefixes.Add("http://localhost:9900/");
|
||||||
_oauthListener.Start();
|
_oauthListener.Start();
|
||||||
|
|
||||||
Task.Run(() => WaitForCallback(twitch, callback, token), token);
|
_oauthWaiterTask = WaitForCallback(callback, _token);
|
||||||
}
|
|
||||||
|
|
||||||
OpenUrl(twitch.GetOAuthUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task WaitForCallback(ITwitchService twitch, Action? callback, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (_oauthListener == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logger.LogDebug("Starting token listener");
|
|
||||||
|
|
||||||
while (!token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
var ctx = await _oauthListener.GetContextAsync();
|
|
||||||
var req = ctx.Request;
|
|
||||||
var res = ctx.Response;
|
|
||||||
|
|
||||||
if (req.Url == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
_logger.LogDebug("{method} {url}", req.HttpMethod, req.Url);
|
|
||||||
|
|
||||||
// Response, that may contain the access token as fragment
|
|
||||||
// It must be extracted client-side in browser
|
|
||||||
if (req.Url.LocalPath == "/")
|
|
||||||
{
|
|
||||||
var buf = new byte[1024];
|
|
||||||
var data = new StringBuilder();
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = await req.InputStream.ReadAsync(buf, token)) > 0)
|
|
||||||
{
|
|
||||||
data.Append(Encoding.UTF8.GetString(buf, 0, bytesRead));
|
|
||||||
}
|
|
||||||
|
|
||||||
req.InputStream.Close();
|
|
||||||
|
|
||||||
_logger.LogTrace("{data}", data);
|
|
||||||
|
|
||||||
res.StatusCode = 200;
|
|
||||||
await res.OutputStream.WriteAsync(Encoding.UTF8.GetBytes(OAUTH_CLIENT_DOCUMENT).AsMemory(0, OAUTH_CLIENT_DOCUMENT.Length), token);
|
|
||||||
res.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.Url.LocalPath == "/login")
|
|
||||||
{
|
|
||||||
var buf = new byte[1024];
|
|
||||||
var data = new StringBuilder();
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = await req.InputStream.ReadAsync(buf, token)) > 0)
|
|
||||||
{
|
|
||||||
data.Append(Encoding.UTF8.GetString(buf, 0, bytesRead));
|
|
||||||
}
|
|
||||||
|
|
||||||
req.InputStream.Close();
|
|
||||||
|
|
||||||
var json = data.ToString();
|
|
||||||
var jsonData = JsonNode.Parse(json);
|
|
||||||
|
|
||||||
if (jsonData == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("Failed to parse JSON data:");
|
|
||||||
_logger.LogError("{json}", json);
|
|
||||||
|
|
||||||
res.StatusCode = 400;
|
|
||||||
res.Close();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonData["access_token"] == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("Missing access_token in JSON data.");
|
|
||||||
|
|
||||||
res.StatusCode = 400;
|
|
||||||
res.Close();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var accessToken = jsonData["access_token"]?.ToString()!;
|
|
||||||
|
|
||||||
await twitch.ConnectApiAsync(Constants.TwitchClientId, accessToken);
|
|
||||||
|
|
||||||
res.StatusCode = 200;
|
|
||||||
res.Close();
|
|
||||||
|
|
||||||
_logger.LogInformation("Received access token!");
|
|
||||||
|
|
||||||
callback?.Invoke();
|
|
||||||
|
|
||||||
_oauthListener.Stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenUrl(string url)
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Process.Start(url);
|
Process.Start(url);
|
||||||
@ -164,6 +55,98 @@ public class WebToolsService : IWebToolsService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task WaitForCallback(Action? callback, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (_oauthListener == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Console.WriteLine("Starting token listener");
|
||||||
|
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var ctx = await _oauthListener.GetContextAsync();
|
||||||
|
var req = ctx.Request;
|
||||||
|
var res = ctx.Response;
|
||||||
|
|
||||||
|
if (req.Url == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Console.WriteLine("{0} {1}", req.HttpMethod, req.Url);
|
||||||
|
|
||||||
|
// Response, that may contain the access token as fragment
|
||||||
|
// It must be extracted client-side in browser
|
||||||
|
if (req.Url.LocalPath == "/")
|
||||||
|
{
|
||||||
|
var buf = new byte[1024];
|
||||||
|
var data = new StringBuilder();
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = await req.InputStream.ReadAsync(buf, token)) > 0)
|
||||||
|
{
|
||||||
|
data.Append(Encoding.UTF8.GetString(buf, 0, bytesRead));
|
||||||
|
}
|
||||||
|
|
||||||
|
req.InputStream.Close();
|
||||||
|
|
||||||
|
Console.WriteLine(data.ToString());
|
||||||
|
|
||||||
|
res.StatusCode = 200;
|
||||||
|
await res.OutputStream.WriteAsync(Encoding.UTF8.GetBytes(OAUTH_CLIENT_DOCUMENT).AsMemory(0, OAUTH_CLIENT_DOCUMENT.Length), token);
|
||||||
|
res.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.Url.LocalPath == "/login")
|
||||||
|
{
|
||||||
|
var buf = new byte[1024];
|
||||||
|
var data = new StringBuilder();
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = await req.InputStream.ReadAsync(buf, token)) > 0)
|
||||||
|
{
|
||||||
|
data.Append(Encoding.UTF8.GetString(buf, 0, bytesRead));
|
||||||
|
}
|
||||||
|
|
||||||
|
req.InputStream.Close();
|
||||||
|
|
||||||
|
var json = data.ToString();
|
||||||
|
var jsonData = JsonObject.Parse(json);
|
||||||
|
|
||||||
|
if (jsonData == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[ERROR] Failed to parse JSON data:");
|
||||||
|
Console.WriteLine(json);
|
||||||
|
|
||||||
|
res.StatusCode = 400;
|
||||||
|
res.Close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonData["access_token"] == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[ERROR] Missing access_token in JSON data.");
|
||||||
|
|
||||||
|
res.StatusCode = 400;
|
||||||
|
res.Close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessToken = jsonData["access_token"]?.ToString();
|
||||||
|
App.TwitchOAuthAccessToken = accessToken!;
|
||||||
|
|
||||||
|
res.StatusCode = 200;
|
||||||
|
res.Close();
|
||||||
|
|
||||||
|
Console.WriteLine("[INFO] Received access token!");
|
||||||
|
|
||||||
|
callback?.Invoke();
|
||||||
|
|
||||||
|
_oauthListener.Stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private const string OAUTH_CLIENT_DOCUMENT =
|
private const string OAUTH_CLIENT_DOCUMENT =
|
||||||
@"
|
@"
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
149
Models/BetterRaidDatabase.cs
Normal file
149
Models/BetterRaidDatabase.cs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BetterRaid.Models;
|
||||||
|
|
||||||
|
[JsonObject]
|
||||||
|
public class BetterRaidDatabase : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
private string? _databaseFilePath;
|
||||||
|
private bool _onlyOnline;
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
public bool OnlyOnline
|
||||||
|
{
|
||||||
|
get => _onlyOnline;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == _onlyOnline)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_onlyOnline = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public List<string> Channels { get; set; } = [];
|
||||||
|
public Dictionary<string, DateTime?> LastRaided = [];
|
||||||
|
public bool AutoSave { get; set; }
|
||||||
|
|
||||||
|
public static BetterRaidDatabase LoadFromFile(string path)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNullOrEmpty(path);
|
||||||
|
|
||||||
|
path = Path.Combine(Environment.CurrentDirectory, path);
|
||||||
|
|
||||||
|
if (File.Exists(path) == false)
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Database file not found", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbStr = File.ReadAllText(path);
|
||||||
|
var dbObj = JsonConvert.DeserializeObject<BetterRaidDatabase>(dbStr);
|
||||||
|
|
||||||
|
if (dbObj == null)
|
||||||
|
{
|
||||||
|
throw new JsonException("Failed to read database file");
|
||||||
|
}
|
||||||
|
|
||||||
|
dbObj._databaseFilePath = path;
|
||||||
|
|
||||||
|
foreach (var channel in dbObj.Channels)
|
||||||
|
{
|
||||||
|
if (dbObj.LastRaided.ContainsKey(channel) == false)
|
||||||
|
{
|
||||||
|
dbObj.LastRaided.Add(channel, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("[DEBUG] Loaded database from {0}", path);
|
||||||
|
|
||||||
|
return dbObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save(string? path = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_databaseFilePath) && string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("No target path given to save database at");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path) == false && string.IsNullOrEmpty(_databaseFilePath))
|
||||||
|
{
|
||||||
|
_databaseFilePath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbStr = JsonConvert.SerializeObject(this);
|
||||||
|
var targetPath = (path ?? _databaseFilePath)!;
|
||||||
|
|
||||||
|
File.WriteAllText(targetPath, dbStr);
|
||||||
|
|
||||||
|
Console.WriteLine("[DEBUG] Saved database to {0}", targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddChannel(string channel)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(channel);
|
||||||
|
|
||||||
|
if (Channels.Contains(channel))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Channels.Add(channel);
|
||||||
|
OnPropertyChanged(nameof(Channels));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveChannel(string channel)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(channel);
|
||||||
|
|
||||||
|
if (Channels.Contains(channel) == false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Channels.Remove(channel);
|
||||||
|
OnPropertyChanged(nameof(Channels));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRaided(string channel, DateTime dateTime)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(channel);
|
||||||
|
|
||||||
|
if (LastRaided.ContainsKey(channel))
|
||||||
|
{
|
||||||
|
LastRaided[channel] = dateTime;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LastRaided.Add(channel, dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(LastRaided));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime? GetLastRaided(string channel)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(channel);
|
||||||
|
|
||||||
|
if (LastRaided.ContainsKey(channel))
|
||||||
|
{
|
||||||
|
return LastRaided[channel];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
|
||||||
|
if (AutoSave && _databaseFilePath != null)
|
||||||
|
{
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace BetterRaid.Models.Database;
|
|
||||||
|
|
||||||
[JsonObject]
|
|
||||||
public class BetterRaidDatabase
|
|
||||||
{
|
|
||||||
public bool OnlyOnline { get; set; }
|
|
||||||
public List<TwitchChannel> Channels { get; set; } = [];
|
|
||||||
}
|
|
@ -1,54 +1,24 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using BetterRaid.Services;
|
using Avalonia.Threading;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using TwitchLib.PubSub.Events;
|
|
||||||
|
|
||||||
namespace BetterRaid.Models;
|
namespace BetterRaid.Models;
|
||||||
|
|
||||||
[JsonObject]
|
|
||||||
public class TwitchChannel : INotifyPropertyChanged
|
public class TwitchChannel : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
private string? _id;
|
|
||||||
private string _name;
|
|
||||||
private string? _broadcasterId;
|
|
||||||
private string? _viewerCount;
|
private string? _viewerCount;
|
||||||
private bool _isLive;
|
private bool _isLive;
|
||||||
|
private string? _name;
|
||||||
private string? _displayName;
|
private string? _displayName;
|
||||||
private string? _thumbnailUrl;
|
private string? _thumbnailUrl;
|
||||||
private string? _category;
|
private string? _category;
|
||||||
private string? _title;
|
|
||||||
private DateTime? _lastRaided;
|
|
||||||
|
|
||||||
public string? Id
|
|
||||||
{
|
|
||||||
get => _id;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value == _id)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_id = value;
|
|
||||||
OnPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? BroadcasterId
|
public string? BroadcasterId
|
||||||
{
|
{
|
||||||
get => _broadcasterId;
|
get;
|
||||||
set
|
set;
|
||||||
{
|
|
||||||
if (value == _broadcasterId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_broadcasterId = value;
|
|
||||||
OnPropertyChanged();
|
|
||||||
}
|
}
|
||||||
}
|
public string? Name
|
||||||
|
|
||||||
public string Name
|
|
||||||
{
|
{
|
||||||
get => _name;
|
get => _name;
|
||||||
set
|
set
|
||||||
@ -60,8 +30,6 @@ public class TwitchChannel : INotifyPropertyChanged
|
|||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public bool IsLive
|
public bool IsLive
|
||||||
{
|
{
|
||||||
get => _isLive;
|
get => _isLive;
|
||||||
@ -74,8 +42,6 @@ public class TwitchChannel : INotifyPropertyChanged
|
|||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public string? ViewerCount
|
public string? ViewerCount
|
||||||
{
|
{
|
||||||
get => _viewerCount;
|
get => _viewerCount;
|
||||||
@ -115,7 +81,6 @@ public class TwitchChannel : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public string? Category
|
public string? Category
|
||||||
{
|
{
|
||||||
get => _category;
|
get => _category;
|
||||||
@ -129,83 +94,9 @@ public class TwitchChannel : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
public TwitchChannel(string channelName)
|
||||||
public string? Title
|
|
||||||
{
|
{
|
||||||
get => _title;
|
Name = channelName;
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value == _title)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_title = value;
|
|
||||||
OnPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public DateTime? LastRaided
|
|
||||||
{
|
|
||||||
get => _lastRaided;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value == _lastRaided)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_lastRaided = value;
|
|
||||||
OnPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public TwitchChannel(string? channelName)
|
|
||||||
{
|
|
||||||
_name = channelName ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateChannelData(ITwitchService service)
|
|
||||||
{
|
|
||||||
var channel = service.TwitchApi.Helix.Search.SearchChannelsAsync(Name).Result.Channels
|
|
||||||
.FirstOrDefault(c => c.BroadcasterLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (channel == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var stream = service.TwitchApi.Helix.Streams.GetStreamsAsync(userLogins: [ Name ]).Result.Streams
|
|
||||||
.FirstOrDefault(s => s.UserLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase));
|
|
||||||
|
|
||||||
Id = channel.Id;
|
|
||||||
BroadcasterId = channel.Id;
|
|
||||||
DisplayName = channel.DisplayName;
|
|
||||||
ThumbnailUrl = channel.ThumbnailUrl;
|
|
||||||
Category = channel.GameName;
|
|
||||||
Title = channel.Title;
|
|
||||||
IsLive = channel.IsLive;
|
|
||||||
ViewerCount = stream?.ViewerCount == null
|
|
||||||
? null
|
|
||||||
: $"{stream.ViewerCount}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnStreamUp(object? sender, OnStreamUpArgs args)
|
|
||||||
{
|
|
||||||
if (args.ChannelId != BroadcasterId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IsLive = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnStreamDown(object? sender, OnStreamDownArgs args)
|
|
||||||
{
|
|
||||||
if (args.ChannelId != BroadcasterId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IsLive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnViewCount(object? sender, OnViewCountArgs args)
|
|
||||||
{
|
|
||||||
if (args.ChannelId != BroadcasterId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ViewerCount = $"{args.Viewers}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using System;
|
using System;
|
||||||
using Avalonia.ReactiveUI;
|
|
||||||
|
|
||||||
namespace BetterRaid;
|
namespace BetterRaid;
|
||||||
|
|
||||||
@ -10,8 +9,7 @@ sealed class Program
|
|||||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
// yet and stuff might break.
|
// yet and stuff might break.
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args) =>
|
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||||
BuildAvaloniaApp()
|
|
||||||
.StartWithClassicDesktopLifetime(args);
|
.StartWithClassicDesktopLifetime(args);
|
||||||
|
|
||||||
// Avalonia configuration, don't remove; also used by visual designer.
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
@ -19,6 +17,5 @@ sealed class Program
|
|||||||
=> AppBuilder.Configure<App>()
|
=> AppBuilder.Configure<App>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
.WithInterFont()
|
.WithInterFont()
|
||||||
.LogToTrace()
|
.LogToTrace();
|
||||||
.UseReactiveUI();
|
|
||||||
}
|
}
|
||||||
|
@ -1,162 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BetterRaid.Misc;
|
|
||||||
using BetterRaid.Models;
|
|
||||||
using BetterRaid.Models.Database;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace BetterRaid.Services;
|
|
||||||
|
|
||||||
public interface IDatabaseService
|
|
||||||
{
|
|
||||||
bool OnlyOnline { get; set; }
|
|
||||||
bool AutoSave { get; set; }
|
|
||||||
BetterRaidDatabase? Database { get; set; }
|
|
||||||
void LoadOrCreate();
|
|
||||||
void LoadFromFile(string path, bool createIfNotExist = false);
|
|
||||||
Task UpdateLoadedChannels();
|
|
||||||
void Save(string? path = null);
|
|
||||||
bool TrySetRaided(TwitchChannel channel, DateTime dateTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DatabaseService : IDatabaseService
|
|
||||||
{
|
|
||||||
private string? _databaseFilePath;
|
|
||||||
private readonly ILogger<DatabaseService> _logger;
|
|
||||||
private readonly ITwitchService _twitch;
|
|
||||||
|
|
||||||
public bool OnlyOnline { get; set; }
|
|
||||||
public bool AutoSave { get; set; }
|
|
||||||
|
|
||||||
public BetterRaidDatabase? Database { get; set; }
|
|
||||||
|
|
||||||
public DatabaseService(ILogger<DatabaseService> logger, ITwitchService twitch)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_twitch = twitch;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadOrCreate()
|
|
||||||
{
|
|
||||||
LoadFromFile(Constants.DatabaseFilePath, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadFromFile(string path, bool createIfNotExist = false)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
|
||||||
|
|
||||||
path = Path.Combine(Environment.CurrentDirectory, path);
|
|
||||||
var exists = File.Exists(path);
|
|
||||||
|
|
||||||
switch (exists)
|
|
||||||
{
|
|
||||||
case false when createIfNotExist == false:
|
|
||||||
throw new FileNotFoundException("Database file not found", path);
|
|
||||||
|
|
||||||
case false when createIfNotExist:
|
|
||||||
_logger.LogWarning("Database file not found, creating new database");
|
|
||||||
|
|
||||||
Database = new BetterRaidDatabase();
|
|
||||||
Save(path);
|
|
||||||
|
|
||||||
_logger.LogDebug("Created new database at {path}", path);
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
case true:
|
|
||||||
var dbStr = File.ReadAllText(path);
|
|
||||||
var dbObj = JsonConvert.DeserializeObject<BetterRaidDatabase>(dbStr);
|
|
||||||
|
|
||||||
_databaseFilePath = path;
|
|
||||||
Database = dbObj ?? throw new JsonException("Failed to read database file");
|
|
||||||
|
|
||||||
_logger.LogDebug("Loaded database from {path}", path);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateLoadedChannels()
|
|
||||||
{
|
|
||||||
if (Database == null || Database.Channels.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await Parallel.ForAsync(0, Database.Channels.Count, (i, c) =>
|
|
||||||
{
|
|
||||||
if (c.IsCancellationRequested)
|
|
||||||
return ValueTask.FromCanceled(c);
|
|
||||||
|
|
||||||
var channel = Database.Channels[i];
|
|
||||||
channel.UpdateChannelData(_twitch);
|
|
||||||
|
|
||||||
return ValueTask.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save(string? path = null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(_databaseFilePath) && string.IsNullOrEmpty(path))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("No target path given to save database at");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(path) == false && string.IsNullOrEmpty(_databaseFilePath))
|
|
||||||
{
|
|
||||||
_databaseFilePath = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dbStr = JsonConvert.SerializeObject(Database, Formatting.Indented);
|
|
||||||
var targetPath = _databaseFilePath!;
|
|
||||||
|
|
||||||
File.WriteAllText(targetPath, dbStr);
|
|
||||||
|
|
||||||
_logger.LogDebug("Saved database to {targetPath}", targetPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddChannel(TwitchChannel channel)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(channel);
|
|
||||||
|
|
||||||
if (Database == null)
|
|
||||||
throw new InvalidOperationException("Database is not loaded");
|
|
||||||
|
|
||||||
if (Database.Channels.Any(c => c.Name?.Equals(c.Name, StringComparison.CurrentCultureIgnoreCase) == true))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Database.Channels.Add(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveChannel(TwitchChannel channel)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(channel);
|
|
||||||
|
|
||||||
if (Database == null)
|
|
||||||
throw new InvalidOperationException("Database is not loaded");
|
|
||||||
|
|
||||||
var index = Database.Channels.FindIndex(c => c.Name?.Equals(c.Name, StringComparison.CurrentCultureIgnoreCase) == true);
|
|
||||||
|
|
||||||
if (index == -1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Database.Channels.RemoveAt(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TrySetRaided(TwitchChannel channel, DateTime dateTime)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(channel);
|
|
||||||
|
|
||||||
if (Database == null)
|
|
||||||
throw new InvalidOperationException("Database is not loaded");
|
|
||||||
|
|
||||||
var twitchChannel = Database.Channels.FirstOrDefault(c => c.Name?.Equals(channel.Name, StringComparison.CurrentCultureIgnoreCase) == true);
|
|
||||||
|
|
||||||
if (twitchChannel == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
twitchChannel.LastRaided = dateTime;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
using BetterRaid.ViewModels;
|
|
||||||
|
|
||||||
namespace BetterRaid.Services;
|
|
||||||
|
|
||||||
public interface IMainViewModelFactory
|
|
||||||
{
|
|
||||||
MainWindowViewModel CreateMainWindowViewModel();
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace BetterRaid.Services;
|
|
||||||
|
|
||||||
public interface ISynchronizaionService
|
|
||||||
{
|
|
||||||
void Invoke(Action action);
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
using Avalonia.Threading;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace BetterRaid.Services.Implementations;
|
|
||||||
public class DispatcherService : ISynchronizaionService
|
|
||||||
{
|
|
||||||
private readonly Dispatcher dispatcher;
|
|
||||||
|
|
||||||
public DispatcherService(Dispatcher dispatcher)
|
|
||||||
{
|
|
||||||
this.dispatcher = dispatcher;
|
|
||||||
}
|
|
||||||
public void Invoke(Action action)
|
|
||||||
{
|
|
||||||
dispatcher.Invoke(action);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
using BetterRaid.ViewModels;
|
|
||||||
|
|
||||||
namespace BetterRaid.Services.Implementations;
|
|
||||||
|
|
||||||
public class MainWindowViewModelFactory// : IMainViewModelFactory
|
|
||||||
{
|
|
||||||
private readonly ITwitchService _twitchService;
|
|
||||||
private readonly ISynchronizaionService _synchronizaionService;
|
|
||||||
|
|
||||||
public MainWindowViewModelFactory(ITwitchService twitchService, ISynchronizaionService synchronizaionService)
|
|
||||||
{
|
|
||||||
_twitchService = twitchService;
|
|
||||||
_synchronizaionService = synchronizaionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
//public MainWindowViewModel CreateMainWindowViewModel()
|
|
||||||
//{
|
|
||||||
// return new MainWindowViewModel(_twitchService, _synchronizaionService);
|
|
||||||
//}
|
|
||||||
}
|
|
@ -1,453 +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 Microsoft.Extensions.Logging;
|
|
||||||
using TwitchLib.Api;
|
|
||||||
using TwitchLib.Api.Helix.Models.Users.GetUsers;
|
|
||||||
using TwitchLib.PubSub;
|
|
||||||
using TwitchLib.PubSub.Events;
|
|
||||||
|
|
||||||
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 EventHandler<EventArgs>? UserLoginChanged;
|
|
||||||
public event EventHandler<TwitchChannel>? TwitchChannelUpdated;
|
|
||||||
public event PropertyChangingEventHandler? PropertyChanging;
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INotifyPropertyChanging
|
|
||||||
{
|
|
||||||
private bool _isRaidStarted;
|
|
||||||
private int _raidParticipants;
|
|
||||||
private TwitchChannel? _userChannel;
|
|
||||||
private User? _user;
|
|
||||||
private readonly ILogger<TwitchService> _logger;
|
|
||||||
private readonly IWebToolsService _webTools;
|
|
||||||
|
|
||||||
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);
|
|
||||||
OnOnUserLoginChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public TwitchAPI TwitchApi { get; }
|
|
||||||
public TwitchPubSub TwitchEvents { get; }
|
|
||||||
|
|
||||||
public int RaidParticipants
|
|
||||||
{
|
|
||||||
get => _raidParticipants;
|
|
||||||
set => SetField(ref _raidParticipants, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler<EventArgs>? UserLoginChanged;
|
|
||||||
public event EventHandler<TwitchChannel>? TwitchChannelUpdated;
|
|
||||||
|
|
||||||
public event EventHandler<OnStreamDownArgs> OnStreamDown
|
|
||||||
{
|
|
||||||
add => TwitchEvents.OnStreamDown += value;
|
|
||||||
remove => TwitchEvents.OnStreamDown -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler<OnStreamUpArgs> OnStreamUp
|
|
||||||
{
|
|
||||||
add => TwitchEvents.OnStreamUp += value;
|
|
||||||
remove => TwitchEvents.OnStreamUp -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TwitchService(ILogger<TwitchService> logger, IWebToolsService webTools)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_webTools = webTools;
|
|
||||||
|
|
||||||
TwitchApi = new TwitchAPI();
|
|
||||||
TwitchEvents = new TwitchPubSub();
|
|
||||||
|
|
||||||
if (TryLoadAccessToken(out var token))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Found access token.");
|
|
||||||
Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token))
|
|
||||||
.ContinueWith(_ => ConnectTwitchEvents());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogInformation("No access token found.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ConnectTwitchEvents()
|
|
||||||
{
|
|
||||||
if (UserChannel == null || User == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logger.LogInformation("Connecting to Twitch Events ...");
|
|
||||||
|
|
||||||
TwitchEvents.OnRaidGo += OnUserRaidGo;
|
|
||||||
TwitchEvents.OnRaidUpdate += OnUserRaidUpdate;
|
|
||||||
TwitchEvents.OnStreamUp += OnUserStreamUp;
|
|
||||||
TwitchEvents.OnStreamDown += OnUserStreamDown;
|
|
||||||
TwitchEvents.OnViewCount += OnViewCount;
|
|
||||||
TwitchEvents.OnLog += OnPubSubLog;
|
|
||||||
TwitchEvents.OnPubSubServiceError += OnPubSubServiceError;
|
|
||||||
TwitchEvents.OnPubSubServiceConnected += OnPubSubServiceConnected;
|
|
||||||
TwitchEvents.OnPubSubServiceClosed += OnPubSubServiceClosed;
|
|
||||||
|
|
||||||
TwitchEvents.ListenToVideoPlayback(UserChannel.BroadcasterId);
|
|
||||||
TwitchEvents.ListenToRaid(UserChannel.BroadcasterId);
|
|
||||||
|
|
||||||
TwitchEvents.Connect();
|
|
||||||
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ConnectApiAsync(string clientId, string accessToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Connecting to Twitch API ...");
|
|
||||||
|
|
||||||
AccessToken = accessToken;
|
|
||||||
|
|
||||||
TwitchApi.Settings.ClientId = clientId;
|
|
||||||
TwitchApi.Settings.AccessToken = accessToken;
|
|
||||||
|
|
||||||
if (TryGetUser(out var user))
|
|
||||||
{
|
|
||||||
User = user;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
User = null;
|
|
||||||
|
|
||||||
_logger.LogError("Could not get user with client id {clientId} - please check your clientId and accessToken", clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryGetUserChannel(out var channel))
|
|
||||||
{
|
|
||||||
UserChannel = channel;
|
|
||||||
_logger.LogInformation("Connected to Twitch API as {channelName} with broadcaster id {channelBroadcasterId}.", channel?.Name, channel?.BroadcasterId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UserChannel = null;
|
|
||||||
|
|
||||||
_logger.LogError("Could not get user channel.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (User == null || UserChannel == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("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)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, "Could not get user.");
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Registering for events for {channelName} with broadcaster id {channelBroadcasterId} ...", channel.Name, channel.BroadcasterId);
|
|
||||||
|
|
||||||
channel.PropertyChanged += OnTwitchChannelUpdated;
|
|
||||||
|
|
||||||
TwitchEvents.OnStreamUp += channel.OnStreamUp;
|
|
||||||
TwitchEvents.OnStreamDown += channel.OnStreamDown;
|
|
||||||
TwitchEvents.OnViewCount += channel.OnViewCount;
|
|
||||||
|
|
||||||
TwitchEvents.ListenToVideoPlayback(channel.Id);
|
|
||||||
|
|
||||||
TwitchEvents.SendTopics(AccessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnregisterFromEvents(TwitchChannel channel)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Unregistering from events for {channelName} with broadcaster id {channelBroadcasterId} ...", channel.Name, channel.BroadcasterId);
|
|
||||||
|
|
||||||
channel.PropertyChanged -= OnTwitchChannelUpdated;
|
|
||||||
|
|
||||||
TwitchEvents.OnStreamUp -= channel.OnStreamUp;
|
|
||||||
TwitchEvents.OnStreamDown -= channel.OnStreamDown;
|
|
||||||
TwitchEvents.OnViewCount -= channel.OnViewCount;
|
|
||||||
|
|
||||||
TwitchEvents.ListenToVideoPlayback(channel.Id);
|
|
||||||
|
|
||||||
TwitchEvents.SendTopics(AccessToken, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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}";
|
|
||||||
|
|
||||||
_webTools.OpenUrl(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPubSubServiceClosed(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("PubSub: Connection closed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPubSubServiceError(object? sender, OnPubSubServiceErrorArgs e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e.Exception, "PubSub: {exception}", e.Exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPubSubLog(object? sender, OnLogArgs e)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("PubSub: {data}", e.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPubSubServiceConnected(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
TwitchEvents.SendTopics(AccessToken);
|
|
||||||
_logger.LogInformation("PubSub: Connected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Not called while raid is ongoing
|
|
||||||
private void OnUserRaidUpdate(object? sender, OnRaidUpdateArgs e)
|
|
||||||
{
|
|
||||||
//if (e.ChannelId != UserChannel?.BroadcasterId)
|
|
||||||
// return;
|
|
||||||
|
|
||||||
RaidParticipants = e.ViewerCount;
|
|
||||||
_logger.LogInformation("Raid participants: {participants}", RaidParticipants);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnViewCount(object? sender, OnViewCountArgs e)
|
|
||||||
{
|
|
||||||
if (UserChannel == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (e.ChannelId != UserChannel.Id)
|
|
||||||
return;
|
|
||||||
|
|
||||||
UserChannel.OnViewCount(sender, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnUserRaidGo(object? sender, OnRaidGoArgs e)
|
|
||||||
{
|
|
||||||
if (e.ChannelId != UserChannel?.Id)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logger.LogInformation("Raid started.");
|
|
||||||
|
|
||||||
IsRaidStarted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnUserStreamDown(object? sender, OnStreamDownArgs e)
|
|
||||||
{
|
|
||||||
if (UserChannel == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (e.ChannelId != UserChannel?.Id)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logger.LogInformation("Stream down.");
|
|
||||||
|
|
||||||
IsRaidStarted = false;
|
|
||||||
|
|
||||||
UserChannel.IsLive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnUserStreamUp(object? sender, OnStreamUpArgs e)
|
|
||||||
{
|
|
||||||
if (UserChannel == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (e.ChannelId != UserChannel?.Id)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logger.LogInformation("Stream up.");
|
|
||||||
|
|
||||||
IsRaidStarted = false;
|
|
||||||
UserChannel.IsLive = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTwitchChannelUpdated(object? sender, PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not TwitchChannel channel)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (e.PropertyName != nameof(TwitchChannel.IsLive))
|
|
||||||
return;
|
|
||||||
|
|
||||||
TwitchChannelUpdated?.Invoke(this, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnOnUserLoginChanged()
|
|
||||||
{
|
|
||||||
UserLoginChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
6
ViewModels/AddChannelWindowViewModel.cs
Normal file
6
ViewModels/AddChannelWindowViewModel.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace BetterRaid.ViewModels;
|
||||||
|
|
||||||
|
public class AddChannelWindowViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
@ -1,103 +1,32 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Reactive.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using BetterRaid.Extensions;
|
using BetterRaid.Extensions;
|
||||||
|
using BetterRaid.Misc;
|
||||||
using BetterRaid.Models;
|
using BetterRaid.Models;
|
||||||
using BetterRaid.Services;
|
|
||||||
using BetterRaid.Views;
|
using BetterRaid.Views;
|
||||||
using DynamicData;
|
|
||||||
using DynamicData.Binding;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace BetterRaid.ViewModels;
|
namespace BetterRaid.ViewModels;
|
||||||
|
|
||||||
public class MainWindowViewModel : ViewModelBase
|
public partial class MainWindowViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly SourceList<TwitchChannel> _sourceList;
|
private string? _filter;
|
||||||
|
|
||||||
private readonly ISynchronizaionService _synchronizationService;
|
private BetterRaidDatabase? _db;
|
||||||
private readonly ILogger<MainWindowViewModel> _logger;
|
|
||||||
private readonly IWebToolsService _webTools;
|
|
||||||
private readonly IDatabaseService _db;
|
|
||||||
private readonly ITwitchService _twitch;
|
|
||||||
|
|
||||||
private string _filter;
|
public BetterRaidDatabase? Database
|
||||||
private bool _onlyOnline;
|
{
|
||||||
private readonly ReadOnlyObservableCollection<TwitchChannel> _filteredChannels;
|
get => _db;
|
||||||
|
set => SetProperty(ref _db, value);
|
||||||
|
}
|
||||||
|
|
||||||
public ITwitchService Twitch => _twitch;
|
public string? Filter
|
||||||
|
|
||||||
public ReadOnlyObservableCollection<TwitchChannel> FilteredChannels => _filteredChannels;
|
|
||||||
|
|
||||||
public string Filter
|
|
||||||
{
|
{
|
||||||
get => _filter;
|
get => _filter;
|
||||||
set
|
set => SetProperty(ref _filter, value);
|
||||||
{
|
|
||||||
this.RaiseAndSetIfChanged(ref _filter, value);
|
|
||||||
|
|
||||||
_sourceList.Edit(innerList =>
|
|
||||||
{
|
|
||||||
if (_db.Database == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
innerList.Clear();
|
|
||||||
innerList.AddRange(_db.Database.Channels);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OnlyOnline
|
public bool IsLoggedIn => App.TwitchApi != null;
|
||||||
{
|
|
||||||
get => _onlyOnline;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
this.RaiseAndSetIfChanged(ref _onlyOnline, value);
|
|
||||||
|
|
||||||
_sourceList.Edit(innerList =>
|
|
||||||
{
|
|
||||||
if (_db.Database == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
innerList.Clear();
|
|
||||||
innerList.AddRange(_db.Database.Channels);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsLoggedIn => _twitch.UserChannel != null;
|
|
||||||
|
|
||||||
public MainWindowViewModel(
|
|
||||||
ILogger<MainWindowViewModel> logger,
|
|
||||||
ITwitchService twitch,
|
|
||||||
IWebToolsService webTools,
|
|
||||||
IDatabaseService db,
|
|
||||||
ISynchronizaionService synchronizationService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_twitch = twitch;
|
|
||||||
_webTools = webTools;
|
|
||||||
_db = db;
|
|
||||||
_synchronizationService = synchronizationService;
|
|
||||||
_filter = string.Empty;
|
|
||||||
|
|
||||||
_twitch.UserLoginChanged += OnUserLoginChanged;
|
|
||||||
|
|
||||||
_sourceList = new SourceList<TwitchChannel>();
|
|
||||||
_sourceList.Connect()
|
|
||||||
.Filter(channel => channel.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.Filter(channel => !OnlyOnline || channel.IsLive)
|
|
||||||
.Sort(SortExpressionComparer<TwitchChannel>.Descending(channel => channel.IsLive))
|
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
|
||||||
.Bind(out _filteredChannels)
|
|
||||||
.Subscribe();
|
|
||||||
|
|
||||||
LoadChannelsFromDb();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ExitApplication()
|
public void ExitApplication()
|
||||||
{
|
{
|
||||||
@ -114,36 +43,13 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
public void LoginWithTwitch()
|
public void LoginWithTwitch()
|
||||||
{
|
{
|
||||||
_webTools.StartOAuthLogin(_twitch, OnTwitchLoginCallback, CancellationToken.None);
|
Tools.StartOAuthLogin(App.TwitchOAuthUrl, OnTwitchLoginCallback, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTwitchLoginCallback()
|
public void OnTwitchLoginCallback()
|
||||||
{
|
{
|
||||||
this.RaisePropertyChanged(nameof(IsLoggedIn));
|
App.InitTwitchClient(overrideToken: true);
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadChannelsFromDb()
|
OnPropertyChanged(nameof(IsLoggedIn));
|
||||||
{
|
|
||||||
if (_db.Database == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("Database is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var channel in _db.Database.Channels)
|
|
||||||
{
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
channel.UpdateChannelData(_twitch);
|
|
||||||
_twitch.RegisterForEvents(channel);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_sourceList.Edit(innerList => innerList.AddRange(_db.Database.Channels));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnUserLoginChanged(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
this.RaisePropertyChanged(nameof(IsLoggedIn));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
195
ViewModels/RaidButtonViewModel.cs
Normal file
195
ViewModels/RaidButtonViewModel.cs
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
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,9 +1,7 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace BetterRaid.ViewModels;
|
namespace BetterRaid.ViewModels;
|
||||||
|
|
||||||
public class ViewModelBase : ReactiveObject
|
public class ViewModelBase : ObservableObject
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
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:vm="clr-namespace:BetterRaid.ViewModels"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="BetterRaid.Views.AboutWindow"
|
x:Class="BetterRaid.Views.AboutWindow"
|
||||||
Title="About"
|
Title="About"
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
namespace BetterRaid.Views;
|
namespace BetterRaid.Views;
|
||||||
|
|
||||||
|
38
Views/AddChannelWindow.axaml
Normal file
38
Views/AddChannelWindow.axaml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<Window 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:vm="using:BetterRaid.ViewModels"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="100" d:DesignHeight="50"
|
||||||
|
x:Class="BetterRaid.Views.AddChannelWindow"
|
||||||
|
x:DataType="vm:AddChannelWindowViewModel"
|
||||||
|
Icon="/Assets/logo.png"
|
||||||
|
Width="200"
|
||||||
|
Height="80"
|
||||||
|
MaxWidth="200"
|
||||||
|
MaxHeight="80"
|
||||||
|
Title="Add Channel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<vm:AddChannelWindowViewModel/>
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical"
|
||||||
|
Margin="5"
|
||||||
|
Spacing="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch">
|
||||||
|
|
||||||
|
<TextBox HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
x:Name="channelNameTxt"
|
||||||
|
Watermark="Enter Channelname" />
|
||||||
|
|
||||||
|
<Button HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="OK"
|
||||||
|
x:Name="okBtn"
|
||||||
|
IsEnabled="True" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
13
Views/AddChannelWindow.axaml.cs
Normal file
13
Views/AddChannelWindow.axaml.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace BetterRaid.Views;
|
||||||
|
|
||||||
|
public partial class AddChannelWindow : Window
|
||||||
|
{
|
||||||
|
public AddChannelWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,9 @@
|
|||||||
<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: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"
|
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="450"
|
||||||
xmlns:misc="using:BetterRaid.Misc"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
d:DesignWidth="600"
|
|
||||||
d:DesignHeight="800"
|
|
||||||
Width="600"
|
Width="600"
|
||||||
Height="800"
|
Height="800"
|
||||||
x:Class="BetterRaid.Views.MainWindow"
|
x:Class="BetterRaid.Views.MainWindow"
|
||||||
@ -17,66 +12,43 @@
|
|||||||
Title="BetterRaid"
|
Title="BetterRaid"
|
||||||
Background="DarkSlateGray">
|
Background="DarkSlateGray">
|
||||||
|
|
||||||
<Window.Resources>
|
<Design.DataContext>
|
||||||
<con:ChannelOnlineColorConverter x:Key="ChannelOnlineColorConverter" />
|
<vm:MainWindowViewModel/>
|
||||||
</Window.Resources>
|
</Design.DataContext>
|
||||||
|
|
||||||
<Grid HorizontalAlignment="Stretch"
|
<Grid HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch">
|
||||||
ColumnDefinitions="Auto,*"
|
|
||||||
RowDefinitions="50,*">
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="0"
|
<Grid.ColumnDefinitions>
|
||||||
Grid.Row="0"
|
<ColumnDefinition Width="*" />
|
||||||
Orientation="Horizontal">
|
<ColumnDefinition Width="Auto" />
|
||||||
<ai:AdvancedImage CornerRadius="20"
|
</Grid.ColumnDefinitions>
|
||||||
Width="40"
|
|
||||||
Height="40"
|
|
||||||
Margin="5"
|
|
||||||
Source="{Binding Twitch.UserChannel.ThumbnailUrl,
|
|
||||||
FallbackValue={x:Static misc:Constants.ChannelPlaceholderImageUrl},
|
|
||||||
TargetNullValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}}" />
|
|
||||||
|
|
||||||
<TextBlock VerticalAlignment="Center"
|
<Grid.RowDefinitions>
|
||||||
Margin="5, 0, 0, 0"
|
<RowDefinition Height="Auto" />
|
||||||
FontWeight="Bold">
|
<RowDefinition Height="*" />
|
||||||
<TextBlock.Text>
|
</Grid.RowDefinitions>
|
||||||
<MultiBinding StringFormat="{}{0} ({1})">
|
|
||||||
<Binding Path="Twitch.UserChannel.DisplayName"
|
<Menu Grid.Column="0"
|
||||||
FallbackValue="-" />
|
Grid.ColumnSpan="2"
|
||||||
<Binding Path="Twitch.UserChannel.ViewerCount"
|
Grid.Row="0">
|
||||||
FallbackValue="Offline"
|
|
||||||
TargetNullValue="Offline" />
|
<MenuItem Header="File">
|
||||||
</MultiBinding>
|
<MenuItem Header="About"
|
||||||
</TextBlock.Text>
|
CommandParameter="{Binding $parent[Window]}"
|
||||||
</TextBlock>
|
Command="{Binding ShowAboutWindow}" />
|
||||||
</StackPanel>
|
<Separator />
|
||||||
|
<MenuItem Header="Exit"
|
||||||
|
Command="{Binding ExitApplication}" />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1"
|
<StackPanel Grid.Column="1"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Spacing="5">
|
Spacing="5">
|
||||||
|
|
||||||
<CheckBox Content="Only Online"
|
<Button Command="{Binding LoginWithTwitch}"
|
||||||
IsChecked="{Binding OnlyOnline, Mode=TwoWay, FallbackValue=False}" />
|
|
||||||
|
|
||||||
<TextBox Width="200"
|
|
||||||
Margin="5, 10, 5, 10"
|
|
||||||
Watermark="Filter Channels"
|
|
||||||
Text="{Binding Filter, Mode=TwoWay}"
|
|
||||||
HorizontalAlignment="Right" />
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Button Grid.Column="0"
|
|
||||||
Grid.ColumnSpan="2"
|
|
||||||
Grid.Row="1"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Width="200"
|
|
||||||
Height="50"
|
|
||||||
Command="{Binding LoginWithTwitch}"
|
|
||||||
IsVisible="{Binding !IsLoggedIn, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
|
IsVisible="{Binding !IsLoggedIn, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
|
||||||
|
|
||||||
<Button.Styles>
|
<Button.Styles>
|
||||||
@ -84,24 +56,45 @@
|
|||||||
<Setter Property="Background" Value="#6441a5" />
|
<Setter Property="Background" Value="#6441a5" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="#6C4CA5" />
|
<Setter Property="Background" Value="#b9a3e3" />
|
||||||
</Style>
|
</Style>
|
||||||
</Button.Styles>
|
</Button.Styles>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="Login with Twitch"
|
<TextBlock Text="Login"
|
||||||
Foreground="#f1f1f1"
|
Foreground="#f1f1f1"
|
||||||
FontSize="18"
|
FontSize="14" />
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<Image Source="avares://BetterRaid/Assets/glitch_flat_white.png"
|
<Image Source="avares://BetterRaid/Assets/glitch_flat_white.png"
|
||||||
Width="24"
|
Width="16"
|
||||||
Height="24"
|
Height="16"
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="5, 0, 0, 0" />
|
Margin="5, 0, 0, 0" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<CheckBox Content="Only Online"
|
||||||
|
IsChecked="{Binding Database.OnlyOnline, Mode=TwoWay}" />
|
||||||
|
|
||||||
|
<TextBox Grid.Column="1"
|
||||||
|
Grid.Row="0"
|
||||||
|
Width="200"
|
||||||
|
Margin="2"
|
||||||
|
Watermark="Filter Channels"
|
||||||
|
Text="{Binding Filter, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Right" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="Please login first!"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
Grid.Row="1"
|
||||||
|
IsVisible="{Binding !IsLoggedIn, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
|
FontSize="24"
|
||||||
|
Foreground="#f1f1f1" />
|
||||||
|
|
||||||
<ScrollViewer Grid.Column="0"
|
<ScrollViewer Grid.Column="0"
|
||||||
Grid.ColumnSpan="2"
|
Grid.ColumnSpan="2"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
@ -115,133 +108,17 @@
|
|||||||
IsScrollInertiaEnabled="True" />
|
IsScrollInertiaEnabled="True" />
|
||||||
</ScrollViewer.GestureRecognizers>
|
</ScrollViewer.GestureRecognizers>
|
||||||
|
|
||||||
<ListBox ItemsSource="{Binding FilteredChannels, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
<Grid HorizontalAlignment="Stretch"
|
||||||
SelectionMode="Single"
|
VerticalAlignment="Stretch"
|
||||||
SelectionChanged="SelectingItemsControl_OnSelectionChanged">
|
x:Name="raidGrid">
|
||||||
|
|
||||||
<ListBox.ItemsPanel>
|
<Grid.ColumnDefinitions>
|
||||||
<ItemsPanelTemplate>
|
<ColumnDefinition Width="*" />
|
||||||
<VirtualizingStackPanel Orientation="Vertical" />
|
<ColumnDefinition Width="*" />
|
||||||
</ItemsPanelTemplate>
|
<ColumnDefinition Width="*" />
|
||||||
</ListBox.ItemsPanel>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Grid ColumnDefinitions="100, *, 120"
|
|
||||||
RowDefinitions="100">
|
|
||||||
|
|
||||||
<ai:AdvancedImage Grid.Column="0"
|
|
||||||
Grid.Row="0"
|
|
||||||
Source="{Binding ThumbnailUrl, TargetNullValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}}" />
|
|
||||||
|
|
||||||
<Border Grid.Column="0"
|
|
||||||
Grid.Row="0"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
Height="25"
|
|
||||||
MinWidth="25"
|
|
||||||
CornerRadius="12.5"
|
|
||||||
Background="{Binding IsLive, Converter={StaticResource ChannelOnlineColorConverter}}"
|
|
||||||
Padding="0"
|
|
||||||
Margin="0, 0, 5, 5">
|
|
||||||
|
|
||||||
<TextBlock Text="{Binding ViewerCount, TargetNullValue='-', Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
FontSize="12"
|
|
||||||
TextAlignment="Center"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Padding="0"
|
|
||||||
Margin="5"
|
|
||||||
Foreground="Black"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Grid Grid.Column="1"
|
|
||||||
Grid.Row="0"
|
|
||||||
Margin="10, 0, 0, 0"
|
|
||||||
ColumnDefinitions="100, *"
|
|
||||||
RowDefinitions="20, 20, 40, 20">
|
|
||||||
<TextBlock Grid.Column="0"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.ColumnSpan="2"
|
|
||||||
FontWeight="Bold"
|
|
||||||
TextDecorations="Underline"
|
|
||||||
Text="{Binding Name, TargetNullValue='???'}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="0"
|
|
||||||
Grid.Row="1"
|
|
||||||
Text="Category:"
|
|
||||||
FontWeight="SemiBold" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="0"
|
|
||||||
Grid.Row="2"
|
|
||||||
Text="Title:"
|
|
||||||
FontWeight="SemiBold" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="0"
|
|
||||||
Grid.Row="3"
|
|
||||||
Text="Last Raided:"
|
|
||||||
FontWeight="SemiBold" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="1"
|
|
||||||
Grid.Row="1"
|
|
||||||
Text="{Binding Category, TargetNullValue='-'}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="1"
|
|
||||||
Grid.Row="2"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Text="{Binding Title, TargetNullValue='-'}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="1"
|
|
||||||
Grid.Row="3"
|
|
||||||
Text="{Binding LastRaided, TargetNullValue='Never Raided'}" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Grid.Column="2"
|
|
||||||
Grid.Row="0"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch">
|
|
||||||
|
|
||||||
<Button Content="Start Raid"
|
|
||||||
Height="50"
|
|
||||||
Margin="0"
|
|
||||||
CornerRadius="0"
|
|
||||||
Background="ForestGreen"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
HorizontalContentAlignment="Center"
|
|
||||||
VerticalContentAlignment="Center"
|
|
||||||
IsEnabled="{Binding IsLive}"
|
|
||||||
IsVisible="{Binding !$parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.IsRaidStarted}"
|
|
||||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.StartRaidCommand}"
|
|
||||||
CommandParameter="{Binding BroadcasterId}" />
|
|
||||||
|
|
||||||
<Button Content="Cancel Raid"
|
|
||||||
Height="50"
|
|
||||||
Margin="0"
|
|
||||||
CornerRadius="0"
|
|
||||||
Background="DarkRed"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
HorizontalContentAlignment="Center"
|
|
||||||
VerticalContentAlignment="Center"
|
|
||||||
IsVisible="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.IsRaidStarted}"
|
|
||||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.StopRaidCommand}" />
|
|
||||||
|
|
||||||
<Button Content="View Channel"
|
|
||||||
Height="50"
|
|
||||||
CornerRadius="0"
|
|
||||||
Margin="0"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
HorizontalContentAlignment="Center"
|
|
||||||
VerticalContentAlignment="Center"
|
|
||||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.OpenChannelCommand}"
|
|
||||||
CommandParameter="{Binding Name}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,19 +1,293 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
|
private ObservableCollection<RaidButtonViewModel> _raidButtonVMs;
|
||||||
|
private RaidButtonViewModel? _znButtonVm;
|
||||||
|
private BackgroundWorker _autoUpdater;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
|
_raidButtonVMs = [];
|
||||||
|
_znButtonVm = null;
|
||||||
|
_autoUpdater = new();
|
||||||
|
|
||||||
|
DataContextChanged += OnDataContextChanged;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
_autoUpdater.WorkerSupportsCancellation = true;
|
||||||
|
_autoUpdater.DoWork += UpdateAllTiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectingItemsControl_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
private void OnDatabaseChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is ListBox listBox)
|
// TODO: Only if new channel was added or existing were removed
|
||||||
|
// InitializeRaidChannels();
|
||||||
|
GenerateRaidGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
listBox.SelectedItems?.Clear();
|
if (DataContext is MainWindowViewModel vm)
|
||||||
|
{
|
||||||
|
var dbPath = Path.Combine(App.BetterRaidDataPath, "db.json");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
vm.Database = BetterRaidDatabase.LoadFromFile(dbPath);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
var db = new BetterRaidDatabase();
|
||||||
|
db.Save(dbPath);
|
||||||
|
|
||||||
|
vm.Database = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.Database.AutoSave = true;
|
||||||
|
vm.Database.PropertyChanged += OnDatabaseChanged;
|
||||||
|
|
||||||
|
vm.PropertyChanged += OnViewModelChanged;
|
||||||
|
|
||||||
|
InitializeRaidChannels();
|
||||||
|
GenerateRaidGrid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnViewModelChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(MainWindowViewModel.Filter))
|
||||||
|
{
|
||||||
|
GenerateRaidGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.PropertyName == nameof(MainWindowViewModel.IsLoggedIn) && DataContext is MainWindowViewModel { IsLoggedIn: true })
|
||||||
|
{
|
||||||
|
InitializeRaidChannels();
|
||||||
|
GenerateRaidGrid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeRaidChannels()
|
||||||
|
{
|
||||||
|
if (DataContext is MainWindowViewModel { IsLoggedIn: false })
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_autoUpdater?.IsBusy == false)
|
||||||
|
{
|
||||||
|
_autoUpdater?.CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_raidButtonVMs.Clear();
|
||||||
|
|
||||||
|
var vm = DataContext as MainWindowViewModel;
|
||||||
|
|
||||||
|
if (vm?.Database == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var channel in vm.Database.Channels)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(channel))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var rbvm = new RaidButtonViewModel(channel)
|
||||||
|
{
|
||||||
|
MainVm = vm
|
||||||
|
};
|
||||||
|
|
||||||
|
_raidButtonVMs.Add(rbvm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (App.HasUserZnSubbed)
|
||||||
|
{
|
||||||
|
_znButtonVm = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_znButtonVm = new RaidButtonViewModel("zionnetworks")
|
||||||
|
{
|
||||||
|
MainVm = vm,
|
||||||
|
HideDeleteButton = true,
|
||||||
|
IsAd = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_autoUpdater?.IsBusy == false)
|
||||||
|
{
|
||||||
|
_autoUpdater?.RunWorkerAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateRaidGrid()
|
||||||
|
{
|
||||||
|
if (DataContext is MainWindowViewModel { IsLoggedIn: false })
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var child in raidGrid.Children)
|
||||||
|
{
|
||||||
|
if (child is Button btn)
|
||||||
|
{
|
||||||
|
btn.Click -= OnAddChannelButtonClicked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raidGrid.Children.Clear();
|
||||||
|
|
||||||
|
var vm = DataContext as MainWindowViewModel;
|
||||||
|
|
||||||
|
if (vm?.Database == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var visibleChannels = _raidButtonVMs.Where(channel =>
|
||||||
|
{
|
||||||
|
var visible = true;
|
||||||
|
if (string.IsNullOrWhiteSpace(vm.Filter) == false)
|
||||||
|
{
|
||||||
|
if (channel.ChannelName.Contains(vm.Filter, StringComparison.OrdinalIgnoreCase) == false)
|
||||||
|
{
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vm.Database.OnlyOnline && channel.Channel?.IsLive == false)
|
||||||
|
{
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return visible;
|
||||||
|
}).OrderByDescending(c => c.Channel?.IsLive).ToList();
|
||||||
|
|
||||||
|
var rows = (int)Math.Ceiling((visibleChannels.Count + (App.HasUserZnSubbed ? 1 : 2)) / 3.0);
|
||||||
|
|
||||||
|
for (var i = 0; i < rows; i++)
|
||||||
|
{
|
||||||
|
raidGrid.RowDefinitions.Add(new RowDefinition(GridLength.Parse("Auto")));
|
||||||
|
}
|
||||||
|
|
||||||
|
var colIndex = App.HasUserZnSubbed ? 0 : 1;
|
||||||
|
var rowIndex = 0;
|
||||||
|
foreach (var channel in visibleChannels)
|
||||||
|
{
|
||||||
|
var btn = new RaidButton
|
||||||
|
{
|
||||||
|
DataContext = channel
|
||||||
|
};
|
||||||
|
|
||||||
|
Grid.SetColumn(btn, colIndex);
|
||||||
|
Grid.SetRow(btn, rowIndex);
|
||||||
|
|
||||||
|
raidGrid.Children.Add(btn);
|
||||||
|
|
||||||
|
colIndex++;
|
||||||
|
if (colIndex % 3 == 0)
|
||||||
|
{
|
||||||
|
colIndex = 0;
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var addButton = new Button
|
||||||
|
{
|
||||||
|
Content = "+",
|
||||||
|
FontSize = 72,
|
||||||
|
Margin = new Avalonia.Thickness(5),
|
||||||
|
MinHeight = 250,
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch,
|
||||||
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch,
|
||||||
|
HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
addButton.Click += OnAddChannelButtonClicked;
|
||||||
|
|
||||||
|
Grid.SetColumn(addButton, colIndex);
|
||||||
|
Grid.SetRow(addButton, rowIndex);
|
||||||
|
|
||||||
|
raidGrid.Children.Add(addButton);
|
||||||
|
|
||||||
|
if (App.HasUserZnSubbed == false)
|
||||||
|
{
|
||||||
|
var znButton = new RaidButton
|
||||||
|
{
|
||||||
|
DataContext = _znButtonVm
|
||||||
|
};
|
||||||
|
|
||||||
|
Grid.SetColumn(znButton, 0);
|
||||||
|
Grid.SetRow(znButton, 0);
|
||||||
|
raidGrid.Children.Add(znButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAddChannelButtonClicked(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dialog = new AddChannelWindow();
|
||||||
|
dialog.CenterToOwner();
|
||||||
|
|
||||||
|
var vm = DataContext as MainWindowViewModel;
|
||||||
|
|
||||||
|
if (vm?.Database == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO Button Command not working, Button remains disabled
|
||||||
|
// This is a dirty workaround
|
||||||
|
dialog.okBtn.Click += (sender, args) => {
|
||||||
|
if (string.IsNullOrWhiteSpace(dialog?.channelNameTxt.Text) == false)
|
||||||
|
{
|
||||||
|
vm.Database.AddChannel(dialog.channelNameTxt.Text);
|
||||||
|
vm.Database.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog?.Close();
|
||||||
|
|
||||||
|
InitializeRaidChannels();
|
||||||
|
GenerateRaidGrid();
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.ShowDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateChannelData()
|
||||||
|
{
|
||||||
|
var loggedIn = Dispatcher.UIThread.Invoke(() => {
|
||||||
|
return (DataContext as MainWindowViewModel)?.IsLoggedIn ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loggedIn == false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var vm in _raidButtonVMs)
|
||||||
|
{
|
||||||
|
Task.Run(vm.GetOrUpdateChannelAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_znButtonVm != null)
|
||||||
|
{
|
||||||
|
Task.Run(_znButtonVm.GetOrUpdateChannelAsync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAllTiles(object? sender, DoWorkEventArgs e)
|
||||||
|
{
|
||||||
|
while (e.Cancel == false)
|
||||||
|
{
|
||||||
|
UpdateChannelData();
|
||||||
|
Task.Delay(App.AutoUpdateDelay).Wait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user