From 47cac7e71ce81b891eea3c411134331001081226 Mon Sep 17 00:00:00 2001 From: "Scott E. Graves" Date: Fri, 5 Sep 2025 10:11:57 -0500 Subject: [PATCH] refactor ui --- web/repertory/lib/helpers.dart | 169 +----------- .../lib/screens/add_mount_screen.dart | 253 +++++++++--------- .../lib/screens/edit_mount_screen.dart | 43 ++- .../lib/screens/edit_settings_screen.dart | 49 ++-- web/repertory/lib/screens/home_screen.dart | 23 +- web/repertory/lib/settings.dart | 4 +- web/repertory/lib/widgets/app_dropdown.dart | 4 +- .../lib/widgets/app_icon_button_framed.dart | 60 +++++ web/repertory/lib/widgets/app_scaffold.dart | 181 +++++++++++++ .../lib/widgets/app_toggle_button_framed.dart | 37 +++ web/repertory/lib/widgets/mount_widget.dart | 97 +------ 11 files changed, 479 insertions(+), 441 deletions(-) create mode 100644 web/repertory/lib/widgets/app_icon_button_framed.dart create mode 100644 web/repertory/lib/widgets/app_scaffold.dart create mode 100644 web/repertory/lib/widgets/app_toggle_button_framed.dart diff --git a/web/repertory/lib/helpers.dart b/web/repertory/lib/helpers.dart index 7fbf1ca0..7d3f28e8 100644 --- a/web/repertory/lib/helpers.dart +++ b/web/repertory/lib/helpers.dart @@ -1,17 +1,12 @@ // helpers.dart -import 'dart:ui'; - import 'package:convert/convert.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -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/widgets/app_dropdown.dart'; -import 'package:repertory/widgets/aurora_sweep.dart'; import 'package:sodium_libs/sodium_libs.dart' show SecureKey, StringX; Future doShowDialog(BuildContext context, Widget child) => showDialog( @@ -419,7 +414,7 @@ Future editMountLocation( controller: controller, onChanged: (value) => setState(() => currentLocation = value), ) - : AppDropdownFormField( + : AppDropdown( labelOf: (s) => s, labelText: "Select drive", onChanged: (value) => setState(() => currentLocation = value), @@ -434,168 +429,6 @@ Future editMountLocation( ); } -Scaffold createCommonScaffold( - BuildContext context, - String title, - List children, { - Widget? advancedWidget, - Widget? floatingActionButton, - bool showBack = false, -}) { - final scheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - - return Scaffold( - body: SafeArea( - child: Stack( - children: [ - Container( - width: double.infinity, - height: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: constants.gradientColors, - ), - ), - ), - Consumer( - builder: (_, settings, _) => - AuroraSweep(enabled: settings.enableAnimations), - ), - Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), - child: Container(color: Colors.black.withValues(alpha: 0.06)), - ), - ), - Padding( - padding: const EdgeInsets.all(constants.padding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - if (!showBack) ...[ - SizedBox( - width: 40, - height: 40, - child: Image.asset( - 'assets/images/repertory.png', - fit: BoxFit.contain, - errorBuilder: (_, _, _) { - return Icon( - Icons.folder, - color: scheme.primary, - size: 32, - ); - }, - ), - ), - const SizedBox(width: constants.padding), - ], - if (showBack) ...[ - Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular( - constants.borderRadius, - ), - onTap: () => Navigator.of(context).pop(), - child: Ink( - width: 40, - height: 40, - decoration: BoxDecoration( - color: scheme.surface.withValues(alpha: 0.40), - borderRadius: BorderRadius.circular( - constants.borderRadius, - ), - border: Border.all( - color: scheme.outlineVariant.withValues( - alpha: 0.08, - ), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.22), - blurRadius: constants.borderRadius, - offset: Offset(0, constants.borderRadius), - ), - ], - ), - child: const Icon(Icons.arrow_back), - ), - ), - ), - const SizedBox(width: constants.padding), - ], - Expanded( - child: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w700, - letterSpacing: 0.2, - color: scheme.onSurface.withValues(alpha: 0.96), - ), - ), - ), - const SizedBox(width: constants.padding), - if (!showBack) ...[ - const Text("Auto-start"), - Consumer( - builder: (context, settings, _) { - return IconButton( - icon: Icon( - settings.autoStart - ? Icons.toggle_on - : Icons.toggle_off, - ), - color: settings.autoStart - ? scheme.primary - : scheme.onSurface.withValues(alpha: 0.70), - onPressed: () => - settings.setAutoStart(!settings.autoStart), - ); - }, - ), - IconButton( - tooltip: 'Settings', - icon: const Icon(Icons.settings), - onPressed: () { - Navigator.pushNamed(context, '/settings'); - }, - ), - const SizedBox(width: constants.padding), - ], - if (showBack && advancedWidget != null) ...[ - advancedWidget, - const SizedBox(width: constants.padding), - ], - Consumer( - builder: (context, auth, _) => IconButton( - tooltip: 'Log out', - icon: const Icon(Icons.logout), - onPressed: auth.logoff, - ), - ), - ], - ), - const SizedBox(height: constants.padding), - ...children, - ], - ), - ), - ], - ), - ), - floatingActionButton: floatingActionButton, - ); -} - InputDecoration createCommonDecoration( ColorScheme colorScheme, String label, { diff --git a/web/repertory/lib/screens/add_mount_screen.dart b/web/repertory/lib/screens/add_mount_screen.dart index 40df94c7..501affe8 100644 --- a/web/repertory/lib/screens/add_mount_screen.dart +++ b/web/repertory/lib/screens/add_mount_screen.dart @@ -11,6 +11,7 @@ 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_dropdown.dart'; +import 'package:repertory/widgets/app_scaffold.dart'; import 'package:repertory/widgets/mount_settings.dart'; class AddMountScreen extends StatefulWidget { @@ -45,148 +46,158 @@ class _AddMountScreenState extends State Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; - return createCommonScaffold(context, widget.title, [ - AppDropdownFormField( - constrainToIntrinsic: true, - isExpanded: false, - labelOf: (s) => s, - labelText: 'Provider Type', - onChanged: (mountType) { - _handleChange( - Provider.of(context, listen: false), - mountType ?? '', - ); - }, - prefixIcon: Icons.miscellaneous_services, - value: _mountType.isEmpty ? null : _mountType, - values: constants.providerTypeList, - widthMultiplier: 2.0, - ), - if (_mountType.isNotEmpty && _mountType != 'Remote') ...[ - const SizedBox(height: constants.padding), - TextField( - autofocus: true, - controller: _mountNameController, - keyboardType: TextInputType.text, - inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))], - onChanged: (_) => _handleChange( - Provider.of(context, listen: false), - _mountType, - ), - decoration: createCommonDecoration( - scheme, - 'Configuration Name', - hintText: 'Enter a unique name', - icon: Icons.drive_file_rename_outline, - ), + return AppScaffold( + title: widget.title, + showBack: true, + children: [ + AppDropdown( + constrainToIntrinsic: true, + isExpanded: false, + labelOf: (s) => s, + labelText: 'Provider Type', + onChanged: (mountType) { + _handleChange( + Provider.of(context, listen: false), + mountType ?? '', + ); + }, + prefixIcon: Icons.miscellaneous_services, + value: _mountType.isEmpty ? null : _mountType, + values: constants.providerTypeList, + widthMultiplier: 2.0, ), - ], - 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, + if (_mountType.isNotEmpty && _mountType != 'Remote') ...[ + const SizedBox(height: constants.padding), + TextField( + autofocus: true, + controller: _mountNameController, + keyboardType: TextInputType.text, + inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))], + onChanged: (_) => _handleChange( + Provider.of(context, listen: false), + _mountType, + ), + decoration: createCommonDecoration( + scheme, + 'Configuration Name', + hintText: 'Enter a unique name', + icon: Icons.drive_file_rename_outline, + ), + ), + ], + 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, + ), ), ), ), - ), - const SizedBox(height: constants.padding), - Row( - children: [ - IntrinsicWidth( - child: ElevatedButton.icon( - label: const Text('Test'), - icon: const Icon(Icons.check), - style: ElevatedButton.styleFrom( - backgroundColor: scheme.primary.withValues(alpha: 0.18), - foregroundColor: scheme.primary, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(constants.borderRadius), - side: BorderSide( - color: scheme.outlineVariant.withValues(alpha: 0.15), - width: 1, + const SizedBox(height: constants.padding), + Row( + children: [ + IntrinsicWidth( + child: ElevatedButton.icon( + label: const Text('Test'), + icon: const Icon(Icons.check), + style: ElevatedButton.styleFrom( + backgroundColor: scheme.primary.withValues(alpha: 0.18), + foregroundColor: scheme.primary, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + constants.borderRadius, + ), + side: BorderSide( + color: scheme.outlineVariant.withValues(alpha: 0.15), + width: 1, + ), ), ), + onPressed: _handleProviderTest, ), - onPressed: _handleProviderTest, ), - ), - const SizedBox(width: constants.padding), - IntrinsicWidth( - child: ElevatedButton.icon( - label: const Text('Add'), - icon: const Icon(Icons.add), - style: ElevatedButton.styleFrom( - backgroundColor: scheme.primary, - foregroundColor: scheme.onPrimary, - elevation: 8, - shadowColor: scheme.primary.withValues(alpha: 0.45), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(constants.borderRadius), + const SizedBox(width: constants.padding), + IntrinsicWidth( + child: ElevatedButton.icon( + label: const Text('Add'), + icon: const Icon(Icons.add), + style: ElevatedButton.styleFrom( + backgroundColor: scheme.primary, + foregroundColor: scheme.onPrimary, + elevation: 8, + shadowColor: scheme.primary.withValues(alpha: 0.45), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + constants.borderRadius, + ), + ), ), - ), - onPressed: () async { - final mountList = Provider.of( - context, - listen: false, - ); - - List failed = []; - if (!validateSettings(_settings[_mountType]!, failed)) { - for (var key in failed) { - displayErrorMessage( - context, - "Setting '$key' is not valid", - ); - } - return; - } - - if (mountList.hasConfigName(_mountNameController.text)) { - return displayErrorMessage( + onPressed: () async { + final mountList = Provider.of( context, - "Configuration name '${_mountNameController.text}' already exists", + listen: false, ); - } - if (_mountType == "Sia" || _mountType == "S3") { - final bucket = - _settings[_mountType]!["${_mountType}Config"]["Bucket"] - as String; - if (mountList.hasBucketName(_mountType, bucket)) { + List failed = []; + if (!validateSettings(_settings[_mountType]!, failed)) { + for (var key in failed) { + displayErrorMessage( + context, + "Setting '$key' is not valid", + ); + } + return; + } + + if (mountList.hasConfigName(_mountNameController.text)) { return displayErrorMessage( context, - "Bucket '$bucket' already exists", + "Configuration name '${_mountNameController.text}' already exists", ); } - } - final success = await mountList.add( - _mountType, - _mountType == 'Remote' - ? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}' - : _mountNameController.text, - _settings[_mountType]!, - ); + if (_mountType == "Sia" || _mountType == "S3") { + final bucket = + _settings[_mountType]!["${_mountType}Config"]["Bucket"] + as String; + if (mountList.hasBucketName(_mountType, bucket)) { + return displayErrorMessage( + context, + "Bucket '$bucket' already exists", + ); + } + } - if (!success || !context.mounted) return; + final success = await mountList.add( + _mountType, + _mountType == 'Remote' + ? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}' + : _mountNameController.text, + _settings[_mountType]!, + ); - Navigator.pop(context); - }, + if (!success || !context.mounted) return; + + Navigator.pop(context); + }, + ), ), - ), - ], - ), + ], + ), + ], ], - ], showBack: true); + ); } void _handleChange(Auth auth, String mountType) { diff --git a/web/repertory/lib/screens/edit_mount_screen.dart b/web/repertory/lib/screens/edit_mount_screen.dart index 3fc59653..7f5fabb1 100644 --- a/web/repertory/lib/screens/edit_mount_screen.dart +++ b/web/repertory/lib/screens/edit_mount_screen.dart @@ -3,9 +3,9 @@ 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/mount_settings.dart'; class EditMountScreen extends StatefulWidget { @@ -25,27 +25,8 @@ class _EditMountScreenState extends State Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - return createCommonScaffold( - context, - widget.title, - [ - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: constants.padding), - child: ClipRRect( - borderRadius: BorderRadius.circular(constants.borderRadius), - child: MountSettingsWidget( - mount: widget.mount, - settings: jsonDecode( - jsonEncode(widget.mount.mountConfig.settings), - ), - showAdvanced: _showAdvanced, - ), - ), - ), - ), - const SizedBox(height: constants.padding), - ], + return AppScaffold( + title: widget.title, showBack: true, advancedWidget: Row( mainAxisSize: MainAxisSize.min, @@ -67,6 +48,24 @@ class _EditMountScreenState extends State ), ], ), + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: constants.padding), + child: ClipRRect( + borderRadius: BorderRadius.circular(constants.borderRadius), + child: MountSettingsWidget( + mount: widget.mount, + settings: jsonDecode( + jsonEncode(widget.mount.mountConfig.settings), + ), + showAdvanced: _showAdvanced, + ), + ), + ), + ), + const SizedBox(height: constants.padding), + ], ); } } diff --git a/web/repertory/lib/screens/edit_settings_screen.dart b/web/repertory/lib/screens/edit_settings_screen.dart index ae010375..c90db254 100644 --- a/web/repertory/lib/screens/edit_settings_screen.dart +++ b/web/repertory/lib/screens/edit_settings_screen.dart @@ -9,6 +9,7 @@ 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/ui_settings.dart'; class EditSettingsScreen extends StatefulWidget { @@ -23,32 +24,36 @@ class _EditSettingsScreenState extends State with SafeSetState { @override Widget build(BuildContext context) { - return createCommonScaffold(context, widget.title, [ - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: constants.padding), - child: ClipRRect( - borderRadius: BorderRadius.circular(constants.borderRadius), - child: FutureBuilder>( - future: _grabSettings(), - initialData: const {}, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); - } + return AppScaffold( + title: widget.title, + showBack: true, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: constants.padding), + child: ClipRRect( + borderRadius: BorderRadius.circular(constants.borderRadius), + 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, - ); - }, + return UISettingsWidget( + origSettings: jsonDecode(jsonEncode(snapshot.requireData)), + settings: snapshot.requireData, + showAdvanced: false, + ); + }, + ), ), ), ), - ), - const SizedBox(height: constants.padding), - ], showBack: true); + const SizedBox(height: constants.padding), + ], + ); } Future> _grabSettings() async { diff --git a/web/repertory/lib/screens/home_screen.dart b/web/repertory/lib/screens/home_screen.dart index 23a81e73..d56e920a 100644 --- a/web/repertory/lib/screens/home_screen.dart +++ b/web/repertory/lib/screens/home_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:repertory/constants.dart' as constants; -import 'package:repertory/helpers.dart'; +import 'package:repertory/widgets/app_scaffold.dart'; import 'package:repertory/widgets/mount_list_widget.dart'; class HomeScreen extends StatefulWidget { @@ -18,17 +18,8 @@ class _HomeScreeState extends State { Widget build(context) { final scheme = Theme.of(context).colorScheme; - return createCommonScaffold( - context, - widget.title, - [ - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: constants.padding), - child: const MountListWidget(), - ), - ), - ], + return AppScaffold( + title: widget.title, floatingActionButton: Padding( padding: const EdgeInsets.all(constants.padding), child: Hero( @@ -74,6 +65,14 @@ class _HomeScreeState extends State { ), ), ), + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: constants.padding), + child: const MountListWidget(), + ), + ), + ], ); } } diff --git a/web/repertory/lib/settings.dart b/web/repertory/lib/settings.dart index 0d52bc1e..d1dc3f1d 100644 --- a/web/repertory/lib/settings.dart +++ b/web/repertory/lib/settings.dart @@ -57,7 +57,7 @@ void createIntListSetting( SettingsTile.navigation( title: createSettingTitle(context, key, description), leading: Icon(icon), - value: AppDropdownFormField( + value: AppDropdown( labelOf: (s) => s, constrainToIntrinsic: true, onChanged: (newValue) { @@ -310,7 +310,7 @@ void createStringListSetting( SettingsTile.navigation( title: createSettingTitle(context, key, description), leading: Icon(icon), - value: AppDropdownFormField( + value: AppDropdown( constrainToIntrinsic: true, labelOf: (s) => s, onChanged: (newValue) => setState(() => settings[key] = newValue), diff --git a/web/repertory/lib/widgets/app_dropdown.dart b/web/repertory/lib/widgets/app_dropdown.dart index 729cfad7..3ae675e4 100644 --- a/web/repertory/lib/widgets/app_dropdown.dart +++ b/web/repertory/lib/widgets/app_dropdown.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:repertory/constants.dart' as constants; import 'package:repertory/helpers.dart'; -class AppDropdownFormField extends StatelessWidget { - const AppDropdownFormField({ +class AppDropdown extends StatelessWidget { + const AppDropdown({ super.key, required this.labelOf, required this.values, diff --git a/web/repertory/lib/widgets/app_icon_button_framed.dart b/web/repertory/lib/widgets/app_icon_button_framed.dart new file mode 100644 index 00000000..a70735ab --- /dev/null +++ b/web/repertory/lib/widgets/app_icon_button_framed.dart @@ -0,0 +1,60 @@ +// app_icon_button_framed.dart + +import 'package:flutter/material.dart'; +import 'package:repertory/constants.dart' as constants; + +class AppIconButtonFramed extends StatelessWidget { + final IconData icon; + final VoidCallback? onTap; + final Color? iconColor; + + const AppIconButtonFramed({ + super.key, + required this.icon, + this.onTap, + this.iconColor, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final radius = BorderRadius.circular(constants.borderRadiusSmall); + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: radius, + child: Ink( + width: 46, + height: 46, + decoration: BoxDecoration( + color: scheme.primary.withValues(alpha: 0.12), + borderRadius: radius, + border: Border.all( + color: scheme.outlineVariant.withValues(alpha: 0.08), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.24), + blurRadius: constants.borderRadiusSmall / 2.0, + offset: const Offset(0, 5), + ), + ], + ), + child: Center( + child: Transform.scale( + scale: 0.90, + child: Icon( + icon, + color: iconColor ?? scheme.onSurface.withValues(alpha: 0.92), + size: 32.0, + ), + ), + ), + ), + ), + ); + } +} diff --git a/web/repertory/lib/widgets/app_scaffold.dart b/web/repertory/lib/widgets/app_scaffold.dart new file mode 100644 index 00000000..fd50cb99 --- /dev/null +++ b/web/repertory/lib/widgets/app_scaffold.dart @@ -0,0 +1,181 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +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/widgets/aurora_sweep.dart'; + +class AppScaffold extends StatelessWidget { + const AppScaffold({ + super.key, + required this.children, + required this.title, + this.advancedWidget, + this.floatingActionButton, + this.showBack = false, + }); + + final List children; + final String title; + final Widget? advancedWidget; + final Widget? floatingActionButton; + final bool showBack; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Scaffold( + body: SafeArea( + child: Stack( + children: [ + Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: constants.gradientColors, + ), + ), + ), + Consumer( + builder: (_, settings, _) => + AuroraSweep(enabled: settings.enableAnimations), + ), + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), + child: Container(color: Colors.black.withValues(alpha: 0.06)), + ), + ), + Padding( + padding: const EdgeInsets.all(constants.padding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + if (!showBack) ...[ + SizedBox( + width: 40, + height: 40, + child: Image.asset( + 'assets/images/repertory.png', + fit: BoxFit.contain, + errorBuilder: (_, _, _) { + return Icon( + Icons.folder, + color: scheme.primary, + size: 32, + ); + }, + ), + ), + const SizedBox(width: constants.padding), + ], + if (showBack) ...[ + Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular( + constants.borderRadius, + ), + onTap: () => Navigator.of(context).pop(), + child: Ink( + width: 40, + height: 40, + decoration: BoxDecoration( + color: scheme.surface.withValues(alpha: 0.40), + borderRadius: BorderRadius.circular( + constants.borderRadius, + ), + border: Border.all( + color: scheme.outlineVariant.withValues( + alpha: 0.08, + ), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.22), + blurRadius: constants.borderRadius, + offset: Offset(0, constants.borderRadius), + ), + ], + ), + child: const Icon(Icons.arrow_back), + ), + ), + ), + const SizedBox(width: constants.padding), + ], + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.2, + color: scheme.onSurface.withValues(alpha: 0.96), + ), + ), + ), + const SizedBox(width: constants.padding), + if (!showBack) ...[ + const Text("Auto-start"), + Consumer( + builder: (context, settings, _) { + return IconButton( + icon: Icon( + settings.autoStart + ? Icons.toggle_on + : Icons.toggle_off, + ), + color: settings.autoStart + ? scheme.primary + : scheme.onSurface.withValues(alpha: 0.70), + onPressed: () => + settings.setAutoStart(!settings.autoStart), + ); + }, + ), + IconButton( + tooltip: 'Settings', + icon: const Icon(Icons.settings), + onPressed: () { + Navigator.pushNamed(context, '/settings'); + }, + ), + const SizedBox(width: constants.padding), + ], + if (showBack && advancedWidget != null) ...[ + advancedWidget!, + const SizedBox(width: constants.padding), + ], + Consumer( + builder: (context, auth, _) => IconButton( + tooltip: 'Log out', + icon: const Icon(Icons.logout), + onPressed: auth.logoff, + ), + ), + ], + ), + const SizedBox(height: constants.padding), + ...children, + ], + ), + ), + ], + ), + ), + floatingActionButton: floatingActionButton, + ); + } +} diff --git a/web/repertory/lib/widgets/app_toggle_button_framed.dart b/web/repertory/lib/widgets/app_toggle_button_framed.dart new file mode 100644 index 00000000..99242617 --- /dev/null +++ b/web/repertory/lib/widgets/app_toggle_button_framed.dart @@ -0,0 +1,37 @@ +// app_toggle_button_framed.dart + +import 'package:flutter/material.dart'; +import 'package:repertory/widgets/app_icon_button_framed.dart'; + +class AppToggleButtonFramed extends StatelessWidget { + final bool? mounted; + final VoidCallback? onPressed; + + const AppToggleButtonFramed({ + super.key, + required this.mounted, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final bool isOn = mounted ?? false; + + IconData icon = Icons.hourglass_top; + Color iconColor = scheme.onSurface.withValues(alpha: 0.60); + + if (mounted != null) { + icon = isOn ? Icons.toggle_on : Icons.toggle_off; + iconColor = isOn + ? scheme.primary + : scheme.onSurface.withValues(alpha: 0.55); + } + + return AppIconButtonFramed( + icon: icon, + iconColor: iconColor, + onTap: mounted == null ? null : onPressed, + ); + } +} diff --git a/web/repertory/lib/widgets/mount_widget.dart b/web/repertory/lib/widgets/mount_widget.dart index 102eac6c..d928eaaf 100644 --- a/web/repertory/lib/widgets/mount_widget.dart +++ b/web/repertory/lib/widgets/mount_widget.dart @@ -7,6 +7,8 @@ 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_icon_button_framed.dart'; +import 'package:repertory/widgets/app_toggle_button_framed.dart'; class MountWidget extends StatefulWidget { const MountWidget({super.key}); @@ -82,7 +84,8 @@ class _MountWidgetState extends State Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - _GearBadge( + AppIconButtonFramed( + icon: Icons.settings, onTap: () { Navigator.pushNamed( context, @@ -107,7 +110,7 @@ class _MountWidgetState extends State ], ), ), - _ToggleFramed( + AppToggleButtonFramed( mounted: mount.mounted, onPressed: _createMountHandler(context, mount), ), @@ -281,96 +284,6 @@ class _MountWidgetState extends State } } -class _FramedBox extends StatelessWidget { - final IconData icon; - final VoidCallback? onTap; - final Color? iconColor; - - const _FramedBox({required this.icon, this.onTap, this.iconColor}); - - @override - Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; - final radius = BorderRadius.circular(constants.borderRadiusSmall); - - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - borderRadius: radius, - child: Ink( - width: 46, - height: 46, - decoration: BoxDecoration( - color: scheme.primary.withValues(alpha: 0.12), - borderRadius: radius, - border: Border.all( - color: scheme.outlineVariant.withValues(alpha: 0.08), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.24), - blurRadius: constants.borderRadiusSmall / 2.0, - offset: const Offset(0, 5), - ), - ], - ), - child: Center( - child: Transform.scale( - scale: 0.90, - child: Icon( - icon, - color: iconColor ?? scheme.onSurface.withValues(alpha: 0.92), - size: 32.0, - ), - ), - ), - ), - ), - ); - } -} - -class _GearBadge extends StatelessWidget { - final VoidCallback onTap; - const _GearBadge({required this.onTap}); - - @override - Widget build(BuildContext context) { - return _FramedBox(icon: Icons.settings, onTap: onTap); - } -} - -class _ToggleFramed extends StatelessWidget { - final bool? mounted; - final VoidCallback? onPressed; - - const _ToggleFramed({required this.mounted, required this.onPressed}); - - @override - Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; - final bool isOn = mounted ?? false; - - IconData icon = Icons.hourglass_top; - Color iconColor = scheme.onSurface.withValues(alpha: 0.60); - - if (mounted != null) { - icon = isOn ? Icons.toggle_on : Icons.toggle_off; - iconColor = isOn - ? scheme.primary - : scheme.onSurface.withValues(alpha: 0.55); - } - - return _FramedBox( - icon: icon, - iconColor: iconColor, - onTap: mounted == null ? null : onPressed, - ); - } -} - class _EditPathButton extends StatelessWidget { final bool enabled; final VoidCallback onPressed;