Completely replaced the previous static auth flow with Twitch OAuth to enable users to login within the application
This commit is contained in:
parent
c236dabb0e
commit
baa2a15d02
135
App.axaml.cs
135
App.axaml.cs
@ -8,88 +8,97 @@ using Avalonia.Markup.Xaml;
|
||||
using BetterRaid.ViewModels;
|
||||
using BetterRaid.Views;
|
||||
using TwitchLib.Api;
|
||||
using TwitchLib.Client;
|
||||
using TwitchLib.Client.Events;
|
||||
using TwitchLib.Client.Models;
|
||||
using TwitchLib.Communication.Clients;
|
||||
using TwitchLib.Communication.Models;
|
||||
|
||||
namespace BetterRaid;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public static int AutoUpdateDelay = 10_000;
|
||||
public static string TwitchBroadcasterId = "";
|
||||
public static string TwitchChannelName = "";
|
||||
public static string TokenClientId = "";
|
||||
public static string TokenClientSecret = "";
|
||||
public static string TokenClientAccess = "";
|
||||
public static TwitchClient? TwitchClient = null;
|
||||
public static TwitchAPI? TwitchAPI = null;
|
||||
internal static TwitchAPI? TwitchApi = null;
|
||||
internal static int AutoUpdateDelay = 10_000;
|
||||
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:chat" ];
|
||||
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()
|
||||
{
|
||||
try
|
||||
var userHomeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var betterRaidDir = "";
|
||||
|
||||
switch (Environment.OSVersion.Platform)
|
||||
{
|
||||
var tokenFile = "zn_twitch.secret";
|
||||
var profilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var tokenFilePath = Path.Combine(profilePath, tokenFile);
|
||||
var tokenFileLines = File.ReadAllLines(tokenFilePath);
|
||||
TwitchChannelName = tokenFileLines[0].Split('=')[1];
|
||||
TokenClientId = tokenFileLines[1].Split('=')[1];
|
||||
TokenClientSecret = tokenFileLines[2].Split('=')[1];
|
||||
TokenClientAccess = tokenFileLines[3].Split('=')[1];
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine("[ERROR] Failed to read token from secret file!");
|
||||
Environment.Exit(1);
|
||||
case PlatformID.Win32NT:
|
||||
betterRaidDir = Path.Combine(userHomeDir, "AppData", "Roaming", "BetterRaid");
|
||||
break;
|
||||
case PlatformID.Unix:
|
||||
betterRaidDir = Path.Combine(userHomeDir, ".config", "BetterRaid");
|
||||
break;
|
||||
case PlatformID.MacOSX:
|
||||
betterRaidDir = Path.Combine(userHomeDir, "Library", "Application Support", "BetterRaid");
|
||||
break;
|
||||
}
|
||||
|
||||
var creds = new ConnectionCredentials(TwitchChannelName, TokenClientAccess);
|
||||
var clientOptions = new ClientOptions
|
||||
if (!Directory.Exists(betterRaidDir))
|
||||
Directory.CreateDirectory(betterRaidDir);
|
||||
|
||||
TwitchOAuthAccessTokenFilePath = Path.Combine(betterRaidDir, ".access_token");
|
||||
|
||||
if (File.Exists(TwitchOAuthAccessTokenFilePath))
|
||||
{
|
||||
MessagesAllowedInPeriod = 750,
|
||||
ThrottlingPeriod = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
var customClient = new WebSocketClient(clientOptions);
|
||||
TwitchClient = new TwitchClient(customClient);
|
||||
|
||||
TwitchClient.Initialize(creds, TwitchChannelName);
|
||||
TwitchClient.OnConnected += OnConnected;
|
||||
TwitchClient.OnConnectionError += OnConnectionError;
|
||||
|
||||
TwitchClient.Connect();
|
||||
|
||||
TwitchAPI = new TwitchAPI();
|
||||
TwitchAPI.Settings.ClientId = TokenClientId;
|
||||
TwitchAPI.Settings.AccessToken = TokenClientAccess;
|
||||
|
||||
var channels = TwitchAPI.Helix.Search.SearchChannelsAsync(TwitchChannelName).Result;
|
||||
var exactChannel = channels.Channels.FirstOrDefault(c => c.BroadcasterLogin == TwitchChannelName);
|
||||
|
||||
if (exactChannel != null)
|
||||
{
|
||||
TwitchBroadcasterId = exactChannel.Id;
|
||||
Console.WriteLine($"TWITCH BROADCASTER ID SET TO {TwitchBroadcasterId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("FAILED TO SET BROADCASTER ID!");
|
||||
TwitchOAuthAccessToken = File.ReadAllText(TwitchOAuthAccessTokenFilePath);
|
||||
InitTwitchClient();
|
||||
}
|
||||
|
||||
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void OnConnectionError(object? sender, OnConnectionErrorArgs e)
|
||||
public static void InitTwitchClient(bool overrideToken = false)
|
||||
{
|
||||
Console.WriteLine("[ERROR] Twitch Client failed to connect!");
|
||||
}
|
||||
Console.WriteLine("[INFO] Initializing Twitch Client...");
|
||||
|
||||
private void OnConnected(object? sender, OnConnectedArgs e)
|
||||
{
|
||||
Console.WriteLine("[INFO] Twitch Client connected!");
|
||||
TwitchApi = new TwitchAPI();
|
||||
TwitchApi.Settings.ClientId = TokenClientId;
|
||||
TwitchApi.Settings.AccessToken = TwitchOAuthAccessToken;
|
||||
|
||||
Console.WriteLine("[INFO] Testing Twitch API connection...");
|
||||
|
||||
var user = TwitchApi.Helix.Users.GetUsersAsync().Result.Users.FirstOrDefault();
|
||||
if (user == null)
|
||||
{
|
||||
TwitchApi = null;
|
||||
Console.WriteLine("[ERROR] Failed to connect to Twitch API!");
|
||||
return;
|
||||
}
|
||||
|
||||
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()
|
||||
|
BIN
Assets/glitch_flat_white.png
Normal file
BIN
Assets/glitch_flat_white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.002.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{6BE742A5-079D-4617-BDC1-2933274CDCB7}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{EDFD12AB-9E05-4D87-9139-C220A703CFDB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -11,10 +11,10 @@ Global
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6BE742A5-079D-4617-BDC1-2933274CDCB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6BE742A5-079D-4617-BDC1-2933274CDCB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6BE742A5-079D-4617-BDC1-2933274CDCB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6BE742A5-079D-4617-BDC1-2933274CDCB7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EDFD12AB-9E05-4D87-9139-C220A703CFDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EDFD12AB-9E05-4D87-9139-C220A703CFDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EDFD12AB-9E05-4D87-9139-C220A703CFDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EDFD12AB-9E05-4D87-9139-C220A703CFDB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
170
Misc/Tools.cs
Normal file
170
Misc/Tools.cs
Normal file
@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterRaid.Misc;
|
||||
|
||||
public static class Tools
|
||||
{
|
||||
private static HttpListener? _oauthListener;
|
||||
private static Task? _oauthWaiterTask;
|
||||
|
||||
// Source: https://stackoverflow.com/a/43232486
|
||||
public static void StartOAuthLogin(string url, Action? callback = null, CancellationToken? token = null)
|
||||
{
|
||||
if (_oauthListener != null)
|
||||
return;
|
||||
|
||||
var _token = token ?? CancellationToken.None;
|
||||
|
||||
_oauthListener = new HttpListener();
|
||||
_oauthListener.Prefixes.Add("http://localhost:9900/");
|
||||
_oauthListener.Start();
|
||||
|
||||
_oauthWaiterTask = WaitForCallback(callback, _token);
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(url);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// hack because of this: https://github.com/dotnet/corefx/issues/10361
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
url = url.Replace("&", "^&");
|
||||
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
Process.Start("xdg-open", url);
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
Process.Start("open", url);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 =
|
||||
@"
|
||||
<!DOCTYPE html>
|
||||
<header>
|
||||
<title>BetterRaid Twitch Login</title>
|
||||
</header>
|
||||
<body>
|
||||
<h1>Successfully logged in!</h1>
|
||||
|
||||
<script>
|
||||
var urlParams = new URLSearchParams(window.location.hash.substr(1));
|
||||
var accessToken = urlParams.get('access_token');
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', 'http://localhost:9900/login', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send(JSON.stringify({ access_token: accessToken }));
|
||||
</script>
|
||||
</body>
|
||||
";
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using System.Threading;
|
||||
using Avalonia.Controls;
|
||||
using BetterRaid.Extensions;
|
||||
using BetterRaid.Misc;
|
||||
using BetterRaid.Models;
|
||||
using BetterRaid.Views;
|
||||
|
||||
@ -25,6 +26,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
set => SetProperty(ref _filter, value);
|
||||
}
|
||||
|
||||
public bool IsLoggedIn => App.TwitchApi != null;
|
||||
|
||||
public void ExitApplication()
|
||||
{
|
||||
//TODO polish later
|
||||
@ -37,4 +40,16 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
about.ShowDialog(owner);
|
||||
about.CenterToOwner();
|
||||
}
|
||||
|
||||
public void LoginWithTwitch()
|
||||
{
|
||||
Tools.StartOAuthLogin(App.TwitchOAuthUrl, OnTwitchLoginCallback, CancellationToken.None);
|
||||
}
|
||||
|
||||
public void OnTwitchLoginCallback()
|
||||
{
|
||||
App.InitTwitchClient(overrideToken: true);
|
||||
|
||||
OnPropertyChanged(nameof(IsLoggedIn));
|
||||
}
|
||||
}
|
||||
|
@ -94,13 +94,13 @@ public class RaidButtonViewModel : ViewModelBase
|
||||
|
||||
private async Task<Channel?> GetChannelAsync(string channelName)
|
||||
{
|
||||
if (App.TwitchAPI == null)
|
||||
if (App.TwitchApi == null)
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrEmpty(channelName))
|
||||
return null;
|
||||
|
||||
var channels = await App.TwitchAPI.Helix.Search.SearchChannelsAsync(channelName);
|
||||
var channels = await App.TwitchApi.Helix.Search.SearchChannelsAsync(channelName);
|
||||
var exactChannel = channels.Channels.FirstOrDefault(c => c.BroadcasterLogin.Equals(channelName, StringComparison.CurrentCultureIgnoreCase));
|
||||
|
||||
return exactChannel;
|
||||
@ -108,13 +108,13 @@ public class RaidButtonViewModel : ViewModelBase
|
||||
|
||||
private async Task<Stream?> GetStreamAsync(Channel currentChannelData)
|
||||
{
|
||||
if (App.TwitchAPI == null)
|
||||
if (App.TwitchApi == null)
|
||||
return null;
|
||||
|
||||
if (currentChannelData == null)
|
||||
return null;
|
||||
|
||||
var streams = await App.TwitchAPI.Helix.Streams.GetStreamsAsync(userLogins: [currentChannelData.BroadcasterLogin]);
|
||||
var streams = await App.TwitchApi.Helix.Streams.GetStreamsAsync(userLogins: [currentChannelData.BroadcasterLogin]);
|
||||
var exactStream = streams.Streams.FirstOrDefault(s => s.UserLogin == currentChannelData.BroadcasterLogin);
|
||||
|
||||
return exactStream;
|
||||
@ -122,7 +122,7 @@ public class RaidButtonViewModel : ViewModelBase
|
||||
|
||||
public async Task RaidChannel()
|
||||
{
|
||||
if (App.TwitchAPI == null)
|
||||
if (App.TwitchApi == null)
|
||||
return;
|
||||
|
||||
if (Channel == null)
|
||||
@ -132,7 +132,8 @@ public class RaidButtonViewModel : ViewModelBase
|
||||
|
||||
try
|
||||
{
|
||||
raid = await App.TwitchAPI.Helix.Raids.StartRaidAsync(App.TwitchBroadcasterId, Channel.BroadcasterId);
|
||||
// TODO: Get own broadcaster id
|
||||
raid = await App.TwitchApi.Helix.Raids.StartRaidAsync("", Channel.BroadcasterId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -48,6 +48,29 @@
|
||||
Orientation="Horizontal"
|
||||
Spacing="5">
|
||||
|
||||
<Button Command="{Binding LoginWithTwitch}"
|
||||
IsVisible="{Binding !IsLoggedIn, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
|
||||
<Button.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Background" Value="#6441a5" />
|
||||
</Style>
|
||||
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="#b9a3e3" />
|
||||
</Style>
|
||||
</Button.Styles>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Login"
|
||||
Foreground="#f1f1f1"
|
||||
FontSize="14" />
|
||||
<Image Source="avares://BetterRaid/Assets/glitch_flat_white.png"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="5, 0, 0, 0" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<CheckBox Content="Only Online"
|
||||
IsChecked="{Binding Database.OnlyOnline, Mode=TwoWay}" />
|
||||
|
||||
@ -61,9 +84,21 @@
|
||||
|
||||
</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"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="1"
|
||||
IsVisible="{Binding IsLoggedIn, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
|
||||
|
@ -58,10 +58,19 @@ public partial class MainWindow : Window
|
||||
{
|
||||
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();
|
||||
@ -100,6 +109,9 @@ public partial class MainWindow : Window
|
||||
|
||||
private void GenerateRaidGrid()
|
||||
{
|
||||
if (DataContext is MainWindowViewModel { IsLoggedIn: false })
|
||||
return;
|
||||
|
||||
foreach (var child in raidGrid.Children)
|
||||
{
|
||||
if (child is Button btn)
|
||||
@ -216,6 +228,13 @@ public partial class MainWindow : Window
|
||||
|
||||
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);
|
||||
|
Reference in New Issue
Block a user