Fix visual issues with bottom navigation bar
This commit is contained in:
parent
1c77951431
commit
1bcebbacc8
8 changed files with 429 additions and 423 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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>(
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue