├── .github └── workflows │ ├── cd.yml │ └── old │ └── tests.yml ├── .gitignore ├── Brigitta.sln ├── BrigittaBlazor ├── .config │ └── dotnet-tools.json ├── App.razor ├── Auth │ └── BrigittaAuthStateProvider.cs ├── BrigittaBlazor.csproj ├── BrigittaBlazor.sln ├── Derivatives │ └── CustomCommandHandler.cs ├── Extensions │ ├── DateTimeExtensions.cs │ └── IrcMessageExtensions.cs ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ ├── Index.razor │ ├── PrimaryDisplay.razor │ ├── Settings.razor │ ├── _Host.cshtml │ └── _Layout.cshtml ├── Program.cs ├── Properties │ └── launchSettings.json ├── Settings │ ├── UserAudioAlert.cs │ ├── UserKeyBind.cs │ └── UserSettings.cs ├── Shared │ ├── Components │ │ ├── AddChannelDialog.razor │ │ ├── ChannelChipList.razor │ │ ├── ChatConsole.razor │ │ ├── ChatConsoleHeader.razor │ │ ├── ChatIconButtonsPanel.razor │ │ ├── Dialogs │ │ │ └── AddAudioAlertDialog.razor │ │ ├── HelpWikiButton.razor │ │ ├── MultiplayerLobby │ │ │ ├── LobbyButtonsPanel.razor │ │ │ ├── LobbyInformationPanel.razor │ │ │ ├── LobbyModSelectionPanel.razor │ │ │ └── LobbyPlayerDisplay.razor │ │ ├── Settings │ │ │ ├── AudioSettings.razor │ │ │ └── HotkeySettings.razor │ │ └── TextChatEntryField.razor │ ├── MainLayout.razor │ └── NavMenu.razor ├── Utils │ ├── ChatNotification.cs │ ├── DebugUtils.cs │ ├── EventRegistrationTracker.cs │ ├── FileUtils.cs │ ├── HotkeyListener.cs │ ├── IScrollUtils.cs │ ├── ScrollUtils.cs │ ├── SoundUtils.cs │ ├── StateMaintainer.cs │ └── UpdaterService.cs ├── _Imports.razor ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── Sounds │ ├── alert-metalgear.mp3 │ ├── alert-pokemon.mp3 │ ├── alert-scifi.mp3 │ ├── anime-wow.mp3 │ ├── hitwhistle.wav │ ├── incoming-transmission.mp3 │ └── nice.mp3 │ ├── favicon.ico │ ├── images │ └── brigitta-logo-text.png │ └── js │ ├── helpers.js │ ├── hotkeys.js │ └── soundUtils.js └── README.md /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment 2 | 3 | # Run on every commit tag which begins with "v" 4 | on: 5 | push: 6 | tags: 7 | - "v*" 8 | 9 | # Automatically create a GitHub Release with details from previous commits 10 | jobs: 11 | release: 12 | name: "Release" 13 | runs-on: "ubuntu-latest" 14 | steps: 15 | - uses: "marvinpinto/action-automatic-releases@latest" 16 | with: 17 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 18 | prerelease: false 19 | deploy-binaries: 20 | name: Publish for "${{ matrix.os }}" 21 | runs-on: "${{ matrix.os }}" 22 | strategy: 23 | matrix: 24 | include: 25 | - os: ubuntu-latest 26 | artifact_name: brigitta-linux-amd64 27 | asset_name: brigitta-linux-amd64.zip 28 | - os: windows-latest 29 | artifact_name: brigitta-windows-amd64 30 | asset_name: brigitta-windows-amd64.zip 31 | - os: macos-latest 32 | artifact_name: brigitta-macos-amd64 33 | asset_name: brigitta-macos-amd64.zip 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Publish 37 | run: dotnet publish -c Release -o BrigittaApp/${{ matrix.artifact_name }} BrigittaBlazor/BrigittaBlazor.csproj 38 | - name: Archive published binary 39 | uses: thedoctor0/zip-release@main 40 | with: 41 | type: zip 42 | filename: "${{ matrix.asset_name }}" 43 | path: "BrigittaApp/${{ matrix.artifact_name }}" 44 | - name: Upload binaries to release 45 | uses: svenstaro/upload-release-action@v2 46 | with: 47 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 48 | file: "${{ matrix.asset_name }}" 49 | asset_name: "${{ matrix.asset_name }}" 50 | -------------------------------------------------------------------------------- /.github/workflows/old/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test-build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Setup .NET 12 | uses: actions/setup-dotnet@v3 13 | with: 14 | dotnet-version: 6.0.x 15 | - name: Restore dependencies 16 | run: dotnet restore 17 | - name: Build 18 | run: dotnet build --no-restore 19 | - name: Run tests 20 | run: dotnet test --verbosity normal 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | .DS_Store 7 | .idea/ 8 | *.vs/** 9 | *.DotSettings.* 10 | logs/ 11 | logs/** -------------------------------------------------------------------------------- /Brigitta.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BrigittaBlazor", "BrigittaBlazor\BrigittaBlazor.csproj", "{7C248D66-701B-42B2-A4C0-B0026CA3A56A}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {1BCE0C96-7F2D-4979-94EC-185298C8CA4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {1BCE0C96-7F2D-4979-94EC-185298C8CA4D}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {1BCE0C96-7F2D-4979-94EC-185298C8CA4D}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {1BCE0C96-7F2D-4979-94EC-185298C8CA4D}.Release|Any CPU.Build.0 = Release|Any CPU 15 | {7C248D66-701B-42B2-A4C0-B0026CA3A56A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {7C248D66-701B-42B2-A4C0-B0026CA3A56A}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {7C248D66-701B-42B2-A4C0-B0026CA3A56A}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {7C248D66-701B-42B2-A4C0-B0026CA3A56A}.Release|Any CPU.Build.0 = Release|Any CPU 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /BrigittaBlazor/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "7.0.2", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /BrigittaBlazor/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 |

Sorry, there's nothing at this address.

10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /BrigittaBlazor/Auth/BrigittaAuthStateProvider.cs: -------------------------------------------------------------------------------- 1 | using BanchoSharp.Interfaces; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.AspNetCore.Components.Authorization; 4 | using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; 5 | using System.Security.Claims; 6 | 7 | namespace BrigittaBlazor.Auth; 8 | 9 | public class BrigittaAuthStateProvider : AuthenticationStateProvider 10 | { 11 | private readonly ClaimsPrincipal _anonymous = new(new ClaimsIdentity()); 12 | private readonly IBanchoClient _client; 13 | private readonly NavigationManager _navManager; 14 | private readonly ProtectedSessionStorage _sessionStorage; 15 | 16 | public BrigittaAuthStateProvider(ProtectedSessionStorage sessionStorage, IBanchoClient client, NavigationManager navManager) 17 | { 18 | _sessionStorage = sessionStorage; 19 | _client = client; 20 | _navManager = navManager; 21 | } 22 | 23 | public override async Task GetAuthenticationStateAsync() 24 | { 25 | try 26 | { 27 | var userSessionStorageResult = await _sessionStorage.GetAsync("UserSession"); 28 | var userSessionClient = userSessionStorageResult.Success ? userSessionStorageResult.Value : null; 29 | if (userSessionClient == null) 30 | { 31 | return await Task.FromResult(new AuthenticationState(_anonymous)); 32 | } 33 | 34 | var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List 35 | { 36 | new(ClaimTypes.Name, userSessionClient.ClientConfig.Credentials.Username) 37 | }, "BrigittaAuth")); 38 | 39 | return await Task.FromResult(new AuthenticationState(claimsPrincipal)); 40 | } 41 | catch 42 | { 43 | return await Task.FromResult(new AuthenticationState(_anonymous)); 44 | } 45 | } 46 | 47 | public async Task UpdateAuthenticationStateAsync(IBanchoClient? client) 48 | { 49 | ClaimsPrincipal claimsPrincipal; 50 | 51 | if (client is { IsAuthenticated: true }) 52 | { 53 | // User is logged in and authenticated 54 | await _sessionStorage.SetAsync("UserSession", client); 55 | claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List 56 | { 57 | new(ClaimTypes.Name, client.ClientConfig.Credentials.Username) 58 | }, "BrigittaAuth")); 59 | } 60 | else 61 | { 62 | await _sessionStorage.DeleteAsync("UserSession"); 63 | claimsPrincipal = _anonymous; 64 | } 65 | 66 | NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal))); 67 | _navManager.NavigateTo("/primarydisplay"); 68 | } 69 | } -------------------------------------------------------------------------------- /BrigittaBlazor/BrigittaBlazor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | Exe 8 | 9 | 10 | 11 | true 12 | true 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ..\..\BanchoSharp\BanchoSharp\bin\Release\net6.0\BanchoSharp.dll 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /BrigittaBlazor/BrigittaBlazor.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31717.71 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrigittaBlazor", "BrigittaBlazor.csproj", "{66DEB30A-9C24-4673-BA49-9A4F3768798F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {66DEB30A-9C24-4673-BA49-9A4F3768798F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {66DEB30A-9C24-4673-BA49-9A4F3768798F}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {66DEB30A-9C24-4673-BA49-9A4F3768798F}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {66DEB30A-9C24-4673-BA49-9A4F3768798F}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {6E823DAC-71D5-498B-BE40-39D989A7F19B} 24 | EndGlobalSection 25 | EndGlobal -------------------------------------------------------------------------------- /BrigittaBlazor/Derivatives/CustomCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using BanchoSharp.Messaging; 2 | 3 | namespace BrigittaBlazor.Derivatives; 4 | 5 | public struct CustomCommand 6 | { 7 | public string Command { get; set; } 8 | public string[]? Aliases { get; set; } 9 | public CustomParameter[]? Parameters { get; set; } 10 | public string Description { get; set; } 11 | public Delegate Function { get; set; } 12 | 13 | /// 14 | /// Executes with dynamically and returns the result. 15 | /// This allows us to invoke a function without knowledge of the parameters or method body. This is the equivalent 16 | /// of a generic Func delegate with a return type. 17 | /// 18 | /// The return type of the delegate, must be 19 | /// The same return value from the delegate 20 | public TResult Execute(Delegate function, params object?[] args) where TResult : IConvertible 21 | { 22 | object result = function.DynamicInvoke(args); 23 | return (TResult)Convert.ChangeType(result, typeof(TResult)); 24 | } 25 | 26 | /// 27 | /// Executes a with dynamically as a . 28 | /// This allows us to invoke a function without knowledge of the parameters or method body. This is the equivalent 29 | /// of a generic Func delegate with as many parameters as necessary. 30 | /// 31 | /// The return type of the delegate 32 | /// The same return value from the delegate 33 | public Task Execute(Delegate function, params object?[] args) => Task.Run(() => function.DynamicInvoke(args)); 34 | 35 | /// 36 | /// True if the command the user is sending corresponds to this custom command. 37 | /// 38 | public bool IsAssociated(string command) => Command.Equals(command, StringComparison.OrdinalIgnoreCase) || 39 | Aliases?.Any(a => a.Equals(command, StringComparison.OrdinalIgnoreCase)) == true; 40 | } 41 | 42 | public struct CustomParameter 43 | { 44 | public string Name { get; set; } 45 | public string Description { get; set; } 46 | public bool Optional { get; set; } 47 | } 48 | 49 | public class CustomCommandHandler : SlashCommandHandler 50 | { 51 | private readonly CustomCommand ChatCommand = new() 52 | { 53 | Command = "chat", 54 | Aliases = new[] { "c", "msg", "message" }, 55 | Description = "Chat a user or channel, opening a line of communication with them (if not present) " + 56 | "and optionally sending a message to them.", 57 | Parameters = new[] 58 | { 59 | new CustomParameter 60 | { 61 | Name = "recipient", 62 | Description = "The user or channel to chat", 63 | Optional = false 64 | }, 65 | new CustomParameter 66 | { 67 | Name = "message", 68 | Description = "The message to send", 69 | Optional = true 70 | } 71 | } 72 | }; 73 | private readonly CustomCommand ClearCommand = new() 74 | { 75 | Command = "clear", 76 | Description = "Clears the chat" 77 | }; 78 | private readonly CustomCommand MatchStartTimerCommand = new() 79 | { 80 | Command = "matchtimer", 81 | Aliases = new[] { "mt", "mst" }, 82 | Description = "Starts a match start timer for the current lobby.", 83 | Parameters = new[] 84 | { 85 | new CustomParameter 86 | { 87 | Name = "time", 88 | Description = "The number of seconds the timer will last", 89 | Optional = false 90 | } 91 | } 92 | }; 93 | private readonly CustomCommand SavelogCommand = new() 94 | { 95 | Command = "savelog", 96 | Aliases = null, 97 | Description = "Saves the current chat log to a file.", 98 | Parameters = null 99 | }; 100 | private readonly CustomCommand TimerCommand = new() 101 | { 102 | Command = "timer", 103 | Aliases = new[] { "t" }, 104 | Description = "Starts a standard timer for a specified amount of time.", 105 | Parameters = new[] 106 | { 107 | new CustomParameter 108 | { 109 | Name = "time", 110 | Description = "The number of seconds the timer will last", 111 | Optional = false 112 | } 113 | } 114 | }; 115 | 116 | public CustomCommandHandler(string prompt) : base(prompt) 117 | { 118 | if (string.IsNullOrEmpty(Command)) 119 | { 120 | return; 121 | } 122 | 123 | if (ClearCommand.IsAssociated(Command)) 124 | { 125 | CustomCommand = ClearCommand; 126 | } 127 | else if (ChatCommand.IsAssociated(Command)) 128 | { 129 | CustomCommand = ChatCommand; 130 | } 131 | else if (TimerCommand.IsAssociated(Command)) 132 | { 133 | CustomCommand = TimerCommand; 134 | } 135 | else if (MatchStartTimerCommand.IsAssociated(Command)) 136 | { 137 | CustomCommand = MatchStartTimerCommand; 138 | } 139 | else if (SavelogCommand.IsAssociated(Command)) 140 | { 141 | CustomCommand = SavelogCommand; 142 | } 143 | } 144 | 145 | /// 146 | /// The custom command, if applicable, that is associated with the user's prompt. 147 | /// Null if no custom command could be found from the prompt. Otherwise, it will 148 | /// contain a fully populated object that then can be acted upon. 149 | /// 150 | public CustomCommand? CustomCommand { get; set; } 151 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Extensions/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace BrigittaBlazor.Extensions; 2 | 3 | public static class DateTimeExtensions 4 | { 5 | public static string ToFormattedTimeString(this DateTime time) => $"{time:HH:mm:ss}"; 6 | public static string ToFormattedDuration(this TimeSpan? ts) => $"{ts:mm\\:ss}"; 7 | public static string ToFileTimeString(this DateTime time) => $"{time:u}"; 8 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Extensions/IrcMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using BanchoSharp.Interfaces; 2 | 3 | namespace BrigittaBlazor.Extensions; 4 | 5 | public static class IrcMessageExtensions 6 | { 7 | public static string ToDisplayString(this IIrcMessage m) => $"{m.Timestamp.ToFormattedTimeString()} {m.Prefix}: {string.Join(" ", m.Parameters)}"; 8 | public static string ToUTCDisplayString(this IIrcMessage m) => $"{m.Timestamp.ToUniversalTime().ToFormattedTimeString()} {m.Prefix}: {string.Join(" ", m.Parameters)}"; 9 | public static string ToDisplayString(this IPrivateIrcMessage pm) => $"{pm.Timestamp.ToFormattedTimeString()} {pm.Sender}: {pm.Content}"; 10 | public static string ToLogString(this IPrivateIrcMessage pm) => $"{pm.Timestamp.ToFormattedTimeString()} {pm.Sender} -> {pm.Recipient}: {pm.Content}"; 11 | public static string ToTimeString(this IIrcMessage m) => $"{m.Timestamp.ToFormattedTimeString()}"; 12 | public static string ToUTCTimeString(this IIrcMessage m) => $"{m.Timestamp.ToUniversalTime().ToFormattedTimeString()}"; 13 | public static bool IsMultiplayerLobbyMessage(this IPrivateIrcMessage pm) => pm.Recipient.StartsWith("#mp_"); 14 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model BrigittaBlazor.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /BrigittaBlazor/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.Diagnostics; 4 | 5 | namespace BrigittaBlazor.Pages 6 | { 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | public class ErrorModel : PageModel 10 | { 11 | private readonly ILogger _logger; 12 | public ErrorModel(ILogger logger) { _logger = logger; } 13 | public string? RequestId { get; set; } 14 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 15 | public void OnGet() => RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 16 | } 17 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using BrigittaBlazor.Utils 3 | @using BanchoSharp 4 | @using BanchoSharp.Exceptions 5 | @using BanchoSharp.Interfaces 6 | @using BrigittaBlazor.Auth 7 | 8 | @inject IBanchoClient Client 9 | @inject ILogger Logger 10 | @inject AuthenticationStateProvider AuthStateProvider 11 | @inject NavigationManager NavigationManager 12 | @inject EventRegistrationTracker EventRegistrationTracker 13 | 14 | @*Do not remove*@ 15 | @* ReSharper disable UnusedMember.Local *@ 16 | @inject ISnackbar Snackbar 17 | @inject UpdaterService UpdaterService 18 | @* ReSharper enable UnusedMember.Local *@ 19 | @*Do not remove*@ 20 | 21 | 22 | IRC Login 23 | Brigitta @("v" + UpdaterService.VERSION) 24 | 25 | 26 | 27 | 28 | 29 | IRC Login 30 | 31 | 32 | 34 | 36 |
37 | 39 | Get IRC Password 40 | 41 | 43 | Login 44 | 45 |
46 |
47 | Your IRC login information is not saved. 48 |
49 |
50 | 51 | Changelog 52 | 53 | @foreach (var update in _latestUpdates) 54 | { 55 | 56 | @update.Version 57 | 58 | 59 | @foreach (var commit in update.Commits) 60 | { 61 | 62 | 63 | 64 | @commit.Hash 65 | 66 | 67 | 68 | 69 | @commit.Description 70 | 71 | 72 | 73 | } 74 | 75 | } 76 | 77 | 78 |
79 |
80 |
81 | 82 | 83 | @code { 84 | [CascadingParameter] 85 | public Task authenticationState { get; set; } 86 | bool success; 87 | MudTextField username; 88 | MudTextField password; 89 | MudForm form; 90 | 91 | // ReSharper disable once FieldCanBeMadeReadOnly.Local 92 | private IEnumerable _latestUpdates = new List(); 93 | 94 | protected override async Task OnInitializedAsync() 95 | { 96 | if (!EventRegistrationTracker.HasRegisteredIndexLocationListener) 97 | { 98 | NavigationManager.LocationChanged += (_, args) => Logger.LogDebug($"Location changed: {args.Location}"); 99 | EventRegistrationTracker.HasRegisteredIndexLocationListener = true; 100 | } 101 | 102 | #if !DEBUG 103 | bool? needsUpdate = await UpdaterService.NeedsUpdateAsync(); 104 | 105 | if (needsUpdate.HasValue && needsUpdate.Value) 106 | { 107 | Snackbar.Add("Brigitta is out of date! Check the changelog for more information.", Severity.Error, 108 | cfg => 109 | { 110 | cfg.VisibleStateDuration = 3000; 111 | cfg.HideTransitionDuration = 500; 112 | }); 113 | } 114 | else if(needsUpdate.HasValue && !needsUpdate.Value) 115 | { 116 | Snackbar.Add("Thanks for keeping Brigitta up to date!", Severity.Success); 117 | } 118 | else 119 | { 120 | Snackbar.Add("Unable to check for updates. Please check the logs.", Severity.Error); 121 | } 122 | 123 | try 124 | { 125 | _latestUpdates = await UpdaterService.GetRecentUpdateInfosAsync(); 126 | } 127 | catch (Exception e) 128 | { 129 | // ignored 130 | } 131 | #endif 132 | } 133 | 134 | public async Task OnSubmit() 135 | { 136 | var brigittaAuth = (BrigittaAuthStateProvider)AuthStateProvider; 137 | Client.ClientConfig.Credentials = new IrcCredentials(username.Text, password.Text); 138 | 139 | Logger.LogInformation($"Username: {username.Text}"); 140 | 141 | Client.OnAuthenticated += async () => { await brigittaAuth.UpdateAuthenticationStateAsync(Client); }; 142 | 143 | try 144 | { 145 | await Client.ConnectAsync(); 146 | } 147 | catch (IrcClientNotAuthenticatedException) 148 | { 149 | form.Reset(); 150 | } 151 | } 152 | 153 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Pages/PrimaryDisplay.razor: -------------------------------------------------------------------------------- 1 | @page "/primarydisplay" 2 | @using BrigittaBlazor.Extensions 3 | @using BrigittaBlazor.Settings 4 | @using BanchoSharp.Interfaces 5 | @using BanchoSharp.Multiplayer 6 | @using BrigittaBlazor.Utils 7 | @using BrigittaBlazor.Shared.Components 8 | @using BrigittaBlazor.Shared.Components.MultiplayerLobby 9 | @using System.Collections.Immutable 10 | @implements IDisposable 11 | 12 | @attribute [Authorize] 13 | 14 | @inject IBanchoClient Client 15 | @inject ISnackbar Snackbar 16 | @inject ILogger Logger 17 | @inject IJSRuntime JS 18 | @inject UserSettings UserSettings 19 | @inject NavigationManager NavigationManager 20 | @inject StateMaintainer StateManager 21 | 22 | 23 | 24 | @*Channels, text box*@ 25 | 26 | 28 | 29 | 34 | 35 | 39 | 40 | @*Button row*@ 41 | 42 | 46 | 47 | 48 | 49 | @*Multiplayer lobby information*@ 50 | @if (_currentlySelectedLobby != null) 51 | { 52 | // We are inside of a multiplayer lobby channel. 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | @*Player display*@ 75 | 76 | 77 | 78 | } 79 | 80 | 81 | @code { 82 | @*Member Variables*@ 83 | private IChatChannel? _currentlySelectedChannel; 84 | private readonly Dictionary _eventListeners = new(); // Key: alert's name, Value: the event handler 85 | 86 | // ReSharper disable once MergeConditionalExpression -- This actually causes an unhandled exception 87 | private IMultiplayerLobby? _currentlySelectedLobby => _currentlySelectedChannel != null ? 88 | _currentlySelectedChannel as IMultiplayerLobby : 89 | null; 90 | private bool _autoScroll = true; 91 | private bool _timestampsInChat = true; 92 | private bool _displayUTC; 93 | private string _consoleDivId => "console"; 94 | private int _chatWidthSm => _currentlySelectedLobby != null ? 6 : 12; 95 | private int _chatWidthLg => _currentlySelectedLobby != null ? 8 : 12; 96 | 97 | protected override async Task OnInitializedAsync() 98 | { 99 | StateManager.OnAudioAlertCreated += OnAudioAlertCreated; 100 | StateManager.OnAudioAlertUpdated += OnAudioAlertUpdated; 101 | StateManager.OnAudioAlertDeleted += OnAudioAlertDeleted; 102 | 103 | // Reload all of the audio alerts 104 | foreach (var alert in StateManager.AudioAlerts) 105 | { 106 | SubscribeToAudioEvent(alert); 107 | } 108 | 109 | InitClientEvents(); 110 | SubscribeToHotkeyPressedEvent(); 111 | 112 | if (!StateManager.EventTracker.HasRegisteredPrimaryDisplayDefaultEvents) 113 | { 114 | await JoinDefaultChannels(); 115 | 116 | StateManager.EventTracker.HasRegisteredPrimaryDisplayDefaultEvents = true; 117 | } 118 | } 119 | 120 | private void OnAudioAlertCreated(object? sender, UserAudioAlert e) => SubscribeToAudioEvent(e); 121 | 122 | private void OnAudioAlertUpdated(object? sender, UserAudioAlert e) 123 | { 124 | UnsubscribeFromAudioEvent(e); 125 | SubscribeToAudioEvent(e); 126 | } 127 | 128 | private void OnAudioAlertDeleted(object? sender, UserAudioAlert e) => UnsubscribeFromAudioEvent(e); 129 | 130 | private void SubscribeToAudioEvent(UserAudioAlert alert) 131 | { 132 | switch (alert.Trigger) 133 | { 134 | case EventTrigger.OnUserMessage: 135 | RegisterOnUserMessageAudioAlert(alert); 136 | break; 137 | case EventTrigger.OnDirectMessage: 138 | RegisterOnDirectMessageAudioAlert(alert); 139 | break; 140 | case EventTrigger.OnUsernameMentioned: 141 | RegisterOnUsernameMentionedAudioAlert(alert); 142 | break; 143 | case EventTrigger.OnKeyword: 144 | RegisterOnKeywordAudioAlert(alert); 145 | break; 146 | case EventTrigger.OnMatchStarted: 147 | RegisterOnMatchStartedAudioAlert(alert); 148 | break; 149 | case EventTrigger.OnMatchFinished: 150 | RegisterOnMatchFinishedAudioAlert(alert); 151 | break; 152 | default: 153 | Logger.LogError("This alert is not implemented! Please report this to the developer. Alert: {Alert}", alert); 154 | break; 155 | } 156 | } 157 | 158 | private void RegisterOnMatchFinishedAudioAlert(UserAudioAlert alert) 159 | { 160 | Action onMatchStartedHandler = async () => 161 | { 162 | if (!alert.Enabled) 163 | { 164 | return; 165 | } 166 | 167 | await HandleAudioEvent(alert); 168 | }; 169 | 170 | foreach (var chatChannel in Client.Channels) 171 | { 172 | if (chatChannel is IMultiplayerLobby mp) 173 | { 174 | mp.OnMatchFinished += onMatchStartedHandler; 175 | } 176 | } 177 | 178 | _eventListeners[alert.Name] = onMatchStartedHandler; 179 | } 180 | 181 | private void RegisterOnMatchStartedAudioAlert(UserAudioAlert alert) 182 | { 183 | Action onMatchStartedHandler = async () => 184 | { 185 | if (!alert.Enabled) 186 | { 187 | return; 188 | } 189 | 190 | await HandleAudioEvent(alert); 191 | }; 192 | 193 | foreach (var chatChannel in Client.Channels) 194 | { 195 | if (chatChannel is IMultiplayerLobby mp) 196 | { 197 | mp.OnMatchStarted += onMatchStartedHandler; 198 | } 199 | } 200 | 201 | _eventListeners[alert.Name] = onMatchStartedHandler; 202 | } 203 | 204 | private void RegisterOnKeywordAudioAlert(UserAudioAlert alert) 205 | { 206 | Action onKeywordHandler = async m => 207 | { 208 | if (!alert.Enabled) 209 | { 210 | return; 211 | } 212 | 213 | if (m.Content.Contains(alert.TriggerWord, StringComparison.OrdinalIgnoreCase)) 214 | { 215 | await HandleAudioEvent(alert); 216 | } 217 | }; 218 | 219 | Client.OnPrivateMessageReceived += onKeywordHandler; 220 | _eventListeners[alert.Name] = onKeywordHandler; 221 | } 222 | 223 | private void RegisterOnUsernameMentionedAudioAlert(UserAudioAlert alert) 224 | { 225 | Action onUsernameMentionedHandler = async m => 226 | { 227 | if (!alert.Enabled) 228 | { 229 | return; 230 | } 231 | 232 | if (m.Content.Contains(Client.ClientConfig.Credentials.Username, StringComparison.OrdinalIgnoreCase)) 233 | { 234 | await HandleAudioEvent(alert); 235 | } 236 | }; 237 | 238 | Client.OnPrivateMessageReceived += onUsernameMentionedHandler; 239 | _eventListeners[alert.Name] = onUsernameMentionedHandler; 240 | } 241 | 242 | private void RegisterOnDirectMessageAudioAlert(UserAudioAlert alert) 243 | { 244 | Action onDirectMessageHandler = async m => 245 | { 246 | if (!alert.Enabled) 247 | { 248 | return; 249 | } 250 | 251 | if (m.IsDirect) 252 | { 253 | await HandleAudioEvent(alert); 254 | } 255 | }; 256 | 257 | Client.OnPrivateMessageReceived += onDirectMessageHandler; 258 | _eventListeners[alert.Name] = onDirectMessageHandler; 259 | } 260 | 261 | private void RegisterOnUserMessageAudioAlert(UserAudioAlert alert) 262 | { 263 | Action onMessageHandler = async m => 264 | { 265 | if (!alert.Enabled) 266 | { 267 | return; 268 | } 269 | 270 | if (alert.MultiplayerLobbySpecific) 271 | { 272 | if (!m.IsMultiplayerLobbyMessage()) 273 | { 274 | return; 275 | } 276 | } 277 | 278 | await HandleAudioEvent(alert); 279 | }; 280 | Client.OnPrivateMessageReceived += onMessageHandler; 281 | _eventListeners[alert.Name] = onMessageHandler; 282 | } 283 | 284 | private void UnsubscribeFromAudioEvent(UserAudioAlert alert) 285 | { 286 | if (_eventListeners.TryGetValue(alert.Name, out var eventHandler)) 287 | { 288 | switch (alert.Trigger) 289 | { 290 | case EventTrigger.OnUserMessage: 291 | Client.OnPrivateMessageReceived -= (Action)eventHandler; 292 | break; 293 | case EventTrigger.OnDirectMessage: 294 | Client.OnPrivateMessageReceived -= (Action)eventHandler; 295 | break; 296 | case EventTrigger.OnUsernameMentioned: 297 | Client.OnPrivateMessageReceived -= (Action)eventHandler; 298 | break; 299 | case EventTrigger.OnKeyword: 300 | Client.OnPrivateMessageReceived -= (Action)eventHandler; 301 | break; 302 | } 303 | 304 | _eventListeners.Remove(alert.Name); 305 | } 306 | } 307 | 308 | private async Task HandleAudioEvent(UserAudioAlert alert) 309 | { 310 | Logger.LogTrace("Handling audio event for alert {AlertName}", alert.Name); 311 | await JS.InvokeVoidAsync("playSound", alert.Path); 312 | } 313 | 314 | // === BEGIN EVENT SUBSCRIPTION MANAGERS === 315 | private Action? _onHotkeyPressedDelegateAsync; 316 | 317 | private void SubscribeToHotkeyPressedEvent() 318 | { 319 | _onHotkeyPressedDelegateAsync = async k => await DeployMacroForHotkeyAsync(k); 320 | HotkeyListener.OnHotkeyPressed += _onHotkeyPressedDelegateAsync; 321 | } 322 | 323 | private void UnsubscribeFromHotkeyPressedEvent() => HotkeyListener.OnHotkeyPressed -= _onHotkeyPressedDelegateAsync; 324 | 325 | // === END EVENT SUBSCRIPTION MANAGERS === 326 | private async Task DeployMacroForHotkeyAsync(ParsedJsonEvent k) 327 | { 328 | if (!NavigationManager.Uri.ToLower().Contains("primarydisplay")) 329 | { 330 | Logger.LogTrace("Halted hotkey deployment due to not being on the primary display page"); 331 | return; 332 | } 333 | 334 | foreach (var bind in UserSettings.KeyBinds) 335 | { 336 | if (bind.Key == k.Key && bind.Alt == k.AltKey && bind.Ctrl == k.CtrlKey && bind.Shift == k.ShiftKey) 337 | { 338 | if (_currentlySelectedChannel != null) 339 | { 340 | await Client.SendPrivateMessageAsync(_currentlySelectedChannel.ChannelName, bind.Message); 341 | await InvokeAsync(StateHasChanged); 342 | Logger.LogTrace($"Hotkey deployed: '{bind}' -> {_currentlySelectedChannel.ChannelName}: {bind.Message}"); 343 | break; 344 | } 345 | } 346 | } 347 | } 348 | 349 | public void Dispose() 350 | { 351 | Logger.LogDebug("PrimaryDisplay disposed"); 352 | // TODO: Test if this method is called when navigating away 353 | UnsubscribeFromHotkeyPressedEvent(); 354 | 355 | // Unsubscribe from all event listeners 356 | foreach (var alert in StateManager.AudioAlerts) 357 | { 358 | UnsubscribeFromAudioEvent(alert); 359 | } 360 | 361 | StateManager.OnAudioAlertCreated -= OnAudioAlertCreated; 362 | StateManager.OnAudioAlertUpdated -= OnAudioAlertUpdated; 363 | StateManager.OnAudioAlertDeleted -= OnAudioAlertDeleted; 364 | } 365 | 366 | private void InitClientEvents() 367 | { 368 | if (!StateManager.EventTracker.HasRegisteredPrimaryDisplayDefaultEvents) 369 | { 370 | // Things that can only be registered once go here 371 | Client.OnMessageReceived += async m => 372 | { 373 | if (m is IPrivateIrcMessage priv) 374 | { 375 | await ChannelChipNotificationRecolor(priv); 376 | } 377 | }; 378 | 379 | Client.OnPrivateMessageReceived += async _ => { await InvokeAsync(StateHasChanged); }; 380 | } 381 | 382 | Client.OnChannelParted += c => { Snackbar.Add($"Left channel {c}", Severity.Success); }; 383 | Client.OnPrivateMessageSent += async _ => await InvokeAsync(StateHasChanged); 384 | Client.OnMessageReceived += async _ => await InvokeAsync(StateHasChanged); 385 | Client.OnAuthenticatedUserDMReceived += async m => 386 | { 387 | StateManager.ChannelNotifications.TryAdd(m.Sender, ChatNotification.DirectMessage); 388 | await InvokeAsync(StateHasChanged); 389 | }; 390 | 391 | Client.OnChannelJoined += async channel => 392 | { 393 | /** 394 | * Because of the way audio alerts work, they need to be reloaded whenever 395 | * a multiplayer channel is added. 396 | */ 397 | if (channel.ChannelName.StartsWith("#mp_")) 398 | { 399 | foreach (var alert in StateManager.AudioAlerts.ToImmutableList()) 400 | { 401 | StateManager.UpdateAudioAlert(alert); 402 | } 403 | } 404 | 405 | StateManager.ChannelNotifications.TryAdd(channel.ChannelName, ChatNotification.None); 406 | 407 | Snackbar.Add($"Joined channel {channel}", Severity.Success); 408 | // _currentlySelectedChannel = channel; 409 | 410 | await InvokeAsync(StateHasChanged); 411 | }; 412 | 413 | Client.OnUserQueried += async user => 414 | { 415 | StateManager.ChannelNotifications.TryAdd(user, ChatNotification.None); 416 | 417 | Snackbar.Add($"Opened conversation with {user}", Severity.Success); 418 | await InvokeAsync(StateHasChanged); 419 | }; 420 | 421 | Client.BanchoBotEvents.OnTournamentLobbyCreated += lobby => 422 | { 423 | Snackbar.Add($"Created the tournament match: {lobby.Name}", Severity.Info); 424 | 425 | // Register lobby events 426 | lobby.OnMatchStarted += () => Snackbar.Add($"Match started: {lobby.Name}", Severity.Info); 427 | lobby.OnMatchFinished += () => Snackbar.Add($"Match finished: {lobby.Name}", Severity.Info); 428 | lobby.OnMatchAborted += () => Snackbar.Add($"Match aborted: {lobby.Name}", Severity.Warning); 429 | lobby.OnClosed += () => Snackbar.Add($"Lobby closed: {lobby.Name}", Severity.Info); 430 | lobby.OnStateChanged += async () => 431 | { 432 | await InvokeAsync(StateHasChanged); 433 | 434 | if (lobby.LobbyTimerInProgress) 435 | { 436 | await Task.Run(async () => 437 | { 438 | while (lobby.LobbyTimerInProgress && !lobby.IsClosed) 439 | { 440 | await Task.Delay(1000); 441 | await InvokeAsync(StateHasChanged); 442 | } 443 | }); 444 | } 445 | }; 446 | }; 447 | } 448 | 449 | private async Task ChannelChipNotificationRecolor(IPrivateIrcMessage priv) 450 | { 451 | Logger.LogDebug($"Private message received: {priv}"); 452 | 453 | // If the message is from the currently selected channel, don't recolor the chip 454 | // otherwise, check it for a potential recolor 455 | string target = _currentlySelectedChannel?.ChannelName ?? string.Empty; 456 | bool needsRecolor = target.StartsWith("#") ? 457 | priv.Recipient != target : 458 | priv.Sender != target; 459 | 460 | if (needsRecolor) 461 | { 462 | // Message received from outside source (not the currently selected channel) 463 | string key = priv.IsDirect ? priv.Sender : priv.Recipient; 464 | if (!StateManager.ChannelNotifications.ContainsKey(key)) 465 | { 466 | return; 467 | } 468 | 469 | if (priv.IsDirect) 470 | { 471 | // New DM from outside user 472 | StateManager.ChannelNotifications[key] = ChatNotification.DirectMessage; 473 | } 474 | else 475 | { 476 | // New message in channel 477 | 478 | // Highlight 479 | if (priv.Content.Contains(Client.ClientConfig.Credentials.Username, StringComparison.OrdinalIgnoreCase)) 480 | { 481 | StateManager.ChannelNotifications[key] = ChatNotification.MentionsUsername; 482 | await InvokeAsync(StateHasChanged); 483 | return; 484 | } 485 | 486 | string[] refKeyWords = 487 | { 488 | "ref", 489 | "referee" 490 | }; 491 | 492 | if (priv.Recipient.StartsWith("#mp_")) 493 | { 494 | // New message in referee lobby 495 | if (refKeyWords.Any(k => priv.Content.Contains(k, StringComparison.OrdinalIgnoreCase))) 496 | { 497 | StateManager.ChannelNotifications[key] = ChatNotification.MentionsRefereeKeywords; 498 | } 499 | else 500 | { 501 | StateManager.ChannelNotifications[key] = ChatNotification.GeneralMessage; 502 | } 503 | } 504 | else 505 | { 506 | // New message in general channel 507 | StateManager.ChannelNotifications[key] = ChatNotification.GeneralMessage; 508 | } 509 | } 510 | 511 | await InvokeAsync(StateHasChanged); 512 | } 513 | } 514 | 515 | private async Task JoinDefaultChannels() 516 | { 517 | // Every channel added here also needs to be added to the notifications dict 518 | await Client.QueryUserAsync("BanchoBot"); 519 | 520 | #if DEBUG 521 | var mp = new MultiplayerLobby(Client, 12345, "OWC: (United States) Vs. (Germany)"); 522 | mp.Players.Add(new MultiplayerPlayer(mp, "mrekk", 1, TeamColor.Red, Mods.NoFail | Mods.Hidden | Mods.HardRock) 523 | { 524 | State = PlayerState.NoMap 525 | }); 526 | mp.Players.Add(new MultiplayerPlayer(mp, "lifeline", 2, TeamColor.Red) 527 | { 528 | State = PlayerState.Ready 529 | }); 530 | mp.Players.Add(new MultiplayerPlayer(mp, "Rimuru", 3, TeamColor.Red, Mods.Easy | Mods.Flashlight | 531 | Mods.Hidden | Mods.NoFail)); 532 | mp.Players.Add(new MultiplayerPlayer(mp, "aetrna", 4, TeamColor.Red)); 533 | mp.Players.Add(new MultiplayerPlayer(mp, "BlackDog5", 5, TeamColor.Red)); 534 | mp.Players.Add(new MultiplayerPlayer(mp, "shimon", 6, TeamColor.Red)); 535 | mp.Players.Add(new MultiplayerPlayer(mp, "Utami", 7, TeamColor.Red, Mods.HardRock)); 536 | mp.Players.Add(new MultiplayerPlayer(mp, "Mathi", 8, TeamColor.Red, Mods.Relax)); 537 | mp.Players.Add(new MultiplayerPlayer(mp, "femboy tummy", 9, TeamColor.Blue, Mods.Perfect | Mods.Hidden)); 538 | mp.Players.Add(new MultiplayerPlayer(mp, "Arnold24x24", 10, TeamColor.Blue)); 539 | mp.Players.Add(new MultiplayerPlayer(mp, "Chicony", 11, TeamColor.Blue)); 540 | mp.Players.Add(new MultiplayerPlayer(mp, "NyanPotato", 12, TeamColor.Blue)); 541 | mp.Players.Add(new MultiplayerPlayer(mp, "WindowLife", 13, TeamColor.Blue)); 542 | mp.Players.Add(new MultiplayerPlayer(mp, "Bocchi the Rock", 14, TeamColor.Blue)); 543 | mp.Players.Add(new MultiplayerPlayer(mp, "Rafis", 15, TeamColor.Blue)); 544 | mp.Players.Add(new MultiplayerPlayer(mp, "maliszewski", 16, TeamColor.Blue)); 545 | Client.Channels.Add(mp); 546 | StateManager.ChannelNotifications.TryAdd("#mp_12345", ChatNotification.None); 547 | #endif 548 | } 549 | 550 | public record ModDisplay(Color Color, string Abbreviation, string Tooltip); 551 | 552 | } 553 | 554 | 555 | -------------------------------------------------------------------------------- /BrigittaBlazor/Pages/Settings.razor: -------------------------------------------------------------------------------- 1 | @page "/settings" 2 | @using BrigittaBlazor.Shared.Components.Settings 3 | 4 | 5 | 6 | 7 |

Settings

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | @code { 24 | 25 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace BrigittaBlazor.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = "_Layout"; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BrigittaBlazor/Pages/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | @namespace BrigittaBlazor.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | @RenderBody() 18 | 19 |
20 | 21 | An error has occurred. This application may no longer respond until reloaded. 22 | 23 | 24 | An unhandled exception has occurred. See browser dev tools for details. 25 | 26 | Reload 27 | 🗙 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /BrigittaBlazor/Program.cs: -------------------------------------------------------------------------------- 1 | using BanchoSharp; 2 | using BanchoSharp.Interfaces; 3 | using BrigittaBlazor.Auth; 4 | using BrigittaBlazor.Settings; 5 | using BrigittaBlazor.Utils; 6 | using Microsoft.AspNetCore.Components.Authorization; 7 | using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; 8 | using Microsoft.AspNetCore.Hosting.StaticWebAssets; 9 | using MudBlazor; 10 | using MudBlazor.Services; 11 | using Octokit; 12 | using Serilog; 13 | using Serilog.Filters; 14 | 15 | var builder = WebApplication.CreateBuilder(args); 16 | 17 | StaticWebAssetsLoader.UseStaticWebAssets(builder.Environment, builder.Configuration); 18 | 19 | builder.WebHost.UseWebRoot("wwwroot").UseStaticWebAssets(); 20 | 21 | // Add services to the container. 22 | builder.Services.AddAuthenticationCore(); 23 | builder.Services.AddRazorPages(); 24 | builder.Services.AddServerSideBlazor().AddCircuitOptions(options => { options.DetailedErrors = true; }); 25 | builder.Services.AddScoped(); 26 | builder.Services.AddScoped(); 27 | builder.Services.AddScoped(_ => new GitHubClient(new ProductHeaderValue("Brigitta"))); 28 | builder.Services.AddScoped(); 29 | builder.Services.AddScoped(); 30 | 31 | // Required in order to ensure the hotkey listener is initialized only once. 32 | // Without this, if the page is refreshed, the hotkey listener will be initialized again, 33 | // resulting in multiple hotkey listeners. 34 | builder.Services.AddSingleton(); 35 | builder.Services.AddSingleton(); 36 | builder.Services.AddSingleton(); 37 | 38 | // Add serilog as the logging provider with file and console sinks 39 | builder.Services.AddLogging(loggingBuilder => { loggingBuilder.AddSerilog(dispose: true); }); 40 | 41 | Log.Logger = new LoggerConfiguration() 42 | .MinimumLevel.Verbose() 43 | .Filter.ByExcluding(Matching.FromSource("Microsoft")) 44 | .WriteTo.Console() 45 | .WriteTo.File("logs/brigitta.log", rollingInterval: RollingInterval.Day) 46 | .CreateLogger(); 47 | 48 | builder.Host.UseSerilog(); 49 | 50 | builder.Services.AddMudServices(config => 51 | { 52 | config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft; 53 | 54 | config.SnackbarConfiguration.VisibleStateDuration = 2500; 55 | config.SnackbarConfiguration.HideTransitionDuration = 500; 56 | config.SnackbarConfiguration.ShowTransitionDuration = 500; 57 | }); 58 | 59 | builder.Services.AddScoped(); 60 | 61 | var app = builder.Build(); 62 | 63 | // Configure the HTTP request pipeline. 64 | if (!app.Environment.IsDevelopment()) 65 | { 66 | app.UseExceptionHandler("/Error"); 67 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 68 | app.UseHsts(); 69 | } 70 | 71 | app.UseHttpsRedirection(); 72 | 73 | app.UseStaticFiles(); 74 | 75 | app.UseRouting(); 76 | 77 | app.MapBlazorHub(); 78 | app.MapFallbackToPage("/_Host"); 79 | 80 | try 81 | { 82 | Log.Information("Launching on http://localhost:5000/ -- Navigate to this address in your browser"); 83 | app.Run(); 84 | } 85 | catch (IOException e) 86 | { 87 | Log.Fatal("It looks like Brigitta is already running or something else is occupying port 5001 on your machine. " + 88 | "If Brigitta is already running, you can close it and try again. " + 89 | "If something else is occupying port 5001, you can close it and try again. " + 90 | "If you are not sure what is occupying port 5001, you can try restarting your PC. " + 91 | // ReSharper disable once LogMessageIsSentenceProblem 92 | "If you are still having issues, please contact the developer."); 93 | } 94 | catch (Exception e) 95 | { 96 | Log.Fatal(e, "Application terminated unexpectedly"); 97 | } 98 | finally 99 | { 100 | Log.CloseAndFlush(); 101 | Console.ReadLine(); 102 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:61949", 7 | "sslPort": 44393 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "BrigittaBlazor": { 19 | "commandName": "Project", 20 | "dotnetRunMessages": true, 21 | "launchBrowser": true, 22 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BrigittaBlazor/Settings/UserAudioAlert.cs: -------------------------------------------------------------------------------- 1 | namespace BrigittaBlazor.Settings; 2 | 3 | public class UserAudioAlert 4 | { 5 | public string Name { get; set; } = string.Empty; 6 | public string Path { get; set; } = string.Empty; 7 | public bool Enabled { get; set; } = true; 8 | public bool MultiplayerLobbySpecific { get; set; } 9 | public double Volume { get; set; } = 0.5; 10 | public EventTrigger Trigger { get; set; } 11 | public string TriggerWord { get; set; } = string.Empty; 12 | 13 | public override string ToString() => $"{Name} => {Trigger}"; 14 | } 15 | 16 | public enum EventTrigger 17 | { 18 | OnUserMessage, 19 | OnDirectMessage, 20 | OnUsernameMentioned, 21 | OnKeyword, 22 | OnMatchStarted, 23 | OnMatchFinished 24 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Settings/UserKeyBind.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace BrigittaBlazor.Settings; 4 | 5 | public class UserKeyBind 6 | { 7 | public string Key { get; set; } 8 | public bool Alt { get; set; } 9 | public bool Ctrl { get; set; } 10 | public bool Shift { get; set; } 11 | /// 12 | /// The message sent to chat when the key combination gets pressed 13 | /// 14 | public string Message { get; set; } 15 | 16 | public override string ToString() 17 | { 18 | var sb = new StringBuilder(); 19 | if (Ctrl) 20 | { 21 | sb.Append("CTRL+"); 22 | } 23 | 24 | if (Shift) 25 | { 26 | sb.Append("SHIFT+"); 27 | } 28 | 29 | if (Alt) 30 | { 31 | sb.Append("ALT+"); 32 | } 33 | 34 | sb.Append(Key.ToUpper().Replace("CONTROL", "").Replace("ALT", "").Replace("SHIFT", "")); 35 | return sb.ToString(); 36 | } 37 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Settings/UserSettings.cs: -------------------------------------------------------------------------------- 1 | using BrigittaBlazor.Utils; 2 | using Newtonsoft.Json; 3 | 4 | namespace BrigittaBlazor.Settings; 5 | 6 | public class UserSettings 7 | { 8 | private readonly ILogger _logger; 9 | public UserSettings() {} 10 | 11 | public UserSettings(ILogger logger, StateMaintainer stateManager) 12 | { 13 | _logger = logger; 14 | var loaded = LoadOrCreate("settings.json"); 15 | 16 | KeyBinds = loaded.KeyBinds; 17 | AudioAlerts = loaded.AudioAlerts; 18 | 19 | // Register the alerts in the state manager 20 | foreach (var alert in AudioAlerts) 21 | { 22 | stateManager.AddAudioAlert(alert); 23 | } 24 | 25 | _logger.LogInformation("Settings loaded"); 26 | } 27 | 28 | public List KeyBinds { get; set; } = new(); 29 | public List AudioAlerts { get; set; } = new(); 30 | 31 | private UserSettings LoadOrCreate(string path) 32 | { 33 | _logger.LogDebug("Attempting to load settings"); 34 | if (!File.Exists(path)) 35 | { 36 | _logger.LogDebug("File settings.json does not exist, creating"); 37 | File.Create(path).Close(); 38 | _logger.LogDebug("Created file settings.json"); 39 | } 40 | 41 | _logger.LogDebug("Loading settings"); 42 | string json = File.ReadAllText(path); 43 | var settings = JsonConvert.DeserializeObject(json); 44 | if (settings == null) 45 | { 46 | // Save default settings 47 | settings = new UserSettings(); 48 | string jsonToSave = JsonConvert.SerializeObject(settings, Formatting.Indented); 49 | File.WriteAllText(path, jsonToSave); 50 | 51 | _logger.LogDebug("Wrote default settings file to settings.json"); 52 | } 53 | 54 | return settings; 55 | } 56 | 57 | public void Save() 58 | { 59 | string json = JsonConvert.SerializeObject(this, Formatting.Indented); 60 | File.WriteAllText("settings.json", json); 61 | _logger.LogInformation("Saved settings"); 62 | } 63 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/AddChannelDialog.razor: -------------------------------------------------------------------------------- 1 | @using BanchoSharp.Interfaces 2 | @inject IBanchoClient Client 3 | @inject ISnackbar Snackbar 4 | 5 | @* Add channel dialog *@ 6 | 7 | 8 |

Add a channel

9 | 11 |
12 | 13 | Cancel 14 | Add 15 | 16 |
17 | 18 | @code { 19 | [CascadingParameter] 20 | MudDialogInstance MudDialog { get; set; } = null!; 21 | private string? _addChannelDialogValue; 22 | 23 | private async Task AddChannelAsync() 24 | { 25 | await Client.JoinChannelAsync(_addChannelDialogValue!); 26 | Close(); 27 | } 28 | 29 | private IEnumerable AddChannelValidation(string channel) 30 | { 31 | if (string.IsNullOrWhiteSpace(channel)) 32 | { 33 | yield return "Channel name must not be empty."; 34 | } 35 | } 36 | 37 | private void Close() => MudDialog.Close(DialogResult.Ok(_addChannelDialogValue)); 38 | private void Cancel() => MudDialog.Close(DialogResult.Cancel()); 39 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/ChannelChipList.razor: -------------------------------------------------------------------------------- 1 | @using BanchoSharp.Interfaces 2 | @using BrigittaBlazor.Utils 3 | @inject IBanchoClient Client 4 | @inject IScrollUtils ScrollUtils 5 | @inject ILogger Logger 6 | 7 | 8 | 19 | 20 | 23 | 24 | @foreach (var ch in Client.Channels) 25 | { 26 | if (!ChannelNotifications.TryGetValue(ch.ChannelName, out var notif)) 27 | { 28 | Logger.LogWarning($"Failed to get notification status for channel '{ch}', " + 29 | "channel name was not in dictionary. Adding..."); 30 | if (!ChannelNotifications.TryAdd(ch.ChannelName, ChatNotification.None)) 31 | { 32 | Logger.LogError($"Something went wrong when trying to add '{ch.ChannelName}' " + 33 | "to the notifications dictionary!"); 34 | } 35 | } 36 | 37 | var props = GetChipPropertiesForUnreadChatMessage(notif); 38 | var current = ChannelNotifications.GetValueOrDefault(ch.ChannelName); 39 | 40 | try 41 | { 42 | props = current > notif ? GetChipPropertiesForUnreadChatMessage(notif) : props; 43 | 44 | 47 | 49 | @ch.ChannelName 50 | 51 | 52 | 53 | } 54 | catch (NullReferenceException e) 55 | { 56 | Logger.LogError("Failed to add channel due to a null reference, likely resolving from " + 57 | "the ChannelNotifications dictionary not containing the channel name.\n" + 58 | $"Value of props: {props}\n" + 59 | $"Value of current: {current}", e); 60 | } 61 | catch (Exception ex) 62 | { 63 | Logger.LogError("Something went seriously wrong when trying to add a channel!", ex); 64 | } 65 | } 66 | 67 | 68 | 69 | @code { 70 | [Parameter] 71 | public IChatChannel? CurrentlySelectedChannel { get; set; } 72 | [Parameter] 73 | public EventCallback CurrentlySelectedChannelChanged { get; set; } 74 | [Parameter] 75 | public Dictionary ChannelNotifications { get; set; } = null!; 76 | 77 | protected override async Task OnInitializedAsync() 78 | { 79 | // When a new channel is joined, simulate a click to that channel. 80 | Client.OnChannelJoined += async c => 81 | { 82 | await SimulateClickAsync(c); 83 | }; 84 | 85 | // Simulate a click to the default joined channel by default (such as BanchoBot) 86 | if (Client.Channels.Any()) 87 | { 88 | await SimulateClickAsync(Client.Channels.First()); 89 | } 90 | 91 | } 92 | 93 | private async Task SimulateClickAsync(IChatChannel channel) 94 | { 95 | var match = Client.Channels.FirstOrDefault(x => x.Equals(channel)); 96 | if (match == null) 97 | { 98 | Logger.LogWarning($"Failed to simulate click on channel '{channel}' because it was not found in Client.Channels"); 99 | return; 100 | } 101 | 102 | await OnSelectedChannelChangedAsync(match); 103 | await OnChannelChipClickedAsync(match.ChannelName); 104 | } 105 | 106 | private async Task OnSelectedChannelChangedAsync(IChatChannel channel) 107 | { 108 | // Select the channel adjacent to this one, if possible 109 | CurrentlySelectedChannel = channel; 110 | 111 | await CurrentlySelectedChannelChanged.InvokeAsync(channel); 112 | Logger.LogDebug("Channel clicked: " + channel); 113 | } 114 | 115 | private async Task OnChannelClosedAsync(IChatChannel ch) 116 | { 117 | Logger.LogDebug($"Closing channel {ch.ChannelName} via chip close button"); 118 | 119 | if (CurrentlySelectedChannel == ch) 120 | { 121 | int chIndex = Client.Channels.IndexOf(CurrentlySelectedChannel); 122 | if (chIndex == -1) 123 | { 124 | Logger.LogWarning($"Failed to find index of channel '{ch}' in Client.Channels"); 125 | return; 126 | } 127 | 128 | if (Client.Channels.Count > (chIndex + 1)) 129 | chIndex++; 130 | else if (Client.Channels.Count > (chIndex - 1)) 131 | chIndex--; 132 | 133 | var resolvedItem = Client.Channels.ElementAtOrDefault(chIndex); 134 | if (resolvedItem != null) 135 | { 136 | CurrentlySelectedChannel = resolvedItem; 137 | await SimulateClickAsync(resolvedItem); 138 | Logger.LogDebug($"Channel '{ch}' closed, auto-selecting " + 139 | $"adjacent channel '{resolvedItem}'"); 140 | } 141 | else 142 | { 143 | Logger.LogDebug($"Failed to find a valid channel to select after closing '{ch}'. The user has " + 144 | "probably closed all channels."); 145 | } 146 | } 147 | 148 | ChannelNotifications.Remove(ch.ChannelName); 149 | await Client.PartChannelAsync(ch.ChannelName); 150 | } 151 | 152 | private async Task OnChannelChipClickedAsync(string channelName) 153 | { 154 | if (!ChannelNotifications.ContainsKey(channelName)) 155 | { 156 | Logger.LogWarning($"ChannelNotifications dictionary did not contain '{channelName}' when trying to reset chip color"); 157 | return; 158 | } 159 | 160 | // Reset all selected states and apply to current 161 | foreach (var item in ChannelNotifications) 162 | { 163 | if (item.Value == ChatNotification.CurrentlySelected) 164 | { 165 | ChannelNotifications[item.Key] = ChatNotification.None; 166 | } 167 | } 168 | 169 | ChannelNotifications[channelName] = ChatNotification.CurrentlySelected; 170 | } 171 | 172 | private (Color color, Variant variant) GetChipPropertiesForUnreadChatMessage(ChatNotification status) => 173 | status switch 174 | { 175 | ChatNotification.CurrentlySelected => (Color.Primary, Variant.Filled), 176 | ChatNotification.DirectMessage => (Color.Error, Variant.Outlined), 177 | ChatNotification.GeneralMessage => (Color.Info, Variant.Text), 178 | ChatNotification.MentionsUsername => (Color.Warning, Variant.Filled), 179 | ChatNotification.MentionsRefereeKeywords => (Color.Error, Variant.Filled), 180 | _ => (Color.Primary, Variant.Text) 181 | }; 182 | 183 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/ChatConsole.razor: -------------------------------------------------------------------------------- 1 | @using System.Collections.Immutable 2 | @using System.Text.RegularExpressions 3 | @using BrigittaBlazor.Extensions 4 | @using BanchoSharp.Interfaces 5 | @using BanchoSharp.Multiplayer 6 | @using BrigittaBlazor.Utils 7 | @inject IBanchoClient Client 8 | @inject ILogger Logger 9 | @inject IJSRuntime JS 10 | @inject IScrollUtils ScrollUtils 11 | 12 | @*Text box*@ 13 |
14 | @if (CurrentChannel != null && CurrentChannel.MessageHistory != null) 15 | { 16 | 18 | 19 | 20 | @if (DisplayTimestamps) 21 | { 22 | 23 | @if (DisplayUTC) 24 | { 25 | 27 | @($"{priv.ToUTCTimeString()}") 28 | 29 | } 30 | else 31 | { 32 | 34 | @($"{priv.ToTimeString()}") 35 | 36 | } 37 | 38 | } 39 | 40 | 42 | 45 | @priv.Sender 46 | 47 | 48 | 49 | 50 | @** 51 | Check for URLs within the string 52 | Second condition checks for parentheses. The regex should match within parentheses 53 | anyway, but it doesn't. So we check for the first character being a ( and the last 54 | character being a ), and if it is, we remove the first and last character and check 55 | *@ 56 | @if (priv.Content.Contains("ACTION is ") && _actionRegex.IsMatch(priv.Content)) 57 | { 58 | @*Process ACTION is listening to, is watching, is playing*@ 59 | string action = ActionSwitcher(priv.Content.Split("ACTION is ")[1].Split()[0]); 60 | string actionMatch = _actionRegex.Match(priv.Content).Value; 61 | string urlMatch = actionMatch.Split()[0][1..]; 62 | if (_urlRegex.IsMatch(urlMatch)) 63 | { 64 | string url = _urlRegex.Match(urlMatch).Value; 65 | string actionHighlight = priv.Content.Split(url)[1]; 66 | 67 | // Replace last ] 68 | int replIdx = actionHighlight.LastIndexOf(']'); 69 | actionHighlight = actionHighlight.Remove(replIdx, 1); 70 | 71 | // Remove trailing unicode character 72 | actionHighlight = actionHighlight.Remove(actionHighlight.Length - 1); 73 | 74 | // Link the action to the beatmap in question 75 | 76 | @priv.Sender is 77 | @action 78 | 79 | 80 | 82 | @actionHighlight 83 | 84 | 85 | 86 | } 87 | } 88 | else if (priv.Content.Split().Any(x => _urlRegex.IsMatch(x) && !(x.Contains(')') || 89 | x.Contains('(')))) 90 | { 91 | string[] splits = priv.Content.Split(); 92 | 93 | var words = new List<(string text, bool isUrl)>(); 94 | foreach (string s in splits) 95 | { 96 | words.Add(_urlRegex.IsMatch(s) ? (s, true) : (s, false)); 97 | } 98 | 99 | @foreach (var word in words) 100 | { 101 | if (word.isUrl) 102 | { 103 | 104 | 106 | @(word.text + " ") 107 | 108 | 109 | } 110 | else 111 | { 112 | @(word.text + " ") 113 | } 114 | } 115 | } 116 | @*Process 'beatmap changed to'*@ 117 | else if (priv.Content.Contains("Beatmap changed to: ") && priv.Sender == "BanchoBot") 118 | { 119 | string data = priv.Content.Split("Beatmap changed to: ")[1]; 120 | 121 | // Capture everything between the parentheses 122 | string url = data[(data.LastIndexOf('(') + 1)..data.LastIndexOf(')')]; 123 | if (!_urlRegex.IsMatch(url)) 124 | { 125 | Logger.LogWarning("URL could not be parsed from beatmap change message"); 126 | return; 127 | } 128 | 129 | // Hyperlink url to the data we're concerned with 130 | data = data[..data.LastIndexOf('(')]; 131 | Beatmap changed to: 132 | 133 | 134 | @data 135 | 136 | 137 | } 138 | else 139 | { 140 | @priv.Content 141 | } 142 | 143 | 144 | 145 | } 146 |
147 | 148 | @code { 149 | private readonly Regex _urlRegex = new(@"^(https?:\/\/)([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w\.-]*)*\/?#?([\/\w\.-]*)\/?#?([\w\.-]*)\/?$"); 150 | private readonly Regex _actionRegex = new(@"\[.+\]"); 151 | private ElementReference _consoleRef; 152 | [Parameter] 153 | public bool AutoScroll { get; set; } 154 | [Parameter] 155 | public IChatChannel? CurrentChannel { get; set; } 156 | [Parameter] 157 | public IMultiplayerLobby? CurrentLobby { get; set; } 158 | [Parameter] 159 | public bool DisplayTimestamps { get; set; } 160 | [Parameter] 161 | public bool DisplayUTC { get; set; } 162 | private string GetUsernameLink(string username) => $"https://osu.ppy.sh/u/{username}"; 163 | 164 | protected override async Task OnAfterRenderAsync(bool _) 165 | { 166 | if (AutoScroll) 167 | { 168 | await ScrollUtils.ScrollToBottomAsync(ScrollUtils.ConsoleId); 169 | } 170 | } 171 | 172 | private string ActionSwitcher(string action) 173 | { 174 | // ReSharper disable once ConvertSwitchStatementToSwitchExpression 175 | switch (action) 176 | { 177 | case "listening": 178 | return "listening to"; 179 | default: 180 | return action; 181 | } 182 | } 183 | 184 | private string GetUsernameColor(string username) 185 | { 186 | string loggedInUsername = Client.ClientConfig.Credentials.Username; 187 | switch (username) 188 | { 189 | case "BanchoBot": 190 | return Colors.Pink.Lighten2; 191 | default: 192 | { 193 | if (username == loggedInUsername) 194 | { 195 | return Colors.Cyan.Lighten1; 196 | } 197 | break; 198 | } 199 | } 200 | 201 | // Check for if in lobby 202 | if (CurrentLobby != null) 203 | { 204 | var match = CurrentLobby.FindPlayer(username); 205 | if (match != null) 206 | { 207 | if (match.Name.Equals(username, StringComparison.OrdinalIgnoreCase)) 208 | { 209 | // Check team 210 | if (match.Team is TeamColor.Red or TeamColor.Blue) 211 | { 212 | return Colors.Blue.Lighten5; 213 | } 214 | } 215 | } 216 | } 217 | 218 | return Colors.Purple.Lighten2; 219 | } 220 | 221 | private string GetUsernameBackground(string username) 222 | { 223 | var match = CurrentLobby?.FindPlayer(username); 224 | string? r = string.Empty; 225 | 226 | if (match == null || CurrentLobby?.Format is not (LobbyFormat.TeamVs or LobbyFormat.TagTeamVs)) 227 | return r; 228 | 229 | if (match.Name.Equals(username, StringComparison.OrdinalIgnoreCase)) 230 | { 231 | // Check team 232 | if (match.Team == TeamColor.Red) 233 | { 234 | r = Colors.Red.Accent3; 235 | } 236 | else if (match.Team == TeamColor.Blue) 237 | { 238 | r = Colors.Blue.Accent3; 239 | } 240 | } 241 | 242 | return r; 243 | } 244 | 245 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/ChatConsoleHeader.razor: -------------------------------------------------------------------------------- 1 | @using BanchoSharp.Interfaces 2 | @using BrigittaBlazor.Utils 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @code { 17 | [Parameter] 18 | public IChatChannel? CurrentItem { get; set; } 19 | [Parameter] 20 | public EventCallback CurrentItemChanged { get; set; } 21 | [Parameter] 22 | public Dictionary ChannelNotifications { get; set; } = null!; 23 | private IChatChannel? BoundValue 24 | { 25 | get => CurrentItem; 26 | set => CurrentItemChanged.InvokeAsync(value); 27 | } 28 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/ChatIconButtonsPanel.razor: -------------------------------------------------------------------------------- 1 | @using BanchoSharp.Interfaces 2 | @using BanchoSharp.Messaging.ChatMessages 3 | @using BrigittaBlazor.Utils 4 | @inject IJSRuntime JS 5 | @inject IBanchoClient Client 6 | @inject ISnackbar Snackbar 7 | @inject IDialogService DialogService 8 | @inject ILogger Logger; 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 43 | 44 | 45 | 46 | 48 | 49 | 51 | 52 | 53 | @if (DebugUtils.IsDebugBuild()) 54 | { 55 | 56 | 57 | 58 | 59 | 60 | 61 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | } 70 | 71 | 72 | 73 | @code { 74 | [Parameter] 75 | public bool AutoScroll { get; set; } = true; 76 | [Parameter] 77 | public bool DisplayTimestamps { get; set; } = true; 78 | [Parameter] 79 | public bool DisplayUTC { get; set; } 80 | [Parameter] 81 | public IChatChannel? CurrentChannel { get; set; } 82 | [Parameter] 83 | public EventCallback AutoScrollChanged { get; set; } 84 | [Parameter] 85 | public EventCallback DisplayTimestampsChanged { get; set; } 86 | [Parameter] 87 | public EventCallback DisplayUTCChanged { get; set; } 88 | 89 | private void OnAutoScrollToggleChanged(bool isToggled) 90 | { 91 | AutoScroll = isToggled; 92 | AutoScrollChanged.InvokeAsync(AutoScroll); 93 | } 94 | 95 | private void OnDisplayTimestampsToggleChanged(bool isToggled) 96 | { 97 | DisplayTimestamps = isToggled; 98 | DisplayTimestampsChanged.InvokeAsync(DisplayTimestamps); 99 | } 100 | 101 | private void OnDisplayUTCToggleChanged(bool isToggled) 102 | { 103 | DisplayUTC = isToggled; 104 | DisplayUTCChanged.InvokeAsync(DisplayUTC); 105 | } 106 | 107 | private int _dbg_SimulateMsgCount { get; set; } = 10; 108 | private bool _dbg_SimulateToCurrentChannel { get; set; } = true; 109 | private string _autoScrollToolTip => AutoScroll ? "AutoScroll (currently enabled)" : "AutoScroll (currently disabled)"; 110 | private async Task OpenChannelDialogAsync() => await DialogService.ShowAsync("Add Channel"); 111 | private async Task DownloadChatHistoryAsync() => await FileUtils.SaveChatHistoryAsync(Logger, CurrentChannel, DisplayUTC, JS, Snackbar); 112 | 113 | private void Dbg_SimulateMessages() 114 | { 115 | Random rand = new(); 116 | 117 | string recipient = _dbg_SimulateToCurrentChannel ? CurrentChannel?.ChannelName ?? "#debug-testing" : "#debug-testing"; 118 | string[] senders = 119 | { 120 | "Foo", 121 | "SomeGuy18", 122 | "Bar", 123 | "Baz", 124 | "Qux", 125 | "Quux", 126 | "Corge", 127 | "Grault", 128 | "Garply", 129 | "Waldo", 130 | "Fred", 131 | "Plugh", 132 | "Xyzzy", 133 | "Thud" 134 | }; 135 | string[] sampleMessages = 136 | { 137 | "Hello world!", 138 | "This is a test message", 139 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 140 | "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 141 | "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", 142 | "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 143 | }; 144 | for (int i = 0; i < _dbg_SimulateMsgCount; i++) 145 | { 146 | string sender = senders[rand.Next(senders.Length)]; 147 | 148 | if (_dbg_SimulateToCurrentChannel && CurrentChannel != null && recipient == CurrentChannel.ChannelName) 149 | { 150 | if(!CurrentChannel.ChannelName.StartsWith("#")) 151 | { 152 | sender = CurrentChannel.ChannelName; 153 | } 154 | } 155 | 156 | string message = sampleMessages[rand.Next(sampleMessages.Length)]; 157 | Client.SimulateMessageReceived(PrivateIrcMessage.CreateFromParameters(sender, recipient, message)); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/Dialogs/AddAudioAlertDialog.razor: -------------------------------------------------------------------------------- 1 | @using BrigittaBlazor.Settings 2 | @using BrigittaBlazor.Utils 3 | @inject ISnackbar Snackbar 4 | @inject UserSettings UserSettings 5 | @inject IJSRuntime JS 6 | 7 | 8 | 9 | 10 | 11 | New Audio Alert 12 | 13 | Audio files are located in the 'wwwroot/Sounds' directory 14 | 15 | 16 | 17 | 18 | 21 | Multiplayer Lobby Specific 22 | 23 | 24 | 25 | 26 | 27 | 28 | @foreach (string file in SoundFilePaths) 29 | { 30 | 31 | } 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | @foreach (EventTrigger item in Enum.GetValues(typeof(EventTrigger))) 45 | { 46 | @item 47 | } 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Enabled 56 | 57 | 58 | 59 | 60 | Cancel 61 | Add Alert 62 | 63 | 64 | 65 | @code { 66 | [CascadingParameter] 67 | public MudDialogInstance MudDialog { get; set; } 68 | 69 | // Disable the field if the user has selected something other than "OnKeyword" 70 | bool TriggerWordDisabled() => Trigger != EventTrigger.OnKeyword; 71 | bool MultiplayerSpecificDisabled(EventTrigger trigger) => trigger is 72 | EventTrigger.OnMatchFinished or 73 | EventTrigger.OnMatchStarted or 74 | EventTrigger.OnDirectMessage; 75 | 76 | bool FormSuccess { get; set; } 77 | 78 | string SelectedAudio { get; set; } 79 | EventTrigger Trigger { get; set; } 80 | [Parameter] 81 | public UserAudioAlert AudioAlert { get; set; } 82 | [Parameter] 83 | public List SoundFilePaths { get; set; } 84 | string TriggerWord 85 | { 86 | get => Trigger != EventTrigger.OnKeyword ? string.Empty : AudioAlert.TriggerWord; 87 | set => AudioAlert.TriggerWord = value; 88 | } 89 | 90 | private void Cancel() => MudDialog.Cancel(); 91 | 92 | private void AddAlert() 93 | { 94 | if (Trigger == EventTrigger.OnKeyword && string.IsNullOrWhiteSpace(TriggerWord)) 95 | { 96 | // todo: Have proper form validation instead. 97 | Snackbar.Add("You must specify a trigger phrase for the OnKeyword trigger.", Severity.Error); 98 | return; 99 | } 100 | 101 | if (UserSettings.AudioAlerts.Any(x => x.Name.Equals(AudioAlert.Name, StringComparison.OrdinalIgnoreCase))) 102 | { 103 | Snackbar.Add("Names for alerts must be unique", Severity.Error); 104 | return; 105 | } 106 | 107 | AudioAlert.Path = SelectedAudio; 108 | AudioAlert.Trigger = Trigger; 109 | 110 | Snackbar.Add("Alert Added", Severity.Success); 111 | 112 | MudDialog.Close(DialogResult.Ok(AudioAlert)); 113 | } 114 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/HelpWikiButton.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/MultiplayerLobby/LobbyButtonsPanel.razor: -------------------------------------------------------------------------------- 1 | @using BanchoSharp.Interfaces 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | @*Refresh !mp settings*@ 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | @*Abort timer*@ 36 | @if (CurrentLobby.IsLocked) 37 | { 38 | 40 | } 41 | else 42 | { 43 | 45 | } 46 | 47 | 48 | 49 | 50 | 51 | @*Abort timer*@ 52 | 54 | 55 | 56 | 57 | 58 | 59 | @*Abort lobby*@ 60 | 62 | 63 | 64 | 65 | 66 | @code { 67 | [Parameter] 68 | public IMultiplayerLobby? CurrentLobby { get; set; } 69 | private int _mpTimerValue = 120; 70 | private int _mpMatchTimerValue = 5; 71 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/MultiplayerLobby/LobbyInformationPanel.razor: -------------------------------------------------------------------------------- 1 | @using BrigittaBlazor.Extensions 2 | @using BanchoSharp.Interfaces 3 | 4 | 5 | 6 | 7 | 8 | @CurrentLobby.Name 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Size: @CurrentLobby.Size 18 | 19 | 20 | 21 | 22 | 23 | Players: @CurrentLobby.PlayerCount 24 | 25 | 26 | 27 | 28 | @(CurrentLobby.LobbyTimerInProgress ? CurrentLobby.LobbyTimerRemaining.ToFormattedDuration() : "No timer active") 29 | 30 | 31 | @*Format*@ 32 | 33 | 34 | Format: @CurrentLobby.Format 35 | 36 | 37 | 38 | @*Win condition*@ 39 | 40 | 41 | WC: @CurrentLobby.WinCondition 42 | 43 | 44 | 45 | @*Gamemode*@ 46 | 47 | 48 | Mode: @CurrentLobby.GameMode 49 | 50 | 51 | 52 | 53 | 54 | 55 | @code { 56 | [Parameter] 57 | public IMultiplayerLobby? CurrentLobby { get; set; } 58 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/MultiplayerLobby/LobbyModSelectionPanel.razor: -------------------------------------------------------------------------------- 1 | @using BanchoSharp.Interfaces 2 | @using BrigittaBlazor.Pages 3 | @using System.Text 4 | @inject IBanchoClient Client 5 | @inject ISnackbar Snackbar 6 | 7 | 8 | 9 | Mod Selection 10 | 11 | 12 | 13 | 14 | @foreach (var mod in ModDisplays) 15 | { 16 | 17 | 18 | 19 | @mod.Abbreviation 20 | 21 | 22 | 23 | 24 | @* ReSharper disable once ConvertIfStatementToSwitchStatement *@ 25 | if (mod.Abbreviation is "FM") 26 | { 27 | 28 | 29 | 30 | } 31 | 32 | if (mod.Abbreviation is "HT" or "FL") 33 | { 34 | 35 | 36 | 37 | } 38 | } 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | Deploy Mods 49 | 50 | 51 | 52 | 54 | Clear 55 | 56 | 57 | 58 | 59 | 60 | 61 | @code { 62 | [Parameter] 63 | public IMultiplayerLobby? CurrentLobby { get; set; } 64 | private MudChip[]? _selectedMods; 65 | private static readonly IEnumerable ModDisplays = new List 66 | { 67 | new(Color.Info, "NM", "No Mod"), 68 | new(Color.Info, "FM", "Freemod"), 69 | new(Color.Success, "NF", "No Fail"), 70 | new(Color.Success, "EZ", "Easy"), 71 | new(Color.Success, "HT", "Half Time"), 72 | new(Color.Error, "HR", "Hard Rock"), 73 | new(Color.Surface, "SD", "Sudden Death"), 74 | new(Color.Primary, "DT", "Double Time"), 75 | new(Color.Primary, "NC", "Nightcore"), 76 | new(Color.Warning, "HD", "Hidden"), 77 | new(Color.Surface, "FL", "Flashlight"), 78 | new(Color.Info, "RX", "Relax"), 79 | new(Color.Info, "AP", "Auto Pilot"), 80 | new(Color.Info, "SO", "Spun Out") 81 | }; 82 | 83 | private async Task DeployModSelectionsAsync() 84 | { 85 | var sb = new StringBuilder("!mp mods "); 86 | 87 | if (_selectedMods == null) 88 | { 89 | return; 90 | } 91 | 92 | foreach (var mod in _selectedMods) 93 | { 94 | var display = mod.Value as PrimaryDisplay.ModDisplay; 95 | if (display == null) 96 | { 97 | continue; 98 | } 99 | 100 | string append = display.Abbreviation; 101 | 102 | if (display.Abbreviation == "FM") 103 | { 104 | append = "Freemod"; 105 | } 106 | else if (display.Abbreviation == "NM") 107 | { 108 | sb = new StringBuilder("!mp mods"); 109 | break; 110 | } 111 | 112 | sb.Append(append + " "); 113 | } 114 | if (CurrentLobby == null) 115 | { 116 | Snackbar.Add("Failed to deploy mods, no lobby selected", Severity.Error); 117 | ClearMods(); 118 | return; 119 | } 120 | 121 | ClearMods(); 122 | string send = sb.ToString().Trim(); 123 | await Client.SendPrivateMessageAsync(CurrentLobby.ChannelName, send); 124 | } 125 | 126 | private void ClearMods() => _selectedMods = Array.Empty(); 127 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/MultiplayerLobby/LobbyPlayerDisplay.razor: -------------------------------------------------------------------------------- 1 | @using BanchoSharp.Interfaces 2 | @using BanchoSharp.Multiplayer 3 | 4 | @if (CurrentlySelectedLobby != null && CurrentlySelectedLobby.Players.Any()) 5 | { 6 | @foreach (var player in CurrentlySelectedLobby.Players.OrderBy(x => x.Slot)) 7 | { 8 | var statusDisplay = new PlayerStatusDisplay(player); 9 | 10 | 11 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | @player.Name 22 | 23 | 24 | 25 | 26 | Mods: @player.Mods.ToAbbreviatedForm() 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | } 36 | } 37 | 38 | 39 | @code { 40 | [Parameter] 41 | public IMultiplayerLobby? CurrentlySelectedLobby { get; set; } 42 | 43 | private string GetPlayerTooltip(IMultiplayerPlayer player) 44 | { 45 | string b = $"[{player.Name} | Slot {player.Slot}] "; 46 | return player.State switch 47 | { 48 | PlayerState.Ready => b + "is ready.", 49 | PlayerState.NotReady => b + "is not ready.", 50 | PlayerState.NoMap => b + "is not ready (no map).", 51 | _ => b + "(state cannot be determined)." 52 | }; 53 | } 54 | 55 | public class PlayerStatusDisplay 56 | { 57 | private readonly IMultiplayerPlayer _player; 58 | public PlayerStatusDisplay(IMultiplayerPlayer player) { _player = player; } 59 | public Color StatusColor => GetStatusColor(); 60 | public Color PlayerTeamColor => GetTeamColor(); 61 | public string StatusIcon => GetStatusIcon(); 62 | public string Url => GetPlayerUrl(); 63 | 64 | private Color GetStatusColor() 65 | { 66 | if (_player.Lobby?.Host?.Equals(_player) ?? false) 67 | { 68 | return Color.Primary; 69 | } 70 | 71 | return _player.State switch 72 | { 73 | PlayerState.Ready => Color.Success, 74 | PlayerState.NotReady => Color.Error, 75 | PlayerState.NoMap => Color.Error, 76 | PlayerState.Undefined => Color.Warning, 77 | _ => Color.Warning 78 | }; 79 | } 80 | 81 | private string GetStatusIcon() 82 | { 83 | if (_player.Lobby?.Host?.Equals(_player) ?? false) 84 | { 85 | // Host == player, give crown 86 | return Icons.Material.Filled.Diamond; 87 | } 88 | 89 | return _player.State switch 90 | { 91 | PlayerState.Ready => Icons.Material.Filled.Check, 92 | PlayerState.NotReady => Icons.Material.Filled.Error, 93 | PlayerState.NoMap => Icons.Material.Filled.Downloading, 94 | _ => Icons.Material.Filled.Warning 95 | }; 96 | 97 | // return Icons.Material.Filled.Check; 98 | } 99 | 100 | private Color GetTeamColor() => _player.Team switch 101 | { 102 | TeamColor.None => Color.Inherit, 103 | TeamColor.Blue => Color.Info, 104 | TeamColor.Red => Color.Error 105 | }; 106 | 107 | private string GetPlayerUrl() 108 | { 109 | if (_player.Id.HasValue) 110 | { 111 | return $"https://osu.ppy.sh/u/{_player.Id}"; 112 | } 113 | 114 | return $"https://osu.ppy.sh/u/{_player.Name}"; 115 | } 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/Settings/AudioSettings.razor: -------------------------------------------------------------------------------- 1 | @using BrigittaBlazor.Settings 2 | @using BrigittaBlazor.Shared.Components.Dialogs 3 | @using BrigittaBlazor.Utils 4 | @inject UserSettings UserSettings 5 | @inject IDialogService DialogService 6 | @inject ILogger Logger 7 | @inject ISnackbar Snackbar 8 | @inject StateMaintainer StateManager 9 | 10 | 11 | 12 | 13 |

Audio Alerts

14 |
15 | 16 | 17 | Add Alert 18 | 19 |
20 | 21 | 22 | @if (UserSettings.AudioAlerts.Any()) 23 | { 24 | 25 | 27 | 28 | Name 29 | Alert File 30 | Enabled 31 | Multiplayer Specific 32 | Trigger Type 33 | Trigger Word 34 | 35 | 36 | 37 | @context.Name 38 | @context.Path 39 | @context.Enabled 40 | @context.MultiplayerLobbySpecific 41 | @context.Trigger 42 | @context.TriggerWord 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | @foreach (string file in GetAllSoundFilePaths()) 57 | { 58 | 59 | } 60 | 61 | 62 | 63 | 64 | 65 | 66 | 69 | 70 | 71 | 72 | @foreach (EventTrigger item in Enum.GetValues(typeof(EventTrigger))) 73 | { 74 | @item 75 | } 76 | 77 | 78 | 79 | 81 | 82 | 83 | 84 | 85 | } 86 | else 87 | { 88 | 89 | No audio alerts 90 | 91 | } 92 | 93 | 94 | 95 | @code { 96 | // Editable values of any alert 97 | string Name { get; set; } 98 | string Path { get; set; } 99 | string TriggerWord { get; set; } 100 | bool Enabled { get; set; } 101 | bool IsMultiplayerSpecific { get; set; } 102 | 103 | bool TriggerWordDisabled(UserAudioAlert context) => context.Trigger != EventTrigger.OnKeyword; 104 | 105 | bool MultiplayerSpecificDisabled(UserAudioAlert context) => context.Trigger is 106 | EventTrigger.OnMatchFinished or 107 | EventTrigger.OnMatchStarted or 108 | EventTrigger.OnDirectMessage; 109 | 110 | private async Task ShowAlertAddDialog() 111 | { 112 | var alert = new UserAudioAlert(); 113 | 114 | var parameters = new DialogParameters 115 | { 116 | ["AudioAlert"] = alert, 117 | ["SoundFilePaths"] = GetAllSoundFilePaths() 118 | }; 119 | 120 | var dialog = await DialogService.ShowAsync("New Audio Alert", parameters); 121 | var result = await dialog.Result; 122 | 123 | if (!result.Canceled) 124 | { 125 | try 126 | { 127 | var newAlert = result.Data as UserAudioAlert; 128 | 129 | if (newAlert == null) 130 | { 131 | Logger.LogError("Failed to add alert"); 132 | Snackbar.Add("Failed to add alert", Severity.Error); 133 | return; 134 | } 135 | 136 | Logger.LogInformation("Alert added"); 137 | StateManager.AddAudioAlert(newAlert); 138 | UserSettings.AudioAlerts.Add(newAlert); 139 | UserSettings.Save(); 140 | } 141 | catch (Exception e) 142 | { 143 | Logger.LogError(e, "Failed to add alert"); 144 | Snackbar.Add("Failed to add alert", Severity.Error); 145 | } 146 | } 147 | } 148 | 149 | private void AlertEdited(object o) 150 | { 151 | if (o is UserAudioAlert alert) 152 | { 153 | var match = UserSettings.AudioAlerts.FirstOrDefault(x => x.Name.Equals(alert.Name, StringComparison.OrdinalIgnoreCase)); 154 | if (match == null) 155 | { 156 | Snackbar.Add("Failed to edit audio alert (no match found)", Severity.Error); 157 | return; 158 | } 159 | 160 | // Update match props 161 | match.Name = alert.Name; 162 | match.Path = alert.Path; 163 | match.Trigger = alert.Trigger; 164 | match.TriggerWord = alert.Trigger == EventTrigger.OnKeyword ? alert.TriggerWord : string.Empty; 165 | match.Enabled = alert.Enabled; 166 | match.MultiplayerLobbySpecific = alert.MultiplayerLobbySpecific; 167 | 168 | StateManager.UpdateAudioAlert(match); 169 | UserSettings.Save(); 170 | 171 | Snackbar.Add($"Edited audio alert: {alert}", Severity.Success); 172 | } 173 | } 174 | 175 | private void DeleteAlert(UserAudioAlert alert) 176 | { 177 | if (!UserSettings.AudioAlerts.Remove(alert)) 178 | { 179 | Snackbar.Add($"Failed to delete audio alert: {alert}", Severity.Warning); 180 | return; 181 | } 182 | 183 | StateManager.DeleteAudioAlert(alert); 184 | UserSettings.Save(); 185 | Snackbar.Add($"Deleted audio alert: {alert}", Severity.Success); 186 | } 187 | 188 | private List GetAllSoundFilePaths() 189 | { 190 | var extensions = new List 191 | { 192 | "wav", 193 | "bwf", 194 | "raw", 195 | "aiff", 196 | "flac", 197 | "m4a", 198 | "pac", 199 | "tta", 200 | "wv", 201 | "ast", 202 | "aac", 203 | "mp2", 204 | "mp3", 205 | "mp4", 206 | "amr", 207 | "s3m", 208 | "3gp", 209 | "act", 210 | "au", 211 | "dct", 212 | "dss", 213 | "gsm", 214 | "m4p", 215 | "mmf", 216 | "mpc", 217 | "ogg", 218 | "oga", 219 | "opus", 220 | "ra", 221 | "sln", 222 | "vox" 223 | }; 224 | 225 | var ret = new List(); 226 | var dir = new DirectoryInfo(System.IO.Path.Combine("wwwroot", "Sounds")); 227 | if (!dir.Exists) 228 | { 229 | Logger.LogWarning($"Failed to locate sounds directory. Path searched: {dir.FullName}"); 230 | return ret; 231 | } 232 | 233 | foreach (var file in dir.GetFiles()) 234 | { 235 | if (string.IsNullOrWhiteSpace(file.Extension)) 236 | { 237 | continue; 238 | } 239 | 240 | if (!extensions.Contains(file.Extension.Split('.')[1])) 241 | { 242 | continue; 243 | } 244 | 245 | ret.Add(file.FullName); 246 | } 247 | 248 | return ret; 249 | } 250 | 251 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/Settings/HotkeySettings.razor: -------------------------------------------------------------------------------- 1 | @using BrigittaBlazor.Settings 2 | @using BrigittaBlazor.Utils 3 | 4 | @inject ILogger Logger 5 | @inject UserSettings UserSettings 6 | @inject ISnackbar Snackbar 7 | 8 | 9 | 10 | 11 |

Hotkeys

12 |
13 | 14 | Add Hotkey 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | @if (UserSettings.KeyBinds.Any()) 39 | { 40 | 41 | 43 | 44 | Hotkey 45 | Macro 46 | 47 | 48 | 49 | @context.ToString() 50 | @context.Message 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | @context.ToString() 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | } 68 | else 69 | { 70 | 71 | No hotkeys 72 | 73 | } 74 | 75 | 76 | 77 | 78 | 79 | 80 | @code { 81 | UserKeyBind? CurrentKeybind; 82 | string Message = string.Empty; 83 | protected override async Task OnInitializedAsync() => HotkeyListener.OnHotkeyPressed += SetCurrentKeybind; 84 | 85 | private void SetCurrentKeybind(ParsedJsonEvent key) 86 | { 87 | CurrentKeybind = new UserKeyBind 88 | { 89 | Key = key.Key, 90 | Alt = key.AltKey, 91 | Ctrl = key.CtrlKey, 92 | Shift = key.ShiftKey, 93 | Message = string.Empty 94 | }; 95 | 96 | InvokeAsync(StateHasChanged); 97 | } 98 | 99 | private void HotkeyAdded() 100 | { 101 | if (CurrentKeybind == null) 102 | { 103 | Snackbar.Add("No hotkey selected", Severity.Error); 104 | return; 105 | } 106 | 107 | if (string.IsNullOrWhiteSpace(Message)) 108 | { 109 | Snackbar.Add("No macro entered", Severity.Error); 110 | return; 111 | } 112 | 113 | string lower = CurrentKeybind.Key.ToLower(); 114 | if (lower is "control" or "alt" or "shift") 115 | { 116 | // User is trying to bind a modifier by itself 117 | Snackbar.Add($"Cannot bind '{lower.ToUpper()}' by itself as it is a modifier", Severity.Error); 118 | return; 119 | } 120 | 121 | if (UserSettings.KeyBinds.Any(x => x.Key == CurrentKeybind.Key && x.Alt == CurrentKeybind.Alt && 122 | x.Ctrl == CurrentKeybind.Ctrl && x.Shift == CurrentKeybind.Shift)) 123 | { 124 | Snackbar.Add($"Hotkey '{CurrentKeybind}' already exists", Severity.Error); 125 | return; 126 | } 127 | 128 | CurrentKeybind.Message = Message; 129 | UserSettings.KeyBinds.Add(CurrentKeybind); 130 | UserSettings.Save(); 131 | 132 | Snackbar.Add($"Successfully bound '{CurrentKeybind}'", Severity.Success); 133 | } 134 | 135 | private void HotkeyEdited(object obj) 136 | { 137 | if (obj is not UserKeyBind keybind) 138 | { 139 | Snackbar.Add("Failed to edit hotkey", Severity.Error); 140 | return; 141 | } 142 | 143 | if (string.IsNullOrWhiteSpace(Message)) 144 | { 145 | Snackbar.Add("No macro entered", Severity.Error); 146 | return; 147 | } 148 | 149 | keybind.Message = Message; 150 | var match = UserSettings.KeyBinds.FirstOrDefault(x => x.Key == keybind.Key && x.Alt == keybind.Alt && 151 | x.Ctrl == keybind.Ctrl && x.Shift == keybind.Shift); 152 | if (match == null) 153 | { 154 | Snackbar.Add("Failed to edit hotkey (match was null) - please report this error!", Severity.Error); 155 | return; 156 | } 157 | 158 | match.Message = Message; 159 | UserSettings.Save(); 160 | Snackbar.Add($"Edited hotkey '{keybind}'", Severity.Success); 161 | } 162 | 163 | private void DeleteHotkey(UserKeyBind key) 164 | { 165 | UserSettings.KeyBinds.Remove(key); 166 | UserSettings.Save(); 167 | Snackbar.Add($"Deleted hotkey '{key}'", Severity.Success); 168 | } 169 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/Components/TextChatEntryField.razor: -------------------------------------------------------------------------------- 1 | @using BanchoSharp.Interfaces 2 | @using BrigittaBlazor.Utils 3 | @using BrigittaBlazor.Derivatives 4 | @inject IBanchoClient Client 5 | @inject ILogger Logger 6 | @inject ISnackbar Snackbar 7 | @inject IJSRuntime JS 8 | @inject IScrollUtils ScrollUtils 9 | 10 | 11 | 15 | 16 | 17 | @code { 18 | [Parameter] 19 | public IChatChannel? CurrentChannel { get; set; } 20 | [Parameter] 21 | public string? ConsoleDiv { get; set; } 22 | [Parameter] 23 | public bool AutoScroll { get; set; } 24 | [Parameter] 25 | public bool DisplayUTC { get; set; } 26 | private MudTextField RefEntryField = null!; 27 | private string? _textChatValue; 28 | 29 | private async Task OnTextChatSend(KeyboardEventArgs args) 30 | { 31 | if (args.Key is not ("Enter" or "NumppadEnter") || string.IsNullOrWhiteSpace(_textChatValue)) 32 | { 33 | return; 34 | } 35 | 36 | if (_textChatValue.StartsWith("/")) 37 | { 38 | if (await ProcessSlashCommand()) 39 | { 40 | return; 41 | } 42 | } 43 | 44 | // It's okay to handle slash commands without a selected 45 | // channel in some cases, but never for regular messages 46 | if (CurrentChannel == null) 47 | { 48 | Snackbar.Add("No channel to deliver message to.", Severity.Error); 49 | await ClearChatEntryField(); 50 | return; 51 | } 52 | 53 | await Client.SendPrivateMessageAsync(CurrentChannel.ChannelName, _textChatValue); 54 | Logger.LogDebug($"Message sent: '{_textChatValue}' to {CurrentChannel}"); 55 | 56 | await ClearChatEntryField(); 57 | } 58 | 59 | private async Task ProcessSlashCommand() 60 | { 61 | if (string.IsNullOrEmpty(_textChatValue)) 62 | { 63 | return false; 64 | } 65 | 66 | var slashCommandHandler = new CustomCommandHandler(_textChatValue); 67 | if (slashCommandHandler is { Command: {}, IsBanchoCommand: true }) 68 | { 69 | // Deploy directly to the server 70 | // TODO: needs documentation 71 | switch (slashCommandHandler.Command.ToLower()) 72 | { 73 | case "j": 74 | case "join": 75 | case "query": 76 | { 77 | if (!slashCommandHandler.Parameters?.Any() ?? true) 78 | { 79 | Snackbar.Add("Invalid arguments for /join", Severity.Error); 80 | break; 81 | } 82 | 83 | await Client.JoinChannelAsync(slashCommandHandler.Parameters[0]); 84 | break; 85 | } 86 | case "p": 87 | case "part": 88 | case "close": 89 | case "leave": 90 | { 91 | if (!slashCommandHandler.Parameters?.Any() ?? true) 92 | { 93 | if (CurrentChannel == null) 94 | { 95 | Snackbar.Add("Cannot determine channel to part. No channel selected.", Severity.Error); 96 | break; 97 | } 98 | 99 | await Client.PartChannelAsync(CurrentChannel.ChannelName); 100 | break; 101 | } 102 | 103 | await Client.PartChannelAsync(slashCommandHandler.Parameters[0]); 104 | break; 105 | } 106 | case "me": 107 | { 108 | Snackbar.Add("This command is not yet supported.", Severity.Warning); 109 | break; 110 | } 111 | default: 112 | await Client.SendAsync(_textChatValue[1..]); 113 | Snackbar.Add($"Executed {_textChatValue}", Severity.Info); 114 | break; 115 | } 116 | 117 | // We've now handled the command, clear the chat field and return 118 | await ClearChatEntryField(); 119 | return true; 120 | } 121 | 122 | if (slashCommandHandler is { Command: {}, IsBanchoCommand: false }) 123 | { 124 | // Process custom command 125 | var command = slashCommandHandler.CustomCommand; 126 | if (command != null) 127 | { 128 | switch (command.Value.Command.ToLower()) 129 | { 130 | case "clear": 131 | await command.Value.Execute(new Func(channel => 132 | { 133 | if (channel == null) 134 | { 135 | return Task.CompletedTask; 136 | } 137 | 138 | channel.MessageHistory!.Clear(); 139 | return Task.CompletedTask; 140 | }), CurrentChannel); 141 | 142 | break; 143 | case "chat": 144 | if (!slashCommandHandler.Parameters?.Any() ?? true) 145 | { 146 | break; 147 | } 148 | 149 | string? recipient = slashCommandHandler.Parameters?[0]; 150 | string message = string.Join(" ", slashCommandHandler.Parameters?[1..] ?? Array.Empty()).Trim(); 151 | 152 | if (recipient == null) 153 | { 154 | break; 155 | } 156 | 157 | await command.Value.Execute(new Func(async (r, m) => 158 | { 159 | if (r.StartsWith("#")) 160 | { 161 | // Recipient is a channel 162 | await Client.JoinChannelAsync(r); 163 | } 164 | else 165 | { 166 | // Recipient is a Bancho user 167 | await Client.QueryUserAsync(r); 168 | } 169 | 170 | // Message exists? Message the channel or user 171 | if (!string.IsNullOrWhiteSpace(m)) 172 | { 173 | await Client.SendPrivateMessageAsync(r, m); 174 | } 175 | }), recipient, message); 176 | 177 | break; 178 | case "savelog": 179 | try 180 | { 181 | await command.Value.Execute(async () => await FileUtils.SaveChatHistoryAsync(Logger, 182 | CurrentChannel, DisplayUTC, JS, Snackbar)); 183 | } 184 | catch (Exception e) 185 | { 186 | Snackbar.Add($"Failed to download chat log for {CurrentChannel?.ChannelName ?? "null"}: " + 187 | $"{e.Message}", Severity.Error); 188 | Logger.LogError($"Failed to download chat log for " + 189 | $"{CurrentChannel?.ChannelName ?? "null"}: {e.Message}"); 190 | } 191 | 192 | break; 193 | } 194 | 195 | // Finally, clear the chat channel and return since we've handled the command. 196 | await ClearChatEntryField(); 197 | return true; 198 | } 199 | } 200 | 201 | return false; 202 | } 203 | 204 | private async Task ForceChatSend() => await OnTextChatSend(new KeyboardEventArgs 205 | { 206 | Key = "Enter" 207 | }); 208 | 209 | private async Task ClearChatEntryField() 210 | { 211 | RefEntryField.TextUpdateSuppression = false; 212 | _textChatValue = string.Empty; 213 | 214 | await InvokeAsync(StateHasChanged); 215 | await Task.Run(async () => 216 | { 217 | await Task.Delay(100); 218 | RefEntryField.TextUpdateSuppression = true; 219 | await InvokeAsync(StateHasChanged); 220 | }); 221 | } 222 | 223 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | Brigitta 21 | 22 | 23 | 24 | 25 | 26 | @Body 27 | 28 | 29 | 30 | 31 | @code { 32 | private bool _drawerOpen; 33 | private readonly MudTheme _theme = new(); 34 | private bool _isDarkMode = true; 35 | void DrawerToggle() => _drawerOpen = !_drawerOpen; 36 | } 37 | -------------------------------------------------------------------------------- /BrigittaBlazor/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 5 | Primary Display 6 | 7 | 8 | 9 | Login 10 | 11 | 12 | Settings 13 | 14 | 15 | -------------------------------------------------------------------------------- /BrigittaBlazor/Utils/ChatNotification.cs: -------------------------------------------------------------------------------- 1 | namespace BrigittaBlazor.Utils; 2 | 3 | [Flags] 4 | public enum ChatNotification 5 | { 6 | CurrentlySelected = 1 << 0, 7 | MentionsRefereeKeywords = 1 << 1, 8 | DirectMessage = 1 << 2, 9 | MentionsUsername = 1 << 3, 10 | GeneralMessage = 1 << 4, 11 | None = 0 12 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Utils/DebugUtils.cs: -------------------------------------------------------------------------------- 1 | namespace BrigittaBlazor.Utils; 2 | 3 | public class DebugUtils 4 | { 5 | public static bool IsDebugBuild() 6 | { 7 | #if DEBUG 8 | return true; 9 | #else 10 | return false; 11 | #endif 12 | } 13 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Utils/EventRegistrationTracker.cs: -------------------------------------------------------------------------------- 1 | namespace BrigittaBlazor.Utils; 2 | 3 | public class EventRegistrationTracker 4 | { 5 | public bool HasRegisteredSettingsHotkeyListener { get; set; } 6 | public bool HasRegisteredPrimaryDisplayDefaultEvents { get; set; } 7 | public bool HasRegisteredIndexLocationListener { get; set; } 8 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Utils/FileUtils.cs: -------------------------------------------------------------------------------- 1 | using BanchoSharp.Interfaces; 2 | using BrigittaBlazor.Extensions; 3 | using Humanizer; 4 | using Humanizer.Localisation; 5 | using Microsoft.JSInterop; 6 | using MudBlazor; 7 | using System.Text; 8 | 9 | namespace BrigittaBlazor.Utils; 10 | 11 | public static class FileUtils 12 | { 13 | /// 14 | /// Downloads a file to the browser 15 | /// 16 | /// 17 | /// The name of the file with extension 18 | /// 19 | public static async Task DownloadAs(IJSRuntime js, string filename, byte[] data) => await js.InvokeVoidAsync( 20 | "saveAsFile", 21 | filename, 22 | Convert.ToBase64String(data)); 23 | 24 | public static async Task SaveChatHistoryAsync(ILogger logger, IChatChannel? currentChannel, bool displayUTC, IJSRuntime js, 25 | ISnackbar snackbar) 26 | { 27 | if (currentChannel == null) 28 | { 29 | snackbar.Add("Cannot download messages, channel is null.", Severity.Error); 30 | logger.LogDebug("User attempted a /savelog, but the current channel was null"); 31 | return; 32 | } 33 | 34 | if (currentChannel.MessageHistory == null) 35 | { 36 | snackbar.Add("Cannot download messages, message history is null.", Severity.Error); 37 | logger.LogDebug($"User attempted a /savelog, but the message history for {currentChannel.ChannelName} was null"); 38 | return; 39 | } 40 | 41 | if (!currentChannel.MessageHistory.Any()) 42 | { 43 | snackbar.Add("There are no chat messages in this channel to save.", Severity.Error); 44 | logger.LogDebug($"User attempted a /savelog, but no messages were found in {currentChannel.ChannelName}"); 45 | return; 46 | } 47 | 48 | var first = currentChannel.MessageHistory.First?.Value; 49 | var last = currentChannel.MessageHistory.Last?.Value; 50 | 51 | if (first == null || last == null) 52 | { 53 | snackbar.Add("Error occurred when saving chat log.", Severity.Error); 54 | logger.LogDebug($"User attempted a /savelog, but the first or last message was null in {currentChannel.ChannelName}"); 55 | return; 56 | } 57 | 58 | var timeDelta = last.Timestamp - first.Timestamp; 59 | 60 | var sb = new StringBuilder($"---- Chat log of {currentChannel.ChannelName} generated by Brigitta ----\n"); 61 | var ts = displayUTC ? currentChannel.MessageHistory!.First!.Value.Timestamp.ToUniversalTime() : currentChannel.MessageHistory!.First!.Value.Timestamp; 62 | sb.AppendLine($"---- Log spans {timeDelta.Humanize(3, minUnit: TimeUnit.Second)}, " + 63 | $"beginning at {ts:R}{ts:zz} ----"); 64 | 65 | sb.AppendLine("---- BEGIN LOG ----"); 66 | foreach (var ircMessage in currentChannel.MessageHistory) 67 | { 68 | if (ircMessage is not IPrivateIrcMessage message) 69 | { 70 | continue; 71 | } 72 | 73 | if (displayUTC) 74 | { 75 | sb.AppendLine(message.ToUTCDisplayString()); 76 | } 77 | else 78 | { 79 | sb.AppendLine(message.ToDisplayString()); 80 | } 81 | } 82 | 83 | sb.AppendLine("---- END LOG ----"); 84 | 85 | await DownloadAs(js, $"{DateTime.Now.ToUniversalTime().ToFileTimeString()}-{currentChannel.ChannelName}.txt", 86 | Encoding.UTF8.GetBytes(sb.ToString())); 87 | 88 | snackbar.Add($"Downloaded chat log for {currentChannel.ChannelName}.", Severity.Success); 89 | logger.LogDebug($"Saved chat log for {currentChannel.ChannelName}"); 90 | } 91 | 92 | public static string ExtractFilename(string path) 93 | { 94 | var fInfo = new FileInfo(path); 95 | return fInfo.Name; 96 | } 97 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Utils/HotkeyListener.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using Newtonsoft.Json; 3 | using System.Text.Json; 4 | 5 | #pragma warning disable CS8618 6 | 7 | namespace BrigittaBlazor.Utils; 8 | 9 | public static class HotkeyListener 10 | { 11 | public static event Action OnHotkeyPressed; 12 | 13 | [JSInvokable] 14 | public static Task OnKeyDown(JsonElement eventObject) 15 | { 16 | var parsed = JsonConvert.DeserializeObject(eventObject.ToString()); 17 | if (parsed != null) 18 | { 19 | OnHotkeyPressed?.Invoke(parsed); 20 | } 21 | 22 | return Task.CompletedTask; 23 | } 24 | } 25 | 26 | public class CurrentTarget 27 | { 28 | [JsonProperty("location")] 29 | public Location Location { get; set; } 30 | } 31 | 32 | public class Location 33 | { 34 | [JsonProperty("href")] 35 | public string Href { get; set; } 36 | [JsonProperty("origin")] 37 | public string Origin { get; set; } 38 | [JsonProperty("protocol")] 39 | public string Protocol { get; set; } 40 | [JsonProperty("host")] 41 | public string Host { get; set; } 42 | [JsonProperty("hostname")] 43 | public string Hostname { get; set; } 44 | [JsonProperty("port")] 45 | public string Port { get; set; } 46 | [JsonProperty("pathname")] 47 | public string Pathname { get; set; } 48 | [JsonProperty("search")] 49 | public string Search { get; set; } 50 | [JsonProperty("hash")] 51 | public string Hash { get; set; } 52 | } 53 | 54 | public class ParsedJsonEvent 55 | { 56 | [JsonProperty("altKey")] 57 | public bool AltKey { get; set; } 58 | [JsonProperty("bubbles")] 59 | public bool Bubbles { get; set; } 60 | [JsonProperty("cancelBubble")] 61 | public bool CancelBubble { get; set; } 62 | [JsonProperty("cancelable")] 63 | public bool Cancelable { get; set; } 64 | [JsonProperty("charCode")] 65 | public int CharCode { get; set; } 66 | [JsonProperty("code")] 67 | public string Code { get; set; } 68 | [JsonProperty("composed")] 69 | public bool Composed { get; set; } 70 | [JsonProperty("ctrlKey")] 71 | public bool CtrlKey { get; set; } 72 | [JsonProperty("currentTarget")] 73 | public CurrentTarget CurrentTarget { get; set; } 74 | [JsonProperty("defaultPrevented")] 75 | public bool DefaultPrevented { get; set; } 76 | [JsonProperty("detail")] 77 | public int Detail { get; set; } 78 | [JsonProperty("eventPhase")] 79 | public int EventPhase { get; set; } 80 | [JsonProperty("isComposing")] 81 | public bool IsComposing { get; set; } 82 | [JsonProperty("isTrusted")] 83 | public bool IsTrusted { get; set; } 84 | [JsonProperty("key")] 85 | public string Key { get; set; } 86 | [JsonProperty("keyCode")] 87 | public int KeyCode { get; set; } 88 | [JsonProperty("location")] 89 | public int Location { get; set; } 90 | [JsonProperty("metaKey")] 91 | public bool MetaKey { get; set; } 92 | [JsonProperty("repeat")] 93 | public bool Repeat { get; set; } 94 | [JsonProperty("returnValue")] 95 | public bool ReturnValue { get; set; } 96 | [JsonProperty("shiftKey")] 97 | public bool ShiftKey { get; set; } 98 | [JsonProperty("target")] 99 | public object Target { get; set; } 100 | [JsonProperty("timeStamp")] 101 | public int TimeStamp { get; set; } 102 | [JsonProperty("type")] 103 | public string Type { get; set; } 104 | [JsonProperty("which")] 105 | public int Which { get; set; } 106 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Utils/IScrollUtils.cs: -------------------------------------------------------------------------------- 1 | namespace BrigittaBlazor.Utils; 2 | 3 | public interface IScrollUtils 4 | { 5 | public string ConsoleId { get; } 6 | public Task ScrollToBottomAsync(string divId); 7 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Utils/ScrollUtils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace BrigittaBlazor.Utils; 4 | 5 | public class ScrollUtils : IScrollUtils 6 | { 7 | private readonly ILogger _logger; 8 | private readonly IJSRuntime JS; 9 | 10 | public ScrollUtils(IJSRuntime js, ILogger logger) 11 | { 12 | JS = js; 13 | _logger = logger; 14 | } 15 | 16 | public string ConsoleId => "console"; 17 | public async Task ScrollToBottomAsync(string divId) => await JS.InvokeVoidAsync("scrollToBottom", divId); 18 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Utils/SoundUtils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace BrigittaBlazor.Utils; 4 | 5 | public static class SoundUtils 6 | { 7 | public static async ValueTask PlaySoundAsync(IJSRuntime jsRuntime, string soundFile) 8 | { 9 | await jsRuntime.InvokeVoidAsync("playSound", "Sounds/" + soundFile); 10 | } 11 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Utils/StateMaintainer.cs: -------------------------------------------------------------------------------- 1 | using BrigittaBlazor.Settings; 2 | 3 | namespace BrigittaBlazor.Utils; 4 | 5 | /// 6 | /// Singleton responsible for maintaining state in components 7 | /// 8 | public class StateMaintainer 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public StateMaintainer(EventRegistrationTracker eventTracker, ILogger logger) 13 | { 14 | _logger = logger; 15 | EventTracker = eventTracker; 16 | ChannelNotifications = new Dictionary(); 17 | 18 | OnAudioAlertCreated += (_, alert) => 19 | { 20 | _logger.LogInformation($"Created audio alert: {alert}"); 21 | }; 22 | 23 | OnAudioAlertUpdated += (_, alert) => 24 | { 25 | _logger.LogInformation($"Updated audio alert: {alert}"); 26 | }; 27 | 28 | OnAudioAlertDeleted += (_, alert) => 29 | { 30 | _logger.LogInformation($"Deleted audio alert: {alert}"); 31 | }; 32 | } 33 | 34 | public List AudioAlerts { get; } = new(); 35 | public event EventHandler OnAudioAlertCreated; 36 | public event EventHandler OnAudioAlertUpdated; 37 | public event EventHandler OnAudioAlertDeleted; 38 | 39 | public void AddAudioAlert(UserAudioAlert audioAlert) 40 | { 41 | AudioAlerts.Add(audioAlert); 42 | OnAudioAlertCreated?.Invoke(this, audioAlert); 43 | } 44 | 45 | public void UpdateAudioAlert(UserAudioAlert audioAlert) 46 | { 47 | var existingAlert = AudioAlerts.FirstOrDefault(a => a.Name == audioAlert.Name); 48 | if (existingAlert != null) 49 | { 50 | AudioAlerts.Remove(existingAlert); 51 | AudioAlerts.Add(audioAlert); 52 | OnAudioAlertUpdated?.Invoke(this, audioAlert); 53 | } 54 | } 55 | 56 | public void DeleteAudioAlert(UserAudioAlert audioAlert) 57 | { 58 | if (AudioAlerts.Remove(audioAlert)) 59 | { 60 | OnAudioAlertDeleted?.Invoke(this, audioAlert); 61 | } 62 | } 63 | 64 | // TODO: Add state such as Referee-view timer settings, etc. 65 | 66 | public EventRegistrationTracker EventTracker { get; } 67 | public Dictionary ChannelNotifications { get; } 68 | } -------------------------------------------------------------------------------- /BrigittaBlazor/Utils/UpdaterService.cs: -------------------------------------------------------------------------------- 1 | using Octokit; 2 | 3 | namespace BrigittaBlazor.Utils; 4 | 5 | public class UpdaterService 6 | { 7 | public const string VERSION = "2.4.1"; 8 | private readonly GitHubClient _ghClient; 9 | private readonly ILogger _logger; 10 | 11 | public UpdaterService(ILogger logger, GitHubClient ghClient) 12 | { 13 | _logger = logger; 14 | _ghClient = ghClient; 15 | } 16 | 17 | public async Task GetLatestReleaseAsync() 18 | { 19 | try 20 | { 21 | return await _ghClient.Repository.Release.GetLatest("hburn7", "Brigitta"); 22 | } 23 | catch (ApiException e) 24 | { 25 | _logger.LogWarning("Github encountered an API exception while trying to get the latest release", e); 26 | return null; 27 | } 28 | } 29 | 30 | public async Task?> GetRecentReleasesAsync(int amount = 5) 31 | { 32 | try 33 | { 34 | return (await _ghClient.Repository.Release.GetAll("hburn7", "Brigitta")).Take(amount); 35 | } 36 | catch (ApiException e) 37 | { 38 | _logger.LogWarning("Github encountered an API exception while trying to get the latest release. " + 39 | "You are probably being ratelimited"); 40 | 41 | return null; 42 | } 43 | } 44 | 45 | public async Task NeedsUpdateAsync() 46 | { 47 | _logger.LogInformation("Checking for updates..."); 48 | try 49 | { 50 | var latestRelease = await GetLatestReleaseAsync(); 51 | 52 | if (latestRelease == null) 53 | { 54 | _logger.LogWarning("Could not identify latest release while checking for an update!"); 55 | return null; 56 | } 57 | 58 | string currentVersion = "v" + VERSION; 59 | 60 | _logger.LogInformation($"Latest release: {latestRelease.TagName}"); 61 | _logger.LogInformation($"Current version: {currentVersion}"); 62 | 63 | bool needsUpdate = latestRelease.TagName != currentVersion; 64 | 65 | if (needsUpdate) 66 | { 67 | _logger.LogWarning("Update detected! Please install from here: https://github.com/hburn7/Brigitta/releases"); 68 | } 69 | else 70 | { 71 | _logger.LogInformation("You're running the latest version of Brigitta!"); 72 | } 73 | 74 | return needsUpdate; 75 | } 76 | catch (ApiException e) 77 | { 78 | _logger.LogWarning($"Github encountered an API exception while trying to check for updates: {e.Message}"); 79 | return null; 80 | } 81 | } 82 | 83 | public async Task> GetRecentUpdateInfosAsync() 84 | { 85 | List updates = new(); 86 | try 87 | { 88 | var latestReleases = await GetRecentReleasesAsync(); 89 | if (latestReleases == null) 90 | { 91 | return updates; 92 | } 93 | 94 | foreach (var release in latestReleases) 95 | { 96 | if (!release.Name.StartsWith("v2")) 97 | { 98 | continue; 99 | } 100 | 101 | string[] commits = release.Body.Split("\n")[1..]; 102 | var updateInfo = new UpdateInfo 103 | { 104 | Version = release.Name, 105 | Url = release.HtmlUrl, 106 | Commits = commits.Select(c => new Commit 107 | { 108 | Description = string.Join(" ", c.Split()[2..]).Trim(), 109 | Hash = c.Split()[1].Split(':')[0] 110 | }) 111 | }; 112 | 113 | updates.Add(updateInfo); 114 | } 115 | 116 | return updates; 117 | } 118 | catch (ApiException e) 119 | { 120 | _logger.LogWarning("Github encountered an API exception while trying to get the latest release", e); 121 | return updates; 122 | } 123 | catch (IndexOutOfRangeException) 124 | { 125 | _logger.LogWarning("Changelog contained no valid commits"); 126 | return updates; 127 | } 128 | } 129 | } 130 | 131 | public class UpdateInfo 132 | { 133 | public string Version { get; set; } 134 | public string Url { get; set; } 135 | public IEnumerable Commits { get; set; } 136 | } 137 | 138 | public class Commit 139 | { 140 | public string Description { get; set; } 141 | public string Hash { get; set; } 142 | public string Url => $"https://github.com/hburn7/Brigitta/commit/{Hash}"; 143 | } -------------------------------------------------------------------------------- /BrigittaBlazor/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using MudBlazor 10 | @using BrigittaBlazor 11 | @using BrigittaBlazor.Shared 12 | -------------------------------------------------------------------------------- /BrigittaBlazor/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Trace", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BrigittaBlazor/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/Sounds/alert-metalgear.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hburn7/Brigitta/c032a9a6462b705bcf434d319ed316e85fa09426/BrigittaBlazor/wwwroot/Sounds/alert-metalgear.mp3 -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/Sounds/alert-pokemon.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hburn7/Brigitta/c032a9a6462b705bcf434d319ed316e85fa09426/BrigittaBlazor/wwwroot/Sounds/alert-pokemon.mp3 -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/Sounds/alert-scifi.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hburn7/Brigitta/c032a9a6462b705bcf434d319ed316e85fa09426/BrigittaBlazor/wwwroot/Sounds/alert-scifi.mp3 -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/Sounds/anime-wow.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hburn7/Brigitta/c032a9a6462b705bcf434d319ed316e85fa09426/BrigittaBlazor/wwwroot/Sounds/anime-wow.mp3 -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/Sounds/hitwhistle.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hburn7/Brigitta/c032a9a6462b705bcf434d319ed316e85fa09426/BrigittaBlazor/wwwroot/Sounds/hitwhistle.wav -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/Sounds/incoming-transmission.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hburn7/Brigitta/c032a9a6462b705bcf434d319ed316e85fa09426/BrigittaBlazor/wwwroot/Sounds/incoming-transmission.mp3 -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/Sounds/nice.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hburn7/Brigitta/c032a9a6462b705bcf434d319ed316e85fa09426/BrigittaBlazor/wwwroot/Sounds/nice.mp3 -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hburn7/Brigitta/c032a9a6462b705bcf434d319ed316e85fa09426/BrigittaBlazor/wwwroot/favicon.ico -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/images/brigitta-logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hburn7/Brigitta/c032a9a6462b705bcf434d319ed316e85fa09426/BrigittaBlazor/wwwroot/images/brigitta-logo-text.png -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/js/helpers.js: -------------------------------------------------------------------------------- 1 | function scrollToBottom(id) { 2 | let item = document.getElementById(id); 3 | item.scrollTop = item.scrollHeight; 4 | } 5 | 6 | function saveAsFile(filename, bytesBase64) { 7 | var link = document.createElement('a'); 8 | link.download = filename; 9 | link.href = "data:application/octet-stream;base64," + bytesBase64; 10 | document.body.appendChild(link); // Needed for Firefox 11 | link.click(); 12 | document.body.removeChild(link); 13 | } -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/js/hotkeys.js: -------------------------------------------------------------------------------- 1 | const keyDownHandler = function(event) { 2 | var serializeEvent = function (event) { 3 | if (!event) { 4 | return null; 5 | } 6 | 7 | var o = { 8 | altKey: event.altKey, 9 | bubbles: event.bubbles, 10 | cancelBubble: event.cancelBubble, 11 | cancelable: event.cancelable, 12 | charCode: event.charCode, 13 | code: event.code, 14 | composed: event.composed, 15 | ctrlKey: event.ctrlKey, 16 | currentTarget: event.currentTarget, 17 | defaultPrevented: event.defaultPrevented, 18 | detail: event.detail, 19 | eventPhase: event.eventPhase, 20 | isComposing: event.isComposing, 21 | isTrusted: event.isTrusted, 22 | key: event.key, 23 | keyCode: event.keyCode, 24 | location: event.location, 25 | metaKey: event.metaKey, 26 | path: event.path, 27 | repeat: event.repeat, 28 | returnValue: event.returnValue, 29 | shiftKey: event.shiftKey, 30 | target: event.target, 31 | timeStamp: event.timeStamp, 32 | type: event.type, 33 | which: event.which, 34 | x: event.x, 35 | y: event.y 36 | }; 37 | 38 | return o; 39 | } 40 | DotNet.invokeMethodAsync('BrigittaBlazor', 'OnKeyDown', serializeEvent(event)); 41 | console.log(event); 42 | 43 | if ((event.ctrlKey && !event.key === 'a') || event.altKey || event.metaKey) { 44 | event.preventDefault(); 45 | } 46 | } 47 | 48 | document.addEventListener('keydown', function (e) { keyDownHandler(e) }); -------------------------------------------------------------------------------- /BrigittaBlazor/wwwroot/js/soundUtils.js: -------------------------------------------------------------------------------- 1 | var lastPlayedTime = 0; 2 | 3 | function playSound(soundFile) { 4 | var currentTime = Date.now(); 5 | if (currentTime - lastPlayedTime < 500) { 6 | console.log("playSound: " + soundFile + " skipped"); 7 | return; 8 | } 9 | 10 | var audio = new Audio("Sounds/" + soundFile); 11 | audio.volume = 0.5; 12 | audio.play(); 13 | 14 | console.log("playSound: " + soundFile + " " + audio.volume); 15 | 16 | lastPlayedTime = currentTime; 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brigitta 2 | 3 | Brigitta is an IRC chat client designed specifically for referees of the rhythm game 4 | [osu!](https://osu.ppy.sh/home). *Unfortunately, I do not have the bandwidth to maintain this like I used to. But, plenty of people still choose Brigitta as their client of choice even today. Hope you enjoy!* 5 | 6 | # Run The Application 7 | **Prerequisites:** 8 | - [.NET 6.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) *Look for ".NET Runtime 6.x.x" on this page* 9 | 10 | **Run:** 11 | - Visit the latest [Release](https://github.com/hburn7/Brigitta/releases) and download the .zip file for your platform. 12 | 13 | | Platform | Instructions | Special Notes | 14 | |----------|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| 15 | | Windows | Extract the downloaded folder and
run `BrigittaBlazor.exe`. | *None* | 16 | | MacOS | Right click on the "BrigittaBlazor"
executable and click Open. | If you get an error stating that
the executable cannot be run,
please click "Show Finder" and then
`Right Click` -> `Run` any executables
you see. This will need to be done each update. | 17 | | Linux | cd into the downloaded folder
and run `./BrigittaBlazor`. Ensure
it has executable permissions. | *None* | 18 | - All platforms will need to navigate to `localhost:5000` in a browser in order to use the application. 19 | - Checkout my [Discord Server](https://discord.gg/TjH3uZ8VgP) and get the `Brigitta` role in `#roles` to gain special access to developer announcements and know exactly when new releases happen! 20 | 21 | ## Contributing 22 | 23 | Please follow the instructions below when considering making a contribution to the project: 24 | 25 | - First and foremost, this is my senior capstone project for my undergraduate degree. I do not expect PRs or any direct code contributions of any kind. 26 | - Please check for any existing issues if you wish to make a PR. I likely will not accept PRs that add additional features without my previous approval. 27 | - Ensure any PRs made have a very detailed description as to what the code does. Ensure all test cases pass. 28 | 29 | ## Building From Source 30 | 31 | **Prerequisites:** 32 | - [.NET 6.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) 33 | 34 | Clone the repo (pick one): 35 | - `git clone https://github.com/hburn7/Brigitta.git` 36 | - `gh repo clone hburn7/Brigitta` 37 | 38 | Target and build: 39 | - `cd Brigitta` 40 | - `cd BrigittaBlazor` 41 | - `dotnet run` 42 | 43 | 49 | 50 | ## Supplementary Note 51 | If you have concerns, please make an issue on this project and I will respond to it very quickly. I hope you enjoy using Brigitta :) 52 | --------------------------------------------------------------------------------