Fix visual issues with bottom navigation bar

This commit is contained in:
Nutcake 2023-10-10 10:57:14 +02:00
parent 1c77951431
commit 1bcebbacc8
8 changed files with 429 additions and 423 deletions

View file

@ -1,16 +1,5 @@
import 'dart:developer'; 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:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -22,6 +11,18 @@ import 'package:intl/intl.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.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'; import 'models/authentication_data.dart';
void main() async { void main() async {
@ -31,6 +32,16 @@ void main() async {
debug: kDebugMode, debug: kDebugMode,
); );
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
systemStatusBarContrastEnforced: true,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
),
);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: [SystemUiOverlay.top]);
await Hive.initFlutter(); await Hive.initFlutter();
final dateFormat = DateFormat.Hms(); final dateFormat = DateFormat.Hms();
@ -168,7 +179,12 @@ class _ReConState extends State<ReCon> {
), ),
) )
], ],
child: const Home(), child: AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Theme.of(context).colorScheme.surfaceVariant,
),
child: const Home(),
),
) )
: LoginScreen( : LoginScreen(
onLoginSuccessful: (AuthenticationData authData) async { onLoginSuccessful: (AuthenticationData authData) async {

View file

@ -4,7 +4,6 @@ import 'package:recon/models/users/online_status.dart';
import 'package:recon/widgets/friends/user_search.dart'; import 'package:recon/widgets/friends/user_search.dart';
import 'package:recon/widgets/my_profile_dialog.dart'; import 'package:recon/widgets/my_profile_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -22,9 +21,6 @@ class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKee
super.build(context); super.build(context);
return AppBar( return AppBar(
title: const Text("ReCon"), title: const Text("ReCon"),
systemOverlayStyle: SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
),
actions: [ actions: [
Consumer<MessagingClient>(builder: (context, client, _) { Consumer<MessagingClient>(builder: (context, client, _) {
return PopupMenuButton<OnlineStatus>( return PopupMenuButton<OnlineStatus>(

View file

@ -58,78 +58,81 @@ class _UserSearchState extends State<UserSearch> {
appBar: AppBar( appBar: AppBar(
title: const Text("Find Users"), title: const Text("Find Users"),
), ),
body: Column( body: SafeArea(
children: [ top: false,
Expanded( child: Column(
child: FutureBuilder( children: [
future: _usersFuture, Expanded(
builder: (context, snapshot) { child: FutureBuilder(
if (snapshot.hasData) { future: _usersFuture,
final users = snapshot.data as List<User>; builder: (context, snapshot) {
return ListView.builder( if (snapshot.hasData) {
itemCount: users.length, final users = snapshot.data as List<User>;
itemBuilder: (context, index) { return ListView.builder(
final user = users[index]; itemCount: users.length,
return UserListTile(user: user, onChanged: () { itemBuilder: (context, index) {
mClient.refreshFriendsList(); final user = users[index];
}, isFriend: mClient.getAsFriend(user.id) != null,); 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 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 { } else {
FlutterError.reportError( return const Column(
FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace)); children: [
return DefaultErrorWidget(title: "${snapshot.error}",); 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);
});
});
},
),
),
],
),
), ),
); );
} }

View file

@ -61,16 +61,10 @@ class _InventoryBrowserAppBarState extends State<InventoryBrowserAppBar> {
? AppBar( ? AppBar(
key: const ValueKey("default-appbar"), key: const ValueKey("default-appbar"),
title: const Text("Inventory"), title: const Text("Inventory"),
systemOverlayStyle: SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
),
) )
: AppBar( : AppBar(
key: const ValueKey("selection-appbar"), key: const ValueKey("selection-appbar"),
title: Text("${iClient.selectedRecordCount} Selected"), title: Text("${iClient.selectedRecordCount} Selected"),
systemOverlayStyle: SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
),
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
iClient.clearSelectedRecords(); iClient.clearSelectedRecords();

View file

@ -56,7 +56,7 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
colors: [Colors.transparent, Colors.transparent, Colors.transparent, Theme colors: [Colors.transparent, Colors.transparent, Colors.transparent, Theme
.of(context) .of(context)
.colorScheme .colorScheme
.background .surfaceVariant
], ],
stops: [0.0, 0.0, _showShadow ? 0.90 : 1.0, 1.0], // 10% purple, 80% transparent, 10% purple stops: [0.0, 0.0, _showShadow ? 0.90 : 1.0, 1.0], // 10% purple, 80% transparent, 10% purple
).createShader(bounds); ).createShader(bounds);
@ -102,7 +102,7 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
foregroundColor: Theme foregroundColor: Theme
.of(context) .of(context)
.colorScheme .colorScheme
.onBackground, .onSurfaceVariant,
side: BorderSide( side: BorderSide(
color: Theme color: Theme
.of(context) .of(context)
@ -245,7 +245,7 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
) : const SizedBox.shrink(), ) : const SizedBox.shrink(),
), ),
Container( Container(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surfaceVariant,
child: IconButton(onPressed: () { child: IconButton(onPressed: () {
setState(() { setState(() {
_popupIsOpen = !_popupIsOpen; _popupIsOpen = !_popupIsOpen;

View file

@ -219,350 +219,353 @@ class _MessageInputBarState extends State<MessageInputBar> {
.surfaceVariant, .surfaceVariant,
), ),
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: Column( child: SafeArea(
children: [ top: false,
if (_isSending && _sendProgress != null) child: Column(
LinearProgressIndicator(value: _sendProgress), children: [
Container( if (_isSending && _sendProgress != null)
decoration: BoxDecoration( LinearProgressIndicator(value: _sendProgress),
color: Theme Container(
.of(context) decoration: BoxDecoration(
.colorScheme color: Theme
.background, .of(context)
), .colorScheme
child: AnimatedSwitcher( .surfaceVariant,
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: AnimatedSwitcher(
),
),
Row(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) => switchInCurve: Curves.easeOut,
FadeTransition( switchOutCurve: Curves.easeOut,
opacity: animation, transitionBuilder: (Widget child, animation) =>
child: RotationTransition( SizeTransition(sizeFactor: animation, child: child,),
turns: Tween<double>(begin: 0.6, end: 1).animate(animation), child: switch ((_attachmentPickerOpen, _loadedFiles)) {
child: child, (true, []) =>
), Row(
), key: const ValueKey("attachment-picker"),
child: switch((_attachmentPickerOpen, _isRecording)) { children: [
(_, true) => IconButton( TextButton.icon(
onPressed: () { 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,), icon: const Icon(Icons.camera),
), label: const Text("Camera"),
(false, _) => IconButton( ),
key: const ValueKey("add-attachment-icon"), TextButton.icon(
onPressed: _isSending ? null : () { onPressed: _isSending ? null : () async {
setState(() { final result = await FilePicker.platform.pickFiles(
_attachmentPickerOpen = true; type: FileType.any, allowMultiple: true);
}); if (result != null) {
}, setState(() {
icon: const Icon(Icons.attach_file,), _loadedFiles.addAll(
), result.files.map((e) =>
(true, _) => IconButton( e.path != null ? (FileType.any, File(e.path!)) : null)
key: const ValueKey("remove-attachment-icon"), .whereNotNull());
onPressed: _isSending ? null : () async { });
if (_loadedFiles.isNotEmpty) { }
await showDialog(context: context, builder: (context) => },
AlertDialog( icon: const Icon(Icons.file_present_rounded),
title: const Text("Remove all attachments"), label: const Text("Document"),
content: const Text("This will remove all attachments, are you sure?"), ),
actions: [ ],
TextButton( ),
onPressed: () { (false, []) => null,
Navigator.of(context).pop(); (_, _) =>
}, MessageAttachmentList(
child: const Text("No"), disabled: _isSending,
), initialFiles: _loadedFiles,
TextButton( onChange: (List<(FileType, File)> loadedFiles) => setState(() {
onPressed: () { _loadedFiles.clear();
setState(() { _loadedFiles.addAll(loadedFiles);
_loadedFiles.clear(); }),
_attachmentPickerOpen = false;
});
Navigator.of(context).pop();
},
child: const Text("Yes"),
)
],
));
} else {
setState(() {
_attachmentPickerOpen = false;
});
}
},
icon: const Icon(Icons.close,),
), ),
}, },
), ),
Expanded( ),
child: Padding( Row(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), children: [
child: Stack( AnimatedSwitcher(
children: [ duration: const Duration(milliseconds: 200),
TextField( transitionBuilder: (Widget child, Animation<double> animation) =>
enabled: (!widget.disabled) && !_isSending, FadeTransition(
autocorrect: true, opacity: animation,
controller: _messageTextController, child: RotationTransition(
showCursor: !_isRecording, turns: Tween<double>(begin: 0.6, end: 1).animate(animation),
maxLines: 4, child: child,
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),
)
), ),
), ),
AnimatedSwitcher( child: switch((_attachmentPickerOpen, _isRecording)) {
duration: const Duration(milliseconds: 200), (_, true) => IconButton(
transitionBuilder: (Widget child, Animation<double> animation) => onPressed: () {
FadeTransition(
opacity: animation, },
child: SlideTransition( icon: Icon(Icons.delete, color: _recordingCancelled ? Theme.of(context).colorScheme.error : null,),
position: Tween<Offset>( ),
begin: const Offset(0, .2), (false, _) => IconButton(
end: const Offset(0, 0), key: const ValueKey("add-attachment-icon"),
).animate(animation), onPressed: _isSending ? null : () {
child: child, setState(() {
), _attachmentPickerOpen = true;
), });
child: _isRecording ? Padding( },
padding: const EdgeInsets.symmetric(vertical: 12.0), icon: const Icon(Icons.attach_file,),
child: _recordingCancelled ? Row( ),
mainAxisAlignment: MainAxisAlignment.start, (true, _) => IconButton(
children: [ key: const ValueKey("remove-attachment-icon"),
const SizedBox(width: 8,), onPressed: _isSending ? null : () async {
const Padding( if (_loadedFiles.isNotEmpty) {
padding: EdgeInsets.symmetric(horizontal: 8.0), await showDialog(context: context, builder: (context) =>
child: Icon(Icons.cancel, color: Colors.red, size: 16,), AlertDialog(
), title: const Text("Remove all attachments"),
Text("Cancel Recording", style: Theme.of(context).textTheme.titleMedium), content: const Text("This will remove all attachments, are you sure?"),
], actions: [
) : Row( TextButton(
mainAxisAlignment: MainAxisAlignment.start, onPressed: () {
children: [ Navigator.of(context).pop();
const SizedBox(width: 8,), },
const Padding( child: const Text("No"),
padding: EdgeInsets.symmetric(horizontal: 8.0), ),
child: Icon(Icons.circle, color: Colors.red, size: 16,), TextButton(
), onPressed: () {
StreamBuilder<Duration>( setState(() {
stream: _recordingDurationStream(), _loadedFiles.clear();
builder: (context, snapshot) { _attachmentPickerOpen = false;
return Text("Recording: ${snapshot.data?.format()}", style: Theme.of(context).textTheme.titleMedium); });
} 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<double> animation) =>
FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
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<Duration>(
stream: _recordingDurationStream(),
builder: (context, snapshot) {
return Text("Recording: ${snapshot.data?.format()}", style: Theme.of(context).textTheme.titleMedium);
}
),
],
),
) : const SizedBox.shrink(),
),
],
),
), ),
), ),
), AnimatedSwitcher(
AnimatedSwitcher( duration: const Duration(milliseconds: 200),
duration: const Duration(milliseconds: 200), transitionBuilder: (Widget child, Animation<double> animation) =>
transitionBuilder: (Widget child, Animation<double> animation) => FadeTransition(opacity: animation, child: RotationTransition(
FadeTransition(opacity: animation, child: RotationTransition( turns: Tween<double>(begin: 0.5, end: 1).animate(animation), child: child,),),
turns: Tween<double>(begin: 0.5, end: 1).animate(animation), child: child,),), child: _currentText.isNotEmpty || _loadedFiles.isNotEmpty ? IconButton(
child: _currentText.isNotEmpty || _loadedFiles.isNotEmpty ? IconButton( key: const ValueKey("send-button"),
key: const ValueKey("send-button"), splashRadius: 24,
splashRadius: 24, padding: EdgeInsets.zero,
padding: EdgeInsets.zero, onPressed: _isSending ? null : () async {
onPressed: _isSending ? null : () async { final cHolder = ClientHolder.of(context);
final cHolder = ClientHolder.of(context); final sMsgnr = ScaffoldMessenger.of(context);
final sMsgnr = ScaffoldMessenger.of(context); final settings = cHolder.settingsClient.currentSettings;
final settings = cHolder.settingsClient.currentSettings; final toSend = List<(FileType, File)>.from(_loadedFiles);
final toSend = List<(FileType, File)>.from(_loadedFiles); setState(() {
setState(() { _isSending = true;
_isSending = true; _sendProgress = 0;
_sendProgress = 0; _attachmentPickerOpen = false;
_attachmentPickerOpen = false; _loadedFiles.clear();
_loadedFiles.clear(); });
}); try {
try { for (int i = 0; i < toSend.length; i++) {
for (int i = 0; i < toSend.length; i++) { final totalProgress = i / toSend.length;
final totalProgress = i / toSend.length; final file = toSend[i];
final file = toSend[i]; if (file.$1 == FileType.image) {
if (file.$1 == FileType.image) { await sendImageMessage(
await sendImageMessage( cHolder.apiClient, mClient, file.$2, settings.machineId.valueOrDefault,
cHolder.apiClient, mClient, file.$2, settings.machineId.valueOrDefault, (progress) =>
(progress) => setState(() {
setState(() { _sendProgress = totalProgress + progress * 1 / toSend.length;
_sendProgress = totalProgress + progress * 1 / toSend.length; }),
}), );
); } else {
} else { await sendRawFileMessage(
await sendRawFileMessage( cHolder.apiClient, mClient, file.$2, settings.machineId.valueOrDefault, (progress) =>
cHolder.apiClient, mClient, file.$2, settings.machineId.valueOrDefault, (progress) => setState(() =>
setState(() => _sendProgress = totalProgress + progress * 1 / toSend.length));
_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(() { setState(() {
_isSending = false;
_sendProgress = null; _sendProgress = null;
}); });
widget.onMessageSent?.call();
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
}, },
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
},
),
), ),
), ),
), ],
], ),
), ],
], ),
), ),
), ),
); );

View file

@ -16,9 +16,6 @@ class _SessionListAppBarState extends State<SessionListAppBar> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppBar( return AppBar(
title: const Text("Sessions"), title: const Text("Sessions"),
systemOverlayStyle: SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
),
actions: [ actions: [
Padding( Padding(
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),

View file

@ -8,9 +8,6 @@ class SettingsAppBar extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppBar( return AppBar(
title: const Text("Settings"), title: const Text("Settings"),
systemOverlayStyle: SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
),
); );
} }
} }