diff --git a/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor b/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor
new file mode 100644
index 0000000..cc6f9f6
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor
@@ -0,0 +1,49 @@
+@using MatrixRoomUtils.Authentication
+@using MatrixRoomUtils.Web.Classes
+@using System.Text.Json
+@using Blazored.LocalStorage
+@using MatrixRoomUtils.Extensions
+@using Index = MatrixRoomUtils.Web.Pages.Index
+@inject ILocalStorageService LocalStorage
+@inject NavigationManager NavigationManager
+
+<div style="margin-bottom: 1em;">
+ <img style="border-radius: 50%; height: 3em; width: 3em;" src="@_avatarUrl"/>
+ <span style="margin-left: 1em;"><input type="radio" name="csa" checked="@(RuntimeStorage.AccessToken == Token)" onclick="@SetCurrent" style="text-decoration-line: unset;"/> <b>@User.Profile.DisplayName</b> on <b>@User.LoginResponse.HomeServer</b></span>
+ <a href="#" onclick="@RemoveUser">Remove</a>
+</div>
+
+@code {
+ [Parameter]
+ public string Token { get; set; }
+ [Parameter]
+ public UserInfo User { get; set; }
+
+ private string _avatarUrl { get; set; }
+ private bool _removed { get; set; } = false;
+
+ protected override async Task OnInitializedAsync()
+ {
+ if(User.Profile.AvatarUrl != null && User.Profile.AvatarUrl != "")
+ _avatarUrl = await User.Profile.AvatarUrl.GetMediaUrl();
+ else _avatarUrl = "https://api.dicebear.com/6.x/identicon/svg?seed=" + User.LoginResponse.UserId;
+ await base.OnInitializedAsync();
+ }
+
+ private async Task RemoveUser()
+ {
+ RuntimeStorage.UsersCache.Remove(Token);
+ await RuntimeStorage.SaveToLocalStorage(LocalStorage);
+ _removed = true;
+
+ StateHasChanged();
+ }
+ private async Task SetCurrent()
+ {
+ RuntimeStorage.AccessToken = Token;
+ RuntimeStorage.CurrentHomeserver = await MatrixAccount.ResolveHomeserverFromWellKnown(RuntimeStorage.UsersCache[Token].LoginResponse.HomeServer);
+ await RuntimeStorage.SaveToLocalStorage(LocalStorage);
+
+ StateHasChanged();
+ }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/LogView.razor b/MatrixRoomUtils.Web/Shared/LogView.razor
new file mode 100644
index 0000000..fbe5264
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/LogView.razor
@@ -0,0 +1,31 @@
+@using System.Text
+<u>Logs</u><br/>
+<pre>
+ @sb
+</pre>
+
+@code {
+ StringBuilder sb = new();
+ protected override void OnInitialized()
+ {
+ //intecept stdout with textwriter to get logs
+ var sw = new StringWriter(sb);
+ Console.SetOut(sw);
+ Console.SetError(sw);
+ //keep updated
+ int length = 0;
+ Task.Run(async () =>
+ {
+ while (true)
+ {
+ await Task.Delay(100);
+ if (sb.Length != length)
+ {
+ StateHasChanged();
+ length = sb.Length;
+ }
+ }
+ });
+ base.OnInitialized();
+ }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/MainLayout.razor b/MatrixRoomUtils.Web/Shared/MainLayout.razor
new file mode 100644
index 0000000..e0c8260
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/MainLayout.razor
@@ -0,0 +1,36 @@
+@inherits LayoutComponentBase
+
+@using Blazored.LocalStorage
+@using MatrixRoomUtils.Web.Classes
+@inject ILocalStorageService LocalStorage
+@inject NavigationManager NavigationManager
+
+<div class="page">
+ <div class="sidebar">
+ <NavMenu/>
+ </div>
+
+ <main>
+ <div class="top-row px-4">
+ <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
+ </div>
+
+ <article class="content px-4">
+ @Body
+ </article>
+ </main>
+</div>
+
+@code {
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (!RuntimeStorage.WasLoaded)
+ {
+ await RuntimeStorage.LoadFromLocalStorage(LocalStorage);
+ Console.WriteLine("Loaded from local storage");
+ StateHasChanged();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/MainLayout.razor.css b/MatrixRoomUtils.Web/Shared/MainLayout.razor.css
new file mode 100644
index 0000000..c865427
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/MainLayout.razor.css
@@ -0,0 +1,81 @@
+.page {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+}
+
+main {
+ flex: 1;
+}
+
+.sidebar {
+ background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
+}
+
+.top-row {
+ background-color: #f7f7f7;
+ border-bottom: 1px solid #d6d5d5;
+ justify-content: flex-end;
+ height: 3.5rem;
+ display: flex;
+ align-items: center;
+}
+
+ .top-row ::deep a, .top-row ::deep .btn-link {
+ white-space: nowrap;
+ margin-left: 1.5rem;
+ text-decoration: none;
+ }
+
+ .top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
+ text-decoration: underline;
+ }
+
+ .top-row ::deep a:first-child {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+@media (max-width: 640.98px) {
+ .top-row:not(.auth) {
+ display: none;
+ }
+
+ .top-row.auth {
+ justify-content: space-between;
+ }
+
+ .top-row ::deep a, .top-row ::deep .btn-link {
+ margin-left: 0;
+ }
+}
+
+@media (min-width: 641px) {
+ .page {
+ flex-direction: row;
+ }
+
+ .sidebar {
+ width: 250px;
+ height: 100vh;
+ position: sticky;
+ top: 0;
+ }
+
+ .top-row {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ }
+
+ .top-row.auth ::deep a:first-child {
+ flex: 1;
+ text-align: right;
+ width: 0;
+ }
+
+ .top-row, article {
+ padding-left: 2rem !important;
+ padding-right: 1.5rem !important;
+ }
+}
diff --git a/MatrixRoomUtils.Web/Shared/NavMenu.razor b/MatrixRoomUtils.Web/Shared/NavMenu.razor
new file mode 100644
index 0000000..5b7d776
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/NavMenu.razor
@@ -0,0 +1,40 @@
+<div class="top-row ps-3 navbar navbar-dark">
+ <div class="container-fluid">
+ <a class="navbar-brand" href="">Rory&::MatrixRoomUtils</a>
+ <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ </div>
+</div>
+
+<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
+ <nav class="flex-column">
+ <div class="nav-item px-3">
+ <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
+ <span class="oi oi-home" aria-hidden="true"></span> Home
+ </NavLink>
+ </div>
+ <div class="nav-item px-3">
+ <NavLink class="nav-link" href="export">
+ <span class="oi oi-plus" aria-hidden="true"></span> Export data
+ </NavLink>
+ </div>
+ <div class="nav-item px-3">
+ <NavLink class="nav-link" href="PolicyListEditor">
+ <span class="oi oi-plus" aria-hidden="true"></span> Policy list editor
+ </NavLink>
+ </div>
+ </nav>
+</div>
+
+@code {
+ private bool collapseNavMenu = true;
+
+ private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
+
+ private void ToggleNavMenu()
+ {
+ collapseNavMenu = !collapseNavMenu;
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/NavMenu.razor.css b/MatrixRoomUtils.Web/Shared/NavMenu.razor.css
new file mode 100644
index 0000000..604b7a1
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/NavMenu.razor.css
@@ -0,0 +1,68 @@
+.navbar-toggler {
+ background-color: rgba(255, 255, 255, 0.1);
+}
+
+.top-row {
+ height: 3.5rem;
+ background-color: rgba(0,0,0,0.4);
+}
+
+.navbar-brand {
+ font-size: 1.1rem;
+}
+
+.oi {
+ width: 2rem;
+ font-size: 1.1rem;
+ vertical-align: text-top;
+ top: -2px;
+}
+
+.nav-item {
+ font-size: 0.9rem;
+ padding-bottom: 0.5rem;
+}
+
+ .nav-item:first-of-type {
+ padding-top: 1rem;
+ }
+
+ .nav-item:last-of-type {
+ padding-bottom: 1rem;
+ }
+
+ .nav-item ::deep a {
+ color: #d7d7d7;
+ border-radius: 4px;
+ height: 3rem;
+ display: flex;
+ align-items: center;
+ line-height: 3rem;
+ }
+
+.nav-item ::deep a.active {
+ background-color: rgba(255,255,255,0.25);
+ color: white;
+}
+
+.nav-item ::deep a:hover {
+ background-color: rgba(255,255,255,0.1);
+ color: white;
+}
+
+@media (min-width: 641px) {
+ .navbar-toggler {
+ display: none;
+ }
+
+ .collapse {
+ /* Never collapse the sidebar for wide screens */
+ display: block;
+ }
+
+ .nav-scrollable {
+ /* Allow sidebar to scroll for tall menus */
+ height: calc(100vh - 3.5rem);
+ overflow-y: auto;
+ }
+}
|