2022-12-15 20:03:40 +01:00
|
|
|
@inherits LocalizableComponentBase
|
|
|
|
|
|
2026-10-06 22:44:57 +02:00
|
|
|
@* On a phone the menu opens from the hamburger over the page, and closes after a choice (any navigation, a persona
|
|
|
|
|
switch), on a tap outside it or on Escape; from md it is the sidebar, which scrolls on its own when the window is
|
|
|
|
|
short (a phone held sideways). *@
|
|
|
|
|
<nav class="flex justify-between items-center md:items-start pt-3 px-3 md:p-0 md:h-full md:overflow-y-auto md:overscroll-contain"
|
|
|
|
|
@onkeydown="OnKeyDown">
|
2022-02-14 01:51:52 +01:00
|
|
|
|
2026-10-06 22:44:57 +02:00
|
|
|
@if (isMenuOpen)
|
|
|
|
|
{
|
|
|
|
|
<div class="md:hidden fixed inset-0 z-40 bg-black/30" aria-hidden="true" @onclick="CloseMenu"></div>
|
|
|
|
|
}
|
2022-02-14 01:51:52 +01:00
|
|
|
<div class="relative md:w-52">
|
2026-10-06 22:44:57 +02:00
|
|
|
<DButton Size="DSize.Sm" class="md:hidden" OnClick="ToggleNavMenu" aria-label="@Localizer["Menu"]" title="@Localizer["Menu"]"
|
|
|
|
|
aria-expanded="@(isMenuOpen ? "true" : "false")" aria-controls="nav-panel" data-nav-toggle>
|
|
|
|
|
<i class="ion-md-menu text-lg" aria-hidden="true"></i>
|
2026-10-03 12:08:11 +02:00
|
|
|
</DButton>
|
2026-10-06 22:44:57 +02:00
|
|
|
<div id="nav-panel" data-nav-panel
|
|
|
|
|
class="@(isMenuOpen ? "block" : "hidden") md:block absolute bg-base-100 md:relative top-full md:top-auto mt-2 w-[min(18rem,calc(100vw-1.5rem))] md:w-full max-h-[calc(100dvh-5rem)] md:max-h-none overflow-y-auto overscroll-contain rounded-lg neo-raised neo-sm md:shadow-none! z-50">
|
2026-10-03 12:08:11 +02:00
|
|
|
<div class="flex flex-col gap-3 p-3 md:p-4">
|
2022-02-14 01:51:52 +01:00
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
@if (CascadingState.IsSignedIn)
|
|
|
|
|
{
|
|
|
|
|
<DDropdown @bind-IsOpen="isPersonaMenuOpen" CloseOnContentClick="true" class="w-full rounded-xl bg-cover bg-no-repeat bg-right"
|
|
|
|
|
TriggerClass="flex w-full gap-3 p-3 md:p-4 neo-raised neo-xs rounded-xl text-left cursor-pointer"
|
|
|
|
|
ContentClass="mt-2 flex flex-col gap-2 p-2 rounded-box w-full" title="@Localizer["Switch persona"]"
|
|
|
|
|
style="@PersonaBackground">
|
|
|
|
|
<Trigger>
|
|
|
|
|
<img alt="" class="h-8 w-8 md:h-12 md:w-12 flex-none object-cover rounded-full neo-raised neo-2xs"
|
|
|
|
|
src="@(CascadingState.Persona?.Avatar ?? "imgs/icon-192.png")" />
|
|
|
|
|
<span class="flex flex-col w-full min-w-0 gap-1">
|
|
|
|
|
<b class="truncate" title="@CascadingState.Persona?.Name">@(CascadingState.Persona?.Name ?? Localizer["No persona"])</b>
|
|
|
|
|
<span class="truncate text-xs opacity-70">@(CascadingState.Persona is { } persona ? "@" + persona.Acct : null)</span>
|
|
|
|
|
</span>
|
|
|
|
|
<i class="ion-md-swap self-center opacity-60"></i>
|
|
|
|
|
</Trigger>
|
|
|
|
|
<ChildContent>
|
|
|
|
|
@foreach (var persona in CascadingState.Personas)
|
|
|
|
|
{
|
|
|
|
|
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Active="persona.Id == CascadingState.PersonaId"
|
|
|
|
|
Disabled="IsDefaultDisabled || persona.Id == CascadingState.PersonaId" OnClick="() => SwitchPersona(persona.Id)">
|
|
|
|
|
<img alt="" class="h-5 w-5 object-cover rounded-full" src="@(persona.PictureURL ?? "imgs/icon-192.png")" />
|
|
|
|
|
<span class="truncate">@(persona.Name ?? persona.UserName)</span>
|
|
|
|
|
</DButton>
|
|
|
|
|
}
|
|
|
|
|
</ChildContent>
|
|
|
|
|
</DDropdown>
|
|
|
|
|
}
|
2022-02-14 01:51:52 +01:00
|
|
|
|
2026-10-03 12:08:11 +02:00
|
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="/" Match="NavLinkMatch.All">
|
|
|
|
|
<i class="ion-md-home"></i>
|
2022-12-15 20:03:40 +01:00
|
|
|
<span>@Localizer["Home"]</span>
|
2022-02-14 01:51:52 +01:00
|
|
|
</NavLink>
|
2026-10-05 01:23:18 +02:00
|
|
|
@if (CascadingState.IsSignedIn)
|
|
|
|
|
{
|
|
|
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="search">
|
|
|
|
|
<i class="ion-md-search"></i>
|
|
|
|
|
<span>@Localizer["Search"]</span>
|
|
|
|
|
</NavLink>
|
2026-10-05 03:53:51 +02:00
|
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="explore">
|
|
|
|
|
<i class="ion-md-compass"></i>
|
|
|
|
|
<span>@Localizer["Explore"]</span>
|
|
|
|
|
</NavLink>
|
2026-10-05 02:49:51 +02:00
|
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="lists">
|
|
|
|
|
<i class="ion-md-list"></i>
|
|
|
|
|
<span>@Localizer["Lists"]</span>
|
|
|
|
|
</NavLink>
|
2026-10-05 18:24:39 +02:00
|
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="groups">
|
|
|
|
|
<i class="ion-md-people"></i>
|
|
|
|
|
<span>@Localizer["Groups"]</span>
|
|
|
|
|
</NavLink>
|
2026-10-05 01:23:18 +02:00
|
|
|
@if (CascadingState.PersonaId is { } personaId)
|
|
|
|
|
{
|
|
|
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="@($"account/{Uri.EscapeDataString(personaId)}")">
|
|
|
|
|
<i class="ion-md-person"></i>
|
|
|
|
|
<span>@Localizer["My profile"]</span>
|
|
|
|
|
</NavLink>
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-03 12:08:11 +02:00
|
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="settings">
|
|
|
|
|
<i class="ion-md-settings"></i>
|
2022-12-15 20:03:40 +01:00
|
|
|
<span>@Localizer["Settings"]</span>
|
2022-02-14 01:51:52 +01:00
|
|
|
</NavLink>
|
2026-10-05 06:20:18 +02:00
|
|
|
<AuthorizeView Policy="@Policies.IsModerator">
|
2026-10-04 11:03:14 +02:00
|
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="administration">
|
|
|
|
|
<i class="ion-md-switch text-error"></i>
|
|
|
|
|
<span>@Localizer["Administration"]</span>
|
|
|
|
|
</NavLink>
|
|
|
|
|
</AuthorizeView>
|
|
|
|
|
@if (CascadingState.IsSignedIn)
|
|
|
|
|
{
|
2026-10-06 23:28:22 +02:00
|
|
|
<DButton class="justify-start" OnClick="Logout" data-logout>
|
2026-10-04 11:03:14 +02:00
|
|
|
<i class="ion-md-log-out text-error"></i>
|
|
|
|
|
<span>@Localizer["Logout"]</span>
|
2026-10-06 23:28:22 +02:00
|
|
|
</DButton>
|
2026-10-04 11:03:14 +02:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<NavLink ActiveClass="btn-active" class="btn justify-start" href="login">
|
|
|
|
|
<i class="ion-md-log-in text-success"></i>
|
|
|
|
|
<span>@Localizer["Login"]</span>
|
|
|
|
|
</NavLink>
|
|
|
|
|
}
|
2022-02-14 01:51:52 +01:00
|
|
|
|
2026-10-03 12:08:11 +02:00
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<DButton Size="DSize.Sm" Shape="DButtonShape.Circle" class="flex-none" OnClick="ResetToOriginalColour"
|
|
|
|
|
title="@Localizer["Reset"]">
|
|
|
|
|
<i class="ion-md-refresh"></i>
|
|
|
|
|
</DButton>
|
|
|
|
|
<DRange TValue="short" class="neo-hue-track w-full" Size="DSize.Xs" Min="0" Max="359" Step="1"
|
|
|
|
|
Value="ThemeIndexColour" ValueChanged="UpdateThemeColour" ValueExpression="() => ThemeIndexColour" />
|
2022-02-14 01:51:52 +01:00
|
|
|
</div>
|
|
|
|
|
|
2026-10-03 12:08:11 +02:00
|
|
|
<div class="flex justify-end">
|
2026-10-06 23:19:59 +02:00
|
|
|
<DToggle Value="IsDark" ValueChanged="UpdateThemeDarkMode" ValueExpression="() => IsDark" class="touch-target"
|
|
|
|
|
aria-label="@Localizer["Dark mode"]" title="@Localizer["Dark mode"]">
|
2026-10-03 12:08:11 +02:00
|
|
|
<UncheckedIcon>
|
|
|
|
|
<i class="ion-md-sunny"></i>
|
|
|
|
|
</UncheckedIcon>
|
|
|
|
|
<CheckedIcon>
|
|
|
|
|
<i class="ion-md-moon"></i>
|
|
|
|
|
</CheckedIcon>
|
|
|
|
|
</DToggle>
|
|
|
|
|
</div>
|
2022-02-14 01:51:52 +01:00
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-10-06 22:44:57 +02:00
|
|
|
<a class="md:hidden grid place-items-center h-11 w-11" href="" aria-label="@Localizer["Home"]" title="@Localizer["Home"]">
|
2022-02-14 01:51:52 +01:00
|
|
|
<img alt="" class="h-[30px]" src="imgs/icon-512.png">
|
2026-10-06 22:44:57 +02:00
|
|
|
</a>
|
2022-02-14 01:51:52 +01:00
|
|
|
|
2026-10-06 22:44:57 +02:00
|
|
|
<a class="md:hidden grid place-items-center h-11 w-11" href="settings" aria-label="@Localizer["Settings"]" title="@Localizer["Settings"]">
|
|
|
|
|
<img alt="@CascadingState.Persona?.Name" class="object-cover w-[30px] h-[30px] rounded-full neo-raised neo-2xs"
|
|
|
|
|
src="@(CascadingState.Persona?.Avatar ?? "imgs/icon-192.png")">
|
|
|
|
|
</a>
|
2022-02-14 01:51:52 +01:00
|
|
|
|
|
|
|
|
</nav>
|
2022-02-02 20:26:14 +01:00
|
|
|
|
|
|
|
|
@code {
|
2026-10-06 23:28:22 +02:00
|
|
|
[Inject] ConfirmService Confirm { get; set; }
|
|
|
|
|
|
2026-10-06 22:44:57 +02:00
|
|
|
bool isMenuOpen;
|
2026-10-04 11:03:14 +02:00
|
|
|
bool isPersonaMenuOpen;
|
|
|
|
|
|
|
|
|
|
string PersonaBackground => CascadingState.Persona?.Header is { Length: > 0 } header && !header.Contains("/missing-")
|
|
|
|
|
? $"background-image:linear-gradient(to left, var(--color-base-100) 55%, transparent), url('{header}');"
|
|
|
|
|
: null;
|
2022-02-14 01:51:52 +01:00
|
|
|
|
2026-10-03 12:43:07 +02:00
|
|
|
// The toggle and the slider work on the variant that is showing; with system theming that is the OS's choice.
|
|
|
|
|
bool IsDark => CascadingState.Status.IsDarkTheme();
|
2026-10-03 15:26:21 +02:00
|
|
|
short ThemeIndexColour => IsDark ? CascadingState.PublicCacheData.PageSettings.DarkThemeIndexColour : CascadingState.PublicCacheData.PageSettings.LightThemeIndexColour;
|
2026-10-03 12:43:07 +02:00
|
|
|
|
|
|
|
|
protected override void OnInitialized()
|
2022-02-14 01:51:52 +01:00
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
IsLoading = false;
|
2026-10-06 22:44:57 +02:00
|
|
|
Navigation.LocationChanged += OnLocationChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnLocationChanged(object sender, LocationChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (!isMenuOpen)
|
|
|
|
|
return;
|
|
|
|
|
isMenuOpen = false;
|
|
|
|
|
InvokeAsync(StateHasChanged);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CloseMenu() => isMenuOpen = false;
|
|
|
|
|
|
|
|
|
|
void OnKeyDown(KeyboardEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.Key == "Escape")
|
|
|
|
|
isMenuOpen = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override async ValueTask DisposeAsync()
|
|
|
|
|
{
|
|
|
|
|
Navigation.LocationChanged -= OnLocationChanged;
|
|
|
|
|
await base.DisposeAsync();
|
2022-02-14 01:51:52 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
async Task SwitchPersona(string personaId)
|
|
|
|
|
{
|
2026-10-06 22:44:57 +02:00
|
|
|
isMenuOpen = false;
|
2026-10-04 11:03:14 +02:00
|
|
|
var result = await CascadingState.SwitchPersona(personaId);
|
|
|
|
|
if (result.IsInvalid)
|
|
|
|
|
Toast.AddResponseError(result);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 22:44:57 +02:00
|
|
|
private void ToggleNavMenu() => isMenuOpen = !isMenuOpen;
|
2022-02-14 01:51:52 +01:00
|
|
|
|
2026-10-06 23:28:22 +02:00
|
|
|
async Task Logout()
|
|
|
|
|
{
|
|
|
|
|
isMenuOpen = false;
|
|
|
|
|
if (!await Confirm.Ask(Localizer["Log out?"], Localizer["Every persona of this login is signed out on this device."], Localizer["Logout"], true))
|
|
|
|
|
return;
|
|
|
|
|
Navigation.NavigateTo("logout");
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-14 01:51:52 +01:00
|
|
|
protected async Task ResetToOriginalColour()
|
|
|
|
|
{
|
2026-10-03 12:43:07 +02:00
|
|
|
var isDark = IsDark;
|
|
|
|
|
await CascadingState.UpdatePageSettings(settings => settings.SetHue(isDark,
|
|
|
|
|
isDark ? PageSettings.DefaultDarkThemeIndexColour : PageSettings.DefaultLightThemeIndexColour));
|
2022-02-14 01:51:52 +01:00
|
|
|
|
|
|
|
|
// if (AuthData?.User != null)
|
|
|
|
|
// {
|
|
|
|
|
// AuthData.User.UserSettings.ThemeIndexColour = ThemeIndexColour;
|
|
|
|
|
// await Storage.SetItemAsync(nameof(AuthData), AuthData);
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 12:43:07 +02:00
|
|
|
// Picking a hue also leaves the grey themes, which otherwise ignore it.
|
2026-10-03 12:08:11 +02:00
|
|
|
protected async Task UpdateThemeColour(short indexColour)
|
2022-02-14 01:51:52 +01:00
|
|
|
{
|
2026-10-03 12:43:07 +02:00
|
|
|
var isDark = IsDark;
|
|
|
|
|
await CascadingState.UpdatePageSettings(settings => settings.SetHue(isDark, indexColour));
|
2022-02-14 01:51:52 +01:00
|
|
|
|
|
|
|
|
// if (AuthData?.User != null)
|
|
|
|
|
// {
|
|
|
|
|
// AuthData.User.UserSettings.ThemeIndexColour = ThemeIndexColour;
|
|
|
|
|
// await Storage.SetItemAsync(nameof(AuthData), AuthData);
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 12:43:07 +02:00
|
|
|
// Choosing light or dark here leaves system theming; Settings can turn it back on.
|
2022-02-14 01:51:52 +01:00
|
|
|
protected async Task UpdateThemeDarkMode(bool isDarkMode)
|
|
|
|
|
{
|
2026-10-03 12:43:07 +02:00
|
|
|
await CascadingState.UpdatePageSettings(settings => settings.SetDarkMode(isDarkMode));
|
2022-02-14 01:51:52 +01:00
|
|
|
|
|
|
|
|
// if (AuthData?.User != null)
|
|
|
|
|
// {
|
|
|
|
|
// AuthData.User.UserSettings.ThemeIsDarkMode = ThemeIsDarkMode;
|
|
|
|
|
// await Storage.SetItemAsync(nameof(AuthData), AuthData);
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
}
|