From 1bcebbacc83728e40b67798d906aeac1580043b8 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Tue, 10 Oct 2023 10:57:14 +0200 Subject: [PATCH] Fix visual issues with bottom navigation bar --- lib/main.dart | 40 +- lib/widgets/friends/friends_list_app_bar.dart | 4 - lib/widgets/friends/user_search.dart | 137 ++-- .../inventory/inventory_browser_app_bar.dart | 6 - .../messages/message_attachment_list.dart | 6 +- lib/widgets/messages/message_input_bar.dart | 653 +++++++++--------- .../sessions/session_list_app_bar.dart | 3 - lib/widgets/settings_app_bar.dart | 3 - 8 files changed, 429 insertions(+), 423 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 6bcdbb8..ddf6bee 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,16 +1,5 @@ import 'dart:developer'; -import 'package:recon/apis/github_api.dart'; -import 'package:recon/client_holder.dart'; -import 'package:recon/clients/api_client.dart'; -import 'package:recon/clients/inventory_client.dart'; -import 'package:recon/clients/messaging_client.dart'; -import 'package:recon/clients/session_client.dart'; -import 'package:recon/clients/settings_client.dart'; -import 'package:recon/models/sem_ver.dart'; -import 'package:recon/widgets/homepage.dart'; -import 'package:recon/widgets/login_screen.dart'; -import 'package:recon/widgets/update_notifier.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -22,6 +11,18 @@ import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; +import 'package:recon/apis/github_api.dart'; +import 'package:recon/client_holder.dart'; +import 'package:recon/clients/api_client.dart'; +import 'package:recon/clients/inventory_client.dart'; +import 'package:recon/clients/messaging_client.dart'; +import 'package:recon/clients/session_client.dart'; +import 'package:recon/clients/settings_client.dart'; +import 'package:recon/models/sem_ver.dart'; +import 'package:recon/widgets/homepage.dart'; +import 'package:recon/widgets/login_screen.dart'; +import 'package:recon/widgets/update_notifier.dart'; + import 'models/authentication_data.dart'; void main() async { @@ -31,6 +32,16 @@ void main() async { debug: kDebugMode, ); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + systemStatusBarContrastEnforced: true, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarDividerColor: Colors.transparent, + ), + ); + + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: [SystemUiOverlay.top]); + await Hive.initFlutter(); final dateFormat = DateFormat.Hms(); @@ -168,7 +179,12 @@ class _ReConState extends State { ), ) ], - child: const Home(), + child: AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Theme.of(context).colorScheme.surfaceVariant, + ), + child: const Home(), + ), ) : LoginScreen( onLoginSuccessful: (AuthenticationData authData) async { diff --git a/lib/widgets/friends/friends_list_app_bar.dart b/lib/widgets/friends/friends_list_app_bar.dart index 45bf404..91fc964 100644 --- a/lib/widgets/friends/friends_list_app_bar.dart +++ b/lib/widgets/friends/friends_list_app_bar.dart @@ -4,7 +4,6 @@ import 'package:recon/models/users/online_status.dart'; import 'package:recon/widgets/friends/user_search.dart'; import 'package:recon/widgets/my_profile_dialog.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -22,9 +21,6 @@ class _FriendsListAppBarState extends State with AutomaticKee super.build(context); return AppBar( title: const Text("ReCon"), - systemOverlayStyle: SystemUiOverlayStyle( - systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor, - ), actions: [ Consumer(builder: (context, client, _) { return PopupMenuButton( diff --git a/lib/widgets/friends/user_search.dart b/lib/widgets/friends/user_search.dart index b1fa175..fa36fe2 100644 --- a/lib/widgets/friends/user_search.dart +++ b/lib/widgets/friends/user_search.dart @@ -58,78 +58,81 @@ class _UserSearchState extends State { appBar: AppBar( title: const Text("Find Users"), ), - body: Column( - children: [ - Expanded( - child: FutureBuilder( - future: _usersFuture, - builder: (context, snapshot) { - if (snapshot.hasData) { - final users = snapshot.data as List; - return ListView.builder( - itemCount: users.length, - itemBuilder: (context, index) { - final user = users[index]; - return UserListTile(user: user, onChanged: () { - mClient.refreshFriendsList(); - }, isFriend: mClient.getAsFriend(user.id) != null,); - }, - ); - } else if (snapshot.hasError) { - final err = snapshot.error; - if (err is SearchError) { - return DefaultErrorWidget( - title: err.message, - iconOverride: err.icon, + body: SafeArea( + top: false, + child: Column( + children: [ + Expanded( + child: FutureBuilder( + future: _usersFuture, + builder: (context, snapshot) { + if (snapshot.hasData) { + final users = snapshot.data as List; + return ListView.builder( + itemCount: users.length, + itemBuilder: (context, index) { + final user = users[index]; + return UserListTile(user: user, onChanged: () { + mClient.refreshFriendsList(); + }, isFriend: mClient.getAsFriend(user.id) != null,); + }, ); + } else if (snapshot.hasError) { + final err = snapshot.error; + if (err is SearchError) { + return DefaultErrorWidget( + title: err.message, + iconOverride: err.icon, + ); + } else { + FlutterError.reportError( + FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace)); + return DefaultErrorWidget(title: "${snapshot.error}",); + } } else { - FlutterError.reportError( - FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace)); - return DefaultErrorWidget(title: "${snapshot.error}",); + return const Column( + children: [ + LinearProgressIndicator(), + ], + ); } - } else { - return const Column( - children: [ - LinearProgressIndicator(), - ], - ); - } - }, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: TextField( - decoration: InputDecoration( - isDense: true, - hintText: "Search for users...", - contentPadding: const EdgeInsets.all(16), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24) - ) + }, ), - autocorrect: false, - controller: _searchInputController, - onChanged: (String value) { - _searchDebouncer?.cancel(); - if (value.isEmpty) { - setState(() { - _querySearch(context, value); - }); - return; - } - setState(() { - _usersFuture = Future(() => null); - }); - _searchDebouncer = Timer(const Duration(milliseconds: 300), () { - setState(() { - _querySearch(context, value); - }); - }); - }, ), - ), - ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: TextField( + decoration: InputDecoration( + isDense: true, + hintText: "Search for users...", + contentPadding: const EdgeInsets.all(16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24) + ) + ), + autocorrect: false, + controller: _searchInputController, + onChanged: (String value) { + _searchDebouncer?.cancel(); + if (value.isEmpty) { + setState(() { + _querySearch(context, value); + }); + return; + } + setState(() { + _usersFuture = Future(() => null); + }); + _searchDebouncer = Timer(const Duration(milliseconds: 300), () { + setState(() { + _querySearch(context, value); + }); + }); + }, + ), + ), + ], + ), ), ); } diff --git a/lib/widgets/inventory/inventory_browser_app_bar.dart b/lib/widgets/inventory/inventory_browser_app_bar.dart index 4100ef0..472181d 100644 --- a/lib/widgets/inventory/inventory_browser_app_bar.dart +++ b/lib/widgets/inventory/inventory_browser_app_bar.dart @@ -61,16 +61,10 @@ class _InventoryBrowserAppBarState extends State { ? AppBar( key: const ValueKey("default-appbar"), title: const Text("Inventory"), - systemOverlayStyle: SystemUiOverlayStyle( - systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor, - ), ) : AppBar( key: const ValueKey("selection-appbar"), title: Text("${iClient.selectedRecordCount} Selected"), - systemOverlayStyle: SystemUiOverlayStyle( - systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor, - ), leading: IconButton( onPressed: () { iClient.clearSelectedRecords(); diff --git a/lib/widgets/messages/message_attachment_list.dart b/lib/widgets/messages/message_attachment_list.dart index 21480ca..1efd8de 100644 --- a/lib/widgets/messages/message_attachment_list.dart +++ b/lib/widgets/messages/message_attachment_list.dart @@ -56,7 +56,7 @@ class _MessageAttachmentListState extends State { colors: [Colors.transparent, Colors.transparent, Colors.transparent, Theme .of(context) .colorScheme - .background + .surfaceVariant ], stops: [0.0, 0.0, _showShadow ? 0.90 : 1.0, 1.0], // 10% purple, 80% transparent, 10% purple ).createShader(bounds); @@ -102,7 +102,7 @@ class _MessageAttachmentListState extends State { foregroundColor: Theme .of(context) .colorScheme - .onBackground, + .onSurfaceVariant, side: BorderSide( color: Theme .of(context) @@ -245,7 +245,7 @@ class _MessageAttachmentListState extends State { ) : const SizedBox.shrink(), ), Container( - color: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.surfaceVariant, child: IconButton(onPressed: () { setState(() { _popupIsOpen = !_popupIsOpen; diff --git a/lib/widgets/messages/message_input_bar.dart b/lib/widgets/messages/message_input_bar.dart index a2e3331..ed67e93 100644 --- a/lib/widgets/messages/message_input_bar.dart +++ b/lib/widgets/messages/message_input_bar.dart @@ -219,350 +219,353 @@ class _MessageInputBarState extends State { .surfaceVariant, ), padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - children: [ - if (_isSending && _sendProgress != null) - LinearProgressIndicator(value: _sendProgress), - Container( - decoration: BoxDecoration( - color: Theme - .of(context) - .colorScheme - .background, - ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - switchInCurve: Curves.easeOut, - switchOutCurve: Curves.easeOut, - transitionBuilder: (Widget child, animation) => - SizeTransition(sizeFactor: animation, child: child,), - child: switch ((_attachmentPickerOpen, _loadedFiles)) { - (true, []) => - Row( - key: const ValueKey("attachment-picker"), - children: [ - TextButton.icon( - onPressed: _isSending ? null : () async { - final result = await FilePicker.platform.pickFiles( - type: FileType.image, allowMultiple: true); - if (result != null) { - setState(() { - _loadedFiles.addAll( - result.files.map((e) => - e.path != null ? (FileType.image, File(e.path!)) : null) - .whereNotNull()); - }); - } - }, - icon: const Icon(Icons.image), - label: const Text("Gallery"), - ), - TextButton.icon( - onPressed: _isSending ? null : () async { - final picture = await _imagePicker.pickImage(source: ImageSource.camera); - if (picture == null) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to get image path"))); - } - return; - } - final file = File(picture.path); - if (await file.exists()) { - setState(() { - _loadedFiles.add((FileType.image, file)); - }); - } else { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to load image file"))); - } - } - - }, - icon: const Icon(Icons.camera), - label: const Text("Camera"), - ), - TextButton.icon( - onPressed: _isSending ? null : () async { - final result = await FilePicker.platform.pickFiles( - type: FileType.any, allowMultiple: true); - if (result != null) { - setState(() { - _loadedFiles.addAll( - result.files.map((e) => - e.path != null ? (FileType.any, File(e.path!)) : null) - .whereNotNull()); - }); - } - }, - icon: const Icon(Icons.file_present_rounded), - label: const Text("Document"), - ), - ], - ), - (false, []) => null, - (_, _) => - MessageAttachmentList( - disabled: _isSending, - initialFiles: _loadedFiles, - onChange: (List<(FileType, File)> loadedFiles) => setState(() { - _loadedFiles.clear(); - _loadedFiles.addAll(loadedFiles); - }), + child: SafeArea( + top: false, + child: Column( + children: [ + if (_isSending && _sendProgress != null) + LinearProgressIndicator(value: _sendProgress), + Container( + decoration: BoxDecoration( + color: Theme + .of(context) + .colorScheme + .surfaceVariant, ), - }, - ), - ), - Row( - children: [ - AnimatedSwitcher( + child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), - transitionBuilder: (Widget child, Animation animation) => - FadeTransition( - opacity: animation, - child: RotationTransition( - turns: Tween(begin: 0.6, end: 1).animate(animation), - child: child, - ), - ), - child: switch((_attachmentPickerOpen, _isRecording)) { - (_, true) => IconButton( - onPressed: () { + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, animation) => + SizeTransition(sizeFactor: animation, child: child,), + child: switch ((_attachmentPickerOpen, _loadedFiles)) { + (true, []) => + Row( + key: const ValueKey("attachment-picker"), + children: [ + TextButton.icon( + onPressed: _isSending ? null : () async { + final result = await FilePicker.platform.pickFiles( + type: FileType.image, allowMultiple: true); + if (result != null) { + setState(() { + _loadedFiles.addAll( + result.files.map((e) => + e.path != null ? (FileType.image, File(e.path!)) : null) + .whereNotNull()); + }); + } + }, + icon: const Icon(Icons.image), + label: const Text("Gallery"), + ), + TextButton.icon( + onPressed: _isSending ? null : () async { + final picture = await _imagePicker.pickImage(source: ImageSource.camera); + if (picture == null) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to get image path"))); + } + return; + } + final file = File(picture.path); + if (await file.exists()) { + setState(() { + _loadedFiles.add((FileType.image, file)); + }); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to load image file"))); + } + } - }, - icon: Icon(Icons.delete, color: _recordingCancelled ? Theme.of(context).colorScheme.error : null,), - ), - (false, _) => IconButton( - key: const ValueKey("add-attachment-icon"), - onPressed: _isSending ? null : () { - setState(() { - _attachmentPickerOpen = true; - }); - }, - icon: const Icon(Icons.attach_file,), - ), - (true, _) => IconButton( - key: const ValueKey("remove-attachment-icon"), - onPressed: _isSending ? null : () async { - if (_loadedFiles.isNotEmpty) { - await showDialog(context: context, builder: (context) => - AlertDialog( - title: const Text("Remove all attachments"), - content: const Text("This will remove all attachments, are you sure?"), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("No"), - ), - TextButton( - onPressed: () { - setState(() { - _loadedFiles.clear(); - _attachmentPickerOpen = false; - }); - Navigator.of(context).pop(); - }, - child: const Text("Yes"), - ) - ], - )); - } else { - setState(() { - _attachmentPickerOpen = false; - }); - } - }, - icon: const Icon(Icons.close,), + }, + icon: const Icon(Icons.camera), + label: const Text("Camera"), + ), + TextButton.icon( + onPressed: _isSending ? null : () async { + final result = await FilePicker.platform.pickFiles( + type: FileType.any, allowMultiple: true); + if (result != null) { + setState(() { + _loadedFiles.addAll( + result.files.map((e) => + e.path != null ? (FileType.any, File(e.path!)) : null) + .whereNotNull()); + }); + } + }, + icon: const Icon(Icons.file_present_rounded), + label: const Text("Document"), + ), + ], + ), + (false, []) => null, + (_, _) => + MessageAttachmentList( + disabled: _isSending, + initialFiles: _loadedFiles, + onChange: (List<(FileType, File)> loadedFiles) => setState(() { + _loadedFiles.clear(); + _loadedFiles.addAll(loadedFiles); + }), ), }, ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), - child: Stack( - children: [ - TextField( - enabled: (!widget.disabled) && !_isSending, - autocorrect: true, - controller: _messageTextController, - showCursor: !_isRecording, - maxLines: 4, - minLines: 1, - onChanged: (text) { - if (text.isEmpty != _currentText.isEmpty) { - setState(() { - _currentText = text; - }); - return; - } - _currentText = text; - }, - style: Theme.of(context).textTheme.bodyLarge, - decoration: InputDecoration( - isDense: true, - hintText: _isRecording ? "" : "Message ${widget.recipient - .username}...", - hintMaxLines: 1, - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - fillColor: Colors.black26, - filled: true, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(24), - ) + ), + Row( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (Widget child, Animation animation) => + FadeTransition( + opacity: animation, + child: RotationTransition( + turns: Tween(begin: 0.6, end: 1).animate(animation), + child: child, ), ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (Widget child, Animation animation) => - FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0, .2), - end: const Offset(0, 0), - ).animate(animation), - child: child, - ), - ), - child: _isRecording ? Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: _recordingCancelled ? Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(width: 8,), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Icon(Icons.cancel, color: Colors.red, size: 16,), - ), - Text("Cancel Recording", style: Theme.of(context).textTheme.titleMedium), - ], - ) : Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(width: 8,), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Icon(Icons.circle, color: Colors.red, size: 16,), - ), - StreamBuilder( - stream: _recordingDurationStream(), - builder: (context, snapshot) { - return Text("Recording: ${snapshot.data?.format()}", style: Theme.of(context).textTheme.titleMedium); - } - ), - ], + child: switch((_attachmentPickerOpen, _isRecording)) { + (_, true) => IconButton( + onPressed: () { + + }, + icon: Icon(Icons.delete, color: _recordingCancelled ? Theme.of(context).colorScheme.error : null,), + ), + (false, _) => IconButton( + key: const ValueKey("add-attachment-icon"), + onPressed: _isSending ? null : () { + setState(() { + _attachmentPickerOpen = true; + }); + }, + icon: const Icon(Icons.attach_file,), + ), + (true, _) => IconButton( + key: const ValueKey("remove-attachment-icon"), + onPressed: _isSending ? null : () async { + if (_loadedFiles.isNotEmpty) { + await showDialog(context: context, builder: (context) => + AlertDialog( + title: const Text("Remove all attachments"), + content: const Text("This will remove all attachments, are you sure?"), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("No"), + ), + TextButton( + onPressed: () { + setState(() { + _loadedFiles.clear(); + _attachmentPickerOpen = false; + }); + Navigator.of(context).pop(); + }, + child: const Text("Yes"), + ) + ], + )); + } else { + setState(() { + _attachmentPickerOpen = false; + }); + } + }, + icon: const Icon(Icons.close,), + ), + }, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + child: Stack( + children: [ + TextField( + enabled: (!widget.disabled) && !_isSending, + autocorrect: true, + controller: _messageTextController, + showCursor: !_isRecording, + maxLines: 4, + minLines: 1, + onChanged: (text) { + if (text.isEmpty != _currentText.isEmpty) { + setState(() { + _currentText = text; + }); + return; + } + _currentText = text; + }, + style: Theme.of(context).textTheme.bodyLarge, + decoration: InputDecoration( + isDense: true, + hintText: _isRecording ? "" : "Message ${widget.recipient + .username}...", + hintMaxLines: 1, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + fillColor: Colors.black26, + filled: true, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(24), + ) ), - ) : const SizedBox.shrink(), - ), - ], + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (Widget child, Animation animation) => + FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0, .2), + end: const Offset(0, 0), + ).animate(animation), + child: child, + ), + ), + child: _isRecording ? Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: _recordingCancelled ? Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 8,), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: Icon(Icons.cancel, color: Colors.red, size: 16,), + ), + Text("Cancel Recording", style: Theme.of(context).textTheme.titleMedium), + ], + ) : Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 8,), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: Icon(Icons.circle, color: Colors.red, size: 16,), + ), + StreamBuilder( + stream: _recordingDurationStream(), + builder: (context, snapshot) { + return Text("Recording: ${snapshot.data?.format()}", style: Theme.of(context).textTheme.titleMedium); + } + ), + ], + ), + ) : const SizedBox.shrink(), + ), + ], + ), ), ), - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (Widget child, Animation animation) => - FadeTransition(opacity: animation, child: RotationTransition( - turns: Tween(begin: 0.5, end: 1).animate(animation), child: child,),), - child: _currentText.isNotEmpty || _loadedFiles.isNotEmpty ? IconButton( - key: const ValueKey("send-button"), - splashRadius: 24, - padding: EdgeInsets.zero, - onPressed: _isSending ? null : () async { - final cHolder = ClientHolder.of(context); - final sMsgnr = ScaffoldMessenger.of(context); - final settings = cHolder.settingsClient.currentSettings; - final toSend = List<(FileType, File)>.from(_loadedFiles); - setState(() { - _isSending = true; - _sendProgress = 0; - _attachmentPickerOpen = false; - _loadedFiles.clear(); - }); - try { - for (int i = 0; i < toSend.length; i++) { - final totalProgress = i / toSend.length; - final file = toSend[i]; - if (file.$1 == FileType.image) { - await sendImageMessage( - cHolder.apiClient, mClient, file.$2, settings.machineId.valueOrDefault, - (progress) => - setState(() { - _sendProgress = totalProgress + progress * 1 / toSend.length; - }), - ); - } else { - await sendRawFileMessage( - cHolder.apiClient, mClient, file.$2, settings.machineId.valueOrDefault, (progress) => - setState(() => - _sendProgress = totalProgress + progress * 1 / toSend.length)); + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (Widget child, Animation animation) => + FadeTransition(opacity: animation, child: RotationTransition( + turns: Tween(begin: 0.5, end: 1).animate(animation), child: child,),), + child: _currentText.isNotEmpty || _loadedFiles.isNotEmpty ? IconButton( + key: const ValueKey("send-button"), + splashRadius: 24, + padding: EdgeInsets.zero, + onPressed: _isSending ? null : () async { + final cHolder = ClientHolder.of(context); + final sMsgnr = ScaffoldMessenger.of(context); + final settings = cHolder.settingsClient.currentSettings; + final toSend = List<(FileType, File)>.from(_loadedFiles); + setState(() { + _isSending = true; + _sendProgress = 0; + _attachmentPickerOpen = false; + _loadedFiles.clear(); + }); + try { + for (int i = 0; i < toSend.length; i++) { + final totalProgress = i / toSend.length; + final file = toSend[i]; + if (file.$1 == FileType.image) { + await sendImageMessage( + cHolder.apiClient, mClient, file.$2, settings.machineId.valueOrDefault, + (progress) => + setState(() { + _sendProgress = totalProgress + progress * 1 / toSend.length; + }), + ); + } else { + await sendRawFileMessage( + cHolder.apiClient, mClient, file.$2, settings.machineId.valueOrDefault, (progress) => + setState(() => + _sendProgress = totalProgress + progress * 1 / toSend.length)); + } } + setState(() { + _sendProgress = null; + }); + + if (_currentText.isNotEmpty) { + await sendTextMessage(cHolder.apiClient, mClient, _messageTextController.text); + } + _messageTextController.clear(); + _currentText = ""; + _loadedFiles.clear(); + _attachmentPickerOpen = false; + } catch (e, s) { + FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); + sMsgnr.showSnackBar(SnackBar(content: Text("Failed to send a message: $e"))); } setState(() { + _isSending = false; _sendProgress = null; }); - - if (_currentText.isNotEmpty) { - await sendTextMessage(cHolder.apiClient, mClient, _messageTextController.text); - } - _messageTextController.clear(); - _currentText = ""; - _loadedFiles.clear(); - _attachmentPickerOpen = false; - } catch (e, s) { - FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); - sMsgnr.showSnackBar(SnackBar(content: Text("Failed to send a message: $e"))); - } - setState(() { - _isSending = false; - _sendProgress = null; - }); - widget.onMessageSent?.call(); - }, - icon: const Icon(Icons.send), - ) : GestureDetector( - onTapUp: (_) { - _recordingCancelled = true; - }, - onTapDown: widget.disabled ? null : (_) async { - HapticFeedback.vibrate(); - final hadToAsk = await Permission.microphone.isDenied; - final hasPermission = !await _recorder.hasPermission(); - if (hasPermission) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text("No permission to record audio."), - )); - } - return; - } - if (hadToAsk) { - // We had to ask for permissions so the user removed their finger from the record button. - return; - } - - final dir = await getTemporaryDirectory(); - await _recorder.start( - path: "${dir.path}/A-${const Uuid().v4()}.wav", - encoder: AudioEncoder.wav, - samplingRate: 44100 - ); - setState(() { - _isRecording = true; - }); - }, - child: IconButton( - icon: const Icon(Icons.mic_outlined), - onPressed: _isSending ? null : () { - // Empty onPressed for that sweet sweet ripple effect + widget.onMessageSent?.call(); }, + icon: const Icon(Icons.send), + ) : GestureDetector( + onTapUp: (_) { + _recordingCancelled = true; + }, + onTapDown: widget.disabled ? null : (_) async { + HapticFeedback.vibrate(); + final hadToAsk = await Permission.microphone.isDenied; + final hasPermission = !await _recorder.hasPermission(); + if (hasPermission) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("No permission to record audio."), + )); + } + return; + } + if (hadToAsk) { + // We had to ask for permissions so the user removed their finger from the record button. + return; + } + + final dir = await getTemporaryDirectory(); + await _recorder.start( + path: "${dir.path}/A-${const Uuid().v4()}.wav", + encoder: AudioEncoder.wav, + samplingRate: 44100 + ); + setState(() { + _isRecording = true; + }); + }, + child: IconButton( + icon: const Icon(Icons.mic_outlined), + onPressed: _isSending ? null : () { + // Empty onPressed for that sweet sweet ripple effect + }, + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ); diff --git a/lib/widgets/sessions/session_list_app_bar.dart b/lib/widgets/sessions/session_list_app_bar.dart index fb67ca4..c09b415 100644 --- a/lib/widgets/sessions/session_list_app_bar.dart +++ b/lib/widgets/sessions/session_list_app_bar.dart @@ -16,9 +16,6 @@ class _SessionListAppBarState extends State { Widget build(BuildContext context) { return AppBar( title: const Text("Sessions"), - systemOverlayStyle: SystemUiOverlayStyle( - systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor, - ), actions: [ Padding( padding: const EdgeInsets.only(right: 4.0), diff --git a/lib/widgets/settings_app_bar.dart b/lib/widgets/settings_app_bar.dart index 3a33243..0a7846c 100644 --- a/lib/widgets/settings_app_bar.dart +++ b/lib/widgets/settings_app_bar.dart @@ -8,9 +8,6 @@ class SettingsAppBar extends StatelessWidget { Widget build(BuildContext context) { return AppBar( title: const Text("Settings"), - systemOverlayStyle: SystemUiOverlayStyle( - systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor, - ), ); } }