This commit is contained in:
		| @@ -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<String?> editMountLocation( | ||||
|                   controller: controller, | ||||
|                   onChanged: (value) => setState(() => currentLocation = value), | ||||
|                 ) | ||||
|               : AppDropdownFormField<String>( | ||||
|               : AppDropdown<String>( | ||||
|                   labelOf: (s) => s, | ||||
|                   labelText: "Select drive", | ||||
|                   onChanged: (value) => setState(() => currentLocation = value), | ||||
| @@ -434,168 +429,6 @@ Future<String?> editMountLocation( | ||||
|   ); | ||||
| } | ||||
|  | ||||
| Scaffold createCommonScaffold( | ||||
|   BuildContext context, | ||||
|   String title, | ||||
|   List<Widget> 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<Settings>( | ||||
|             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<Settings>( | ||||
|                         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<Auth>( | ||||
|                       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, { | ||||
|   | ||||
| @@ -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<AddMountScreen> | ||||
|   Widget build(BuildContext context) { | ||||
|     final scheme = Theme.of(context).colorScheme; | ||||
|  | ||||
|     return createCommonScaffold(context, widget.title, [ | ||||
|       AppDropdownFormField<String>( | ||||
|         constrainToIntrinsic: true, | ||||
|         isExpanded: false, | ||||
|         labelOf: (s) => s, | ||||
|         labelText: 'Provider Type', | ||||
|         onChanged: (mountType) { | ||||
|           _handleChange( | ||||
|             Provider.of<Auth>(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<Auth>(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<String>( | ||||
|           constrainToIntrinsic: true, | ||||
|           isExpanded: false, | ||||
|           labelOf: (s) => s, | ||||
|           labelText: 'Provider Type', | ||||
|           onChanged: (mountType) { | ||||
|             _handleChange( | ||||
|               Provider.of<Auth>(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<Auth>(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<MountList>( | ||||
|                     context, | ||||
|                     listen: false, | ||||
|                   ); | ||||
|  | ||||
|                   List<String> 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<MountList>( | ||||
|                       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<String> 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) { | ||||
|   | ||||
| @@ -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<EditMountScreen> | ||||
|   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<EditMountScreen> | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       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), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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<EditSettingsScreen> | ||||
|     with SafeSetState<EditSettingsScreen> { | ||||
|   @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<Map<String, dynamic>>( | ||||
|               future: _grabSettings(), | ||||
|               initialData: const <String, dynamic>{}, | ||||
|               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<Map<String, dynamic>>( | ||||
|                 future: _grabSettings(), | ||||
|                 initialData: const <String, dynamic>{}, | ||||
|                 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<Map<String, dynamic>> _grabSettings() async { | ||||
|   | ||||
| @@ -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<HomeScreen> { | ||||
|   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<HomeScreen> { | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|       children: [ | ||||
|         Expanded( | ||||
|           child: Padding( | ||||
|             padding: const EdgeInsets.symmetric(horizontal: constants.padding), | ||||
|             child: const MountListWidget(), | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -57,7 +57,7 @@ void createIntListSetting( | ||||
|       SettingsTile.navigation( | ||||
|         title: createSettingTitle(context, key, description), | ||||
|         leading: Icon(icon), | ||||
|         value: AppDropdownFormField<String>( | ||||
|         value: AppDropdown<String>( | ||||
|           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<String>( | ||||
|         value: AppDropdown<String>( | ||||
|           constrainToIntrinsic: true, | ||||
|           labelOf: (s) => s, | ||||
|           onChanged: (newValue) => setState(() => settings[key] = newValue), | ||||
|   | ||||
| @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:repertory/constants.dart' as constants; | ||||
| import 'package:repertory/helpers.dart'; | ||||
|  | ||||
| class AppDropdownFormField<T> extends StatelessWidget { | ||||
|   const AppDropdownFormField({ | ||||
| class AppDropdown<T> extends StatelessWidget { | ||||
|   const AppDropdown({ | ||||
|     super.key, | ||||
|     required this.labelOf, | ||||
|     required this.values, | ||||
|   | ||||
							
								
								
									
										60
									
								
								web/repertory/lib/widgets/app_icon_button_framed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								web/repertory/lib/widgets/app_icon_button_framed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										181
									
								
								web/repertory/lib/widgets/app_scaffold.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								web/repertory/lib/widgets/app_scaffold.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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<Widget> 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<Settings>( | ||||
|               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<Settings>( | ||||
|                           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<Auth>( | ||||
|                         builder: (context, auth, _) => IconButton( | ||||
|                           tooltip: 'Log out', | ||||
|                           icon: const Icon(Icons.logout), | ||||
|                           onPressed: auth.logoff, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   const SizedBox(height: constants.padding), | ||||
|                   ...children, | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|       floatingActionButton: floatingActionButton, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										37
									
								
								web/repertory/lib/widgets/app_toggle_button_framed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/repertory/lib/widgets/app_toggle_button_framed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -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<MountWidget> | ||||
|                       Row( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           _GearBadge( | ||||
|                           AppIconButtonFramed( | ||||
|                             icon: Icons.settings, | ||||
|                             onTap: () { | ||||
|                               Navigator.pushNamed( | ||||
|                                 context, | ||||
| @@ -107,7 +110,7 @@ class _MountWidgetState extends State<MountWidget> | ||||
|                               ], | ||||
|                             ), | ||||
|                           ), | ||||
|                           _ToggleFramed( | ||||
|                           AppToggleButtonFramed( | ||||
|                             mounted: mount.mounted, | ||||
|                             onPressed: _createMountHandler(context, mount), | ||||
|                           ), | ||||
| @@ -281,96 +284,6 @@ class _MountWidgetState extends State<MountWidget> | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user