diff --git a/App.axaml.cs b/App.axaml.cs index 22f740a..ddfd03b 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; @@ -6,27 +7,69 @@ using Avalonia.Markup.Xaml; using Avalonia.Threading; using BetterRaid.Services; using BetterRaid.Services.Implementations; +using BetterRaid.ViewModels; using BetterRaid.Views; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace BetterRaid; public class App : Application { private ServiceProvider? _serviceProvider; + private ILogger? _logger; public override void Initialize() { _serviceProvider = InitializeServices(); + _logger = _serviceProvider.GetRequiredService>(); + + if (TryLoadDatabase() == false) + { + _logger?.LogError("Failed to load or initialize database"); + + Environment.Exit(1); + } + AvaloniaXamlLoader.Load(_serviceProvider, this); } + private bool TryLoadDatabase() + { + if (_serviceProvider == null) + { + throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null"); + } + + var db = _serviceProvider.GetRequiredService(); + + try + { + db.LoadOrCreate(); + Task.Run(db.UpdateLoadedChannels); + + return true; + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to load database"); + return false; + } + } + private ServiceProvider InitializeServices() { var services = new ServiceCollection(); + services.AddLogging(logging => + { + logging.SetMinimumLevel(LogLevel.Debug); + logging.AddConsole(); + }); services.AddSingleton(); - services.AddSingleton(serviceProvider => new DispatcherService(Dispatcher.UIThread)); - services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(_ => new DispatcherService(Dispatcher.UIThread)); + services.AddTransient(); return services.BuildServiceProvider(); } @@ -40,17 +83,16 @@ public class App : Application throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null"); } - var viewModelFactory = _serviceProvider.GetRequiredService(); - var mainWindowViewModel = viewModelFactory.CreateMainWindowViewModel(); var mainWindow = new MainWindow { - DataContext = mainWindowViewModel + DataContext = _serviceProvider.GetRequiredService() }; switch (ApplicationLifetime) { case IClassicDesktopStyleApplicationLifetime desktop: desktop.MainWindow = mainWindow; + desktop.Exit += OnDesktopOnExit; break; case ISingleViewApplicationLifetime singleViewPlatform: @@ -60,4 +102,22 @@ public class App : Application base.OnFrameworkInitializationCompleted(); } + + private void OnDesktopOnExit(object? o, ControlledApplicationLifetimeExitEventArgs controlledApplicationLifetimeExitEventArgs) + { + if (_serviceProvider == null) + { + throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null"); + } + + try + { + var db = _serviceProvider.GetRequiredService(); + db.Save(); + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to save database"); + } + } } \ No newline at end of file diff --git a/BetterRaid.csproj b/BetterRaid.csproj index edd60cd..1b87177 100644 --- a/BetterRaid.csproj +++ b/BetterRaid.csproj @@ -27,6 +27,9 @@ + + + diff --git a/Models/BetterRaidDatabase.cs b/Models/BetterRaidDatabase.cs deleted file mode 100644 index c525170..0000000 --- a/Models/BetterRaidDatabase.cs +++ /dev/null @@ -1,149 +0,0 @@ -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 Channels { get; set; } = []; - public Dictionary 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(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(); - } - } -} \ No newline at end of file diff --git a/Models/Database/BetterRaidDatabase.cs b/Models/Database/BetterRaidDatabase.cs new file mode 100644 index 0000000..8e29049 --- /dev/null +++ b/Models/Database/BetterRaidDatabase.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace BetterRaid.Models.Database; + +[JsonObject] +public class BetterRaidDatabase +{ + public bool OnlyOnline { get; set; } + public List Channels { get; set; } = []; +} \ No newline at end of file diff --git a/Models/TwitchChannel.cs b/Models/TwitchChannel.cs index 2ed262a..0b839a7 100644 --- a/Models/TwitchChannel.cs +++ b/Models/TwitchChannel.cs @@ -3,12 +3,15 @@ using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using BetterRaid.Services; +using Newtonsoft.Json; using TwitchLib.PubSub.Events; namespace BetterRaid.Models; +[JsonObject] public class TwitchChannel : INotifyPropertyChanged { + private string? _id; private string? _broadcasterId; private string? _viewerCount; private bool _isLive; @@ -18,7 +21,6 @@ public class TwitchChannel : INotifyPropertyChanged private string? _category; private string? _title; private DateTime? _lastRaided; - private string? _id; public string? Id { @@ -59,6 +61,7 @@ public class TwitchChannel : INotifyPropertyChanged } } + [JsonIgnore] public bool IsLive { get => _isLive; @@ -72,6 +75,7 @@ public class TwitchChannel : INotifyPropertyChanged } } + [JsonIgnore] public string? ViewerCount { get => _viewerCount; @@ -111,6 +115,7 @@ public class TwitchChannel : INotifyPropertyChanged } } + [JsonIgnore] public string? Category { get => _category; @@ -124,6 +129,7 @@ public class TwitchChannel : INotifyPropertyChanged } } + [JsonIgnore] public string? Title { get => _title; diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs new file mode 100644 index 0000000..a60ecf1 --- /dev/null +++ b/Services/DatabaseService.cs @@ -0,0 +1,162 @@ +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 _logger; + private readonly ITwitchService _twitch; + + public bool OnlyOnline { get; set; } + public bool AutoSave { get; set; } + + public BetterRaidDatabase? Database { get; set; } + + public DatabaseService(ILogger 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(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; + } +} \ No newline at end of file diff --git a/Services/Implementations/MainWindowViewModelFactory.cs b/Services/Implementations/MainWindowViewModelFactory.cs index 24cd545..768a591 100644 --- a/Services/Implementations/MainWindowViewModelFactory.cs +++ b/Services/Implementations/MainWindowViewModelFactory.cs @@ -2,7 +2,7 @@ namespace BetterRaid.Services.Implementations; -public class MainWindowViewModelFactory : IMainViewModelFactory +public class MainWindowViewModelFactory// : IMainViewModelFactory { private readonly ITwitchService _twitchService; private readonly ISynchronizaionService _synchronizaionService; @@ -13,8 +13,8 @@ public class MainWindowViewModelFactory : IMainViewModelFactory _synchronizaionService = synchronizaionService; } - public MainWindowViewModel CreateMainWindowViewModel() - { - return new MainWindowViewModel(_twitchService, _synchronizaionService); - } + //public MainWindowViewModel CreateMainWindowViewModel() + //{ + // return new MainWindowViewModel(_twitchService, _synchronizaionService); + //} } diff --git a/Services/TwitchService.cs b/Services/TwitchService.cs index a2c8fe3..17a4144 100644 --- a/Services/TwitchService.cs +++ b/Services/TwitchService.cs @@ -6,6 +6,7 @@ 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; @@ -40,8 +41,9 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot private bool _isRaidStarted; private int _raidParticipants; private TwitchChannel? _userChannel; - private readonly List _registeredChannels; private User? _user; + private readonly ILogger _logger; + private readonly IWebToolsService _webTools; public string AccessToken { get; private set; } = string.Empty; @@ -80,46 +82,46 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot set => SetField(ref _raidParticipants, value); } - public TwitchService() + public TwitchService(ILogger logger, IWebToolsService webTools) { - _registeredChannels = []; + _logger = logger; + _webTools = webTools; TwitchApi = new TwitchAPI(); TwitchEvents = new TwitchPubSub(); if (TryLoadAccessToken(out var token)) { - Console.WriteLine($"[INFO][{nameof(TwitchService)}] Found access token."); + _logger.LogInformation("Found access token."); Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token)) - .ContinueWith(_ => ConnectTwitchEvents(token)); + .ContinueWith(_ => ConnectTwitchEvents()); } else { - Console.WriteLine($"[INFO][{nameof(TwitchService)}] No access token found."); + _logger.LogInformation("No access token found."); } } - private async Task ConnectTwitchEvents(string token) + private async Task ConnectTwitchEvents() { if (UserChannel == null || User == null) return; - Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connecting to Twitch Events ..."); + _logger.LogInformation("Connecting to Twitch Events ..."); TwitchEvents.OnRaidGo += OnUserRaidGo; TwitchEvents.OnRaidUpdate += OnUserRaidUpdate; TwitchEvents.OnStreamUp += OnUserStreamUp; TwitchEvents.OnStreamDown += OnUserStreamDown; TwitchEvents.OnViewCount += OnViewCount; - TwitchEvents.OnLog += (sender, args) => Console.WriteLine($"[INFO][{nameof(TwitchService)}] {args.Data}"); - TwitchEvents.OnPubSubServiceError += (sender, args) => Console.WriteLine($"[ERROR][{nameof(TwitchService)}] {args.Exception.Message}"); - TwitchEvents.OnPubSubServiceConnected += (sender, args) => Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connected to Twitch PubSub."); - TwitchEvents.OnPubSubServiceClosed += (sender, args) => Console.WriteLine($"[INFO][{nameof(TwitchService)}] Disconnected from Twitch PubSub."); + TwitchEvents.OnLog += OnPubSubLog; + TwitchEvents.OnPubSubServiceError += OnPubSubServiceError; + TwitchEvents.OnPubSubServiceConnected += OnPubSubServiceConnected; + TwitchEvents.OnPubSubServiceClosed += OnPubSubServiceClosed; TwitchEvents.ListenToVideoPlayback(UserChannel.BroadcasterId); TwitchEvents.ListenToRaid(UserChannel.BroadcasterId); - TwitchEvents.SendTopics(token); TwitchEvents.Connect(); await Task.CompletedTask; @@ -127,7 +129,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot public async Task ConnectApiAsync(string clientId, string accessToken) { - Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connecting to Twitch API ..."); + _logger.LogInformation("Connecting to Twitch API ..."); AccessToken = accessToken; @@ -142,24 +144,24 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot { User = null; - Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not get user."); + _logger.LogError("Could not get user with client id {clientId} - please check your clientId and accessToken", clientId); } if (TryGetUserChannel(out var channel)) { UserChannel = channel; - Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connected to Twitch API as {channel?.Name}."); + _logger.LogInformation("Connected to Twitch API as {channelName} with broadcaster id {channelBroadcasterId}.", channel?.Name, channel?.BroadcasterId); } else { UserChannel = null; - Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not get user channel."); + _logger.LogError("Could not get user channel."); } if (User == null || UserChannel == null) { - Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not connect to Twitch API."); + _logger.LogError("Could not connect to Twitch API."); } await Task.CompletedTask; @@ -199,7 +201,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot } catch (Exception e) { - Console.WriteLine($"[ERROR][{nameof(TwitchService)}] {e.Message}"); + _logger.LogError(e, "Could not get user."); return false; } } @@ -218,7 +220,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot public void RegisterForEvents(TwitchChannel channel) { - Console.WriteLine($"[DEBUG][{nameof(TwitchService)}] Registering for events for {channel.Name} ..."); + _logger.LogDebug("Registering for events for {channelName} with broadcaster id {channelBroadcasterId} ...", channel.Name, channel.BroadcasterId); TwitchEvents.OnStreamUp += channel.OnStreamUp; TwitchEvents.OnStreamDown += channel.OnStreamDown; @@ -227,12 +229,12 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot TwitchEvents.ListenToVideoPlayback(channel.Id); TwitchEvents.SendTopics(AccessToken); - - _registeredChannels.Add(channel); } public void UnregisterFromEvents(TwitchChannel channel) { + _logger.LogDebug("Unregistering from events for {channelName} with broadcaster id {channelBroadcasterId} ...", channel.Name, channel.BroadcasterId); + TwitchEvents.OnStreamUp -= channel.OnStreamUp; TwitchEvents.OnStreamDown -= channel.OnStreamDown; TwitchEvents.OnViewCount -= channel.OnViewCount; @@ -240,8 +242,6 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot TwitchEvents.ListenToVideoPlayback(channel.Id); TwitchEvents.SendTopics(AccessToken, true); - - _registeredChannels.Remove(channel); } public string GetOAuthUrl() @@ -304,7 +304,28 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot var url = $"https://twitch.tv/{channelName}"; - Tools.OpenUrl(url); + _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 @@ -314,7 +335,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot // return; RaidParticipants = e.ViewerCount; - Console.WriteLine($"[INFO][{nameof(TwitchService)}] Raid participants: {RaidParticipants}"); + _logger.LogInformation("Raid participants: {participants}", RaidParticipants); } private void OnViewCount(object? sender, OnViewCountArgs e) @@ -333,7 +354,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot if (e.ChannelId != UserChannel?.Id) return; - Console.WriteLine($"[INFO][{nameof(TwitchService)}] Raid started."); + _logger.LogInformation("Raid started."); IsRaidStarted = false; } @@ -346,7 +367,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot if (e.ChannelId != UserChannel?.Id) return; - Console.WriteLine($"[INFO][{nameof(TwitchService)}] Stream down."); + _logger.LogInformation("Stream down."); IsRaidStarted = false; @@ -361,10 +382,9 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot if (e.ChannelId != UserChannel?.Id) return; - Console.WriteLine($"[INFO][{nameof(TwitchService)}] Stream up."); + _logger.LogInformation("Stream up."); IsRaidStarted = false; - UserChannel.IsLive = true; } diff --git a/Misc/Tools.cs b/Services/WebToolsService.cs similarity index 74% rename from Misc/Tools.cs rename to Services/WebToolsService.cs index f53d550..0491d26 100644 --- a/Misc/Tools.cs +++ b/Services/WebToolsService.cs @@ -6,16 +6,29 @@ using System.Text; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; -using BetterRaid.Services; +using BetterRaid.Misc; +using Microsoft.Extensions.Logging; -namespace BetterRaid.Misc; +namespace BetterRaid.Services; -public static class Tools +public interface IWebToolsService { - private static HttpListener? _oauthListener; + void StartOAuthLogin(ITwitchService twitch, Action? callback = null, CancellationToken token = default); + void OpenUrl(string url); +} + +public class WebToolsService : IWebToolsService +{ + private HttpListener? _oauthListener; + private readonly ILogger _logger; + + public WebToolsService(ILogger logger) + { + _logger = logger; + } // Source: https://stackoverflow.com/a/43232486 - public static void StartOAuthLogin(ITwitchService twitchService, Action? callback = null, CancellationToken token = default) + public void StartOAuthLogin(ITwitchService twitch, Action? callback = null, CancellationToken token = default) { if (_oauthListener == null) { @@ -23,13 +36,13 @@ public static class Tools _oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/"); _oauthListener.Start(); - Task.Run(() => WaitForCallback(callback, token, twitchService), token); + Task.Run(() => WaitForCallback(twitch, callback, token), token); } - OpenUrl(twitchService.GetOAuthUrl()); + OpenUrl(twitch.GetOAuthUrl()); } - private static async Task WaitForCallback(Action? callback, CancellationToken token, ITwitchService twitchService) + private async Task WaitForCallback(ITwitchService twitch, Action? callback, CancellationToken token) { if (_oauthListener == null) return; @@ -37,7 +50,7 @@ public static class Tools if (token.IsCancellationRequested) return; - Console.WriteLine("Starting token listener"); + _logger.LogDebug("Starting token listener"); while (!token.IsCancellationRequested) { @@ -48,7 +61,7 @@ public static class Tools if (req.Url == null) continue; - Console.WriteLine("{0} {1}", req.HttpMethod, req.Url); + _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 @@ -64,7 +77,7 @@ public static class Tools req.InputStream.Close(); - Console.WriteLine(data.ToString()); + _logger.LogTrace("{data}", data); res.StatusCode = 200; await res.OutputStream.WriteAsync(Encoding.UTF8.GetBytes(OAUTH_CLIENT_DOCUMENT).AsMemory(0, OAUTH_CLIENT_DOCUMENT.Length), token); @@ -88,8 +101,8 @@ public static class Tools if (jsonData == null) { - Console.WriteLine("[ERROR] Failed to parse JSON data:"); - Console.WriteLine(json); + _logger.LogError("Failed to parse JSON data:"); + _logger.LogError("{json}", json); res.StatusCode = 400; res.Close(); @@ -98,21 +111,21 @@ public static class Tools if (jsonData["access_token"] == null) { - Console.WriteLine("[ERROR] Missing access_token in JSON data."); + _logger.LogError("Missing access_token in JSON data."); res.StatusCode = 400; res.Close(); continue; } - var accessToken = jsonData["access_token"]?.ToString(); + var accessToken = jsonData["access_token"]?.ToString()!; - twitchService.ConnectApiAsync(Constants.TwitchClientId, accessToken!); + await twitch.ConnectApiAsync(Constants.TwitchClientId, accessToken); res.StatusCode = 200; res.Close(); - Console.WriteLine("[INFO] Received access token!"); + _logger.LogInformation("Received access token!"); callback?.Invoke(); @@ -122,7 +135,7 @@ public static class Tools } } - public static void OpenUrl(string url) + public void OpenUrl(string url) { try { diff --git a/ViewModels/AddChannelWindowViewModel.cs b/ViewModels/AddChannelWindowViewModel.cs index ba02f68..5c4bf01 100644 --- a/ViewModels/AddChannelWindowViewModel.cs +++ b/ViewModels/AddChannelWindowViewModel.cs @@ -1,11 +1,12 @@ using System; +using Microsoft.Extensions.Logging; namespace BetterRaid.ViewModels; public class AddChannelWindowViewModel : ViewModelBase { - public AddChannelWindowViewModel() + public AddChannelWindowViewModel(ILogger logger) { - Console.WriteLine("[DEBUG] AddChannelWindowViewModel created"); + logger.LogDebug("AddChannelWindowViewModel created"); } } \ No newline at end of file diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index c3941d6..a50e7b5 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -10,27 +10,24 @@ using BetterRaid.Misc; using BetterRaid.Models; using BetterRaid.Services; using BetterRaid.Views; +using Microsoft.Extensions.Logging; namespace BetterRaid.ViewModels; public class MainWindowViewModel : ViewModelBase { - private string? _filter; private ObservableCollection _channels = []; - private readonly BetterRaidDatabase? _db; - private readonly ISynchronizaionService _synchronizaionService; - - public BetterRaidDatabase? Database - { - get => _db; - private init - { - if (SetProperty(ref _db, value) && _db != null) - { - LoadChannelsFromDb(); - } - } - } + + private readonly ISynchronizaionService _synchronizationService; + private readonly ILogger _logger; + private readonly IWebToolsService _webTools; + private readonly IDatabaseService _db; + private readonly ITwitchService _twitch; + + private string? _filter; + private bool _onlyOnline; + + public ITwitchService Twitch => _twitch; public ObservableCollection Channels { @@ -40,25 +37,36 @@ public class MainWindowViewModel : ViewModelBase public ObservableCollection FilteredChannels => GetFilteredChannels(); - public ITwitchService Twitch { get; } - public string? Filter { get => _filter; set => SetProperty(ref _filter, value); } - - public bool IsLoggedIn => Twitch.UserChannel != null; - - public MainWindowViewModel(ITwitchService twitch, ISynchronizaionService synchronizaionService) + + public bool OnlyOnline { - _synchronizaionService = synchronizaionService; - - Twitch = twitch; - Twitch.PropertyChanged += OnTwitchPropertyChanged; + get => _db.OnlyOnline; + set => SetProperty(ref _onlyOnline, value); + } - Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath); - Database.PropertyChanged += OnDatabasePropertyChanged; + public bool IsLoggedIn => _twitch.UserChannel != null; + + public MainWindowViewModel( + ILogger logger, + ITwitchService twitch, + IWebToolsService webTools, + IDatabaseService db, + ISynchronizaionService synchronizationService) + { + _logger = logger; + _twitch = twitch; + _webTools = webTools; + _db = db; + _synchronizationService = synchronizationService; + + _twitch.PropertyChanged += OnTwitchPropertyChanged; + + LoadChannelsFromDb(); } public void ExitApplication() @@ -76,7 +84,7 @@ public class MainWindowViewModel : ViewModelBase public void LoginWithTwitch() { - Tools.StartOAuthLogin(Twitch, OnTwitchLoginCallback, CancellationToken.None); + _webTools.StartOAuthLogin(_twitch, OnTwitchLoginCallback, CancellationToken.None); } private void OnTwitchLoginCallback() @@ -86,28 +94,25 @@ public class MainWindowViewModel : ViewModelBase private void LoadChannelsFromDb() { - if (_db == null) + if (_db.Database == null) { + _logger.LogError("Database is null"); return; } - + foreach (var channel in Channels) { - Twitch.UnregisterFromEvents(channel); + _twitch.UnregisterFromEvents(channel); } Channels.Clear(); - var channels = _db.Channels - .Select(channelName => new TwitchChannel(channelName)) - .ToList(); - - foreach (var channel in channels) + foreach (var channel in _db.Database.Channels) { Task.Run(() => { - channel.UpdateChannelData(Twitch); - Twitch.RegisterForEvents(channel); + channel.UpdateChannelData(_twitch); + _twitch.RegisterForEvents(channel); }); Channels.Add(channel); @@ -117,7 +122,7 @@ public class MainWindowViewModel : ViewModelBase private ObservableCollection GetFilteredChannels() { var filteredChannels = Channels - .Where(channel => Database?.OnlyOnline == false || channel.IsLive) + .Where(channel => OnlyOnline == false || channel.IsLive) .Where(channel => string.IsNullOrWhiteSpace(Filter) || channel.Name?.Contains(Filter, StringComparison.OrdinalIgnoreCase) == true) .ToList(); @@ -126,7 +131,7 @@ public class MainWindowViewModel : ViewModelBase private void OnTwitchPropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (e.PropertyName != nameof(Twitch.UserChannel)) + if (e.PropertyName != nameof(_twitch.UserChannel)) return; OnPropertyChanged(nameof(IsLoggedIn)); @@ -141,12 +146,4 @@ public class MainWindowViewModel : ViewModelBase OnPropertyChanged(nameof(FilteredChannels)); } } - - private void OnDatabasePropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName != nameof(BetterRaidDatabase.OnlyOnline)) - return; - - OnPropertyChanged(nameof(FilteredChannels)); - } } diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index 90d9d0e..e333cb4 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -59,7 +59,7 @@ Spacing="5"> + IsChecked="{Binding OnlyOnline, Mode=TwoWay, FallbackValue=False}" />