💄 More transparency
This commit is contained in:
		| @@ -4,7 +4,6 @@ import 'package:get/get.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:solian/widgets/root_container.dart'; | ||||
| import 'package:solian/widgets/sized_container.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| @@ -16,132 +15,130 @@ class AboutScreen extends StatelessWidget { | ||||
|     const denseButtonStyle = | ||||
|         ButtonStyle(visualDensity: VisualDensity(vertical: -4)); | ||||
|  | ||||
|     return RootContainer( | ||||
|       child: SizedBox( | ||||
|         width: double.infinity, | ||||
|         child: Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: [ | ||||
|             ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|               child: Image.asset('assets/logo.png', width: 120, height: 120), | ||||
|             ), | ||||
|             const Gap(8), | ||||
|             Text( | ||||
|               'Solian', | ||||
|               style: Theme.of(context).textTheme.headlineMedium, | ||||
|             ), | ||||
|             const Text( | ||||
|               'The Solar Network', | ||||
|               style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||
|             ), | ||||
|             const Gap(8), | ||||
|             FutureBuilder( | ||||
|               future: PackageInfo.fromPlatform(), | ||||
|               builder: (context, snapshot) { | ||||
|                 if (!snapshot.hasData) { | ||||
|                   return const SizedBox.shrink(); | ||||
|                 } | ||||
|     return SizedBox( | ||||
|       width: double.infinity, | ||||
|       child: Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: [ | ||||
|           ClipRRect( | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|             child: Image.asset('assets/logo.png', width: 120, height: 120), | ||||
|           ), | ||||
|           const Gap(8), | ||||
|           Text( | ||||
|             'Solian', | ||||
|             style: Theme.of(context).textTheme.headlineMedium, | ||||
|           ), | ||||
|           const Text( | ||||
|             'The Solar Network', | ||||
|             style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||
|           ), | ||||
|           const Gap(8), | ||||
|           FutureBuilder( | ||||
|             future: PackageInfo.fromPlatform(), | ||||
|             builder: (context, snapshot) { | ||||
|               if (!snapshot.hasData) { | ||||
|                 return const SizedBox.shrink(); | ||||
|               } | ||||
|  | ||||
|                 return Text( | ||||
|                   'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}', | ||||
|                   style: const TextStyle(fontFamily: 'monospace'), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|             Text('Copyright © ${DateTime.now().year} Solsynth LLC'), | ||||
|             const Gap(16), | ||||
|             CenteredContainer( | ||||
|               maxWidth: 280, | ||||
|               child: Wrap( | ||||
|                 spacing: 4, | ||||
|                 runSpacing: 4, | ||||
|                 alignment: WrapAlignment.center, | ||||
|                 children: [ | ||||
|                   TextButton( | ||||
|                     style: denseButtonStyle, | ||||
|                     child: Text('appDetails'.tr), | ||||
|                     onPressed: () async { | ||||
|                       final info = await PackageInfo.fromPlatform(); | ||||
|               return Text( | ||||
|                 'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}', | ||||
|                 style: const TextStyle(fontFamily: 'monospace'), | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|           Text('Copyright © ${DateTime.now().year} Solsynth LLC'), | ||||
|           const Gap(16), | ||||
|           CenteredContainer( | ||||
|             maxWidth: 280, | ||||
|             child: Wrap( | ||||
|               spacing: 4, | ||||
|               runSpacing: 4, | ||||
|               alignment: WrapAlignment.center, | ||||
|               children: [ | ||||
|                 TextButton( | ||||
|                   style: denseButtonStyle, | ||||
|                   child: Text('appDetails'.tr), | ||||
|                   onPressed: () async { | ||||
|                     final info = await PackageInfo.fromPlatform(); | ||||
|  | ||||
|                       showAboutDialog( | ||||
|                         context: context, | ||||
|                         applicationVersion: | ||||
|                             '${info.version} (${info.buildNumber})', | ||||
|                         applicationLegalese: | ||||
|                             'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', | ||||
|                         applicationIcon: ClipRRect( | ||||
|                           borderRadius: | ||||
|                               const BorderRadius.all(Radius.circular(16)), | ||||
|                           child: Image.asset('assets/logo.png', | ||||
|                               width: 60, height: 60), | ||||
|                         ), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                   TextButton( | ||||
|                     style: denseButtonStyle, | ||||
|                     child: Text('projectWebsite'.tr), | ||||
|                     onPressed: () { | ||||
|                       launchUrlString( | ||||
|                           'https://solsynth.dev/products/solar-network'); | ||||
|                     }, | ||||
|                   ), | ||||
|                   TextButton( | ||||
|                     style: denseButtonStyle, | ||||
|                     child: Text('termRelated'.tr), | ||||
|                     onPressed: () { | ||||
|                       launchUrlString('https://solsynth.dev/terms'); | ||||
|                     }, | ||||
|                   ), | ||||
|                   TextButton( | ||||
|                     style: denseButtonStyle, | ||||
|                     child: Text('serviceStatus'.tr), | ||||
|                     onPressed: () { | ||||
|                       launchUrlString('https://status.solsynth.dev'); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|                     showAboutDialog( | ||||
|                       context: context, | ||||
|                       applicationVersion: | ||||
|                           '${info.version} (${info.buildNumber})', | ||||
|                       applicationLegalese: | ||||
|                           'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', | ||||
|                       applicationIcon: ClipRRect( | ||||
|                         borderRadius: | ||||
|                             const BorderRadius.all(Radius.circular(16)), | ||||
|                         child: Image.asset('assets/logo.png', | ||||
|                             width: 60, height: 60), | ||||
|                       ), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|                 TextButton( | ||||
|                   style: denseButtonStyle, | ||||
|                   child: Text('projectWebsite'.tr), | ||||
|                   onPressed: () { | ||||
|                     launchUrlString( | ||||
|                         'https://solsynth.dev/products/solar-network'); | ||||
|                   }, | ||||
|                 ), | ||||
|                 TextButton( | ||||
|                   style: denseButtonStyle, | ||||
|                   child: Text('termRelated'.tr), | ||||
|                   onPressed: () { | ||||
|                     launchUrlString('https://solsynth.dev/terms'); | ||||
|                   }, | ||||
|                 ), | ||||
|                 TextButton( | ||||
|                   style: denseButtonStyle, | ||||
|                   child: Text('serviceStatus'.tr), | ||||
|                   onPressed: () { | ||||
|                     launchUrlString('https://status.solsynth.dev'); | ||||
|                   }, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             const Gap(16), | ||||
|             const Text( | ||||
|               'Open-sourced under AGPLv3', | ||||
|               style: TextStyle( | ||||
|           ), | ||||
|           const Gap(16), | ||||
|           const Text( | ||||
|             'Open-sourced under AGPLv3', | ||||
|             style: TextStyle( | ||||
|               fontWeight: FontWeight.w300, | ||||
|               fontSize: 12, | ||||
|             ), | ||||
|           ), | ||||
|           FutureBuilder( | ||||
|             future: SharedPreferences.getInstance(), | ||||
|             builder: (context, snapshot) { | ||||
|               const textStyle = TextStyle( | ||||
|                 fontWeight: FontWeight.w300, | ||||
|                 fontSize: 12, | ||||
|               ), | ||||
|             ), | ||||
|             FutureBuilder( | ||||
|               future: SharedPreferences.getInstance(), | ||||
|               builder: (context, snapshot) { | ||||
|                 const textStyle = TextStyle( | ||||
|                   fontWeight: FontWeight.w300, | ||||
|                   fontSize: 12, | ||||
|               ); | ||||
|               if (!snapshot.hasData || | ||||
|                   !snapshot.data!.containsKey('first_boot_time')) { | ||||
|                 return Text( | ||||
|                   'firstBootTime'.trParams({'time': 'unknown'.tr}), | ||||
|                   style: textStyle, | ||||
|                 ); | ||||
|                 if (!snapshot.hasData || | ||||
|                     !snapshot.data!.containsKey('first_boot_time')) { | ||||
|                   return Text( | ||||
|                     'firstBootTime'.trParams({'time': 'unknown'.tr}), | ||||
|                     style: textStyle, | ||||
|                   ); | ||||
|                 } else { | ||||
|                   return Text( | ||||
|                     'firstBootTime'.trParams({ | ||||
|                       'time': DateFormat('yyyy-MM-dd').format( | ||||
|                         DateTime.tryParse( | ||||
|                               snapshot.data!.getString('first_boot_time')!, | ||||
|                             )?.toLocal() ?? | ||||
|                             DateTime.now(), | ||||
|                       ), | ||||
|                     }), | ||||
|                     style: textStyle, | ||||
|                   ); | ||||
|                 } | ||||
|               }, | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|               } else { | ||||
|                 return Text( | ||||
|                   'firstBootTime'.trParams({ | ||||
|                     'time': DateFormat('yyyy-MM-dd').format( | ||||
|                       DateTime.tryParse( | ||||
|                             snapshot.data!.getString('first_boot_time')!, | ||||
|                           )?.toLocal() ?? | ||||
|                           DateTime.now(), | ||||
|                     ), | ||||
|                   }), | ||||
|                   style: textStyle, | ||||
|                 ); | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import 'package:solian/providers/account_status.dart'; | ||||
| import 'package:solian/providers/relation.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:solian/widgets/account/account_heading.dart'; | ||||
| import 'package:solian/widgets/root_container.dart'; | ||||
| import 'package:solian/widgets/sized_container.dart'; | ||||
| import 'package:badges/badges.dart' as badges; | ||||
|  | ||||
| @@ -50,112 +49,110 @@ class _AccountScreenState extends State<AccountScreen> { | ||||
|  | ||||
|     final AuthProvider auth = Get.find(); | ||||
|  | ||||
|     return RootContainer( | ||||
|       child: SafeArea( | ||||
|         child: Obx(() { | ||||
|           if (auth.isAuthorized.isFalse) { | ||||
|             return Center( | ||||
|               child: Column( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 children: [ | ||||
|                   _ActionCard( | ||||
|                     icon: Icon( | ||||
|                       Icons.login, | ||||
|                       color: Theme.of(context).colorScheme.onPrimary, | ||||
|                     ), | ||||
|                     title: 'signin'.tr, | ||||
|                     caption: 'signinCaption'.tr, | ||||
|                     onTap: () { | ||||
|                       AppRouter.instance.pushNamed('signin').then((val) async { | ||||
|                         if (val == true) { | ||||
|                           await auth.refreshUserProfile(); | ||||
|                         } | ||||
|                       }); | ||||
|                     }, | ||||
|                   ), | ||||
|                   _ActionCard( | ||||
|                     icon: Icon( | ||||
|                       Icons.add, | ||||
|                       color: Theme.of(context).colorScheme.onPrimary, | ||||
|                     ), | ||||
|                     title: 'signup'.tr, | ||||
|                     caption: 'signupCaption'.tr, | ||||
|                     onTap: () { | ||||
|                       AppRouter.instance.pushNamed('signup').then((_) { | ||||
|                         setState(() {}); | ||||
|                       }); | ||||
|                     }, | ||||
|                   ), | ||||
|                   const Gap(4), | ||||
|                   TextButton( | ||||
|                     style: const ButtonStyle( | ||||
|                       visualDensity: VisualDensity( | ||||
|                         horizontal: -4, | ||||
|                         vertical: -2, | ||||
|                       ), | ||||
|                     ), | ||||
|                     onPressed: () { | ||||
|                       AppRouter.instance.pushNamed('settings'); | ||||
|                     }, | ||||
|                     child: Text('settings'.tr), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ); | ||||
|           } | ||||
|  | ||||
|           return CenteredContainer( | ||||
|             child: ListView( | ||||
|     return SafeArea( | ||||
|       child: Obx(() { | ||||
|         if (auth.isAuthorized.isFalse) { | ||||
|           return Center( | ||||
|             child: Column( | ||||
|               mainAxisSize: MainAxisSize.min, | ||||
|               children: [ | ||||
|                 if (auth.userProfile.value != null) | ||||
|                   const AccountHeading().paddingOnly(bottom: 8, top: 16), | ||||
|                 ...(actionItems.map( | ||||
|                   (x) => ListTile( | ||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 34), | ||||
|                     leading: x.$1, | ||||
|                     title: Text(x.$2), | ||||
|                     onTap: () { | ||||
|                       AppRouter.instance | ||||
|                           .pushNamed(x.$3) | ||||
|                           .then((_) => setState(() {})); | ||||
|                     }, | ||||
|                 _ActionCard( | ||||
|                   icon: Icon( | ||||
|                     Icons.login, | ||||
|                     color: Theme.of(context).colorScheme.onPrimary, | ||||
|                   ), | ||||
|                 )), | ||||
|                 const Divider(thickness: 0.3, height: 1) | ||||
|                     .paddingSymmetric(vertical: 4), | ||||
|                 ListTile( | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 34), | ||||
|                   leading: const Icon(Icons.settings), | ||||
|                   title: Text('settings'.tr), | ||||
|                   title: 'signin'.tr, | ||||
|                   caption: 'signinCaption'.tr, | ||||
|                   onTap: () { | ||||
|                     AppRouter.instance.pushNamed('settings'); | ||||
|                     AppRouter.instance.pushNamed('signin').then((val) async { | ||||
|                       if (val == true) { | ||||
|                         await auth.refreshUserProfile(); | ||||
|                       } | ||||
|                     }); | ||||
|                   }, | ||||
|                 ), | ||||
|                 if (auth.isAuthorized.value) | ||||
|                   ListTile( | ||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 34), | ||||
|                     leading: const Icon(Icons.edit_notifications), | ||||
|                     title: Text('notificationPreferences'.tr), | ||||
|                     onTap: () { | ||||
|                       AppRouter.instance.pushNamed('notificationPreferences'); | ||||
|                     }, | ||||
|                 _ActionCard( | ||||
|                   icon: Icon( | ||||
|                     Icons.add, | ||||
|                     color: Theme.of(context).colorScheme.onPrimary, | ||||
|                   ), | ||||
|                 const Divider(thickness: 0.3, height: 1) | ||||
|                     .paddingSymmetric(vertical: 4), | ||||
|                 ListTile( | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 34), | ||||
|                   leading: const Icon(Icons.logout), | ||||
|                   title: Text('signout'.tr), | ||||
|                   title: 'signup'.tr, | ||||
|                   caption: 'signupCaption'.tr, | ||||
|                   onTap: () { | ||||
|                     auth.signout(); | ||||
|                     setState(() {}); | ||||
|                     AppRouter.instance.pushNamed('signup').then((_) { | ||||
|                       setState(() {}); | ||||
|                     }); | ||||
|                   }, | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 TextButton( | ||||
|                   style: const ButtonStyle( | ||||
|                     visualDensity: VisualDensity( | ||||
|                       horizontal: -4, | ||||
|                       vertical: -2, | ||||
|                     ), | ||||
|                   ), | ||||
|                   onPressed: () { | ||||
|                     AppRouter.instance.pushNamed('settings'); | ||||
|                   }, | ||||
|                   child: Text('settings'.tr), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ); | ||||
|         }), | ||||
|       ), | ||||
|         } | ||||
|  | ||||
|         return CenteredContainer( | ||||
|           child: ListView( | ||||
|             children: [ | ||||
|               if (auth.userProfile.value != null) | ||||
|                 const AccountHeading().paddingOnly(bottom: 8, top: 16), | ||||
|               ...(actionItems.map( | ||||
|                 (x) => ListTile( | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 34), | ||||
|                   leading: x.$1, | ||||
|                   title: Text(x.$2), | ||||
|                   onTap: () { | ||||
|                     AppRouter.instance | ||||
|                         .pushNamed(x.$3) | ||||
|                         .then((_) => setState(() {})); | ||||
|                   }, | ||||
|                 ), | ||||
|               )), | ||||
|               const Divider(thickness: 0.3, height: 1) | ||||
|                   .paddingSymmetric(vertical: 4), | ||||
|               ListTile( | ||||
|                 contentPadding: const EdgeInsets.symmetric(horizontal: 34), | ||||
|                 leading: const Icon(Icons.settings), | ||||
|                 title: Text('settings'.tr), | ||||
|                 onTap: () { | ||||
|                   AppRouter.instance.pushNamed('settings'); | ||||
|                 }, | ||||
|               ), | ||||
|               if (auth.isAuthorized.value) | ||||
|                 ListTile( | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 34), | ||||
|                   leading: const Icon(Icons.edit_notifications), | ||||
|                   title: Text('notificationPreferences'.tr), | ||||
|                   onTap: () { | ||||
|                     AppRouter.instance.pushNamed('notificationPreferences'); | ||||
|                   }, | ||||
|                 ), | ||||
|               const Divider(thickness: 0.3, height: 1) | ||||
|                   .paddingSymmetric(vertical: 4), | ||||
|               ListTile( | ||||
|                 contentPadding: const EdgeInsets.symmetric(horizontal: 34), | ||||
|                 leading: const Icon(Icons.logout), | ||||
|                 title: Text('signout'.tr), | ||||
|                 onTap: () { | ||||
|                   auth.signout(); | ||||
|                   setState(() {}); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ); | ||||
|       }), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:solian/exceptions/request.dart'; | ||||
| import 'package:solian/exts.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/widgets/root_container.dart'; | ||||
|  | ||||
| class NotificationPreferencesScreen extends StatefulWidget { | ||||
|   const NotificationPreferencesScreen({super.key}); | ||||
| @@ -75,44 +74,42 @@ class _NotificationPreferencesScreenState | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return RootContainer( | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           if (_isBusy) const LinearProgressIndicator().animate().scaleX(), | ||||
|           ListTile( | ||||
|             tileColor: Theme.of(context).colorScheme.surfaceContainer, | ||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|             leading: const Icon(Icons.save), | ||||
|             title: Text('save'.tr), | ||||
|             enabled: !_isBusy, | ||||
|             onTap: () { | ||||
|               _savePreferences(); | ||||
|     return Column( | ||||
|       children: [ | ||||
|         if (_isBusy) const LinearProgressIndicator().animate().scaleX(), | ||||
|         ListTile( | ||||
|           tileColor: Theme.of(context).colorScheme.surfaceContainer, | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|           leading: const Icon(Icons.save), | ||||
|           title: Text('save'.tr), | ||||
|           enabled: !_isBusy, | ||||
|           onTap: () { | ||||
|             _savePreferences(); | ||||
|           }, | ||||
|         ), | ||||
|         Expanded( | ||||
|           child: ListView.builder( | ||||
|             itemCount: _topicMap.length, | ||||
|             itemBuilder: (context, index) { | ||||
|               final element = _topicMap.entries.elementAt(index); | ||||
|               return CheckboxListTile( | ||||
|                 title: Text(element.value), | ||||
|                 subtitle: Text( | ||||
|                   element.key, | ||||
|                   style: GoogleFonts.robotoMono(fontSize: 12), | ||||
|                 ), | ||||
|                 value: _config[element.key] ?? true, | ||||
|                 contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                 onChanged: (value) { | ||||
|                   setState(() { | ||||
|                     _config[element.key] = value ?? false; | ||||
|                   }); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|           Expanded( | ||||
|             child: ListView.builder( | ||||
|               itemCount: _topicMap.length, | ||||
|               itemBuilder: (context, index) { | ||||
|                 final element = _topicMap.entries.elementAt(index); | ||||
|                 return CheckboxListTile( | ||||
|                   title: Text(element.value), | ||||
|                   subtitle: Text( | ||||
|                     element.key, | ||||
|                     style: GoogleFonts.robotoMono(fontSize: 12), | ||||
|                   ), | ||||
|                   value: _config[element.key] ?? true, | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                   onChanged: (value) { | ||||
|                     setState(() { | ||||
|                       _config[element.key] = value ?? false; | ||||
|                     }); | ||||
|                   }, | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -187,163 +187,161 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|   Widget build(BuildContext context) { | ||||
|     const double padding = 32; | ||||
|  | ||||
|     return RootContainer( | ||||
|       child: ListView( | ||||
|         children: [ | ||||
|           if (_isBusy) const LinearProgressIndicator().animate().scaleX(), | ||||
|           const Gap(24), | ||||
|           Stack( | ||||
|             children: [ | ||||
|               AccountAvatar(content: _avatar, radius: 40), | ||||
|               Positioned( | ||||
|                 bottom: 0, | ||||
|                 left: 40, | ||||
|                 child: FloatingActionButton.small( | ||||
|                   heroTag: const Key('avatar-editor'), | ||||
|                   onPressed: () => _editImage('avatar'), | ||||
|                   child: const Icon( | ||||
|                     Icons.camera, | ||||
|                   ), | ||||
|     return ListView( | ||||
|       children: [ | ||||
|         if (_isBusy) const LinearProgressIndicator().animate().scaleX(), | ||||
|         const Gap(24), | ||||
|         Stack( | ||||
|           children: [ | ||||
|             AccountAvatar(content: _avatar, radius: 40), | ||||
|             Positioned( | ||||
|               bottom: 0, | ||||
|               left: 40, | ||||
|               child: FloatingActionButton.small( | ||||
|                 heroTag: const Key('avatar-editor'), | ||||
|                 onPressed: () => _editImage('avatar'), | ||||
|                 child: const Icon( | ||||
|                   Icons.camera, | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ).paddingSymmetric(horizontal: padding), | ||||
|           const Gap(16), | ||||
|           Stack( | ||||
|             children: [ | ||||
|               ClipRRect( | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                 child: AspectRatio( | ||||
|                   aspectRatio: 16 / 9, | ||||
|                   child: Container( | ||||
|                     color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                     child: _banner != null | ||||
|                         ? Image.network( | ||||
|                             ServiceFinder.buildUrl( | ||||
|                                 'files', '/attachments/$_banner'), | ||||
|                             fit: BoxFit.cover, | ||||
|                             loadingBuilder: (BuildContext context, Widget child, | ||||
|                                 ImageChunkEvent? loadingProgress) { | ||||
|                               if (loadingProgress == null) return child; | ||||
|                               return Center( | ||||
|                                 child: CircularProgressIndicator( | ||||
|                                   value: loadingProgress.expectedTotalBytes != | ||||
|                                           null | ||||
|                                       ? loadingProgress.cumulativeBytesLoaded / | ||||
|                                           loadingProgress.expectedTotalBytes! | ||||
|                                       : null, | ||||
|                                 ), | ||||
|                               ); | ||||
|                             }, | ||||
|                           ) | ||||
|                         : Container(), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               Positioned( | ||||
|                 bottom: 16, | ||||
|                 right: 16, | ||||
|                 child: FloatingActionButton( | ||||
|                   heroTag: const Key('banner-editor'), | ||||
|                   onPressed: () => _editImage('banner'), | ||||
|                   child: const Icon( | ||||
|                     Icons.camera_alt, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ).paddingSymmetric(horizontal: padding), | ||||
|           const Gap(24), | ||||
|           Row( | ||||
|             children: [ | ||||
|               Flexible( | ||||
|                 flex: 1, | ||||
|                 child: TextField( | ||||
|                   readOnly: true, | ||||
|                   controller: _usernameController, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const OutlineInputBorder(), | ||||
|                     labelText: 'username'.tr, | ||||
|                     prefixText: '@', | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               const Gap(16), | ||||
|               Flexible( | ||||
|                 flex: 1, | ||||
|                 child: TextField( | ||||
|                   controller: _nicknameController, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const OutlineInputBorder(), | ||||
|                     labelText: 'nickname'.tr, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ).paddingSymmetric(horizontal: padding), | ||||
|           const Gap(16), | ||||
|           Row( | ||||
|             children: [ | ||||
|               Flexible( | ||||
|                 flex: 1, | ||||
|                 child: TextField( | ||||
|                   controller: _firstNameController, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const OutlineInputBorder(), | ||||
|                     labelText: 'firstName'.tr, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               const Gap(16), | ||||
|               Flexible( | ||||
|                 flex: 1, | ||||
|                 child: TextField( | ||||
|                   controller: _lastNameController, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const OutlineInputBorder(), | ||||
|                     labelText: 'lastName'.tr, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ).paddingSymmetric(horizontal: padding), | ||||
|           const Gap(16), | ||||
|           TextField( | ||||
|             controller: _descriptionController, | ||||
|             keyboardType: TextInputType.multiline, | ||||
|             maxLines: null, | ||||
|             minLines: 3, | ||||
|             decoration: InputDecoration( | ||||
|               border: const OutlineInputBorder(), | ||||
|               labelText: 'description'.tr, | ||||
|             ), | ||||
|           ).paddingSymmetric(horizontal: padding), | ||||
|           const Gap(16), | ||||
|           TextField( | ||||
|             controller: _birthdayController, | ||||
|             readOnly: true, | ||||
|             decoration: InputDecoration( | ||||
|               border: const OutlineInputBorder(), | ||||
|               labelText: 'birthday'.tr, | ||||
|           ], | ||||
|         ).paddingSymmetric(horizontal: padding), | ||||
|         const Gap(16), | ||||
|         Stack( | ||||
|           children: [ | ||||
|             ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|               child: AspectRatio( | ||||
|                 aspectRatio: 16 / 9, | ||||
|                 child: Container( | ||||
|                   color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                   child: _banner != null | ||||
|                       ? Image.network( | ||||
|                           ServiceFinder.buildUrl( | ||||
|                               'files', '/attachments/$_banner'), | ||||
|                           fit: BoxFit.cover, | ||||
|                           loadingBuilder: (BuildContext context, Widget child, | ||||
|                               ImageChunkEvent? loadingProgress) { | ||||
|                             if (loadingProgress == null) return child; | ||||
|                             return Center( | ||||
|                               child: CircularProgressIndicator( | ||||
|                                 value: loadingProgress.expectedTotalBytes != | ||||
|                                         null | ||||
|                                     ? loadingProgress.cumulativeBytesLoaded / | ||||
|                                         loadingProgress.expectedTotalBytes! | ||||
|                                     : null, | ||||
|                               ), | ||||
|                             ); | ||||
|                           }, | ||||
|                         ) | ||||
|                       : Container(), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             onTap: () => _selectBirthday(), | ||||
|           ).paddingSymmetric(horizontal: padding), | ||||
|           const Gap(16), | ||||
|           Row( | ||||
|             mainAxisAlignment: MainAxisAlignment.end, | ||||
|             children: [ | ||||
|               TextButton( | ||||
|                 onPressed: _isBusy ? null : () => _syncWidget(), | ||||
|                 child: Text('reset'.tr), | ||||
|             Positioned( | ||||
|               bottom: 16, | ||||
|               right: 16, | ||||
|               child: FloatingActionButton( | ||||
|                 heroTag: const Key('banner-editor'), | ||||
|                 onPressed: () => _editImage('banner'), | ||||
|                 child: const Icon( | ||||
|                   Icons.camera_alt, | ||||
|                 ), | ||||
|               ), | ||||
|               ElevatedButton( | ||||
|                 onPressed: _isBusy ? null : () => _editUserInfo(), | ||||
|                 child: Text('apply'.tr), | ||||
|             ), | ||||
|           ], | ||||
|         ).paddingSymmetric(horizontal: padding), | ||||
|         const Gap(24), | ||||
|         Row( | ||||
|           children: [ | ||||
|             Flexible( | ||||
|               flex: 1, | ||||
|               child: TextField( | ||||
|                 readOnly: true, | ||||
|                 controller: _usernameController, | ||||
|                 decoration: InputDecoration( | ||||
|                   border: const OutlineInputBorder(), | ||||
|                   labelText: 'username'.tr, | ||||
|                   prefixText: '@', | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ).paddingSymmetric(horizontal: padding), | ||||
|         ], | ||||
|       ), | ||||
|             ), | ||||
|             const Gap(16), | ||||
|             Flexible( | ||||
|               flex: 1, | ||||
|               child: TextField( | ||||
|                 controller: _nicknameController, | ||||
|                 decoration: InputDecoration( | ||||
|                   border: const OutlineInputBorder(), | ||||
|                   labelText: 'nickname'.tr, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ).paddingSymmetric(horizontal: padding), | ||||
|         const Gap(16), | ||||
|         Row( | ||||
|           children: [ | ||||
|             Flexible( | ||||
|               flex: 1, | ||||
|               child: TextField( | ||||
|                 controller: _firstNameController, | ||||
|                 decoration: InputDecoration( | ||||
|                   border: const OutlineInputBorder(), | ||||
|                   labelText: 'firstName'.tr, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             const Gap(16), | ||||
|             Flexible( | ||||
|               flex: 1, | ||||
|               child: TextField( | ||||
|                 controller: _lastNameController, | ||||
|                 decoration: InputDecoration( | ||||
|                   border: const OutlineInputBorder(), | ||||
|                   labelText: 'lastName'.tr, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ).paddingSymmetric(horizontal: padding), | ||||
|         const Gap(16), | ||||
|         TextField( | ||||
|           controller: _descriptionController, | ||||
|           keyboardType: TextInputType.multiline, | ||||
|           maxLines: null, | ||||
|           minLines: 3, | ||||
|           decoration: InputDecoration( | ||||
|             border: const OutlineInputBorder(), | ||||
|             labelText: 'description'.tr, | ||||
|           ), | ||||
|         ).paddingSymmetric(horizontal: padding), | ||||
|         const Gap(16), | ||||
|         TextField( | ||||
|           controller: _birthdayController, | ||||
|           readOnly: true, | ||||
|           decoration: InputDecoration( | ||||
|             border: const OutlineInputBorder(), | ||||
|             labelText: 'birthday'.tr, | ||||
|           ), | ||||
|           onTap: () => _selectBirthday(), | ||||
|         ).paddingSymmetric(horizontal: padding), | ||||
|         const Gap(16), | ||||
|         Row( | ||||
|           mainAxisAlignment: MainAxisAlignment.end, | ||||
|           children: [ | ||||
|             TextButton( | ||||
|               onPressed: _isBusy ? null : () => _syncWidget(), | ||||
|               child: Text('reset'.tr), | ||||
|             ), | ||||
|             ElevatedButton( | ||||
|               onPressed: _isBusy ? null : () => _editUserInfo(), | ||||
|               child: Text('apply'.tr), | ||||
|             ), | ||||
|           ], | ||||
|         ).paddingSymmetric(horizontal: padding), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -217,298 +217,288 @@ class _SignInScreenState extends State<SignInScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return RootContainer( | ||||
|       child: CenteredContainer( | ||||
|         maxWidth: 360, | ||||
|         child: Theme( | ||||
|           data: Theme.of(context).copyWith(canvasColor: Colors.transparent), | ||||
|           child: PageTransitionSwitcher( | ||||
|             transitionBuilder: ( | ||||
|               Widget child, | ||||
|               Animation<double> primaryAnimation, | ||||
|               Animation<double> secondaryAnimation, | ||||
|             ) { | ||||
|               return SharedAxisTransition( | ||||
|                 animation: primaryAnimation, | ||||
|                 secondaryAnimation: secondaryAnimation, | ||||
|                 transitionType: SharedAxisTransitionType.horizontal, | ||||
|                 child: child, | ||||
|               ); | ||||
|             }, | ||||
|             child: switch (_period % 3) { | ||||
|               1 => ListView( | ||||
|                   shrinkWrap: true, | ||||
|                   key: const ValueKey<int>(1), | ||||
|                   children: [ | ||||
|                     Align( | ||||
|                       alignment: Alignment.centerLeft, | ||||
|                       child: ClipRRect( | ||||
|                         borderRadius: | ||||
|                             const BorderRadius.all(Radius.circular(8)), | ||||
|                         child: Image.asset('assets/logo.png', | ||||
|                             width: 64, height: 64), | ||||
|                       ).paddingOnly(bottom: 8, left: 4), | ||||
|     return CenteredContainer( | ||||
|       maxWidth: 360, | ||||
|       child: Theme( | ||||
|         data: Theme.of(context).copyWith(canvasColor: Colors.transparent), | ||||
|         child: PageTransitionSwitcher( | ||||
|           transitionBuilder: ( | ||||
|             Widget child, | ||||
|             Animation<double> primaryAnimation, | ||||
|             Animation<double> secondaryAnimation, | ||||
|           ) { | ||||
|             return SharedAxisTransition( | ||||
|               animation: primaryAnimation, | ||||
|               secondaryAnimation: secondaryAnimation, | ||||
|               transitionType: SharedAxisTransitionType.horizontal, | ||||
|               child: child, | ||||
|             ); | ||||
|           }, | ||||
|           child: switch (_period % 3) { | ||||
|             1 => ListView( | ||||
|                 shrinkWrap: true, | ||||
|                 key: const ValueKey<int>(1), | ||||
|                 children: [ | ||||
|                   Align( | ||||
|                     alignment: Alignment.centerLeft, | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: | ||||
|                           Image.asset('assets/logo.png', width: 64, height: 64), | ||||
|                     ).paddingOnly(bottom: 8, left: 4), | ||||
|                   ), | ||||
|                   Text( | ||||
|                     'signinPickFactor'.tr, | ||||
|                     style: const TextStyle( | ||||
|                       fontSize: 28, | ||||
|                       fontWeight: FontWeight.w900, | ||||
|                     ), | ||||
|                     Text( | ||||
|                       'signinPickFactor'.tr, | ||||
|                       style: const TextStyle( | ||||
|                         fontSize: 28, | ||||
|                         fontWeight: FontWeight.w900, | ||||
|                       ), | ||||
|                     ).paddingOnly(left: 4, bottom: 16), | ||||
|                     Card( | ||||
|                       margin: const EdgeInsets.symmetric(vertical: 4), | ||||
|                       child: Column( | ||||
|                         children: _factors | ||||
|                                 ?.map( | ||||
|                                   (x) => CheckboxListTile( | ||||
|                                     shape: const RoundedRectangleBorder( | ||||
|                                       borderRadius: BorderRadius.all( | ||||
|                                         Radius.circular(8), | ||||
|                                       ), | ||||
|                   ).paddingOnly(left: 4, bottom: 16), | ||||
|                   Card( | ||||
|                     margin: const EdgeInsets.symmetric(vertical: 4), | ||||
|                     child: Column( | ||||
|                       children: _factors | ||||
|                               ?.map( | ||||
|                                 (x) => CheckboxListTile( | ||||
|                                   shape: const RoundedRectangleBorder( | ||||
|                                     borderRadius: BorderRadius.all( | ||||
|                                       Radius.circular(8), | ||||
|                                     ), | ||||
|                                     secondary: Icon( | ||||
|                                       _factorLabelMap[x.type]?.$2 ?? | ||||
|                                           Icons.question_mark, | ||||
|                                     ), | ||||
|                                     title: Text( | ||||
|                                       _factorLabelMap[x.type]?.$1 ?? | ||||
|                                           'unknown'.tr, | ||||
|                                     ), | ||||
|                                     enabled: !_currentTicket!.factorTrail | ||||
|                                         .contains(x.id), | ||||
|                                     value: _factorPicked == x.id, | ||||
|                                     onChanged: (value) { | ||||
|                                       if (value == true) { | ||||
|                                         setState(() => _factorPicked = x.id); | ||||
|                                       } | ||||
|                                     }, | ||||
|                                   ), | ||||
|                                 ) | ||||
|                                 .toList() ?? | ||||
|                             List.empty(), | ||||
|                       ), | ||||
|                     ), | ||||
|                     Text( | ||||
|                       'signinMultiFactor'.trParams( | ||||
|                         {'n': _currentTicket!.stepRemain.toString()}, | ||||
|                       ), | ||||
|                       style: TextStyle(color: _unFocusColor, fontSize: 12), | ||||
|                     ).paddingOnly(left: 16, right: 16), | ||||
|                     const Gap(12), | ||||
|                     Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                       children: [ | ||||
|                         TextButton( | ||||
|                           onPressed: (_isBusy || _period > 1) | ||||
|                               ? null | ||||
|                               : () => _previousStep(), | ||||
|                           style: TextButton.styleFrom( | ||||
|                               foregroundColor: Colors.grey), | ||||
|                           child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: [ | ||||
|                               const Icon(Icons.chevron_left), | ||||
|                               Text('prev'.tr), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                         TextButton( | ||||
|                           onPressed: | ||||
|                               _isBusy ? null : () => _performGetFactorCode(), | ||||
|                           child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: [ | ||||
|                               Text('next'.tr), | ||||
|                               const Icon(Icons.chevron_right), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               2 => ListView( | ||||
|                   key: const ValueKey<int>(2), | ||||
|                   shrinkWrap: true, | ||||
|                   children: [ | ||||
|                     Align( | ||||
|                       alignment: Alignment.centerLeft, | ||||
|                       child: ClipRRect( | ||||
|                         borderRadius: | ||||
|                             const BorderRadius.all(Radius.circular(8)), | ||||
|                         child: Image.asset('assets/logo.png', | ||||
|                             width: 64, height: 64), | ||||
|                       ).paddingOnly(bottom: 8, left: 4), | ||||
|                     ), | ||||
|                     Text( | ||||
|                       'signinEnterPassword'.tr, | ||||
|                       style: const TextStyle( | ||||
|                         fontSize: 28, | ||||
|                         fontWeight: FontWeight.w900, | ||||
|                       ), | ||||
|                     ).paddingOnly(left: 4, bottom: 16), | ||||
|                     TextField( | ||||
|                       autocorrect: false, | ||||
|                       enableSuggestions: false, | ||||
|                       controller: _passwordController, | ||||
|                       obscureText: true, | ||||
|                       autofillHints: [ | ||||
|                         (_factorLabelMap[_factorPickedType]?.$3 ?? true) | ||||
|                             ? AutofillHints.password | ||||
|                             : AutofillHints.oneTimeCode | ||||
|                       ], | ||||
|                       decoration: InputDecoration( | ||||
|                         isDense: true, | ||||
|                         border: const OutlineInputBorder(), | ||||
|                         labelText: | ||||
|                             (_factorLabelMap[_factorPickedType]?.$3 ?? true) | ||||
|                                 ? 'passwordOneTime'.tr | ||||
|                                 : 'password'.tr, | ||||
|                         helperText: | ||||
|                             (_factorLabelMap[_factorPickedType]?.$3 ?? true) | ||||
|                                 ? 'passwordOneTimeInputHint'.tr | ||||
|                                 : 'passwordInputHint'.tr, | ||||
|                       ), | ||||
|                       onTapOutside: (_) => | ||||
|                           FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                       onSubmitted: | ||||
|                           _isBusy ? null : (_) => _performCheckTicket(), | ||||
|                     ), | ||||
|                     const Gap(12), | ||||
|                     Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                       children: [ | ||||
|                         TextButton( | ||||
|                           onPressed: _isBusy ? null : () => _previousStep(), | ||||
|                           style: TextButton.styleFrom( | ||||
|                               foregroundColor: Colors.grey), | ||||
|                           child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: [ | ||||
|                               const Icon(Icons.chevron_left), | ||||
|                               Text('prev'.tr), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                         TextButton( | ||||
|                           onPressed: | ||||
|                               _isBusy ? null : () => _performCheckTicket(), | ||||
|                           child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: [ | ||||
|                               Text('next'.tr), | ||||
|                               const Icon(Icons.chevron_right), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               _ => ListView( | ||||
|                   key: const ValueKey<int>(0), | ||||
|                   shrinkWrap: true, | ||||
|                   children: [ | ||||
|                     Align( | ||||
|                       alignment: Alignment.centerLeft, | ||||
|                       child: ClipRRect( | ||||
|                         borderRadius: | ||||
|                             const BorderRadius.all(Radius.circular(8)), | ||||
|                         child: Image.asset('assets/logo.png', | ||||
|                             width: 64, height: 64), | ||||
|                       ).paddingOnly(bottom: 8, left: 4), | ||||
|                     ), | ||||
|                     Text( | ||||
|                       'signinGreeting'.tr, | ||||
|                       style: const TextStyle( | ||||
|                         fontSize: 28, | ||||
|                         fontWeight: FontWeight.w900, | ||||
|                       ), | ||||
|                     ).paddingOnly(left: 4, bottom: 16), | ||||
|                     TextField( | ||||
|                       autocorrect: false, | ||||
|                       enableSuggestions: false, | ||||
|                       controller: _usernameController, | ||||
|                       autofillHints: const [AutofillHints.username], | ||||
|                       decoration: InputDecoration( | ||||
|                         isDense: true, | ||||
|                         border: const OutlineInputBorder(), | ||||
|                         labelText: 'username'.tr, | ||||
|                         helperText: 'usernameInputHint'.tr, | ||||
|                       ), | ||||
|                       onTapOutside: (_) => | ||||
|                           FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                       onSubmitted: _isBusy ? null : (_) => _performNewTicket(), | ||||
|                     ), | ||||
|                     const Gap(12), | ||||
|                     Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                       children: [ | ||||
|                         TextButton( | ||||
|                           onPressed: | ||||
|                               _isBusy ? null : () => _requestResetPassword(), | ||||
|                           style: TextButton.styleFrom( | ||||
|                               foregroundColor: Colors.grey), | ||||
|                           child: Text('forgotPassword'.tr), | ||||
|                         ), | ||||
|                         TextButton( | ||||
|                           onPressed: _isBusy ? null : () => _performNewTicket(), | ||||
|                           child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: [ | ||||
|                               Text('next'.tr), | ||||
|                               const Icon(Icons.chevron_right), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     const Gap(12), | ||||
|                     Align( | ||||
|                       alignment: Alignment.centerRight, | ||||
|                       child: Container( | ||||
|                         constraints: const BoxConstraints(maxWidth: 290), | ||||
|                         child: Column( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                           children: [ | ||||
|                             Text( | ||||
|                               'termAcceptNextWithAgree'.tr, | ||||
|                               textAlign: TextAlign.end, | ||||
|                               style: Theme.of(context) | ||||
|                                   .textTheme | ||||
|                                   .bodySmall! | ||||
|                                   .copyWith( | ||||
|                                     color: Theme.of(context) | ||||
|                                         .colorScheme | ||||
|                                         .onSurface | ||||
|                                         .withOpacity(0.75), | ||||
|                                   secondary: Icon( | ||||
|                                     _factorLabelMap[x.type]?.$2 ?? | ||||
|                                         Icons.question_mark, | ||||
|                                   ), | ||||
|                             ), | ||||
|                             Material( | ||||
|                               color: Colors.transparent, | ||||
|                               child: InkWell( | ||||
|                                 child: Row( | ||||
|                                   mainAxisSize: MainAxisSize.min, | ||||
|                                   children: [ | ||||
|                                     Text('termAcceptLink'.tr), | ||||
|                                     const Gap(4), | ||||
|                                     const Icon(Icons.launch, size: 14), | ||||
|                                   ], | ||||
|                                   title: Text( | ||||
|                                     _factorLabelMap[x.type]?.$1 ?? 'unknown'.tr, | ||||
|                                   ), | ||||
|                                   enabled: !_currentTicket!.factorTrail | ||||
|                                       .contains(x.id), | ||||
|                                   value: _factorPicked == x.id, | ||||
|                                   onChanged: (value) { | ||||
|                                     if (value == true) { | ||||
|                                       setState(() => _factorPicked = x.id); | ||||
|                                     } | ||||
|                                   }, | ||||
|                                 ), | ||||
|                                 onTap: () { | ||||
|                                   launchUrlString('https://solsynth.dev/terms'); | ||||
|                                 }, | ||||
|                               ), | ||||
|                             ), | ||||
|                               ) | ||||
|                               .toList() ?? | ||||
|                           List.empty(), | ||||
|                     ), | ||||
|                   ), | ||||
|                   Text( | ||||
|                     'signinMultiFactor'.trParams( | ||||
|                       {'n': _currentTicket!.stepRemain.toString()}, | ||||
|                     ), | ||||
|                     style: TextStyle(color: _unFocusColor, fontSize: 12), | ||||
|                   ).paddingOnly(left: 16, right: 16), | ||||
|                   const Gap(12), | ||||
|                   Row( | ||||
|                     mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                     children: [ | ||||
|                       TextButton( | ||||
|                         onPressed: (_isBusy || _period > 1) | ||||
|                             ? null | ||||
|                             : () => _previousStep(), | ||||
|                         style: | ||||
|                             TextButton.styleFrom(foregroundColor: Colors.grey), | ||||
|                         child: Row( | ||||
|                           mainAxisSize: MainAxisSize.min, | ||||
|                           children: [ | ||||
|                             const Icon(Icons.chevron_left), | ||||
|                             Text('prev'.tr), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ).paddingSymmetric(horizontal: 16), | ||||
|                       ), | ||||
|                       TextButton( | ||||
|                         onPressed: | ||||
|                             _isBusy ? null : () => _performGetFactorCode(), | ||||
|                         child: Row( | ||||
|                           mainAxisSize: MainAxisSize.min, | ||||
|                           children: [ | ||||
|                             Text('next'.tr), | ||||
|                             const Icon(Icons.chevron_right), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             2 => ListView( | ||||
|                 key: const ValueKey<int>(2), | ||||
|                 shrinkWrap: true, | ||||
|                 children: [ | ||||
|                   Align( | ||||
|                     alignment: Alignment.centerLeft, | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: | ||||
|                           Image.asset('assets/logo.png', width: 64, height: 64), | ||||
|                     ).paddingOnly(bottom: 8, left: 4), | ||||
|                   ), | ||||
|                   Text( | ||||
|                     'signinEnterPassword'.tr, | ||||
|                     style: const TextStyle( | ||||
|                       fontSize: 28, | ||||
|                       fontWeight: FontWeight.w900, | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|             }, | ||||
|           ), | ||||
|                   ).paddingOnly(left: 4, bottom: 16), | ||||
|                   TextField( | ||||
|                     autocorrect: false, | ||||
|                     enableSuggestions: false, | ||||
|                     controller: _passwordController, | ||||
|                     obscureText: true, | ||||
|                     autofillHints: [ | ||||
|                       (_factorLabelMap[_factorPickedType]?.$3 ?? true) | ||||
|                           ? AutofillHints.password | ||||
|                           : AutofillHints.oneTimeCode | ||||
|                     ], | ||||
|                     decoration: InputDecoration( | ||||
|                       isDense: true, | ||||
|                       border: const OutlineInputBorder(), | ||||
|                       labelText: | ||||
|                           (_factorLabelMap[_factorPickedType]?.$3 ?? true) | ||||
|                               ? 'passwordOneTime'.tr | ||||
|                               : 'password'.tr, | ||||
|                       helperText: | ||||
|                           (_factorLabelMap[_factorPickedType]?.$3 ?? true) | ||||
|                               ? 'passwordOneTimeInputHint'.tr | ||||
|                               : 'passwordInputHint'.tr, | ||||
|                     ), | ||||
|                     onTapOutside: (_) => | ||||
|                         FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     onSubmitted: _isBusy ? null : (_) => _performCheckTicket(), | ||||
|                   ), | ||||
|                   const Gap(12), | ||||
|                   Row( | ||||
|                     mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                     children: [ | ||||
|                       TextButton( | ||||
|                         onPressed: _isBusy ? null : () => _previousStep(), | ||||
|                         style: | ||||
|                             TextButton.styleFrom(foregroundColor: Colors.grey), | ||||
|                         child: Row( | ||||
|                           mainAxisSize: MainAxisSize.min, | ||||
|                           children: [ | ||||
|                             const Icon(Icons.chevron_left), | ||||
|                             Text('prev'.tr), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                       TextButton( | ||||
|                         onPressed: _isBusy ? null : () => _performCheckTicket(), | ||||
|                         child: Row( | ||||
|                           mainAxisSize: MainAxisSize.min, | ||||
|                           children: [ | ||||
|                             Text('next'.tr), | ||||
|                             const Icon(Icons.chevron_right), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             _ => ListView( | ||||
|                 key: const ValueKey<int>(0), | ||||
|                 shrinkWrap: true, | ||||
|                 children: [ | ||||
|                   Align( | ||||
|                     alignment: Alignment.centerLeft, | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: | ||||
|                           Image.asset('assets/logo.png', width: 64, height: 64), | ||||
|                     ).paddingOnly(bottom: 8, left: 4), | ||||
|                   ), | ||||
|                   Text( | ||||
|                     'signinGreeting'.tr, | ||||
|                     style: const TextStyle( | ||||
|                       fontSize: 28, | ||||
|                       fontWeight: FontWeight.w900, | ||||
|                     ), | ||||
|                   ).paddingOnly(left: 4, bottom: 16), | ||||
|                   TextField( | ||||
|                     autocorrect: false, | ||||
|                     enableSuggestions: false, | ||||
|                     controller: _usernameController, | ||||
|                     autofillHints: const [AutofillHints.username], | ||||
|                     decoration: InputDecoration( | ||||
|                       isDense: true, | ||||
|                       border: const OutlineInputBorder(), | ||||
|                       labelText: 'username'.tr, | ||||
|                       helperText: 'usernameInputHint'.tr, | ||||
|                     ), | ||||
|                     onTapOutside: (_) => | ||||
|                         FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     onSubmitted: _isBusy ? null : (_) => _performNewTicket(), | ||||
|                   ), | ||||
|                   const Gap(12), | ||||
|                   Row( | ||||
|                     mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                     children: [ | ||||
|                       TextButton( | ||||
|                         onPressed: | ||||
|                             _isBusy ? null : () => _requestResetPassword(), | ||||
|                         style: | ||||
|                             TextButton.styleFrom(foregroundColor: Colors.grey), | ||||
|                         child: Text('forgotPassword'.tr), | ||||
|                       ), | ||||
|                       TextButton( | ||||
|                         onPressed: _isBusy ? null : () => _performNewTicket(), | ||||
|                         child: Row( | ||||
|                           mainAxisSize: MainAxisSize.min, | ||||
|                           children: [ | ||||
|                             Text('next'.tr), | ||||
|                             const Icon(Icons.chevron_right), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   const Gap(12), | ||||
|                   Align( | ||||
|                     alignment: Alignment.centerRight, | ||||
|                     child: Container( | ||||
|                       constraints: const BoxConstraints(maxWidth: 290), | ||||
|                       child: Column( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             'termAcceptNextWithAgree'.tr, | ||||
|                             textAlign: TextAlign.end, | ||||
|                             style: | ||||
|                                 Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                                       color: Theme.of(context) | ||||
|                                           .colorScheme | ||||
|                                           .onSurface | ||||
|                                           .withOpacity(0.75), | ||||
|                                     ), | ||||
|                           ), | ||||
|                           Material( | ||||
|                             color: Colors.transparent, | ||||
|                             child: InkWell( | ||||
|                               child: Row( | ||||
|                                 mainAxisSize: MainAxisSize.min, | ||||
|                                 children: [ | ||||
|                                   Text('termAcceptLink'.tr), | ||||
|                                   const Gap(4), | ||||
|                                   const Icon(Icons.launch, size: 14), | ||||
|                                 ], | ||||
|                               ), | ||||
|                               onTap: () { | ||||
|                                 launchUrlString('https://solsynth.dev/terms'); | ||||
|                               }, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ).paddingSymmetric(horizontal: 16), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|           }, | ||||
|         ), | ||||
|       ).paddingAll(24), | ||||
|     ); | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import 'package:gap/gap.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:solian/exts.dart'; | ||||
| import 'package:solian/services.dart'; | ||||
| import 'package:solian/widgets/root_container.dart'; | ||||
| import 'package:solian/widgets/sized_container.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| @@ -66,147 +65,141 @@ class _SignUpScreenState extends State<SignUpScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return RootContainer( | ||||
|       child: CenteredContainer( | ||||
|         maxWidth: 360, | ||||
|         child: ListView( | ||||
|           shrinkWrap: true, | ||||
|           children: [ | ||||
|             Align( | ||||
|               alignment: Alignment.centerLeft, | ||||
|               child: ClipRRect( | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                 child: Image.asset('assets/logo.png', width: 64, height: 64), | ||||
|               ).paddingOnly(bottom: 8, left: 4), | ||||
|     return CenteredContainer( | ||||
|       maxWidth: 360, | ||||
|       child: ListView( | ||||
|         shrinkWrap: true, | ||||
|         children: [ | ||||
|           Align( | ||||
|             alignment: Alignment.centerLeft, | ||||
|             child: ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|               child: Image.asset('assets/logo.png', width: 64, height: 64), | ||||
|             ).paddingOnly(bottom: 8, left: 4), | ||||
|           ), | ||||
|           Text( | ||||
|             'signupGreeting'.tr, | ||||
|             style: const TextStyle( | ||||
|               fontSize: 28, | ||||
|               fontWeight: FontWeight.w900, | ||||
|             ), | ||||
|             Text( | ||||
|               'signupGreeting'.tr, | ||||
|               style: const TextStyle( | ||||
|                 fontSize: 28, | ||||
|                 fontWeight: FontWeight.w900, | ||||
|               ), | ||||
|             ).paddingOnly(left: 4, bottom: 16), | ||||
|             TextField( | ||||
|               autocorrect: false, | ||||
|               enableSuggestions: false, | ||||
|               controller: _usernameController, | ||||
|               autofillHints: const [AutofillHints.username], | ||||
|               decoration: InputDecoration( | ||||
|                 isDense: true, | ||||
|                 border: const OutlineInputBorder(), | ||||
|                 labelText: 'username'.tr, | ||||
|               ), | ||||
|               onTapOutside: (_) => | ||||
|                   FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ).paddingOnly(left: 4, bottom: 16), | ||||
|           TextField( | ||||
|             autocorrect: false, | ||||
|             enableSuggestions: false, | ||||
|             controller: _usernameController, | ||||
|             autofillHints: const [AutofillHints.username], | ||||
|             decoration: InputDecoration( | ||||
|               isDense: true, | ||||
|               border: const OutlineInputBorder(), | ||||
|               labelText: 'username'.tr, | ||||
|             ), | ||||
|             const Gap(12), | ||||
|             TextField( | ||||
|               autocorrect: false, | ||||
|               enableSuggestions: false, | ||||
|               controller: _nicknameController, | ||||
|               autofillHints: const [AutofillHints.nickname], | ||||
|               decoration: InputDecoration( | ||||
|                 isDense: true, | ||||
|                 border: const OutlineInputBorder(), | ||||
|                 labelText: 'nickname'.tr, | ||||
|               ), | ||||
|               onTapOutside: (_) => | ||||
|                   FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
|           const Gap(12), | ||||
|           TextField( | ||||
|             autocorrect: false, | ||||
|             enableSuggestions: false, | ||||
|             controller: _nicknameController, | ||||
|             autofillHints: const [AutofillHints.nickname], | ||||
|             decoration: InputDecoration( | ||||
|               isDense: true, | ||||
|               border: const OutlineInputBorder(), | ||||
|               labelText: 'nickname'.tr, | ||||
|             ), | ||||
|             const Gap(12), | ||||
|             TextField( | ||||
|               autocorrect: false, | ||||
|               enableSuggestions: false, | ||||
|               controller: _emailController, | ||||
|               autofillHints: const [AutofillHints.email], | ||||
|               decoration: InputDecoration( | ||||
|                 isDense: true, | ||||
|                 border: const OutlineInputBorder(), | ||||
|                 labelText: 'email'.tr, | ||||
|               ), | ||||
|               onTapOutside: (_) => | ||||
|                   FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
|           const Gap(12), | ||||
|           TextField( | ||||
|             autocorrect: false, | ||||
|             enableSuggestions: false, | ||||
|             controller: _emailController, | ||||
|             autofillHints: const [AutofillHints.email], | ||||
|             decoration: InputDecoration( | ||||
|               isDense: true, | ||||
|               border: const OutlineInputBorder(), | ||||
|               labelText: 'email'.tr, | ||||
|             ), | ||||
|             const Gap(12), | ||||
|             TextField( | ||||
|               obscureText: true, | ||||
|               autocorrect: false, | ||||
|               enableSuggestions: false, | ||||
|               autofillHints: const [AutofillHints.password], | ||||
|               controller: _passwordController, | ||||
|               decoration: InputDecoration( | ||||
|                 isDense: true, | ||||
|                 border: const OutlineInputBorder(), | ||||
|                 labelText: 'password'.tr, | ||||
|               ), | ||||
|               onTapOutside: (_) => | ||||
|                   FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               onSubmitted: (_) => _performAction(context), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
|           const Gap(12), | ||||
|           TextField( | ||||
|             obscureText: true, | ||||
|             autocorrect: false, | ||||
|             enableSuggestions: false, | ||||
|             autofillHints: const [AutofillHints.password], | ||||
|             controller: _passwordController, | ||||
|             decoration: InputDecoration( | ||||
|               isDense: true, | ||||
|               border: const OutlineInputBorder(), | ||||
|               labelText: 'password'.tr, | ||||
|             ), | ||||
|             const Gap(8), | ||||
|             CheckboxListTile( | ||||
|               value: _isTermAccepted, | ||||
|               title: Text( | ||||
|                 'termAccept'.tr, | ||||
|                 style: const TextStyle(height: 1.2), | ||||
|               ).paddingOnly(bottom: 4), | ||||
|               shape: const RoundedRectangleBorder( | ||||
|                 borderRadius: BorderRadius.all( | ||||
|                   Radius.circular(8), | ||||
|                 ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             onSubmitted: (_) => _performAction(context), | ||||
|           ), | ||||
|           const Gap(8), | ||||
|           CheckboxListTile( | ||||
|             value: _isTermAccepted, | ||||
|             title: Text( | ||||
|               'termAccept'.tr, | ||||
|               style: const TextStyle(height: 1.2), | ||||
|             ).paddingOnly(bottom: 4), | ||||
|             shape: const RoundedRectangleBorder( | ||||
|               borderRadius: BorderRadius.all( | ||||
|                 Radius.circular(8), | ||||
|               ), | ||||
|               subtitle: RichText( | ||||
|                 text: TextSpan( | ||||
|                   style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                         color: Theme.of(context) | ||||
|                             .colorScheme | ||||
|                             .onSurface | ||||
|                             .withOpacity(0.75), | ||||
|                       ), | ||||
|                   children: [ | ||||
|                     TextSpan(text: 'termAcceptDesc'.tr), | ||||
|                     WidgetSpan( | ||||
|                       child: Material( | ||||
|                         color: Colors.transparent, | ||||
|                         child: InkWell( | ||||
|                           child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: [ | ||||
|                               Text('termAcceptLink'.tr), | ||||
|                               const Gap(4), | ||||
|                               const Icon(Icons.launch, size: 14), | ||||
|                             ], | ||||
|                           ), | ||||
|                           onTap: () { | ||||
|                             launchUrlString('https://solsynth.dev/terms'); | ||||
|                           }, | ||||
|             ), | ||||
|             subtitle: RichText( | ||||
|               text: TextSpan( | ||||
|                 style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                       color: Theme.of(context) | ||||
|                           .colorScheme | ||||
|                           .onSurface | ||||
|                           .withOpacity(0.75), | ||||
|                     ), | ||||
|                 children: [ | ||||
|                   TextSpan(text: 'termAcceptDesc'.tr), | ||||
|                   WidgetSpan( | ||||
|                     child: Material( | ||||
|                       color: Colors.transparent, | ||||
|                       child: InkWell( | ||||
|                         child: Row( | ||||
|                           mainAxisSize: MainAxisSize.min, | ||||
|                           children: [ | ||||
|                             Text('termAcceptLink'.tr), | ||||
|                             const Gap(4), | ||||
|                             const Icon(Icons.launch, size: 14), | ||||
|                           ], | ||||
|                         ), | ||||
|                         onTap: () { | ||||
|                           launchUrlString('https://solsynth.dev/terms'); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               onChanged: (value) { | ||||
|                 setState(() => _isTermAccepted = value ?? false); | ||||
|               }, | ||||
|             ), | ||||
|             const Gap(16), | ||||
|             Align( | ||||
|               alignment: Alignment.centerRight, | ||||
|               child: TextButton( | ||||
|                 onPressed: | ||||
|                     !_isTermAccepted ? null : () => _performAction(context), | ||||
|                 child: Row( | ||||
|                   mainAxisSize: MainAxisSize.min, | ||||
|                   children: [ | ||||
|                     Text('next'.tr), | ||||
|                     const Icon(Icons.chevron_right), | ||||
|                   ], | ||||
|                 ), | ||||
|             onChanged: (value) { | ||||
|               setState(() => _isTermAccepted = value ?? false); | ||||
|             }, | ||||
|           ), | ||||
|           const Gap(16), | ||||
|           Align( | ||||
|             alignment: Alignment.centerRight, | ||||
|             child: TextButton( | ||||
|               onPressed: | ||||
|                   !_isTermAccepted ? null : () => _performAction(context), | ||||
|               child: Row( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 children: [ | ||||
|                   Text('next'.tr), | ||||
|                   const Icon(Icons.chevron_right), | ||||
|                 ], | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ), | ||||
|             ), | ||||
|           ) | ||||
|         ], | ||||
|       ).paddingAll(24), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -19,7 +19,6 @@ import 'package:solian/providers/database/database.dart'; | ||||
| import 'package:solian/providers/theme_switcher.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:solian/widgets/reports/abuse_report.dart'; | ||||
| import 'package:solian/widgets/root_container.dart'; | ||||
|  | ||||
| class SettingScreen extends StatefulWidget { | ||||
|   const SettingScreen({super.key}); | ||||
| @@ -83,259 +82,258 @@ class _SettingScreenState extends State<SettingScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return RootContainer( | ||||
|       child: ListView( | ||||
|         children: [ | ||||
|           _buildCaptionHeader('theme'.tr), | ||||
|           ListTile( | ||||
|             leading: const Icon(Icons.palette), | ||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|             title: Text('globalTheme'.tr), | ||||
|             trailing: DropdownButtonHideUnderline( | ||||
|               child: DropdownButton2<SolianThemeData>( | ||||
|                 isExpanded: true, | ||||
|                 hint: Text( | ||||
|                   'theme'.tr, | ||||
|                   style: TextStyle( | ||||
|                     fontSize: 14, | ||||
|                     color: Theme.of(context).hintColor, | ||||
|                   ), | ||||
|     return ListView( | ||||
|       children: [ | ||||
|         _buildCaptionHeader('theme'.tr), | ||||
|         ListTile( | ||||
|           leading: const Icon(Icons.palette), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|           title: Text('globalTheme'.tr), | ||||
|           trailing: DropdownButtonHideUnderline( | ||||
|             child: DropdownButton2<SolianThemeData>( | ||||
|               isExpanded: true, | ||||
|               hint: Text( | ||||
|                 'theme'.tr, | ||||
|                 style: TextStyle( | ||||
|                   fontSize: 14, | ||||
|                   color: Theme.of(context).hintColor, | ||||
|                 ), | ||||
|                 items: _presentTheme | ||||
|                     .map((SolianThemeData item) => | ||||
|                         DropdownMenuItem<SolianThemeData>( | ||||
|                           value: item, | ||||
|                           child: Row( | ||||
|                             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                             children: [ | ||||
|                               Icon(Icons.circle, color: item.seedColor), | ||||
|                               const Gap(8), | ||||
|                               Text( | ||||
|               ), | ||||
|               items: _presentTheme | ||||
|                   .map((SolianThemeData item) => | ||||
|                       DropdownMenuItem<SolianThemeData>( | ||||
|                         value: item, | ||||
|                         child: Row( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                           children: [ | ||||
|                             Icon(Icons.circle, color: item.seedColor), | ||||
|                             const Gap(8), | ||||
|                             Expanded( | ||||
|                               child: Text( | ||||
|                                 item.id.tr, | ||||
|                                 maxLines: 1, | ||||
|                                 overflow: TextOverflow.ellipsis, | ||||
|                                 style: const TextStyle( | ||||
|                                   fontSize: 14, | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         )) | ||||
|                     .toList(), | ||||
|                 value: (_prefs?.containsKey('global_theme') ?? false) | ||||
|                     ? SolianThemeData.fromJson( | ||||
|                         jsonDecode(_prefs!.getString('global_theme')!), | ||||
|                       ) | ||||
|                     : null, | ||||
|                 onChanged: (SolianThemeData? value) { | ||||
|                   context.read<ThemeSwitcher>().setThemeData(value); | ||||
|                   setState(() {}); | ||||
|                 }, | ||||
|                 buttonStyleData: const ButtonStyleData( | ||||
|                   padding: EdgeInsets.symmetric(horizontal: 8), | ||||
|                   height: 40, | ||||
|                   width: 140, | ||||
|                 ), | ||||
|                 menuItemStyleData: const MenuItemStyleData( | ||||
|                   height: 40, | ||||
|                 ), | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|                       )) | ||||
|                   .toList(), | ||||
|               value: (_prefs?.containsKey('global_theme') ?? false) | ||||
|                   ? SolianThemeData.fromJson( | ||||
|                       jsonDecode(_prefs!.getString('global_theme')!), | ||||
|                     ) | ||||
|                   : null, | ||||
|               onChanged: (SolianThemeData? value) { | ||||
|                 context.read<ThemeSwitcher>().setThemeData(value); | ||||
|                 setState(() {}); | ||||
|               }, | ||||
|               buttonStyleData: const ButtonStyleData( | ||||
|                 padding: EdgeInsets.symmetric(horizontal: 8), | ||||
|                 height: 40, | ||||
|                 width: 140, | ||||
|               ), | ||||
|               menuItemStyleData: const MenuItemStyleData( | ||||
|                 height: 40, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           CheckboxListTile( | ||||
|             secondary: const Icon(Icons.military_tech), | ||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|             title: Text('agedTheme'.tr), | ||||
|             subtitle: Text('agedThemeDesc'.tr), | ||||
|             value: _prefs?.getBool('aged_theme') ?? false, | ||||
|             onChanged: (value) { | ||||
|               if (value != null) { | ||||
|                 context.read<ThemeSwitcher>().setAgedTheme(value); | ||||
|         ), | ||||
|         CheckboxListTile( | ||||
|           secondary: const Icon(Icons.military_tech), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|           title: Text('agedTheme'.tr), | ||||
|           subtitle: Text('agedThemeDesc'.tr), | ||||
|           value: _prefs?.getBool('aged_theme') ?? false, | ||||
|           onChanged: (value) { | ||||
|             if (value != null) { | ||||
|               context.read<ThemeSwitcher>().setAgedTheme(value); | ||||
|             } | ||||
|             setState(() {}); | ||||
|           }, | ||||
|         ), | ||||
|         if (!PlatformInfo.isWeb) | ||||
|           ListTile( | ||||
|             leading: const Icon(Icons.wallpaper), | ||||
|             contentPadding: const EdgeInsets.only(left: 22, right: 31), | ||||
|             title: Text('appBackgroundImage'.tr), | ||||
|             subtitle: Text('appBackgroundImageDesc'.tr), | ||||
|             trailing: File('$_docBasepath/app_background_image').existsSync() | ||||
|                 ? const Icon(Icons.check_box) | ||||
|                 : const Icon(Icons.check_box_outline_blank), | ||||
|             onTap: () async { | ||||
|               if (File('$_docBasepath/app_background_image').existsSync()) { | ||||
|                 File('$_docBasepath/app_background_image').deleteSync(); | ||||
|               } else { | ||||
|                 final image = await ImagePicker().pickImage( | ||||
|                   source: ImageSource.gallery, | ||||
|                 ); | ||||
|                 if (image == null) return; | ||||
|  | ||||
|                 await File(image.path) | ||||
|                     .copy('$_docBasepath/app_background_image'); | ||||
|               } | ||||
|  | ||||
|               setState(() {}); | ||||
|             }, | ||||
|           ), | ||||
|           if (!PlatformInfo.isWeb) | ||||
|             ListTile( | ||||
|               leading: const Icon(Icons.wallpaper), | ||||
|               contentPadding: const EdgeInsets.only(left: 22, right: 31), | ||||
|               title: Text('appBackgroundImage'.tr), | ||||
|               subtitle: Text('appBackgroundImageDesc'.tr), | ||||
|               trailing: File('$_docBasepath/app_background_image').existsSync() | ||||
|                   ? const Icon(Icons.check_box) | ||||
|                   : const Icon(Icons.check_box_outline_blank), | ||||
|               onTap: () async { | ||||
|                 if (File('$_docBasepath/app_background_image').existsSync()) { | ||||
|                   File('$_docBasepath/app_background_image').deleteSync(); | ||||
|                 } else { | ||||
|                   final image = await ImagePicker().pickImage( | ||||
|                     source: ImageSource.gallery, | ||||
|                   ); | ||||
|                   if (image == null) return; | ||||
|  | ||||
|                   await File(image.path) | ||||
|                       .copy('$_docBasepath/app_background_image'); | ||||
|                 } | ||||
|  | ||||
|                 setState(() {}); | ||||
|               }, | ||||
|             ), | ||||
|           _buildCaptionHeader('notification'.tr), | ||||
|           Tooltip( | ||||
|             message: 'settingsNotificationBgServiceDesc'.tr, | ||||
|             child: CheckboxListTile( | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|               secondary: const Icon(Icons.system_security_update_warning), | ||||
|               enabled: PlatformInfo.isAndroid, | ||||
|               title: Text('settingsNotificationBgService'.tr), | ||||
|               subtitle: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   Text('holdToSeeDetail'.tr), | ||||
|                   Text( | ||||
|                     'needRestartToApply'.tr, | ||||
|                     style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|               value: | ||||
|                   _prefs?.getBool('service_background_notification') ?? false, | ||||
|               onChanged: (value) { | ||||
|                 _prefs | ||||
|                     ?.setBool('service_background_notification', value ?? false) | ||||
|                     .then((_) { | ||||
|                   setState(() {}); | ||||
|                 }); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|           _buildCaptionHeader('update'.tr), | ||||
|           CheckboxListTile( | ||||
|         _buildCaptionHeader('notification'.tr), | ||||
|         Tooltip( | ||||
|           message: 'settingsNotificationBgServiceDesc'.tr, | ||||
|           child: CheckboxListTile( | ||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|             secondary: const Icon(Icons.sync_alt), | ||||
|             title: Text('updateCheckStrictly'.tr), | ||||
|             subtitle: Text('updateCheckStrictlyDesc'.tr), | ||||
|             value: _prefs?.getBool('check_update_strictly') ?? false, | ||||
|             onChanged: (value) { | ||||
|               _prefs | ||||
|                   ?.setBool('check_update_strictly', value ?? false) | ||||
|                   .then((_) { | ||||
|                 setState(() {}); | ||||
|               }); | ||||
|             }, | ||||
|           ), | ||||
|           Obx(() { | ||||
|             final AuthProvider auth = Get.find<AuthProvider>(); | ||||
|             if (!auth.isAuthorized.value) return const SizedBox.shrink(); | ||||
|             return Column( | ||||
|             secondary: const Icon(Icons.system_security_update_warning), | ||||
|             enabled: PlatformInfo.isAndroid, | ||||
|             title: Text('settingsNotificationBgService'.tr), | ||||
|             subtitle: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 _buildCaptionHeader('account'.tr), | ||||
|                 ListTile( | ||||
|                   leading: const Icon(Icons.flag), | ||||
|                   trailing: const Icon(Icons.chevron_right), | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|                   title: Text('reportAbuse'.tr), | ||||
|                   subtitle: Text('reportAbuseDesc'.tr), | ||||
|                   onTap: () { | ||||
|                     showDialog( | ||||
|                       context: context, | ||||
|                       builder: (context) => const AbuseReportDialog(), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   leading: const Icon(Icons.person_remove), | ||||
|                   trailing: const Icon(Icons.chevron_right), | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|                   title: Text('accountDeletion'.tr), | ||||
|                   subtitle: Text('accountDeletionDesc'.tr), | ||||
|                   onTap: () { | ||||
|                     context | ||||
|                         .showSlideToConfirmDialog( | ||||
|                       'accountDeletionConfirm'.tr, | ||||
|                       'accountDeletionConfirmDesc'.trParams({ | ||||
|                         'account': '@${auth.userProfile.value!['name']}', | ||||
|                       }), | ||||
|                     ) | ||||
|                         .then((value) async { | ||||
|                       if (value != true) return; | ||||
|                       final client = await auth.configureClient('id'); | ||||
|                       final resp = await client.post('/users/me/deletion', {}); | ||||
|                       if (resp.statusCode != 200) { | ||||
|                         context.showErrorDialog(RequestException(resp)); | ||||
|                       } else { | ||||
|                         context.showSnackbar('accountDeletionRequested'.tr); | ||||
|                       } | ||||
|                     }); | ||||
|                   }, | ||||
|                 ), | ||||
|                 Text('holdToSeeDetail'.tr), | ||||
|                 Text( | ||||
|                   'needRestartToApply'.tr, | ||||
|                   style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|                 ) | ||||
|               ], | ||||
|             ); | ||||
|           }), | ||||
|           _buildCaptionHeader('performance'.tr), | ||||
|           CheckboxListTile( | ||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|             secondary: const Icon(Icons.message), | ||||
|             title: Text('animatedMessageList'.tr), | ||||
|             subtitle: Text('animatedMessageListDesc'.tr), | ||||
|             value: _prefs?.getBool('non_animated_message_list') ?? false, | ||||
|             ), | ||||
|             value: _prefs?.getBool('service_background_notification') ?? false, | ||||
|             onChanged: (value) { | ||||
|               _prefs | ||||
|                   ?.setBool('non_animated_message_list', value ?? false) | ||||
|                   ?.setBool('service_background_notification', value ?? false) | ||||
|                   .then((_) { | ||||
|                 setState(() {}); | ||||
|               }); | ||||
|             }, | ||||
|           ), | ||||
|           _buildCaptionHeader('more'.tr), | ||||
|           ListTile( | ||||
|             leading: const Icon(Icons.delete_sweep), | ||||
|             trailing: const Icon(Icons.chevron_right), | ||||
|             subtitle: FutureBuilder( | ||||
|               future: AppDatabase.getDatabaseSize(), | ||||
|               builder: (context, snapshot) { | ||||
|                 if (!snapshot.hasData) { | ||||
|                   return Text('localDatabaseSize'.trParams( | ||||
|                     {'size': 'unknown'.tr}, | ||||
|                   )); | ||||
|                 } | ||||
|         ), | ||||
|         _buildCaptionHeader('update'.tr), | ||||
|         CheckboxListTile( | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|           secondary: const Icon(Icons.sync_alt), | ||||
|           title: Text('updateCheckStrictly'.tr), | ||||
|           subtitle: Text('updateCheckStrictlyDesc'.tr), | ||||
|           value: _prefs?.getBool('check_update_strictly') ?? false, | ||||
|           onChanged: (value) { | ||||
|             _prefs?.setBool('check_update_strictly', value ?? false).then((_) { | ||||
|               setState(() {}); | ||||
|             }); | ||||
|           }, | ||||
|         ), | ||||
|         Obx(() { | ||||
|           final AuthProvider auth = Get.find<AuthProvider>(); | ||||
|           if (!auth.isAuthorized.value) return const SizedBox.shrink(); | ||||
|           return Column( | ||||
|             children: [ | ||||
|               _buildCaptionHeader('account'.tr), | ||||
|               ListTile( | ||||
|                 leading: const Icon(Icons.flag), | ||||
|                 trailing: const Icon(Icons.chevron_right), | ||||
|                 contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|                 title: Text('reportAbuse'.tr), | ||||
|                 subtitle: Text('reportAbuseDesc'.tr), | ||||
|                 onTap: () { | ||||
|                   showDialog( | ||||
|                     context: context, | ||||
|                     builder: (context) => const AbuseReportDialog(), | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|               ListTile( | ||||
|                 leading: const Icon(Icons.person_remove), | ||||
|                 trailing: const Icon(Icons.chevron_right), | ||||
|                 contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|                 title: Text('accountDeletion'.tr), | ||||
|                 subtitle: Text('accountDeletionDesc'.tr), | ||||
|                 onTap: () { | ||||
|                   context | ||||
|                       .showSlideToConfirmDialog( | ||||
|                     'accountDeletionConfirm'.tr, | ||||
|                     'accountDeletionConfirmDesc'.trParams({ | ||||
|                       'account': '@${auth.userProfile.value!['name']}', | ||||
|                     }), | ||||
|                   ) | ||||
|                       .then((value) async { | ||||
|                     if (value != true) return; | ||||
|                     final client = await auth.configureClient('id'); | ||||
|                     final resp = await client.post('/users/me/deletion', {}); | ||||
|                     if (resp.statusCode != 200) { | ||||
|                       context.showErrorDialog(RequestException(resp)); | ||||
|                     } else { | ||||
|                       context.showSnackbar('accountDeletionRequested'.tr); | ||||
|                     } | ||||
|                   }); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         }), | ||||
|         _buildCaptionHeader('performance'.tr), | ||||
|         CheckboxListTile( | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|           secondary: const Icon(Icons.message), | ||||
|           title: Text('animatedMessageList'.tr), | ||||
|           subtitle: Text('animatedMessageListDesc'.tr), | ||||
|           value: _prefs?.getBool('non_animated_message_list') ?? false, | ||||
|           onChanged: (value) { | ||||
|             _prefs | ||||
|                 ?.setBool('non_animated_message_list', value ?? false) | ||||
|                 .then((_) { | ||||
|               setState(() {}); | ||||
|             }); | ||||
|           }, | ||||
|         ), | ||||
|         _buildCaptionHeader('more'.tr), | ||||
|         ListTile( | ||||
|           leading: const Icon(Icons.delete_sweep), | ||||
|           trailing: const Icon(Icons.chevron_right), | ||||
|           subtitle: FutureBuilder( | ||||
|             future: AppDatabase.getDatabaseSize(), | ||||
|             builder: (context, snapshot) { | ||||
|               if (!snapshot.hasData) { | ||||
|                 return Text('localDatabaseSize'.trParams( | ||||
|                   {'size': snapshot.data!.formatBytes()}, | ||||
|                   {'size': 'unknown'.tr}, | ||||
|                 )); | ||||
|               }, | ||||
|             ), | ||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|             title: Text('localDatabaseWipe'.tr), | ||||
|             onTap: () { | ||||
|               AppDatabase.removeDatabase().then((_) { | ||||
|                 setState(() {}); | ||||
|               }); | ||||
|               } | ||||
|               return Text('localDatabaseSize'.trParams( | ||||
|                 {'size': snapshot.data!.formatBytes()}, | ||||
|               )); | ||||
|             }, | ||||
|           ), | ||||
|           if (PlatformInfo.canRateTheApp) | ||||
|             ListTile( | ||||
|               leading: const Icon(Icons.star), | ||||
|               trailing: const Icon(Icons.chevron_right), | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|               title: Text('rateTheApp'.tr), | ||||
|               subtitle: Text('rateTheAppDesc'.tr), | ||||
|               onTap: () { | ||||
|                 final inAppReview = InAppReview.instance; | ||||
|  | ||||
|                 inAppReview.openStoreListing( | ||||
|                   appStoreId: '6499032345', | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|           title: Text('localDatabaseWipe'.tr), | ||||
|           onTap: () { | ||||
|             AppDatabase.removeDatabase().then((_) { | ||||
|               setState(() {}); | ||||
|             }); | ||||
|           }, | ||||
|         ), | ||||
|         if (PlatformInfo.canRateTheApp) | ||||
|           ListTile( | ||||
|             leading: const Icon(Icons.info_outline), | ||||
|             leading: const Icon(Icons.star), | ||||
|             trailing: const Icon(Icons.chevron_right), | ||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|             title: Text('about'.tr), | ||||
|             title: Text('rateTheApp'.tr), | ||||
|             subtitle: Text('rateTheAppDesc'.tr), | ||||
|             onTap: () { | ||||
|               AppRouter.instance.pushNamed('about'); | ||||
|               final inAppReview = InAppReview.instance; | ||||
|  | ||||
|               inAppReview.openStoreListing( | ||||
|                 appStoreId: '6499032345', | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|         ListTile( | ||||
|           leading: const Icon(Icons.info_outline), | ||||
|           trailing: const Icon(Icons.chevron_right), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 22), | ||||
|           title: Text('about'.tr), | ||||
|           onTap: () { | ||||
|             AppRouter.instance.pushNamed('about'); | ||||
|           }, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -39,10 +39,13 @@ abstract class AppTheme { | ||||
|         brightness: brightness, | ||||
|         seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1), | ||||
|       ), | ||||
|       scaffoldBackgroundColor: Colors.transparent, | ||||
|       snackBarTheme: const SnackBarThemeData( | ||||
|         behavior: SnackBarBehavior.floating, | ||||
|       ), | ||||
|       scaffoldBackgroundColor: Colors.transparent, | ||||
|       appBarTheme: const AppBarTheme( | ||||
|         backgroundColor: Colors.transparent, | ||||
|       ), | ||||
|       fontFamily: 'Comfortaa', | ||||
|       fontFamilyFallback: [ | ||||
|         'NotoSansSC', | ||||
| @@ -74,6 +77,7 @@ abstract class AppTheme { | ||||
|         behavior: SnackBarBehavior.floating, | ||||
|       ), | ||||
|       scaffoldBackgroundColor: Colors.transparent, | ||||
|       appBarTheme: const AppBarTheme(backgroundColor: Colors.transparent), | ||||
|       fontFamily: data.fontFamily ?? 'Comfortaa', | ||||
|       fontFamilyFallback: data.fontFamilyFallback ?? | ||||
|           [ | ||||
|   | ||||
| @@ -43,6 +43,10 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|       if (isAutoWarp) { | ||||
|         paragraph = paragraph.replaceAll('\n', '\\\n'); | ||||
|       } | ||||
|       const charactersToTrim = '\\\n\t\r '; | ||||
|       final trimPattern = | ||||
|           RegExp('^[$charactersToTrim]+|[$charactersToTrim]+\$'); | ||||
|       paragraph = paragraph.trim().replaceAll(trimPattern, ''); | ||||
|  | ||||
|       // Matching stickers | ||||
|       final stickerMatch = stickerRegex.allMatches(paragraph); | ||||
| @@ -184,7 +188,7 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|       ); | ||||
|  | ||||
|       if (idx < paragraphs.length - 1) { | ||||
|         contentWidgets.add(const Gap(4)); | ||||
|         contentWidgets.add(isAutoWarp ? const Gap(4) : const Gap(8)); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user