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.ViewModels;
|
||||||
using BetterRaid.Views;
|
using BetterRaid.Views;
|
||||||
using TwitchLib.Api;
|
using TwitchLib.Api;
|
||||||
using TwitchLib.Client;
|
|
||||||
using TwitchLib.Client.Events;
|
|
||||||
using TwitchLib.Client.Models;
|
|
||||||
using TwitchLib.Communication.Clients;
|
|
||||||
using TwitchLib.Communication.Models;
|
|
||||||
|
|
||||||
namespace BetterRaid;
|
namespace BetterRaid;
|
||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
public static int AutoUpdateDelay = 10_000;
|
internal static TwitchAPI? TwitchApi = null;
|
||||||
public static string TwitchBroadcasterId = "";
|
internal static int AutoUpdateDelay = 10_000;
|
||||||
public static string TwitchChannelName = "";
|
internal static string TwitchOAuthAccessToken = "";
|
||||||
public static string TokenClientId = "";
|
internal static string TwitchOAuthAccessTokenFilePath = "";
|
||||||
public static string TokenClientSecret = "";
|
internal static string TokenClientId = "kkxu4jorjrrc5jch1ito5i61hbev2o";
|
||||||
public static string TokenClientAccess = "";
|
internal static readonly string TwitchOAuthRedirectUrl = "http://localhost:9900";
|
||||||
public static TwitchClient? TwitchClient = null;
|
internal static readonly string TwitchOAuthResponseType = "token";
|
||||||
public static TwitchAPI? TwitchAPI = null;
|
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()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
try
|
var userHomeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
var betterRaidDir = "";
|
||||||
|
|
||||||
|
switch (Environment.OSVersion.Platform)
|
||||||
{
|
{
|
||||||
var tokenFile = "zn_twitch.secret";
|
case PlatformID.Win32NT:
|
||||||
var profilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
betterRaidDir = Path.Combine(userHomeDir, "AppData", "Roaming", "BetterRaid");
|
||||||
var tokenFilePath = Path.Combine(profilePath, tokenFile);
|
break;
|
||||||
var tokenFileLines = File.ReadAllLines(tokenFilePath);
|
case PlatformID.Unix:
|
||||||
TwitchChannelName = tokenFileLines[0].Split('=')[1];
|
betterRaidDir = Path.Combine(userHomeDir, ".config", "BetterRaid");
|
||||||
TokenClientId = tokenFileLines[1].Split('=')[1];
|
break;
|
||||||
TokenClientSecret = tokenFileLines[2].Split('=')[1];
|
case PlatformID.MacOSX:
|
||||||
TokenClientAccess = tokenFileLines[3].Split('=')[1];
|
betterRaidDir = Path.Combine(userHomeDir, "Library", "Application Support", "BetterRaid");
|
||||||
}
|
break;
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
Console.WriteLine("[ERROR] Failed to read token from secret file!");
|
|
||||||
Environment.Exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var creds = new ConnectionCredentials(TwitchChannelName, TokenClientAccess);
|
if (!Directory.Exists(betterRaidDir))
|
||||||
var clientOptions = new ClientOptions
|
Directory.CreateDirectory(betterRaidDir);
|
||||||
|
|
||||||
|
TwitchOAuthAccessTokenFilePath = Path.Combine(betterRaidDir, ".access_token");
|
||||||
|
|
||||||
|
if (File.Exists(TwitchOAuthAccessTokenFilePath))
|
||||||
{
|
{
|
||||||
MessagesAllowedInPeriod = 750,
|
TwitchOAuthAccessToken = File.ReadAllText(TwitchOAuthAccessTokenFilePath);
|
||||||
ThrottlingPeriod = TimeSpan.FromSeconds(30)
|
InitTwitchClient();
|
||||||
};
|
|
||||||
|
|
||||||
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!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
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)
|
TwitchApi = new TwitchAPI();
|
||||||
{
|
TwitchApi.Settings.ClientId = TokenClientId;
|
||||||
Console.WriteLine("[INFO] Twitch Client connected!");
|
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()
|
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
|
# 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", "{6BE742A5-079D-4617-BDC1-2933274CDCB7}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{EDFD12AB-9E05-4D87-9139-C220A703CFDB}"
|
||||||
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
|
||||||
{6BE742A5-079D-4617-BDC1-2933274CDCB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{EDFD12AB-9E05-4D87-9139-C220A703CFDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{6BE742A5-079D-4617-BDC1-2933274CDCB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{EDFD12AB-9E05-4D87-9139-C220A703CFDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{6BE742A5-079D-4617-BDC1-2933274CDCB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{EDFD12AB-9E05-4D87-9139-C220A703CFDB}.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}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
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 System;
|
||||||
using Avalonia;
|
using System.Threading;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using BetterRaid.Extensions;
|
using BetterRaid.Extensions;
|
||||||
|
using BetterRaid.Misc;
|
||||||
using BetterRaid.Models;
|
using BetterRaid.Models;
|
||||||
using BetterRaid.Views;
|
using BetterRaid.Views;
|
||||||
|
|
||||||
@ -25,6 +26,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
set => SetProperty(ref _filter, value);
|
set => SetProperty(ref _filter, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsLoggedIn => App.TwitchApi != null;
|
||||||
|
|
||||||
public void ExitApplication()
|
public void ExitApplication()
|
||||||
{
|
{
|
||||||
//TODO polish later
|
//TODO polish later
|
||||||
@ -37,4 +40,16 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
about.ShowDialog(owner);
|
about.ShowDialog(owner);
|
||||||
about.CenterToOwner();
|
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)
|
private async Task<Channel?> GetChannelAsync(string channelName)
|
||||||
{
|
{
|
||||||
if (App.TwitchAPI == null)
|
if (App.TwitchApi == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(channelName))
|
if (string.IsNullOrEmpty(channelName))
|
||||||
return null;
|
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));
|
var exactChannel = channels.Channels.FirstOrDefault(c => c.BroadcasterLogin.Equals(channelName, StringComparison.CurrentCultureIgnoreCase));
|
||||||
|
|
||||||
return exactChannel;
|
return exactChannel;
|
||||||
@ -108,13 +108,13 @@ public class RaidButtonViewModel : ViewModelBase
|
|||||||
|
|
||||||
private async Task<Stream?> GetStreamAsync(Channel currentChannelData)
|
private async Task<Stream?> GetStreamAsync(Channel currentChannelData)
|
||||||
{
|
{
|
||||||
if (App.TwitchAPI == null)
|
if (App.TwitchApi == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (currentChannelData == null)
|
if (currentChannelData == null)
|
||||||
return 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);
|
var exactStream = streams.Streams.FirstOrDefault(s => s.UserLogin == currentChannelData.BroadcasterLogin);
|
||||||
|
|
||||||
return exactStream;
|
return exactStream;
|
||||||
@ -122,7 +122,7 @@ public class RaidButtonViewModel : ViewModelBase
|
|||||||
|
|
||||||
public async Task RaidChannel()
|
public async Task RaidChannel()
|
||||||
{
|
{
|
||||||
if (App.TwitchAPI == null)
|
if (App.TwitchApi == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (Channel == null)
|
if (Channel == null)
|
||||||
@ -132,7 +132,8 @@ public class RaidButtonViewModel : ViewModelBase
|
|||||||
|
|
||||||
try
|
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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -48,6 +48,29 @@
|
|||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
Spacing="5">
|
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"
|
<CheckBox Content="Only Online"
|
||||||
IsChecked="{Binding Database.OnlyOnline, Mode=TwoWay}" />
|
IsChecked="{Binding Database.OnlyOnline, Mode=TwoWay}" />
|
||||||
|
|
||||||
@ -61,9 +84,21 @@
|
|||||||
|
|
||||||
</StackPanel>
|
</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"
|
||||||
|
IsVisible="{Binding IsLoggedIn, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
HorizontalScrollBarVisibility="Disabled"
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
VerticalScrollBarVisibility="Auto">
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
|
||||||
|
@ -58,10 +58,19 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
GenerateRaidGrid();
|
GenerateRaidGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.PropertyName == nameof(MainWindowViewModel.IsLoggedIn) && DataContext is MainWindowViewModel { IsLoggedIn: true })
|
||||||
|
{
|
||||||
|
InitializeRaidChannels();
|
||||||
|
GenerateRaidGrid();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeRaidChannels()
|
private void InitializeRaidChannels()
|
||||||
{
|
{
|
||||||
|
if (DataContext is MainWindowViewModel { IsLoggedIn: false })
|
||||||
|
return;
|
||||||
|
|
||||||
if (_autoUpdater?.IsBusy == false)
|
if (_autoUpdater?.IsBusy == false)
|
||||||
{
|
{
|
||||||
_autoUpdater?.CancelAsync();
|
_autoUpdater?.CancelAsync();
|
||||||
@ -100,6 +109,9 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void GenerateRaidGrid()
|
private void GenerateRaidGrid()
|
||||||
{
|
{
|
||||||
|
if (DataContext is MainWindowViewModel { IsLoggedIn: false })
|
||||||
|
return;
|
||||||
|
|
||||||
foreach (var child in raidGrid.Children)
|
foreach (var child in raidGrid.Children)
|
||||||
{
|
{
|
||||||
if (child is Button btn)
|
if (child is Button btn)
|
||||||
@ -216,6 +228,13 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
public void UpdateChannelData()
|
public void UpdateChannelData()
|
||||||
{
|
{
|
||||||
|
var loggedIn = Dispatcher.UIThread.Invoke(() => {
|
||||||
|
return (DataContext as MainWindowViewModel)?.IsLoggedIn ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loggedIn == false)
|
||||||
|
return;
|
||||||
|
|
||||||
foreach (var vm in _raidButtonVMs)
|
foreach (var vm in _raidButtonVMs)
|
||||||
{
|
{
|
||||||
Task.Run(vm.GetOrUpdateChannelAsync);
|
Task.Run(vm.GetOrUpdateChannelAsync);
|
||||||
|
Reference in New Issue
Block a user