From a8715c8ec4255aa3f4937457173e842c5e567e22 Mon Sep 17 00:00:00 2001 From: "Scott E. Graves" Date: Wed, 7 Oct 2026 15:32:24 -0500 Subject: [PATCH] theme changes --- CHANGELOG.md | 2 + web/repertory/lib/constants.dart | 186 ++++++++++++++---- web/repertory/lib/helpers.dart | 46 ++++- web/repertory/lib/main.dart | 74 ++++--- web/repertory/lib/models/mount.dart | 6 +- .../lib/screens/add_mount_screen.dart | 61 +++--- web/repertory/lib/screens/auth_screen.dart | 10 +- .../lib/screens/edit_mount_screen.dart | 53 ++--- .../lib/screens/edit_settings_screen.dart | 61 +++--- web/repertory/lib/screens/home_screen.dart | 13 +- web/repertory/lib/settings.dart | 8 +- web/repertory/lib/widgets/app_dialog.dart | 105 ++++++++++ web/repertory/lib/widgets/app_dropdown.dart | 5 +- .../lib/widgets/app_icon_button_framed.dart | 6 +- web/repertory/lib/widgets/app_scaffold.dart | 25 +-- .../lib/widgets/app_toggle_button_framed.dart | 3 +- web/repertory/lib/widgets/mount_settings.dart | 7 +- web/repertory/lib/widgets/mount_widget.dart | 34 ++-- .../widgets/settings/settings_section.dart | 14 +- .../settings/web_settings_section.dart | 10 +- web/repertory/lib/widgets/ui_settings.dart | 6 +- 21 files changed, 487 insertions(+), 248 deletions(-) create mode 100644 web/repertory/lib/widgets/app_dialog.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 5107fc3..7a2dd7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ ### Changes from v2.0.7-release * Fixed Windows setup icon location +* Upgraded `cpp-build-system` to v2.0.1 +* Updated theme colors and layout --- diff --git a/web/repertory/lib/constants.dart b/web/repertory/lib/constants.dart index 3ebf47b..c919eb7 100644 --- a/web/repertory/lib/constants.dart +++ b/web/repertory/lib/constants.dart @@ -1,49 +1,157 @@ // constants.dart -import 'package:flutter/material.dart' show GlobalKey, NavigatorState, Color; +import 'package:flutter/material.dart'; import 'package:sodium_libs/sodium_libs.dart'; -const accentBlue = Color(0xFF1050A0); -const addMountTitle = 'Add New Mount'; -const appLogonTitle = 'Repertory Portal Login'; -const appSettingsTitle = 'Portal Settings'; -const appTitle = 'Repertory Management Portal'; -const borderRadius = 16.0; -const borderRadiusSmall = borderRadius / 2.0; -const borderRadiusTiny = borderRadiusSmall / 2.0; -const boxShadowAlpha = 0.20; -const databaseTypeList = ['rocksdb', 'sqlite']; -const dialogAlpha = 0.95; -const downloadTypeList = ['default', 'direct', 'ring_buffer']; -const dropDownAlpha = 99.0; -const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace']; -const gradientColors = [Color(0xFF0A0F1F), Color(0xFF1B1C1F)]; -const gradientColors2 = [Color(0x07FFFFFF), Color(0x00000000)]; -const highlightAlpha = 0.10; -const largeIconSize = 32.0; -const loginIconSize = 54.0; -const logonWidth = 420.0; -const outlineAlpha = 0.15; -const padding = 16.0; -const paddingLarge = padding * 2.0; -const paddingMedium = 12.0; -const paddingSmall = padding / 2.0; -const primaryAlpha = 0.12; -const primarySurfaceAlpha = 92.0; -const protocolTypeList = ['http', 'https']; -const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia']; -const ringBufferSizeList = ['128', '256', '512', '1024', '2048']; -const secondaryAlpha = 0.45; -const secondarySurfaceAlpha = 70.0; -const smallIconSize = 18.0; -const surfaceContainerLowDark = Color(0xFF292A2D); -const surfaceDark = Color(0xFF202124); +// --------------------------------------------------------------------------- +// App strings +// Window/screen titles shown in the UI. +// --------------------------------------------------------------------------- +const addMountTitle = 'Add New Mount'; // Title for the add-mount dialog. +const appLogonTitle = 'Repertory Portal Login'; // Title for the login screen. +const appSettingsTitle = 'Portal Settings'; // Title for the settings dialog. +const appTitle = 'Repertory Management Portal'; // App/home screen title. +// --------------------------------------------------------------------------- +// Palette +// Seed/accent blue, muted dark-theme primary, dark grey surfaces, error red. +// --------------------------------------------------------------------------- +const accentBlue = Color(0xFF1050A0); // Seed color the ColorScheme is built from. +const primaryBlue = Color(0xFF6E95C9); // Muted primary accent used in dark mode. +const surfaceDark = Color(0xFF1E2127); // Scaffold/base surface (blue-tinted grey). +const surfaceContainerLowDark = Color(0xFF272B33); // Elevated surface (cards/dialogs). +const errorRed = Color(0xFFD98A86); // Muted error red for validation/error text. + +// --------------------------------------------------------------------------- +// Gradients +// Full-screen background and the subtle FAB sheen overlay. +// --------------------------------------------------------------------------- +const gradientColors = [Color(0xFF0A0F1F), Color(0xFF1B1C1F)]; // App background gradient. +const gradientColors2 = [Color(0x07FFFFFF), Color(0x00000000)]; // Faint top-down FAB sheen. + +// --------------------------------------------------------------------------- +// Alpha tints +// Translucency applied over primary/surface/outline colors. +// --------------------------------------------------------------------------- +const boxShadowAlpha = 0.20; // Opacity of drop shadows. +const dialogTintAlpha = 0.06; // Accent tint blended onto dialog/dropdown surfaces. +const highlightAlpha = 0.10; // Subtle highlight/border tint. +const outlineAlpha = 0.15; // Outline/border and scrim tint. +const primaryAlpha = 0.12; // Primary fill tint for cards/fields. +const primarySurfaceAlpha = 92.0; // Near-opaque primary surface text/icon tint. +const secondaryAlpha = 0.45; // Medium primary tint for buttons/toggles. +const secondarySurfaceAlpha = 70.0; // Dimmer surface text tint (descriptions). + +// --------------------------------------------------------------------------- +// Corner radii +// Shared rounding scale for cards, buttons, and dialogs. +// --------------------------------------------------------------------------- +const borderRadius = 16.0; // Base corner radius. +const borderRadiusSmall = borderRadius / 2.0; // Half radius for cards/fields (8). +const borderRadiusTiny = borderRadiusSmall / 2.0; // Quarter radius for small chips (4). + +// --------------------------------------------------------------------------- +// Border widths +// Default hairline and the thicker focused-input border. +// --------------------------------------------------------------------------- +const borderWidth = 1.0; // Standard hairline border for bevels. +const focusBorderWidth = 2.0; // Emphasized border for focused inputs. + +// --------------------------------------------------------------------------- +// Elevations +// Card drop shadow and the stronger floating-element shadow. +// --------------------------------------------------------------------------- +const cardElevation = 4.0; // Subtle shadow for content cards. +const fabElevation = 12.0; // Stronger shadow for the floating action button. + +// --------------------------------------------------------------------------- +// Spacing +// Shared padding scale used across layouts. +// --------------------------------------------------------------------------- +const padding = 16.0; // Base padding/gap unit. +const paddingLarge = padding * 2.0; // Double padding for larger gaps (32). +const paddingMedium = 12.0; // Medium padding between base and small. +const paddingSmall = padding / 2.0; // Half padding for tight gaps (8). + +// --------------------------------------------------------------------------- +// Icon sizes +// Shared scale for iconography. +// --------------------------------------------------------------------------- +const largeIconSize = 32.0; // Large icons (FAB, prominent actions). +const loginIconSize = 54.0; // Oversized icon on the login screen. +const smallIconSize = 18.0; // Small inline icons. + +// --------------------------------------------------------------------------- +// Icons +// Shared iconography reused across the UI (actions, toggles, fields). +// --------------------------------------------------------------------------- +const iconAdd = Icons.add; // Add / create actions (FAB, add mount). +const iconAutoMount = Icons.auto_mode; // Auto-mount indicator. +const iconBack = Icons.arrow_back; // Back navigation. +const iconClose = Icons.close; // Close/dismiss (dialogs). +const iconDelete = Icons.delete; // Delete/remove actions. +const iconEdit = Icons.edit; // Edit actions. +const iconFolder = Icons.folder; // Folder/path and app logo fallback. +const iconLock = Icons.lock; // Password/lock fields. +const iconPassword = Icons.password; // Password-style settings. +const iconRename = Icons.drive_file_rename_outline; // Rename/config-name field. +const iconRemoteMount = Icons.lan; // Remote mount (networking). +const iconSettings = Icons.settings; // Settings/gear actions. +const iconTest = Icons.check; // Test/confirm actions. +const iconToggleOff = Icons.toggle_off; // Toggle (off state). +const iconToggleOn = Icons.toggle_on; // Toggle (on state). +const iconUser = Icons.person; // User/username fields. +const iconVisibility = Icons.visibility; // Reveal password. +const iconVisibilityOff = Icons.visibility_off; // Hide password. + +// --------------------------------------------------------------------------- +// Control sizes +// Fixed dimensions for buttons, framed controls, and the FAB. +// --------------------------------------------------------------------------- +const buttonHeight = 40.0; // Minimum height of primary action buttons. +const fabInnerSize = 56.0; // Inner tappable square of the add-mount FAB. +const framedControlSize = 46.0; // Bordered icon buttons on mount cards. +const headerControlSize = 40.0; // App-bar logo/back box. +const mountCardMinHeight = 120.0; // Minimum height of a mount list card. + +// --------------------------------------------------------------------------- +// Layout widths/factors +// Fixed widths and viewport-fraction sizing. +// --------------------------------------------------------------------------- +const logonWidth = 420.0; // Fixed width of the login card. +const snackBarWidthFactor = 0.50; // Snackbar width as a fraction of the viewport. + +// --------------------------------------------------------------------------- +// Dialog sizing +// Viewport-fraction width/height with absolute pixel clamps. +// --------------------------------------------------------------------------- +const dialogMinWidthFactor = 0.40; // Min dialog width as a viewport fraction. +const dialogMaxWidthFactor = 0.70; // Max dialog width as a viewport fraction. +const dialogMaxHeightFactor = 0.85; // Max dialog height before it scrolls. +const dialogMinWidth = 360.0; // Absolute minimum dialog width (px). +const dialogMaxWidth = 820.0; // Absolute maximum dialog width (px). + +// --------------------------------------------------------------------------- +// Option lists +// Selectable values for configuration dropdowns. +// --------------------------------------------------------------------------- +const databaseTypeList = ['rocksdb', 'sqlite']; // Database backend options. +const downloadTypeList = ['default', 'direct', 'ring_buffer']; // Download strategy options. +const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace']; // Log level options. +const protocolTypeList = ['http', 'https']; // Connection protocol options. +const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia']; // Mount provider options. +const ringBufferSizeList = ['128', '256', '512', '1024', '2048']; // Ring-buffer size options (MiB). + +// --------------------------------------------------------------------------- +// Runtime globals +// Navigator key and lazily-initialized libsodium instance. +// --------------------------------------------------------------------------- +// Enables navigation/context access from outside the widget tree. final GlobalKey navigatorKey = GlobalKey(); -Sodium? _sodium; +Sodium? _sodium; // Backing store for the libsodium instance. void setSodium(Sodium sodium) { - _sodium = sodium; + _sodium = sodium; // Set once during app initialization. } -Sodium get sodium => _sodium!; +Sodium get sodium => _sodium!; // Access the initialized libsodium instance. diff --git a/web/repertory/lib/helpers.dart b/web/repertory/lib/helpers.dart index 2f091b1..ddfceb4 100644 --- a/web/repertory/lib/helpers.dart +++ b/web/repertory/lib/helpers.dart @@ -18,10 +18,14 @@ Future doShowDialog(BuildContext context, Widget child) => showDialog( return Theme( data: theme.copyWith( dialogTheme: DialogThemeData( - backgroundColor: darken( - scheme.primary, - 0.95, - ).withValues(alpha: constants.dropDownAlpha), + // Elevated surface: slightly lighter than the scaffold background + // (surfaceDark) with a faint accent tint so the popup reads as part + // of the dark theme rather than a near-black panel. The backdrop blur + // and barrier scrim already provide separation, so keep it opaque. + backgroundColor: Color.alphaBlend( + scheme.primary.withValues(alpha: constants.dialogTintAlpha), + constants.surfaceContainerLowDark, + ), surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(constants.borderRadius), @@ -29,7 +33,7 @@ Future doShowDialog(BuildContext context, Widget child) => showDialog( color: scheme.outlineVariant.withValues( alpha: constants.outlineAlpha, ), - width: 1, + width: constants.borderWidth, ), ), ), @@ -90,8 +94,8 @@ final Validator portIsValid = (value) { // ignore: prefer_function_declarations_over_variables final Validator trimNotEmptyValidator = (value) => value.trim().isNotEmpty; -createUriValidator({host, port}) { - return (value) => +Validator createUriValidator({Object? host, Object? port}) { + return (String value) => Uri.tryParse('http://${host ?? value}:${port ?? value}/') != null; } @@ -448,14 +452,39 @@ InputDecoration createCommonDecoration( filled: filled, fillColor: colorScheme.primary.withValues(alpha: constants.primaryAlpha), hintText: hintText, + // Keep label and prefix/suffix icons neutral so the error state does not + // flood the whole field with red; the error signal stays on the border and + // helper text only. + labelStyle: TextStyle(color: colorScheme.onSurface), + floatingLabelStyle: TextStyle(color: colorScheme.onSurface), + prefixIconColor: colorScheme.onSurface, + suffixIconColor: colorScheme.onSurface, border: OutlineInputBorder( borderRadius: BorderRadius.circular(constants.borderRadiusSmall), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(constants.borderRadiusSmall), - borderSide: BorderSide(color: colorScheme.primary, width: 2), + borderSide: BorderSide( + color: colorScheme.primary, + width: constants.focusBorderWidth, + ), ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(constants.borderRadiusSmall), + borderSide: BorderSide( + color: colorScheme.error, + width: constants.borderWidth, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(constants.borderRadiusSmall), + borderSide: BorderSide( + color: colorScheme.error, + width: constants.focusBorderWidth, + ), + ), + errorStyle: TextStyle(color: colorScheme.error), contentPadding: const EdgeInsets.all(constants.paddingSmall), ); @@ -504,3 +533,4 @@ Color darken(Color color, [double percentage = 0.1]) { ); return hslDark.toColor(); } + diff --git a/web/repertory/lib/main.dart b/web/repertory/lib/main.dart index 12ed453..81fa46a 100644 --- a/web/repertory/lib/main.dart +++ b/web/repertory/lib/main.dart @@ -3,15 +3,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:repertory/constants.dart' as constants; -import 'package:repertory/helpers.dart'; import 'package:repertory/models/auth.dart'; -import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount_list.dart'; import 'package:repertory/models/settings.dart'; -import 'package:repertory/screens/add_mount_screen.dart'; import 'package:repertory/screens/auth_screen.dart'; -import 'package:repertory/screens/edit_mount_screen.dart'; -import 'package:repertory/screens/edit_settings_screen.dart'; import 'package:repertory/screens/home_screen.dart'; import 'package:repertory/widgets/auth_check.dart'; import 'package:sodium_libs/sodium_libs.dart' show SodiumInit; @@ -47,36 +42,57 @@ class _MyAppState extends State { @override Widget build(context) { final snackBarTheme = SnackBarThemeData( - width: MediaQuery.of(context).size.width * 0.50, + width: + MediaQuery.of(context).size.width * constants.snackBarWidthFactor, behavior: SnackBarBehavior.floating, ); + final colorScheme = ColorScheme.fromSeed( + brightness: Brightness.dark, + seedColor: constants.accentBlue, + primary: constants.primaryBlue, + error: constants.errorRed, + onSurface: Colors.white70, + surface: constants.surfaceDark, + surfaceContainerLow: constants.surfaceContainerLowDark, + ); + return MaterialApp( navigatorKey: constants.navigatorKey, themeMode: ThemeMode.dark, darkTheme: ThemeData( useMaterial3: true, brightness: Brightness.dark, - colorScheme: ColorScheme.fromSeed( - brightness: Brightness.dark, - seedColor: constants.accentBlue, - onSurface: Colors.white70, - surface: constants.surfaceDark, - surfaceContainerLow: constants.surfaceContainerLowDark, - ), + colorScheme: colorScheme, scaffoldBackgroundColor: constants.surfaceDark, snackBarTheme: snackBarTheme, appBarTheme: const AppBarTheme(scrolledUnderElevation: 0), - inputDecorationTheme: const InputDecorationTheme( - focusedBorder: UnderlineInputBorder(borderSide: BorderSide(width: 2)), + cardTheme: CardThemeData( + elevation: constants.cardElevation, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(constants.borderRadiusSmall), + side: BorderSide( + color: colorScheme.outlineVariant.withValues( + alpha: constants.highlightAlpha, + ), + width: constants.borderWidth, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(width: constants.focusBorderWidth), + ), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(40), + minimumSize: const Size.fromHeight(constants.buttonHeight), ), ), filledButtonTheme: FilledButtonThemeData( - style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(40)), + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight(constants.buttonHeight), + ), ), dividerTheme: const DividerThemeData(thickness: 0.6, space: 0), ), @@ -85,31 +101,7 @@ class _MyAppState extends State { routes: { '/': (context) => const AuthCheck(child: HomeScreen(title: constants.appTitle)), - '/add': (context) => const AuthCheck( - child: AddMountScreen(title: constants.addMountTitle), - ), '/auth': (context) => const AuthScreen(title: constants.appTitle), - '/settings': (context) => const AuthCheck( - child: EditSettingsScreen(title: constants.appSettingsTitle), - ), - }, - onGenerateRoute: (settings) { - if (settings.name != '/edit') { - return null; - } - - final mount = settings.arguments as Mount; - return MaterialPageRoute( - builder: (context) { - return AuthCheck( - child: EditMountScreen( - mount: mount, - title: - '${mount.provider} Settings • ${formatMountName(mount.type, mount.name)}', - ), - ); - }, - ); }, ); } diff --git a/web/repertory/lib/models/mount.dart b/web/repertory/lib/models/mount.dart index 6ec3f72..f632a07 100644 --- a/web/repertory/lib/models/mount.dart +++ b/web/repertory/lib/models/mount.dart @@ -125,7 +125,7 @@ class Mount with ChangeNotifier { return; } - return refresh(); + return await refresh(); } catch (e) { debugPrint('$e'); } @@ -154,7 +154,7 @@ class Mount with ChangeNotifier { return; } - return refresh(); + return await refresh(); } catch (e) { debugPrint('$e'); } @@ -330,7 +330,7 @@ class Mount with ChangeNotifier { return; } - return refresh(); + return await refresh(); } catch (e) { debugPrint('$e'); } diff --git a/web/repertory/lib/screens/add_mount_screen.dart b/web/repertory/lib/screens/add_mount_screen.dart index 7bfa333..131c0da 100644 --- a/web/repertory/lib/screens/add_mount_screen.dart +++ b/web/repertory/lib/screens/add_mount_screen.dart @@ -10,21 +10,31 @@ import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount_list.dart'; import 'package:repertory/types/mount_config.dart'; import 'package:repertory/utils/safe_set_state_mixin.dart'; +import 'package:repertory/widgets/app_dialog.dart'; import 'package:repertory/widgets/app_dropdown.dart'; import 'package:repertory/widgets/app_outlined_icon_button.dart'; -import 'package:repertory/widgets/app_scaffold.dart'; import 'package:repertory/widgets/mount_settings.dart'; -class AddMountScreen extends StatefulWidget { - final String title; - const AddMountScreen({super.key, required this.title}); - - @override - State createState() => _AddMountScreenState(); +/// Opens the "Add New Mount" modal dialog. +Future showAddMountDialog(BuildContext context) { + return doShowDialog( + context, + const AppDialog( + title: constants.addMountTitle, + child: AddMountContent(), + ), + ); } -class _AddMountScreenState extends State - with SafeSetState { +class AddMountContent extends StatefulWidget { + const AddMountContent({super.key}); + + @override + State createState() => _AddMountContentState(); +} + +class _AddMountContentState extends State + with SafeSetState { Mount? _mount; final _mountNameController = TextEditingController(); String _mountType = ""; @@ -48,9 +58,9 @@ class _AddMountScreenState extends State Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; - return AppScaffold( - title: widget.title, - showBack: true, + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AppDropdown( constrainToIntrinsic: true, @@ -83,26 +93,19 @@ class _AddMountScreenState extends State scheme, 'Configuration Name', hintText: 'Enter a unique name', - icon: Icons.drive_file_rename_outline, + icon: constants.iconRename, ), ), ], if (_mount != null) ...[ const SizedBox(height: constants.padding), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: constants.padding, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(constants.borderRadius), - child: MountSettingsWidget( - isAdd: true, - mount: _mount!, - settings: _settings[_mountType]!, - showAdvanced: false, - ), - ), + ClipRRect( + borderRadius: BorderRadius.circular(constants.borderRadius), + child: MountSettingsWidget( + isAdd: true, + mount: _mount!, + settings: _settings[_mountType]!, + showAdvanced: false, ), ), const SizedBox(height: constants.padding), @@ -111,7 +114,7 @@ class _AddMountScreenState extends State IntrinsicWidth( child: AppOutlinedIconButton( enabled: _enabled, - icon: Icons.check, + icon: constants.iconTest, text: 'Test', onPressed: _handleProviderTest, ), @@ -120,7 +123,7 @@ class _AddMountScreenState extends State IntrinsicWidth( child: AppOutlinedIconButton( enabled: _enabled, - icon: Icons.add, + icon: constants.iconAdd, text: 'Add', onPressed: () async { setState(() { diff --git a/web/repertory/lib/screens/auth_screen.dart b/web/repertory/lib/screens/auth_screen.dart index 3266171..779a09d 100644 --- a/web/repertory/lib/screens/auth_screen.dart +++ b/web/repertory/lib/screens/auth_screen.dart @@ -198,7 +198,7 @@ class _AuthScreenState extends State { fit: BoxFit.contain, errorBuilder: (_, _, _) { return Icon( - Icons.folder, + constants.iconFolder, color: scheme.primary, size: constants.loginIconSize, ); @@ -234,7 +234,7 @@ class _AuthScreenState extends State { AppTextField( autofocus: true, controller: _userController, - icon: Icons.person, + icon: constants.iconUser, labelText: 'Username', textInputAction: TextInputAction.next, validator: (v) { @@ -250,7 +250,7 @@ class _AuthScreenState extends State { const SizedBox(height: constants.padding), AppTextField( controller: _passwordController, - icon: Icons.lock, + icon: constants.iconLock, labelText: 'Password', obscureText: _obscure, suffixIcon: IconButton( @@ -259,8 +259,8 @@ class _AuthScreenState extends State { : 'Hide password', icon: Icon( _obscure - ? Icons.visibility - : Icons.visibility_off, + ? constants.iconVisibility + : constants.iconVisibilityOff, ), onPressed: () { setState(() { diff --git a/web/repertory/lib/screens/edit_mount_screen.dart b/web/repertory/lib/screens/edit_mount_screen.dart index 62511cb..c7d0293 100644 --- a/web/repertory/lib/screens/edit_mount_screen.dart +++ b/web/repertory/lib/screens/edit_mount_screen.dart @@ -3,32 +3,38 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:repertory/constants.dart' as constants; +import 'package:repertory/helpers.dart'; import 'package:repertory/models/mount.dart'; import 'package:repertory/utils/safe_set_state_mixin.dart'; -import 'package:repertory/widgets/app_scaffold.dart'; +import 'package:repertory/widgets/app_dialog.dart'; import 'package:repertory/widgets/mount_settings.dart'; -class EditMountScreen extends StatefulWidget { - final Mount mount; - final String title; - const EditMountScreen({super.key, required this.mount, required this.title}); - - @override - State createState() => _EditMountScreenState(); +/// Opens the "Edit Mount" modal dialog for the given [mount]. +Future showEditMountDialog(BuildContext context, Mount mount) { + return doShowDialog(context, EditMountContent(mount: mount)); } -class _EditMountScreenState extends State - with SafeSetState { +class EditMountContent extends StatefulWidget { + final Mount mount; + const EditMountContent({super.key, required this.mount}); + + @override + State createState() => _EditMountContentState(); +} + +class _EditMountContentState extends State + with SafeSetState { bool _showAdvanced = false; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - return AppScaffold( - title: widget.title, - showBack: true, - advancedWidget: Row( + + return AppDialog( + title: + '${widget.mount.provider} Settings • ${formatMountName(widget.mount.type, widget.mount.name)}', + trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text( @@ -38,22 +44,19 @@ class _EditMountScreenState extends State const SizedBox(width: 6), IconButton( tooltip: _showAdvanced ? 'Hide advanced' : 'Show advanced', - icon: Icon(_showAdvanced ? Icons.toggle_on : Icons.toggle_off), + icon: Icon( + _showAdvanced ? constants.iconToggleOn : constants.iconToggleOff, + ), color: _showAdvanced ? scheme.primary : scheme.onSurface, onPressed: () => setState(() => _showAdvanced = !_showAdvanced), ), ], ), - children: [ - Expanded( - child: MountSettingsWidget( - mount: widget.mount, - settings: jsonDecode(jsonEncode(widget.mount.mountConfig.settings)), - showAdvanced: _showAdvanced, - ), - ), - const SizedBox(height: constants.padding), - ], + child: MountSettingsWidget( + mount: widget.mount, + settings: jsonDecode(jsonEncode(widget.mount.mountConfig.settings)), + showAdvanced: _showAdvanced, + ), ); } } diff --git a/web/repertory/lib/screens/edit_settings_screen.dart b/web/repertory/lib/screens/edit_settings_screen.dart index 060a583..cf1d026 100644 --- a/web/repertory/lib/screens/edit_settings_screen.dart +++ b/web/repertory/lib/screens/edit_settings_screen.dart @@ -9,45 +9,42 @@ import 'package:repertory/constants.dart' as constants; import 'package:repertory/helpers.dart'; import 'package:repertory/models/auth.dart'; import 'package:repertory/utils/safe_set_state_mixin.dart'; -import 'package:repertory/widgets/app_scaffold.dart'; +import 'package:repertory/widgets/app_dialog.dart'; import 'package:repertory/widgets/ui_settings.dart'; -class EditSettingsScreen extends StatefulWidget { - final String title; - const EditSettingsScreen({super.key, required this.title}); - - @override - State createState() => _EditSettingsScreenState(); +/// Opens the "Portal Settings" modal dialog. +Future showPortalSettingsDialog(BuildContext context) { + return doShowDialog(context, const PortalSettingsContent()); } -class _EditSettingsScreenState extends State - with SafeSetState { +class PortalSettingsContent extends StatefulWidget { + const PortalSettingsContent({super.key}); + + @override + State createState() => _PortalSettingsContentState(); +} + +class _PortalSettingsContentState extends State + with SafeSetState { @override Widget build(BuildContext context) { - return AppScaffold( - title: widget.title, - showBack: true, - showUISettings: true, - children: [ - Expanded( - child: FutureBuilder>( - future: _grabSettings(), - initialData: const {}, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); - } + return AppDialog( + title: constants.appSettingsTitle, + child: FutureBuilder>( + future: _grabSettings(), + initialData: const {}, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } - return UISettingsWidget( - origSettings: jsonDecode(jsonEncode(snapshot.requireData)), - settings: snapshot.requireData, - showAdvanced: false, - ); - }, - ), - ), - const SizedBox(height: constants.padding), - ], + return UISettingsWidget( + origSettings: jsonDecode(jsonEncode(snapshot.requireData)), + settings: snapshot.requireData, + showAdvanced: false, + ); + }, + ), ); } diff --git a/web/repertory/lib/screens/home_screen.dart b/web/repertory/lib/screens/home_screen.dart index 2ca0738..b901fbb 100644 --- a/web/repertory/lib/screens/home_screen.dart +++ b/web/repertory/lib/screens/home_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:repertory/constants.dart' as constants; +import 'package:repertory/screens/add_mount_screen.dart'; import 'package:repertory/widgets/app_scaffold.dart'; import 'package:repertory/widgets/mount_list_widget.dart'; @@ -26,7 +27,7 @@ class _HomeScreeState extends State { tag: 'add_mount_fab', child: Material( color: scheme.primary.withValues(alpha: constants.secondaryAlpha), - elevation: 12, + elevation: constants.fabElevation, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(constants.borderRadius), ), @@ -37,7 +38,7 @@ class _HomeScreeState extends State { color: scheme.outlineVariant.withValues( alpha: constants.outlineAlpha, ), - width: 1, + width: constants.borderWidth, ), gradient: const LinearGradient( begin: Alignment.topCenter, @@ -57,13 +58,13 @@ class _HomeScreeState extends State { child: InkWell( borderRadius: BorderRadius.circular(constants.borderRadius), onTap: () { - Navigator.pushNamed(context, '/add'); + showAddMountDialog(context); }, child: const SizedBox( - width: 56, - height: 56, + width: constants.fabInnerSize, + height: constants.fabInnerSize, child: Center( - child: Icon(Icons.add, size: constants.largeIconSize), + child: Icon(constants.iconAdd, size: constants.largeIconSize), ), ), ), diff --git a/web/repertory/lib/settings.dart b/web/repertory/lib/settings.dart index 65faaba..17eeed2 100644 --- a/web/repertory/lib/settings.dart +++ b/web/repertory/lib/settings.dart @@ -20,7 +20,7 @@ void createBooleanSetting( widget, Function setState, { String? description, - IconData icon = Icons.quiz, + IconData icon = constants.iconToggleOn, }) { if (!isAdvanced || showAdvanced) { list.add( @@ -149,7 +149,7 @@ void createPasswordSetting( widget, Function setState, { String? description, - IconData icon = Icons.password, + IconData icon = constants.iconPassword, List validators = const [], }) { if (!isAdvanced || widget.showAdvanced) { @@ -222,8 +222,8 @@ void createPasswordSetting( ), icon: Icon( hidePassword1 - ? Icons.visibility - : Icons.visibility_off, + ? constants.iconVisibility + : constants.iconVisibilityOff, ), ), ], diff --git a/web/repertory/lib/widgets/app_dialog.dart b/web/repertory/lib/widgets/app_dialog.dart new file mode 100644 index 0000000..b54c74f --- /dev/null +++ b/web/repertory/lib/widgets/app_dialog.dart @@ -0,0 +1,105 @@ +// app_dialog.dart + +import 'package:flutter/material.dart'; +import 'package:repertory/constants.dart' as constants; + +/// A themed modal dialog shell that sizes to its content. +/// +/// The width is sized between [constants.dialogMinWidthFactor] and +/// [constants.dialogMaxWidthFactor] of the viewport (clamped to the absolute +/// pixel bounds in [constants]). The dialog hugs its content vertically and +/// only begins scrolling once the content would exceed +/// [constants.dialogMaxHeightFactor] of the viewport. For this to work, the +/// content ([child]) must size itself to its intrinsic height (e.g. settings +/// lists built with `shrinkWrap: true`) rather than expanding to fill. +/// +/// A fixed header row (title, optional [trailing] widget, close button) stays +/// pinned above the scrollable content. +class AppDialog extends StatelessWidget { + const AppDialog({ + super.key, + required this.title, + required this.child, + this.trailing, + }); + + final String title; + final Widget child; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + final media = MediaQuery.of(context).size; + + final maxWidth = (media.width * constants.dialogMaxWidthFactor).clamp( + constants.dialogMinWidth, + constants.dialogMaxWidth, + ); + final minWidth = (media.width * constants.dialogMinWidthFactor).clamp( + constants.dialogMinWidth, + maxWidth, + ); + final maxHeight = media.height * constants.dialogMaxHeightFactor; + + final header = Row( + children: [ + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + color: scheme.primary, + letterSpacing: 0.2, + ), + ), + ), + if (trailing != null) ...[ + const SizedBox(width: constants.padding), + trailing!, + ], + const SizedBox(width: constants.paddingSmall), + IconButton( + tooltip: 'Close', + icon: const Icon(constants.iconClose), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + + return Dialog( + insetPadding: const EdgeInsets.all(constants.padding), + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: minWidth, + maxWidth: maxWidth, + maxHeight: maxHeight, + ), + child: Padding( + // Tighter top/bottom than left/right so the dialog does not look + // vertically empty around the settings list. + padding: const EdgeInsets.symmetric( + horizontal: constants.padding, + vertical: constants.paddingSmall, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + header, + const SizedBox(height: constants.paddingSmall), + // Hug the content height; scroll only once it would exceed the + // dialog's max height. + Flexible( + child: SingleChildScrollView(child: child), + ), + ], + ), + ), + ), + ); + } +} diff --git a/web/repertory/lib/widgets/app_dropdown.dart b/web/repertory/lib/widgets/app_dropdown.dart index 2ee7057..07efb9d 100644 --- a/web/repertory/lib/widgets/app_dropdown.dart +++ b/web/repertory/lib/widgets/app_dropdown.dart @@ -94,7 +94,10 @@ class AppDropdown extends StatelessWidget { final effectiveFill = fillColor ?? - darken(scheme.primary, 0.95).withValues(alpha: constants.dropDownAlpha); + Color.alphaBlend( + scheme.primary.withValues(alpha: constants.dialogTintAlpha), + constants.surfaceContainerLowDark, + ); final effectiveTextStyle = textStyle ?? diff --git a/web/repertory/lib/widgets/app_icon_button_framed.dart b/web/repertory/lib/widgets/app_icon_button_framed.dart index 4da5698..8ed2f22 100644 --- a/web/repertory/lib/widgets/app_icon_button_framed.dart +++ b/web/repertory/lib/widgets/app_icon_button_framed.dart @@ -26,8 +26,8 @@ class AppIconButtonFramed extends StatelessWidget { onTap: onPressed, borderRadius: radius, child: Ink( - width: 46, - height: 46, + width: constants.framedControlSize, + height: constants.framedControlSize, decoration: BoxDecoration( color: scheme.primary.withValues(alpha: constants.outlineAlpha), borderRadius: radius, @@ -35,7 +35,7 @@ class AppIconButtonFramed extends StatelessWidget { color: scheme.outlineVariant.withValues( alpha: constants.outlineAlpha, ), - width: 1, + width: constants.borderWidth, ), boxShadow: [ BoxShadow( diff --git a/web/repertory/lib/widgets/app_scaffold.dart b/web/repertory/lib/widgets/app_scaffold.dart index 0740d7b..db3ae77 100644 --- a/web/repertory/lib/widgets/app_scaffold.dart +++ b/web/repertory/lib/widgets/app_scaffold.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import 'package:repertory/constants.dart' as constants; import 'package:repertory/models/auth.dart'; import 'package:repertory/models/settings.dart'; +import 'package:repertory/screens/edit_settings_screen.dart'; import 'package:repertory/widgets/aurora_sweep.dart'; class AppScaffold extends StatelessWidget { @@ -67,14 +68,14 @@ class AppScaffold extends StatelessWidget { children: [ if (!showBack) ...[ SizedBox( - width: 40, - height: 40, + width: constants.headerControlSize, + height: constants.headerControlSize, child: Image.asset( 'assets/images/repertory.png', fit: BoxFit.contain, errorBuilder: (_, _, _) { return Icon( - Icons.folder, + constants.iconFolder, color: scheme.primary, size: constants.largeIconSize, ); @@ -92,8 +93,8 @@ class AppScaffold extends StatelessWidget { ), onTap: () => Navigator.of(context).pop(), child: Ink( - width: 40, - height: 40, + width: constants.headerControlSize, + height: constants.headerControlSize, decoration: BoxDecoration( color: scheme.surface.withValues( alpha: constants.secondaryAlpha, @@ -105,7 +106,7 @@ class AppScaffold extends StatelessWidget { color: scheme.outlineVariant.withValues( alpha: constants.highlightAlpha, ), - width: 1, + width: constants.borderWidth, ), boxShadow: [ BoxShadow( @@ -117,7 +118,7 @@ class AppScaffold extends StatelessWidget { ), ], ), - child: const Icon(Icons.arrow_back), + child: Icon(constants.iconBack), ), ), ), @@ -141,8 +142,8 @@ class AppScaffold extends StatelessWidget { builder: (context, settings, _) => IconButton( icon: Icon( settings.enableAnimations - ? Icons.toggle_on - : Icons.toggle_off, + ? constants.iconToggleOn + : constants.iconToggleOff, ), color: settings.enableAnimations ? scheme.primary @@ -157,8 +158,8 @@ class AppScaffold extends StatelessWidget { builder: (context, settings, _) => IconButton( icon: Icon( settings.autoStart - ? Icons.toggle_on - : Icons.toggle_off, + ? constants.iconToggleOn + : constants.iconToggleOff, ), color: settings.autoStart ? scheme.primary @@ -175,7 +176,7 @@ class AppScaffold extends StatelessWidget { tooltip: 'Settings', icon: const Icon(Icons.settings), onPressed: () { - Navigator.pushNamed(context, '/settings'); + showPortalSettingsDialog(context); }, ), const SizedBox(width: constants.padding), diff --git a/web/repertory/lib/widgets/app_toggle_button_framed.dart b/web/repertory/lib/widgets/app_toggle_button_framed.dart index d6012ab..4c03c6a 100644 --- a/web/repertory/lib/widgets/app_toggle_button_framed.dart +++ b/web/repertory/lib/widgets/app_toggle_button_framed.dart @@ -1,6 +1,7 @@ // app_toggle_button_framed.dart import 'package:flutter/material.dart'; +import 'package:repertory/constants.dart' as constants; import 'package:repertory/widgets/app_icon_button_framed.dart'; class AppToggleButtonFramed extends StatelessWidget { @@ -22,7 +23,7 @@ class AppToggleButtonFramed extends StatelessWidget { Color iconColor = scheme.onSurface; if (mounted != null) { - icon = isOn ? Icons.toggle_on : Icons.toggle_off; + icon = isOn ? constants.iconToggleOn : constants.iconToggleOff; iconColor = isOn ? scheme.primary : scheme.onSurface; } diff --git a/web/repertory/lib/widgets/mount_settings.dart b/web/repertory/lib/widgets/mount_settings.dart index 3bd9f4e..34621f9 100644 --- a/web/repertory/lib/widgets/mount_settings.dart +++ b/web/repertory/lib/widgets/mount_settings.dart @@ -96,7 +96,7 @@ class _MountSettingsWidgetState extends State { widget.settings, key, value, - Icons.person, + constants.iconUser, true, widget.showAdvanced, widget, @@ -389,11 +389,11 @@ class _MountSettingsWidgetState extends State { final titleStyle = theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, - color: scheme.onSurface, + color: scheme.primary, ); return SettingsList( - shrinkWrap: false, + shrinkWrap: true, platform: DevicePlatform.web, lightTheme: settingsTheme, darkTheme: settingsTheme, @@ -840,6 +840,7 @@ class _MountSettingsWidgetState extends State { widget, setState, description: getSettingDescription('$key.$subKey'), + icon: constants.iconRemoteMount, ); remoteMountSettings.insertAll(0, tempSettings); } diff --git a/web/repertory/lib/widgets/mount_widget.dart b/web/repertory/lib/widgets/mount_widget.dart index 3ffa987..7e91e0c 100644 --- a/web/repertory/lib/widgets/mount_widget.dart +++ b/web/repertory/lib/widgets/mount_widget.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import 'package:repertory/constants.dart' as constants; import 'package:repertory/helpers.dart'; import 'package:repertory/models/mount.dart'; +import 'package:repertory/screens/edit_mount_screen.dart'; import 'package:repertory/utils/safe_set_state_mixin.dart'; import 'package:repertory/widgets/app_outlined_icon_button.dart'; import 'package:repertory/widgets/app_icon_button_framed.dart'; @@ -40,20 +41,10 @@ class _MountWidgetState extends State ); return ConstrainedBox( - constraints: const BoxConstraints(minHeight: 120), + constraints: const BoxConstraints(minHeight: constants.mountCardMinHeight), child: Card( margin: const EdgeInsets.all(0.0), - elevation: 12, color: scheme.primary.withValues(alpha: constants.primaryAlpha), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(constants.borderRadiusSmall), - side: BorderSide( - color: scheme.outlineVariant.withValues( - alpha: constants.outlineAlpha, - ), - width: 1, - ), - ), child: Padding( padding: const EdgeInsets.all(constants.padding), child: Consumer( @@ -66,12 +57,8 @@ class _MountWidgetState extends State crossAxisAlignment: CrossAxisAlignment.center, children: [ AppIconButtonFramed( - icon: Icons.settings, - onPressed: () => Navigator.pushNamed( - context, - '/edit', - arguments: mount, - ), + icon: constants.iconSettings, + onPressed: () => showEditMountDialog(context, mount), ), const SizedBox(width: constants.padding), Expanded( @@ -88,13 +75,13 @@ class _MountWidgetState extends State ), if (mount.mounted == false) ...[ AppIconButtonFramed( - icon: Icons.delete, + icon: constants.iconDelete, onPressed: _enabled - ? () async { + ? () { setState(() { _enabled = false; }); - return doShowDialog( + doShowDialog( context, AlertDialog( actions: [ @@ -102,6 +89,9 @@ class _MountWidgetState extends State child: const Text('Yes'), onPressed: () async { await mount.remove(); + if (!context.mounted) { + return; + } setState(() { _enabled = true; }); @@ -140,7 +130,7 @@ class _MountWidgetState extends State children: [ AppOutlinedIconButton( text: 'Edit path', - icon: Icons.edit, + icon: constants.iconEdit, enabled: _enabled && mount.mounted == false, onPressed: () async { setState(() { @@ -211,7 +201,7 @@ class _MountWidgetState extends State mainAxisSize: MainAxisSize.min, children: const [ Icon( - Icons.auto_mode, + constants.iconAutoMount, size: constants.smallIconSize, ), SizedBox(width: constants.paddingSmall), diff --git a/web/repertory/lib/widgets/settings/settings_section.dart b/web/repertory/lib/widgets/settings/settings_section.dart index 219a829..b4e075e 100644 --- a/web/repertory/lib/widgets/settings/settings_section.dart +++ b/web/repertory/lib/widgets/settings/settings_section.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter_settings_ui/src/sections/abstract_settings_section.dart'; -import 'package:flutter_settings_ui/src/sections/platforms/android_settings_section.dart'; -import 'package:flutter_settings_ui/src/sections/platforms/ios_settings_section.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:repertory/widgets/settings/web_settings_section.dart'; -import 'package:flutter_settings_ui/src/tiles/abstract_settings_tile.dart'; -import 'package:flutter_settings_ui/src/utils/platform_utils.dart'; -import 'package:flutter_settings_ui/src/utils/settings_theme.dart'; +// These platform section widgets are not re-exported by the public barrel, so +// they must be imported directly. They are only reached on non-web platforms; +// this app targets web, but the switch below handles them for completeness. +// ignore: implementation_imports +import 'package:flutter_settings_ui/src/sections/platforms/android_settings_section.dart'; +// ignore: implementation_imports +import 'package:flutter_settings_ui/src/sections/platforms/ios_settings_section.dart'; class SettingsSection extends AbstractSettingsSection { const SettingsSection({ diff --git a/web/repertory/lib/widgets/settings/web_settings_section.dart b/web/repertory/lib/widgets/settings/web_settings_section.dart index ad763d8..edcb023 100644 --- a/web/repertory/lib/widgets/settings/web_settings_section.dart +++ b/web/repertory/lib/widgets/settings/web_settings_section.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:repertory/constants.dart' as constants; // Container( // height: 65 * scaleFactor, @@ -30,7 +31,6 @@ class WebSettingsSection extends StatelessWidget { Widget buildSectionBody(BuildContext context) { final theme = SettingsTheme.of(context); - final scaleFactor = MediaQuery.textScalerOf(context).scale(1); return Padding( padding: margin ?? EdgeInsets.zero, @@ -39,10 +39,10 @@ class WebSettingsSection extends StatelessWidget { children: [ if (title != null) Padding( - padding: EdgeInsetsDirectional.only( - bottom: 5 * scaleFactor, + padding: const EdgeInsetsDirectional.only( + bottom: constants.paddingSmall, start: 6, - top: 20 * scaleFactor, + top: constants.paddingSmall, ), child: title!, ), @@ -50,7 +50,7 @@ class WebSettingsSection extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), - elevation: 4, + elevation: constants.cardElevation, color: theme.themeData.settingsSectionBackground, child: buildTileList(), ), diff --git a/web/repertory/lib/widgets/ui_settings.dart b/web/repertory/lib/widgets/ui_settings.dart index aafde84..b6cdab9 100644 --- a/web/repertory/lib/widgets/ui_settings.dart +++ b/web/repertory/lib/widgets/ui_settings.dart @@ -94,7 +94,7 @@ class _UISettingsWidgetState extends State widget.settings, key, value, - Icons.person, + constants.iconUser, false, widget.showAdvanced, widget, @@ -109,11 +109,11 @@ class _UISettingsWidgetState extends State final titleStyle = theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, - color: scheme.onSurface, + color: scheme.primary, ); return SettingsList( - shrinkWrap: false, + shrinkWrap: true, platform: DevicePlatform.web, lightTheme: settingsTheme, darkTheme: settingsTheme,