summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--player-android/lib/screens/admin_users_screen.dart115
-rw-r--r--player-android/lib/screens/settings_screen.dart68
2 files changed, 109 insertions, 74 deletions
diff --git a/player-android/lib/screens/admin_users_screen.dart b/player-android/lib/screens/admin_users_screen.dart
index 77c2db8..a51aabb 100644
--- a/player-android/lib/screens/admin_users_screen.dart
+++ b/player-android/lib/screens/admin_users_screen.dart
@@ -118,7 +118,13 @@ class _AdminUsersScreenState extends ConsumerState<AdminUsersScreen> {
);
if (!mounted) return;
// Replace the placeholder slot with the real user returned by the server.
- setState(() { _users![placeholderIdx] = created; });
+ // Guard bounds in case a concurrent refresh shrank the list while the
+ // create request was in flight (mirrors the bounds check in the error path).
+ setState(() {
+ if (placeholderIdx < (_users?.length ?? 0)) {
+ _users![placeholderIdx] = created;
+ }
+ });
} catch (e) {
if (!mounted) return;
// Revert optimistic insertion on error by removing the known slot.
@@ -530,10 +536,64 @@ class _CreateUserDialogState extends State<_CreateUserDialog> {
);
}
+ /// Builds the username TextFormField with non-empty validation.
+ Widget _buildUsernameField() {
+ return TextFormField(
+ key: const Key('admin_create_username'),
+ controller: _usernameController,
+ decoration: const InputDecoration(
+ labelText: 'Username',
+ border: OutlineInputBorder(),
+ ),
+ textInputAction: TextInputAction.next,
+ autocorrect: false,
+ validator: (value) {
+ if (value == null || value.trim().isEmpty) {
+ return 'Username is required.';
+ }
+ return null;
+ },
+ );
+ }
+
+ /// Builds the password TextFormField with a visibility toggle and length
+ /// validation (minimum 8 characters).
+ Widget _buildPasswordField() {
+ return TextFormField(
+ key: const Key('admin_create_password'),
+ controller: _passwordController,
+ decoration: InputDecoration(
+ labelText: 'Password',
+ border: const OutlineInputBorder(),
+ // Toggle visibility icon so the admin can verify the typed password.
+ suffixIcon: IconButton(
+ icon: Icon(
+ _passwordVisible
+ ? Icons.visibility_off_outlined
+ : Icons.visibility_outlined,
+ ),
+ tooltip: _passwordVisible ? 'Hide password' : 'Show password',
+ onPressed: () =>
+ setState(() => _passwordVisible = !_passwordVisible),
+ ),
+ ),
+ obscureText: !_passwordVisible,
+ textInputAction: TextInputAction.done,
+ onFieldSubmitted: (_) => _submit(),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Password is required.';
+ }
+ if (value.length < 8) {
+ return 'Password must be at least 8 characters.';
+ }
+ return null;
+ },
+ );
+ }
+
@override
Widget build(BuildContext context) {
- // _buildForm and _buildActions were single-call-site helpers; inlined here
- // to reduce indirection. The merged build() stays well under 50 lines.
return AlertDialog(
key: const Key('admin_create_user_dialog'),
title: const Text('Create user'),
@@ -542,54 +602,9 @@ class _CreateUserDialogState extends State<_CreateUserDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- TextFormField(
- key: const Key('admin_create_username'),
- controller: _usernameController,
- decoration: const InputDecoration(
- labelText: 'Username',
- border: OutlineInputBorder(),
- ),
- textInputAction: TextInputAction.next,
- autocorrect: false,
- validator: (value) {
- if (value == null || value.trim().isEmpty) {
- return 'Username is required.';
- }
- return null;
- },
- ),
+ _buildUsernameField(),
const SizedBox(height: 16),
- TextFormField(
- key: const Key('admin_create_password'),
- controller: _passwordController,
- decoration: InputDecoration(
- labelText: 'Password',
- border: const OutlineInputBorder(),
- // Toggle visibility icon so the admin can verify the typed password.
- suffixIcon: IconButton(
- icon: Icon(
- _passwordVisible
- ? Icons.visibility_off_outlined
- : Icons.visibility_outlined,
- ),
- tooltip: _passwordVisible ? 'Hide password' : 'Show password',
- onPressed: () =>
- setState(() => _passwordVisible = !_passwordVisible),
- ),
- ),
- obscureText: !_passwordVisible,
- textInputAction: TextInputAction.done,
- onFieldSubmitted: (_) => _submit(),
- validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Password is required.';
- }
- if (value.length < 8) {
- return 'Password must be at least 8 characters.';
- }
- return null;
- },
- ),
+ _buildPasswordField(),
const SizedBox(height: 8),
CheckboxListTile(
key: const Key('admin_create_is_admin'),
diff --git a/player-android/lib/screens/settings_screen.dart b/player-android/lib/screens/settings_screen.dart
index 1260273..388acbe 100644
--- a/player-android/lib/screens/settings_screen.dart
+++ b/player-android/lib/screens/settings_screen.dart
@@ -257,33 +257,10 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
onTap: () => context.go(AppRoutes.shares),
),
- // ----------------------------------------------------------------
// Admin section: only visible to admin users.
// Non-admin users are gated out here; the server enforces this
// independently via 403 responses, making this defence-in-depth.
- // ----------------------------------------------------------------
- if (isAdmin) ...[
- const SizedBox(height: 32),
- const Divider(),
- const SizedBox(height: 24),
-
- Text(
- 'Administration',
- style: Theme.of(context).textTheme.titleMedium,
- ),
- const SizedBox(height: 12),
-
- // Manage Users tile — navigates to /admin/users.
- ListTile(
- key: const Key('settings_manage_users'),
- contentPadding: EdgeInsets.zero,
- leading: const Icon(Icons.manage_accounts_outlined),
- title: const Text('Manage Users'),
- subtitle: const Text('Create and delete user accounts'),
- trailing: const Icon(Icons.chevron_right),
- onTap: () => context.go(AppRoutes.adminUsers),
- ),
- ],
+ if (isAdmin) const _AdminSection(),
],
),
),
@@ -341,6 +318,49 @@ class _ThemeToggle extends ConsumerWidget {
}
// ---------------------------------------------------------------------------
+// Admin section widget
+// ---------------------------------------------------------------------------
+
+/// Administration section shown only to admin users in [SettingsScreen].
+///
+/// Extracted as a [ConsumerWidget] (following the [_ThemeToggle] pattern) so
+/// [_SettingsScreenState.build] does not need to reference [AppRoutes.adminUsers]
+/// directly and stays focused on layout concerns. The server independently
+/// enforces admin-only access via 403, so this UI gate is defence-in-depth.
+class _AdminSection extends ConsumerWidget {
+ const _AdminSection();
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ const SizedBox(height: 32),
+ const Divider(),
+ const SizedBox(height: 24),
+
+ Text(
+ 'Administration',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 12),
+
+ // Manage Users tile — navigates to /admin/users.
+ ListTile(
+ key: const Key('settings_manage_users'),
+ contentPadding: EdgeInsets.zero,
+ leading: const Icon(Icons.manage_accounts_outlined),
+ title: const Text('Manage Users'),
+ subtitle: const Text('Create and delete user accounts'),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => context.go(AppRoutes.adminUsers),
+ ),
+ ],
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
// File-level helpers
// ---------------------------------------------------------------------------