Add splash screen SVG graphic for Admin Mobile application

This commit is contained in:
Alexa
2026-06-14 22:39:23 +03:00
parent cce20ebdb6
commit 4ef463f664
15 changed files with 640 additions and 120 deletions
+20 -5
View File
@@ -43,6 +43,13 @@ public sealed class LoginPage : ContentPage
Spacing = 14,
Children =
{
new Image
{
Source = "dorknet.svg",
WidthRequest = 96,
HeightRequest = 96,
HorizontalOptions = LayoutOptions.Start,
},
AppDesign.Title("Admin mobile"),
new Label
{
@@ -86,11 +93,19 @@ public sealed class LoginPage : ContentPage
base.OnAppearing();
if (loaded) return;
loaded = true;
var saved = await settings.LoadConnectionAsync();
baseUrl.Text = saved.BaseUrl;
cfClientId.Text = saved.CloudflareAccessClientId;
cfClientSecret.Text = saved.CloudflareAccessClientSecret;
cfJwt.Text = saved.CloudflareAccessJwt;
try
{
var saved = await settings.LoadConnectionAsync();
baseUrl.Text = saved.BaseUrl;
cfClientId.Text = saved.CloudflareAccessClientId;
cfClientSecret.Text = saved.CloudflareAccessClientSecret;
cfJwt.Text = saved.CloudflareAccessJwt;
}
catch (Exception ex)
{
baseUrl.Text = "https://admin.rec.net";
status.Text = $"Secure storage was reset: {ex.Message}";
}
}
private async void OnLogin(object? sender, EventArgs e)
+136 -31
View File
@@ -11,6 +11,7 @@ public sealed class PlayersPage : ContentPage
private readonly Entry search = AppDesign.Entry("Search players");
private readonly CollectionView list = new();
private readonly Label status = AppDesign.StatusLabel();
private readonly Button refresh = AppDesign.SecondaryButton("Refresh");
private List<PlayerSummary> players = [];
public PlayersPage(AdminApiClient api)
@@ -19,10 +20,26 @@ public sealed class PlayersPage : ContentPage
Title = "Players";
Background = AppDesign.PageBackground;
var refresh = AppDesign.SecondaryButton("Refresh");
refresh.Clicked += async (_, _) => await LoadAsync();
search.Completed += async (_, _) => await LoadAsync();
list.SelectionMode = SelectionMode.Single;
list.ItemSizingStrategy = ItemSizingStrategy.MeasureFirstItem;
list.SelectionChanged += async (_, e) =>
{
if (e.CurrentSelection.FirstOrDefault() is PlayerSummary player)
{
list.SelectedItem = null;
await PlayerActionsAsync(player);
}
};
list.EmptyView = new Label
{
Text = "No players loaded",
TextColor = AppDesign.Subtle,
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
};
list.ItemTemplate = new DataTemplate(() =>
{
var name = new Label { TextColor = AppDesign.Text, FontAttributes = FontAttributes.Bold, FontSize = 16 };
@@ -34,40 +51,75 @@ public sealed class PlayersPage : ContentPage
var online = new Label { TextColor = AppDesign.AndroidAccent, FontSize = 12, FontAttributes = FontAttributes.Bold };
online.SetBinding(Label.IsVisibleProperty, nameof(PlayerSummary.Online));
online.Text = "ONLINE";
var kick = MiniButton("Kick");
kick.Clicked += async (s, _) =>
var chevron = new Label
{
if ((s as Button)?.BindingContext is PlayerSummary p)
await ConfirmAction($"Kick @{p.Username}?", () => api.KickPlayerAsync(p.Id, "Kicked from mobile admin."));
Text = ">",
TextColor = AppDesign.Subtle,
FontSize = 18,
VerticalTextAlignment = TextAlignment.Center,
};
var resetAvatar = MiniButton("Reset avatar");
resetAvatar.Clicked += async (s, _) =>
var row = new Grid
{
if ((s as Button)?.BindingContext is PlayerSummary p)
await ConfirmAction($"Reset @{p.Username}'s avatar?", () => api.ResetAvatarAsync(p.Id));
RowDefinitions =
{
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Auto },
},
ColumnDefinitions =
{
new ColumnDefinition(),
new ColumnDefinition { Width = GridLength.Auto },
new ColumnDefinition { Width = GridLength.Auto },
},
Padding = new Thickness(12, 10),
RowSpacing = 2,
ColumnSpacing = 10,
BackgroundColor = AppDesign.RowSurface,
};
row.Add(name, 0, 0);
row.Add(online, 1, 0);
row.Add(handle, 0, 1);
row.Add(detail, 0, 2);
row.Add(chevron, 2, 0);
Grid.SetRowSpan(chevron, 3);
var actions = new HorizontalStackLayout { Spacing = 8, Children = { kick, resetAvatar } };
return AppDesign.GlassCard(new VerticalStackLayout
return new VerticalStackLayout
{
Spacing = 6,
Children = { name, handle, detail, online, actions },
}, new Thickness(12, 10));
Spacing = 0,
Children = { row, AppDesign.Divider() },
};
});
Content = new VerticalStackLayout
var searchRow = new Grid
{
Padding = 16,
Spacing = 12,
Children =
ColumnSpacing = 8,
ColumnDefinitions =
{
AppDesign.Title("Players"),
new HorizontalStackLayout { Spacing = 8, Children = { search, refresh } },
list,
status,
new ColumnDefinition(),
new ColumnDefinition { Width = GridLength.Auto },
},
};
searchRow.Add(search, 0, 0);
searchRow.Add(refresh, 1, 0);
var grid = new Grid
{
Padding = 16,
RowSpacing = 12,
RowDefinitions =
{
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Auto },
new RowDefinition(),
new RowDefinition { Height = GridLength.Auto },
},
};
grid.Add(AppDesign.Title("Players"), 0, 0);
grid.Add(searchRow, 0, 1);
grid.Add(list, 0, 2);
grid.Add(status, 0, 3);
Content = grid;
}
protected override async void OnAppearing()
@@ -80,6 +132,7 @@ public sealed class PlayersPage : ContentPage
{
try
{
refresh.IsEnabled = false;
status.Text = "Loading...";
players = await api.GetPlayersAsync(search.Text);
list.ItemsSource = players;
@@ -89,6 +142,33 @@ public sealed class PlayersPage : ContentPage
{
status.Text = ex.Message;
}
finally
{
refresh.IsEnabled = true;
}
}
private async Task PlayerActionsAsync(PlayerSummary player)
{
var action = await DisplayActionSheet(
$"@{player.Username}",
"Cancel",
"Remove account",
"Kick",
"Reset avatar");
switch (action)
{
case "Kick":
await ConfirmAction($"Kick @{player.Username}?", () => api.KickPlayerAsync(player.Id, "Kicked from mobile admin."));
break;
case "Reset avatar":
await ConfirmAction($"Reset @{player.Username}'s avatar?", () => api.ResetAvatarAsync(player.Id));
break;
case "Remove account":
await RemoveAccountAsync(player);
break;
}
}
private async Task ConfirmAction(string message, Func<Task> action)
@@ -105,13 +185,38 @@ public sealed class PlayersPage : ContentPage
}
}
private static Button MiniButton(string text) => new()
private async Task RemoveAccountAsync(PlayerSummary player)
{
Text = text,
FontSize = 12,
Padding = new Thickness(10, 6),
CornerRadius = 6,
BackgroundColor = AppDesign.SurfaceLifted,
TextColor = AppDesign.Text,
};
var username = await DisplayPromptAsync(
"Verify username",
$"Type {player.Username} exactly to remove @{player.Username}.",
"Next",
"Cancel",
maxLength: Math.Max(32, player.Username.Length),
keyboard: Keyboard.Text);
if (!string.Equals(username?.Trim(), player.Username, StringComparison.Ordinal)) return;
var phrase = $"DELETE {player.Id}";
var typedPhrase = await DisplayPromptAsync(
"Verify removal",
$"Type {phrase} exactly. This cannot be undone.",
"Remove",
"Cancel",
maxLength: phrase.Length,
keyboard: Keyboard.Text);
if (!string.Equals(typedPhrase?.Trim(), phrase, StringComparison.Ordinal)) return;
if (!await DisplayAlert("Remove account", $"Permanently remove @{player.Username}?", "Remove", "Cancel")) return;
try
{
status.Text = $"Removing @{player.Username}...";
await api.DeletePlayerAsync(player.Id, player.Username, phrase, "Removed from mobile admin.");
await LoadAsync();
}
catch (Exception ex)
{
await DisplayAlert("Removal failed", ex.Message, "OK");
}
}
}
+51 -19
View File
@@ -11,6 +11,7 @@ public sealed class RoomsPage : ContentPage
private readonly CollectionView rooms = new();
private readonly CollectionView instances = new();
private readonly Label status = AppDesign.StatusLabel();
private readonly Button refresh = AppDesign.SecondaryButton("Refresh");
private bool loaded;
public RoomsPage(AdminApiClient api)
@@ -19,18 +20,28 @@ public sealed class RoomsPage : ContentPage
Title = "Rooms";
Background = AppDesign.PageBackground;
rooms.SelectionMode = SelectionMode.None;
rooms.ItemSizingStrategy = ItemSizingStrategy.MeasureFirstItem;
rooms.EmptyView = EmptyList("No recent rooms");
rooms.ItemTemplate = new DataTemplate(() =>
{
var name = new Label { TextColor = AppDesign.Text, FontAttributes = FontAttributes.Bold, FontSize = 15 };
name.SetBinding(Label.TextProperty, nameof(RoomSummary.Name));
var meta = new Label { TextColor = AppDesign.Muted, FontSize = 12 };
meta.SetBinding(Label.TextProperty, new Binding(nameof(RoomSummary.Id), stringFormat: "Room #{0}"));
return AppDesign.GlassCard(new VerticalStackLayout
var row = new VerticalStackLayout
{
Padding = new Thickness(12, 9),
Spacing = 2,
BackgroundColor = AppDesign.RowSurface,
Children = { name, meta },
}, new Thickness(12, 9));
};
return new VerticalStackLayout { Spacing = 0, Children = { row, AppDesign.Divider() } };
});
instances.SelectionMode = SelectionMode.None;
instances.ItemSizingStrategy = ItemSizingStrategy.MeasureFirstItem;
instances.EmptyView = EmptyList("No active instances");
instances.ItemTemplate = new DataTemplate(() =>
{
var name = new Label { TextColor = AppDesign.Text, FontAttributes = FontAttributes.Bold, FontSize = 15 };
@@ -39,35 +50,51 @@ public sealed class RoomsPage : ContentPage
meta.SetBinding(Label.TextProperty, new Binding(nameof(InstanceSummary.PhotonRegionId), stringFormat: "Region {0}"));
var count = new Label { TextColor = AppDesign.Subtle, FontSize = 12 };
count.SetBinding(Label.TextProperty, new Binding("Participants.Count", stringFormat: "{0} participants"));
return AppDesign.GlassCard(new VerticalStackLayout
var row = new VerticalStackLayout
{
Padding = new Thickness(12, 9),
Spacing = 2,
BackgroundColor = AppDesign.RowSurface,
Children = { name, meta, count },
}, new Thickness(12, 9));
};
return new VerticalStackLayout { Spacing = 0, Children = { row, AppDesign.Divider() } };
});
var refresh = AppDesign.SecondaryButton("Refresh");
refresh.Clicked += async (_, _) => await LoadAsync();
Content = new ScrollView
var grid = new Grid
{
Content = new VerticalStackLayout
Padding = 16,
RowSpacing = 10,
RowDefinitions =
{
Padding = 16,
Spacing = 14,
Children =
{
AppDesign.Title("Rooms"),
refresh,
new Label { Text = "Live instances", TextColor = AppDesign.Muted, FontAttributes = FontAttributes.Bold },
instances,
new Label { Text = "Recent rooms", TextColor = AppDesign.Muted, FontAttributes = FontAttributes.Bold },
rooms,
status,
},
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = new GridLength(1, GridUnitType.Star) },
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = new GridLength(2, GridUnitType.Star) },
new RowDefinition { Height = GridLength.Auto },
},
};
grid.Add(AppDesign.Title("Rooms"), 0, 0);
grid.Add(refresh, 0, 1);
grid.Add(new Label { Text = "Live instances", TextColor = AppDesign.Muted, FontAttributes = FontAttributes.Bold }, 0, 2);
grid.Add(instances, 0, 3);
grid.Add(new Label { Text = "Recent rooms", TextColor = AppDesign.Muted, FontAttributes = FontAttributes.Bold }, 0, 4);
grid.Add(rooms, 0, 5);
grid.Add(status, 0, 6);
Content = grid;
}
private static Label EmptyList(string text) => new()
{
Text = text,
TextColor = AppDesign.Subtle,
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
};
protected override async void OnAppearing()
{
base.OnAppearing();
@@ -82,6 +109,7 @@ public sealed class RoomsPage : ContentPage
{
try
{
refresh.IsEnabled = false;
status.Text = "Loading...";
var instanceRows = await api.GetInstancesAsync();
var roomRows = await api.GetRoomsAsync();
@@ -93,5 +121,9 @@ public sealed class RoomsPage : ContentPage
{
status.Text = ex.Message;
}
finally
{
refresh.IsEnabled = true;
}
}
}
+1 -1
View File
@@ -96,7 +96,7 @@ public sealed class SettingsPage : ContentPage
}
}
private static Grid Row(string label, View control)
private static View Row(string label, View control)
{
var grid = new Grid
{