Compare commits

...
This repository has been archived on 2024-09-13. You can view files and clone it, but cannot push or open issues or pull requests.

5 Commits

18 changed files with 344 additions and 366 deletions

1
.gitignore vendored
View File

@ -20,6 +20,7 @@
mono_crash.* mono_crash.*
# Build results # Build results
[Bb]iuld/
[Dd]ebug/ [Dd]ebug/
[Dd]ebugPublic/ [Dd]ebugPublic/
[Rr]elease/ [Rr]elease/

View File

@ -5,140 +5,209 @@ using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using BetterRaid.Services;
using BetterRaid.ViewModels; using BetterRaid.ViewModels;
using BetterRaid.Views; using BetterRaid.Views;
using Microsoft.Extensions.DependencyInjection;
using TwitchLib.Api; using TwitchLib.Api;
namespace BetterRaid; namespace BetterRaid;
public partial class App : Application public partial class App : Application
{ {
internal static TwitchAPI? TwitchApi = null; private readonly ServiceCollection _services = [];
internal static int AutoUpdateDelay = 10_000; private ServiceProvider? _provider;
internal static bool HasUserZnSubbed = false;
internal static string BetterRaidDataPath = ""; private static TwitchAPI? _twitchApi;
internal static string TwitchBroadcasterId = ""; private static bool _hasUserZnSubbed;
internal static string TwitchOAuthAccessToken = ""; private static string _betterRaidDataPath = "";
internal static string TwitchOAuthAccessTokenFilePath = ""; private static string _twitchBroadcasterId = "";
internal static string TokenClientId = "kkxu4jorjrrc5jch1ito5i61hbev2o"; private static string _twitchOAuthAccessToken = "";
internal static readonly string TwitchOAuthRedirectUrl = "http://localhost:9900"; private static string _twitchOAuthAccessTokenFilePath = "";
internal static readonly string TwitchOAuthResponseType = "token"; private const string TokenClientId = "kkxu4jorjrrc5jch1ito5i61hbev2o";
internal static readonly string[] TwitchOAuthScopes = [ private const string TwitchOAuthRedirectUrl = "http://localhost:9900";
private const string TwitchOAuthResponseType = "token";
private static readonly string[] TwitchOAuthScopes = [
"channel:manage:raids", "channel:manage:raids",
"user:read:subscriptions" "user:read:subscriptions"
]; ];
internal static readonly string TwitchOAuthUrl = $"https://id.twitch.tv/oauth2/authorize" internal static readonly string TwitchOAuthUrl = $"https://id.twitch.tv/oauth2/authorize"
+ $"?client_id={TokenClientId}" + $"?client_id={TokenClientId}"
+ "&redirect_uri=http://localhost:9900" + $"&redirect_uri={TwitchOAuthRedirectUrl}"
+ $"&response_type={TwitchOAuthResponseType}" + $"&response_type={TwitchOAuthResponseType}"
+ $"&scope={string.Join("+", TwitchOAuthScopes)}"; + $"&scope={string.Join("+", TwitchOAuthScopes)}";
public const string ChannelPlaceholderImageUrl = "https://cdn.pixabay.com/photo/2018/11/13/22/01/avatar-3814081_1280.png";
public static TwitchAPI? TwitchApi => _twitchApi;
public static bool HasUserZnSubbed => _hasUserZnSubbed;
public IServiceProvider? Provider => _provider;
public static string? TwitchBroadcasterId => _twitchBroadcasterId;
public static string TwitchOAuthAccessToken
{
get => _twitchOAuthAccessToken;
set
{
_twitchOAuthAccessToken = value;
InitTwitchClient(true);
}
}
public override void Initialize() public override void Initialize()
{
InitializeServices();
LoadTwitchToken();
AvaloniaXamlLoader.Load(_provider, this);
}
private void LoadTwitchToken()
{ {
var userHomeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var userHomeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
switch (Environment.OSVersion.Platform) _betterRaidDataPath = Environment.OSVersion.Platform switch
{ {
case PlatformID.Win32NT: PlatformID.Win32NT => Path.Combine(userHomeDir, "AppData", "Roaming", "BetterRaid"),
BetterRaidDataPath = Path.Combine(userHomeDir, "AppData", "Roaming", "BetterRaid"); PlatformID.Unix => Path.Combine(userHomeDir, ".config", "BetterRaid"),
break; PlatformID.MacOSX => Path.Combine(userHomeDir, "Library", "Application Support", "BetterRaid"),
case PlatformID.Unix: _ => throw new PlatformNotSupportedException($"Your platform '{Environment.OSVersion.Platform}' is not supported. Please report this issue here: https://www.github.com/zion-networks/BetterRaid/issues")
BetterRaidDataPath = Path.Combine(userHomeDir, ".config", "BetterRaid"); };
break;
case PlatformID.MacOSX: if (!Directory.Exists(_betterRaidDataPath))
BetterRaidDataPath = Path.Combine(userHomeDir, "Library", "Application Support", "BetterRaid"); {
break; var di = Directory.CreateDirectory(_betterRaidDataPath);
if (di.Exists == false)
{
throw new Exception($"Failed to create directory '{_betterRaidDataPath}'");
}
} }
if (!Directory.Exists(BetterRaidDataPath)) _twitchOAuthAccessTokenFilePath = Path.Combine(_betterRaidDataPath, ".access_token");
Directory.CreateDirectory(BetterRaidDataPath);
TwitchOAuthAccessTokenFilePath = Path.Combine(BetterRaidDataPath, ".access_token"); if (!File.Exists(_twitchOAuthAccessTokenFilePath))
return;
if (File.Exists(TwitchOAuthAccessTokenFilePath)) _twitchOAuthAccessToken = File.ReadAllText(_twitchOAuthAccessTokenFilePath);
{
TwitchOAuthAccessToken = File.ReadAllText(TwitchOAuthAccessTokenFilePath);
InitTwitchClient(); InitTwitchClient();
} }
AvaloniaXamlLoader.Load(this); private void InitializeServices()
{
_services.AddSingleton<ITwitchDataService, TwitchDataService>();
_services.AddTransient<MainWindowViewModel>();
_services.AddTransient<AboutWindowViewModel>();
_provider = _services.BuildServiceProvider();
} }
public static void InitTwitchClient(bool overrideToken = false) public static void InitTwitchClient(bool overrideToken = false)
{ {
Console.WriteLine("[INFO] Initializing Twitch Client..."); Console.WriteLine("[INFO] Initializing Twitch Client...");
TwitchApi = new TwitchAPI(); if (string.IsNullOrEmpty(_twitchOAuthAccessToken))
TwitchApi.Settings.ClientId = TokenClientId; {
TwitchApi.Settings.AccessToken = TwitchOAuthAccessToken; Console.WriteLine("[ERROR] Failed to initialize Twitch Client: Access Token is empty!");
return;
}
_twitchApi = new TwitchAPI
{
Settings =
{
ClientId = TokenClientId,
AccessToken = _twitchOAuthAccessToken
}
};
Console.WriteLine("[INFO] Testing Twitch API connection..."); Console.WriteLine("[INFO] Testing Twitch API connection...");
var user = TwitchApi.Helix.Users.GetUsersAsync().Result.Users.FirstOrDefault(); var user = _twitchApi.Helix.Users.GetUsersAsync().Result.Users.FirstOrDefault();
if (user == null) if (user == null)
{ {
TwitchApi = null; _twitchApi = null;
Console.WriteLine("[ERROR] Failed to connect to Twitch API!"); Console.WriteLine("[ERROR] Failed to connect to Twitch API!");
return; return;
} }
var channel = TwitchApi.Helix.Search var channel = _twitchApi.Helix.Search
.SearchChannelsAsync(user.Login).Result.Channels .SearchChannelsAsync(user.Login).Result.Channels
.FirstOrDefault(c => c.BroadcasterLogin == user.Login); .FirstOrDefault(c => c.BroadcasterLogin == user.Login);
var userSubs = TwitchApi.Helix.Subscriptions.CheckUserSubscriptionAsync( var userSubs = _twitchApi.Helix.Subscriptions.CheckUserSubscriptionAsync(
userId: user.Id, userId: user.Id,
broadcasterId: "1120558409" broadcasterId: "1120558409"
).Result.Data; ).Result.Data;
if (userSubs.Length > 0 && userSubs.Any(s => s.BroadcasterId == "1120558409")) if (userSubs is { Length: > 0 } && userSubs.Any(s => s.BroadcasterId == "1120558409"))
{ {
HasUserZnSubbed = true; _hasUserZnSubbed = true;
} }
if (channel == null) if (channel == null)
{ {
Console.WriteLine("[ERROR] User channel could not be found!"); Console.WriteLine($"[ERROR] Failed to get channel information for '{user.Login}'!");
return; return;
} }
TwitchBroadcasterId = channel.Id; _twitchBroadcasterId = channel.Id;
System.Console.WriteLine(TwitchBroadcasterId); Console.WriteLine(_twitchBroadcasterId);
Console.WriteLine("[INFO] Connected to Twitch API as '{0}'!", user.DisplayName); Console.WriteLine("[INFO] Connected to Twitch API as '{0}'!", user.DisplayName);
if (overrideToken) if (!overrideToken)
{ return;
File.WriteAllText(TwitchOAuthAccessTokenFilePath, TwitchOAuthAccessToken);
File.WriteAllText(_twitchOAuthAccessTokenFilePath, _twitchOAuthAccessToken);
switch (Environment.OSVersion.Platform) switch (Environment.OSVersion.Platform)
{ {
case PlatformID.Win32NT: case PlatformID.Win32NT:
File.SetAttributes(TwitchOAuthAccessTokenFilePath, File.GetAttributes(TwitchOAuthAccessTokenFilePath) | FileAttributes.Hidden); File.SetAttributes(_twitchOAuthAccessTokenFilePath, File.GetAttributes(_twitchOAuthAccessTokenFilePath) | FileAttributes.Hidden);
break; break;
case PlatformID.Unix: case PlatformID.Unix:
#pragma warning disable CA1416 // Validate platform compatibility #pragma warning disable CA1416 // Validate platform compatibility
File.SetUnixFileMode(TwitchOAuthAccessTokenFilePath, UnixFileMode.UserRead); File.SetUnixFileMode(_twitchOAuthAccessTokenFilePath, UnixFileMode.UserRead);
#pragma warning restore CA1416 // Validate platform compatibility #pragma warning restore CA1416 // Validate platform compatibility
break; break;
case PlatformID.MacOSX: case PlatformID.MacOSX:
File.SetAttributes(TwitchOAuthAccessTokenFilePath, File.GetAttributes(TwitchOAuthAccessTokenFilePath) | FileAttributes.Hidden); File.SetAttributes(_twitchOAuthAccessTokenFilePath, File.GetAttributes(_twitchOAuthAccessTokenFilePath) | FileAttributes.Hidden);
break; break;
}
default:
throw new PlatformNotSupportedException($"Your platform '{Environment.OSVersion.Platform}' is not supported. Please report this issue here: https://www.github.com/zion-networks/BetterRaid/issues");
} }
} }
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) BindingPlugins.DataValidators.RemoveAt(0);
var vm = _provider?.GetRequiredService<MainWindowViewModel>();
switch (ApplicationLifetime)
{ {
case IClassicDesktopStyleApplicationLifetime desktop:
// Line below is needed to remove Avalonia data validation. // Line below is needed to remove Avalonia data validation.
// Without this line you will get duplicate validations from both Avalonia and CT // Without this line you will get duplicate validations from both Avalonia and CT
BindingPlugins.DataValidators.RemoveAt(0);
desktop.MainWindow = new MainWindow desktop.MainWindow = new MainWindow
{ {
DataContext = new MainWindowViewModel(), DataContext = vm
}; };
break;
case ISingleViewApplicationLifetime singleViewPlatform:
singleViewPlatform.MainView = new MainWindow
{
DataContext = vm
};
break;
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();

View File

@ -27,6 +27,7 @@
<!--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.0" /> <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="TwitchLib" Version="3.5.3" /> <PackageReference Include="TwitchLib" Version="3.5.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.5.002.0 VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{77D4100D-424A-4E36-BFF2-14A40F217605}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{49E459C8-9DCF-4D6D-95FC-75303243F248}"
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
{77D4100D-424A-4E36-BFF2-14A40F217605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {49E459C8-9DCF-4D6D-95FC-75303243F248}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77D4100D-424A-4E36-BFF2-14A40F217605}.Debug|Any CPU.Build.0 = Debug|Any CPU {49E459C8-9DCF-4D6D-95FC-75303243F248}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77D4100D-424A-4E36-BFF2-14A40F217605}.Release|Any CPU.ActiveCfg = Release|Any CPU {49E459C8-9DCF-4D6D-95FC-75303243F248}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77D4100D-424A-4E36-BFF2-14A40F217605}.Release|Any CPU.Build.0 = Release|Any CPU {49E459C8-9DCF-4D6D-95FC-75303243F248}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

Binary file not shown.

Binary file not shown.

View File

@ -1,11 +1,22 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Microsoft.Extensions.DependencyInjection;
namespace BetterRaid.Extensions; namespace BetterRaid.Extensions;
public static class DataContextExtensions public static class DataContextExtensions
{ {
public static T? GetDataContextAs<T>(this T obj) where T : Window public static T? GetDataContextAs<T>(this T obj) where T : StyledElement
{ {
return obj.DataContext as T; return obj.DataContext as T;
} }
public static void InjectDataContext<T>(this StyledElement e) where T : class
{
if (Application.Current is not App { Provider: not null } app)
return;
var vm = app.Provider.GetRequiredService<T>();
e.DataContext = vm;
}
} }

View File

@ -1,6 +1,6 @@
using System;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia.Threading;
namespace BetterRaid.Models; namespace BetterRaid.Models;
@ -12,6 +12,8 @@ public class TwitchChannel : INotifyPropertyChanged
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? BroadcasterId public string? BroadcasterId
{ {
@ -94,6 +96,32 @@ public class TwitchChannel : INotifyPropertyChanged
} }
} }
public string? Title
{
get => _title;
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) public TwitchChannel(string channelName)
{ {
Name = channelName; Name = channelName;

View File

@ -9,7 +9,8 @@ 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) => BuildAvaloniaApp() public static void Main(string[] args) =>
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.

View File

@ -0,0 +1,6 @@
namespace BetterRaid.Services;
public interface ITwitchDataService
{
}

View File

@ -0,0 +1,6 @@
namespace BetterRaid.Services;
public class TwitchDataService : ITwitchDataService
{
}

View File

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

View File

@ -1,6 +1,11 @@
using System;
namespace BetterRaid.ViewModels; namespace BetterRaid.ViewModels;
public class AddChannelWindowViewModel : ViewModelBase public class AddChannelWindowViewModel : ViewModelBase
{ {
public AddChannelWindowViewModel()
{
Console.WriteLine("[DEBUG] AddChannelWindowViewModel created");
}
} }

View File

@ -1,9 +1,12 @@
using System; using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading; using System.Threading;
using Avalonia.Controls; using Avalonia.Controls;
using BetterRaid.Extensions; using BetterRaid.Extensions;
using BetterRaid.Misc; using BetterRaid.Misc;
using BetterRaid.Models; using BetterRaid.Models;
using BetterRaid.Services;
using BetterRaid.Views; using BetterRaid.Views;
namespace BetterRaid.ViewModels; namespace BetterRaid.ViewModels;
@ -11,13 +14,25 @@ namespace BetterRaid.ViewModels;
public partial class MainWindowViewModel : ViewModelBase public partial class MainWindowViewModel : ViewModelBase
{ {
private string? _filter; private string? _filter;
private ObservableCollection<TwitchChannel> _channels = [];
private BetterRaidDatabase? _db; private BetterRaidDatabase? _db;
public BetterRaidDatabase? Database public BetterRaidDatabase? Database
{ {
get => _db; get => _db;
set => SetProperty(ref _db, value); set
{
if (SetProperty(ref _db, value) && _db != null)
{
LoadChannelsFromDb();
}
}
}
public ObservableCollection<TwitchChannel> Channels
{
get => _channels;
set => SetProperty(ref _channels, value);
} }
public string? Filter public string? Filter
@ -28,6 +43,12 @@ public partial class MainWindowViewModel : ViewModelBase
public bool IsLoggedIn => App.TwitchApi != null; public bool IsLoggedIn => App.TwitchApi != null;
public MainWindowViewModel(ITwitchDataService t)
{
Console.WriteLine(t);
Console.WriteLine("[DEBUG] MainWindowViewModel created");
}
public void ExitApplication() public void ExitApplication()
{ {
//TODO polish later //TODO polish later
@ -37,6 +58,7 @@ public partial class MainWindowViewModel : ViewModelBase
public void ShowAboutWindow(Window owner) public void ShowAboutWindow(Window owner)
{ {
var about = new AboutWindow(); var about = new AboutWindow();
about.InjectDataContext<AboutWindowViewModel>();
about.ShowDialog(owner); about.ShowDialog(owner);
about.CenterToOwner(); about.CenterToOwner();
} }
@ -46,10 +68,27 @@ public partial class MainWindowViewModel : ViewModelBase
Tools.StartOAuthLogin(App.TwitchOAuthUrl, OnTwitchLoginCallback, CancellationToken.None); Tools.StartOAuthLogin(App.TwitchOAuthUrl, OnTwitchLoginCallback, CancellationToken.None);
} }
public void OnTwitchLoginCallback() private void OnTwitchLoginCallback()
{ {
App.InitTwitchClient(overrideToken: true);
OnPropertyChanged(nameof(IsLoggedIn)); OnPropertyChanged(nameof(IsLoggedIn));
} }
private void LoadChannelsFromDb()
{
if (_db == null)
{
return;
}
Channels.Clear();
var channels = _db.Channels
.Select(channelName => new TwitchChannel(channelName))
.ToList();
foreach (var c in channels)
{
Channels.Add(c);
}
}
} }

View File

@ -4,4 +4,5 @@ namespace BetterRaid.ViewModels;
public class ViewModelBase : ObservableObject public class ViewModelBase : ObservableObject
{ {
} }

View File

@ -2,8 +2,10 @@
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"
x:DataType="vm:AboutWindowViewModel"
Title="About" Title="About"
MaxWidth="300" MaxWidth="300"
MinWidth="300" MinWidth="300"

View File

@ -1,9 +1,11 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:BetterRaid.ViewModels" xmlns:vm="using:BetterRaid.ViewModels"
xmlns:br="using:BetterRaid"
xmlns: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"
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="450" xmlns:ai="using:AsyncImageLoader"
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"
@ -108,17 +110,81 @@
IsScrollInertiaEnabled="True" /> IsScrollInertiaEnabled="True" />
</ScrollViewer.GestureRecognizers> </ScrollViewer.GestureRecognizers>
<Grid HorizontalAlignment="Stretch" <ListBox ItemsSource="{Binding Channels}">
VerticalAlignment="Stretch" <ListBox.ItemTemplate>
x:Name="raidGrid"> <DataTemplate>
<Grid ColumnDefinitions="200,*"
RowDefinitions="200">
<Grid.ColumnDefinitions> <ai:AdvancedImage Grid.Column="0"
<ColumnDefinition Width="*" /> Grid.Row="0"
<ColumnDefinition Width="*" /> Source="{Binding ThumbnailUrl, TargetNullValue={x:Static br:App.ChannelPlaceholderImageUrl}}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
Grid.Row="0"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Height="20"
MinWidth="20"
CornerRadius="10"
Background="#FFFFFF"
Padding="3"
Margin="0, 0, 10, 10">
<TextBlock Text="{Binding ViewerCount, TargetNullValue='-'}"/>
</Border>
<Grid Grid.Column="1"
Grid.Row="0"
ColumnDefinitions="30*, 70*"
RowDefinitions="40, 40, 40, 40, 40">
<TextBlock Grid.Column="0"
Grid.Row="0"
Grid.ColumnSpan="2"
FontWeight="Bold"
TextDecorations="Underline"
Text="{Binding DisplayName, 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="0"
Grid.Row="4"
Text=""
FontWeight="SemiBold" />
<TextBlock Grid.Column="1"
Grid.Row="1"
Text="{Binding Category, TargetNullValue='-'}" />
<TextBlock Grid.Column="1"
Grid.Row="2"
Text="{Binding Title, TargetNullValue='-'}" />
<TextBlock Grid.Column="1"
Grid.Row="3"
Text="{Binding LastRaided, TargetNullValue='Never Raided'}" />
<TextBlock Grid.Column="1"
Grid.Row="4"
Text="" />
</Grid> </Grid>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>

View File

@ -15,279 +15,8 @@ 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 OnDatabaseChanged(object? sender, PropertyChangedEventArgs e)
{
// TODO: Only if new channel was added or existing were removed
// InitializeRaidChannels();
GenerateRaidGrid();
}
private void OnDataContextChanged(object? sender, EventArgs e)
{
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();
}
} }
} }