diff --git a/lib/apis/friend_api.dart b/lib/apis/friend_api.dart index 6098cbf..9bdce6d 100644 --- a/lib/apis/friend_api.dart +++ b/lib/apis/friend_api.dart @@ -6,7 +6,7 @@ import 'package:contacts_plus_plus/models/friend.dart'; class FriendApi { static Future> getFriendsList(ApiClient client, {DateTime? lastStatusUpdate}) async { final response = await client.get("/users/${client.userId}/friends${lastStatusUpdate != null ? "?lastStatusUpdate=${lastStatusUpdate.toUtc().toIso8601String()}" : ""}"); - ApiClient.checkResponse(response); + client.checkResponse(response); final data = jsonDecode(response.body) as List; return data.map((e) => Friend.fromMap(e)).toList(); } diff --git a/lib/apis/message_api.dart b/lib/apis/message_api.dart index ab79e33..335d748 100644 --- a/lib/apis/message_api.dart +++ b/lib/apis/message_api.dart @@ -13,7 +13,7 @@ class MessageApi { "${userId.isEmpty ? "" : "&user=$userId"}" "&unread=$unreadOnly" ); - ApiClient.checkResponse(response); + client.checkResponse(response); final data = jsonDecode(response.body) as List; return data.map((e) => Message.fromMap(e)).toList(); } diff --git a/lib/apis/record_api.dart b/lib/apis/record_api.dart index 95d0403..aa763d7 100644 --- a/lib/apis/record_api.dart +++ b/lib/apis/record_api.dart @@ -19,7 +19,7 @@ import 'package:path/path.dart'; class RecordApi { static Future> getRecordsAt(ApiClient client, {required String path}) async { final response = await client.get("/users/${client.userId}/records?path=$path"); - ApiClient.checkResponse(response); + client.checkResponse(response); final body = jsonDecode(response.body) as List; return body.map((e) => Record.fromMap(e)).toList(); } @@ -28,7 +28,7 @@ class RecordApi { final body = jsonEncode(record.toMap()); final response = await client.post( "/users/${record.ownerId}/records/${record.id}/preprocess", body: body); - ApiClient.checkResponse(response); + client.checkResponse(response); final resultBody = jsonDecode(response.body); return PreprocessStatus.fromMap(resultBody); } @@ -38,7 +38,7 @@ class RecordApi { final response = await client.get( "/users/${preprocessStatus.ownerId}/records/${preprocessStatus.recordId}/preprocess/${preprocessStatus.id}" ); - ApiClient.checkResponse(response); + client.checkResponse(response); final body = jsonDecode(response.body); return PreprocessStatus.fromMap(body); } @@ -58,7 +58,7 @@ class RecordApi { static Future beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async { final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks"); - ApiClient.checkResponse(response); + client.checkResponse(response); final body = jsonDecode(response.body); final res = AssetUploadData.fromMap(body); if (res.uploadState == UploadState.failed) throw body; @@ -68,7 +68,7 @@ class RecordApi { static Future upsertRecord(ApiClient client, {required Record record}) async { final body = jsonEncode(record.toMap()); final response = await client.put("/users/${client.userId}/records/${record.id}", body: body); - ApiClient.checkResponse(response); + client.checkResponse(response); } static Future uploadAsset(ApiClient client, @@ -87,14 +87,14 @@ class RecordApi { ..headers.addAll(client.authorizationHeader); final response = await request.send(); final bodyBytes = await response.stream.toBytes(); - ApiClient.checkResponse(http.Response.bytes(bodyBytes, response.statusCode)); + client.checkResponse(http.Response.bytes(bodyBytes, response.statusCode)); progressCallback?.call(1); } } static Future finishUpload(ApiClient client, {required NeosDBAsset asset}) async { final response = await client.patch("/users/${client.userId}/assets/${asset.hash}/chunks"); - ApiClient.checkResponse(response); + client.checkResponse(response); } static Future uploadAssets(ApiClient client, {required List assets, void Function(double progress)? progressCallback}) async { @@ -123,14 +123,14 @@ class RecordApi { progressCallback?.call(0); final imageDigest = await AssetDigest.fromData(await image.readAsBytes(), basename(image.path)); final imageData = await decodeImageFromList(imageDigest.data); + final filename = basenameWithoutExtension(image.path); final objectJson = jsonEncode( - JsonTemplate.image(imageUri: imageDigest.dbUri, width: imageData.width, height: imageData.height).data); + JsonTemplate.image(imageUri: imageDigest.dbUri, filename: filename, width: imageData.width, height: imageData.height).data); final objectBytes = Uint8List.fromList(utf8.encode(objectJson)); final objectDigest = await AssetDigest.fromData(objectBytes, "${basenameWithoutExtension(image.path)}.json"); - final filename = basenameWithoutExtension(image.path); final digests = [imageDigest, objectDigest]; final record = Record.fromRequiredData( diff --git a/lib/apis/user_api.dart b/lib/apis/user_api.dart index 9b0bb0b..e85d5a5 100644 --- a/lib/apis/user_api.dart +++ b/lib/apis/user_api.dart @@ -10,28 +10,28 @@ import 'package:package_info_plus/package_info_plus.dart'; class UserApi { static Future> searchUsers(ApiClient client, {required String needle}) async { final response = await client.get("/users?name=$needle"); - ApiClient.checkResponse(response); + client.checkResponse(response); final data = jsonDecode(response.body) as List; return data.map((e) => User.fromMap(e)); } static Future getUser(ApiClient client, {required String userId}) async { final response = await client.get("/users/$userId/"); - ApiClient.checkResponse(response); + client.checkResponse(response); final data = jsonDecode(response.body); return User.fromMap(data); } static Future getUserStatus(ApiClient client, {required String userId}) async { final response = await client.get("/users/$userId/status"); - ApiClient.checkResponse(response); + client.checkResponse(response); final data = jsonDecode(response.body); return UserStatus.fromMap(data); } static Future notifyOnlineInstance(ApiClient client) async { final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineId.hashCode}"); - ApiClient.checkResponse(response); + client.checkResponse(response); } static Future setStatus(ApiClient client, {required UserStatus status}) async { @@ -42,12 +42,12 @@ class UserApi { ); final body = jsonEncode(status.toMap(shallow: true)); final response = await client.put("/users/${client.userId}/status", body: body); - ApiClient.checkResponse(response); + client.checkResponse(response); } static Future getPersonalProfile(ApiClient client) async { final response = await client.get("/users/${client.userId}"); - ApiClient.checkResponse(response); + client.checkResponse(response); final data = jsonDecode(response.body); return PersonalProfile.fromMap(data); } @@ -64,11 +64,11 @@ class UserApi { ); final body = jsonEncode(friend.toMap(shallow: true)); final response = await client.put("/users/${client.userId}/friends/${user.id}", body: body); - ApiClient.checkResponse(response); + client.checkResponse(response); } static Future removeUserAsFriend(ApiClient client, {required User user}) async { final response = await client.delete("/users/${client.userId}/friends/${user.id}"); - ApiClient.checkResponse(response); + client.checkResponse(response); } } \ No newline at end of file diff --git a/lib/client_holder.dart b/lib/client_holder.dart index 988d578..cc7536f 100644 --- a/lib/client_holder.dart +++ b/lib/client_holder.dart @@ -30,5 +30,6 @@ class ClientHolder extends InheritedWidget { @override bool updateShouldNotify(covariant ClientHolder oldWidget) => oldWidget.apiClient != apiClient - || oldWidget.settingsClient != settingsClient; + || oldWidget.settingsClient != settingsClient + || oldWidget.notificationClient != notificationClient; } diff --git a/lib/clients/api_client.dart b/lib/clients/api_client.dart index 6aa4c96..8d0bb44 100644 --- a/lib/clients/api_client.dart +++ b/lib/clients/api_client.dart @@ -31,7 +31,7 @@ class ApiClient { required String username, required String password, bool rememberMe=true, - bool rememberPass=false, + bool rememberPass=true, String? oneTimePad, }) async { final body = { @@ -54,11 +54,13 @@ class ApiClient { if (response.statusCode == 400) { throw "Invalid Credentials"; } - checkResponse(response); + checkResponseCode(response); final authData = AuthenticationData.fromMap(jsonDecode(response.body)); if (authData.isAuthenticated) { - const FlutterSecureStorage storage = FlutterSecureStorage(); + const FlutterSecureStorage storage = FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ); await storage.write(key: userIdKey, value: authData.userId); await storage.write(key: machineIdKey, value: authData.secretMachineId); await storage.write(key: tokenKey, value: authData.token); @@ -68,7 +70,9 @@ class ApiClient { } static Future tryCachedLogin() async { - const FlutterSecureStorage storage = FlutterSecureStorage(); + const FlutterSecureStorage storage = FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ); String? userId = await storage.read(key: userIdKey); String? machineId = await storage.read(key: machineIdKey); String? token = await storage.read(key: tokenKey); @@ -79,7 +83,7 @@ class ApiClient { } if (token != null) { - final response = await http.get(buildFullUri("/users/$userId"), headers: { + final response = await http.patch(buildFullUri("/userSessions"), headers: { "Authorization": "neos $userId:$token" }); if (response.statusCode == 200) { @@ -100,7 +104,9 @@ class ApiClient { } Future logout(BuildContext context) async { - const FlutterSecureStorage storage = FlutterSecureStorage(); + const FlutterSecureStorage storage = FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ); await storage.delete(key: userIdKey); await storage.delete(key: machineIdKey); await storage.delete(key: tokenKey); @@ -117,28 +123,30 @@ class ApiClient { } } - static void checkResponse(http.Response response) { - final error = "(${response.statusCode}${kDebugMode ? "|${response.body}" : ""})"; - if (response.statusCode >= 300) { - FlutterError.reportError(FlutterErrorDetails(exception: error)); - } - if (response.statusCode == 429) { - throw "Sorry, you are being rate limited. $error"; - } + void checkResponse(http.Response response) { if (response.statusCode == 403) { - tryCachedLogin(); - // TODO: Show the login screen again if cached login was unsuccessful. - throw "You are not authorized to do that. $error"; - } - if (response.statusCode == 404) { - throw "Resource not found. $error"; - } - if (response.statusCode == 500) { - throw "Internal server error. $error"; - } - if (response.statusCode >= 300) { - throw "Unknown Error. $error"; + tryCachedLogin().then((value) { + if (!value.isAuthenticated) { + // TODO: Turn api-client into a change notifier to present login screen when logged out + } + }); } + checkResponseCode(response); + } + + static void checkResponseCode(http.Response response) { + if (response.statusCode < 300) return; + + final error = "${switch (response.statusCode) { + 429 => "Sorry, you are being rate limited.", + 403 => "You are not authorized to do that.", + 404 => "Resource not found.", + 500 => "Internal server error.", + _ => "Unknown Error." + }} (${response.statusCode}${kDebugMode ? "|${response.body}" : ""})"; + + FlutterError.reportError(FlutterErrorDetails(exception: error)); + throw error; } Map get authorizationHeader => _authenticationData.authorizationHeader; diff --git a/lib/clients/audio_cache_client.dart b/lib/clients/audio_cache_client.dart index 06dffed..d097732 100644 --- a/lib/clients/audio_cache_client.dart +++ b/lib/clients/audio_cache_client.dart @@ -16,7 +16,7 @@ class AudioCacheClient { if (!await file.exists()) { await file.create(recursive: true); final response = await http.get(Uri.parse(Aux.neosDbToHttp(clip.assetUri))); - ApiClient.checkResponse(response); + ApiClient.checkResponseCode(response); await file.writeAsBytes(response.bodyBytes); } return file; diff --git a/lib/clients/messaging_client.dart b/lib/clients/messaging_client.dart index dffb3e8..110a050 100644 --- a/lib/clients/messaging_client.dart +++ b/lib/clients/messaging_client.dart @@ -286,7 +286,7 @@ class MessagingClient extends ChangeNotifier { Uri.parse("${Config.neosHubUrl}/negotiate"), headers: _apiClient.authorizationHeader, ); - ApiClient.checkResponse(response); + _apiClient.checkResponse(response); } catch (e) { throw "Failed to acquire connection info from Neos API: $e"; } diff --git a/lib/models/records/json_template.dart b/lib/models/records/json_template.dart index dace2b6..4a13bbd 100644 --- a/lib/models/records/json_template.dart +++ b/lib/models/records/json_template.dart @@ -7,12 +7,13 @@ class JsonTemplate { JsonTemplate({required this.data}); - factory JsonTemplate.image({required String imageUri, required int width, required int height}) { + factory JsonTemplate.image({required String imageUri, required String filename, required int width, required int height}) { final texture2dUid = const Uuid().v4(); final quadMeshUid = const Uuid().v4(); final quadMeshSizeUid = const Uuid().v4(); final materialId = const Uuid().v4(); final boxColliderSizeUid = const Uuid().v4(); + final ratio = height/width; final data = { "Object": { "ID": const Uuid().v4(), @@ -508,8 +509,8 @@ class JsonTemplate { "Size": { "ID": quadMeshSizeUid, "Data": [ - 1, - height/width + ratio > 1 ? ratio : 1, + ratio > 1 ? 1 : ratio ] }, "UVScale": { @@ -706,7 +707,7 @@ class JsonTemplate { }, "Name": { "ID": const Uuid().v4(), - "Data": "alice" + "Data": filename }, "Tag": { "ID": const Uuid().v4(), diff --git a/lib/widgets/messages/camera_image_view.dart b/lib/widgets/messages/camera_image_view.dart new file mode 100644 index 0000000..de20b04 --- /dev/null +++ b/lib/widgets/messages/camera_image_view.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; + +class CameraImageView extends StatelessWidget { + const CameraImageView({required this.file, super.key}); + + final File file; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Stack( + children: [ + PhotoView( + imageProvider: FileImage( + file, + ), + initialScale: PhotoViewComputedScale.covered, + minScale: PhotoViewComputedScale.contained, + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton.icon( + onPressed: () { + Navigator.of(context).pop(false); + }, + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + backgroundColor: Theme.of(context).colorScheme.surface, + side: BorderSide(width: 1, color: Theme.of(context).colorScheme.error) + ), + icon: const Icon(Icons.close), + label: const Text("Cancel",), + ), + TextButton.icon( + onPressed: () { + Navigator.of(context).pop(true); + }, + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + backgroundColor: Theme.of(context).colorScheme.surface, + side: BorderSide(width: 1, color: Theme.of(context).colorScheme.primary) + ), + icon: const Icon(Icons.check), + label: const Text("Okay"), + ) + ], + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/messages/message_attachment_list.dart b/lib/widgets/messages/message_attachment_list.dart index a56193a..21480ca 100644 --- a/lib/widgets/messages/message_attachment_list.dart +++ b/lib/widgets/messages/message_attachment_list.dart @@ -1,9 +1,9 @@ import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:contacts_plus_plus/widgets/messages/message_camera_view.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart'; class MessageAttachmentList extends StatefulWidget { @@ -22,7 +22,6 @@ class _MessageAttachmentListState extends State { final ScrollController _scrollController = ScrollController(); bool _showShadow = true; bool _popupIsOpen = false; - @override void initState() { super.initState(); @@ -88,7 +87,9 @@ class _MessageAttachmentListState extends State { TextButton( onPressed: () async { Navigator.of(context).pop(); - _loadedFiles.remove(file); + setState(() { + _loadedFiles.remove(file); + }); await widget.onChange(_loadedFiles); }, child: const Text("Yes"), @@ -156,7 +157,7 @@ class _MessageAttachmentListState extends State { .secondary, ) ), - padding: EdgeInsets.zero, + padding: EdgeInsets.zero, onPressed: () async { final result = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true); if (result != null) { @@ -191,11 +192,19 @@ class _MessageAttachmentListState extends State { ), padding: EdgeInsets.zero, onPressed: () async { - final picture = await Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const MessageCameraView())) as File?; + final picture = await ImagePicker().pickImage(source: ImageSource.camera); if (picture != null) { - _loadedFiles.add((FileType.image, picture)); - await widget.onChange(_loadedFiles); + final file = File(picture.path); + if (await file.exists()) { + setState(() { + _loadedFiles.add((FileType.image, file)); + }); + await widget.onChange(_loadedFiles); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to load image file"))); + } + } } }, icon: const Icon(Icons.camera,), diff --git a/lib/widgets/messages/message_audio_player.dart b/lib/widgets/messages/message_audio_player.dart index 445b51d..1b98f44 100644 --- a/lib/widgets/messages/message_audio_player.dart +++ b/lib/widgets/messages/message_audio_player.dart @@ -19,7 +19,7 @@ class MessageAudioPlayer extends StatefulWidget { State createState() => _MessageAudioPlayerState(); } -class _MessageAudioPlayerState extends State with WidgetsBindingObserver { +class _MessageAudioPlayerState extends State with WidgetsBindingObserver, AutomaticKeepAliveClientMixin { final AudioPlayer _audioPlayer = AudioPlayer(); Future? _audioFileFuture; double _sliderValue = 0; @@ -82,6 +82,7 @@ class _MessageAudioPlayerState extends State with WidgetsBin @override Widget build(BuildContext context) { + super.build(context); if (!Platform.isAndroid) { return _createErrorWidget("Sorry, audio-messages are not\n supported on this platform."); } @@ -220,4 +221,8 @@ class _MessageAudioPlayerState extends State with WidgetsBin } ); } + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; } \ No newline at end of file diff --git a/lib/widgets/messages/message_camera_view.dart b/lib/widgets/messages/message_camera_view.dart deleted file mode 100644 index 73d32ff..0000000 --- a/lib/widgets/messages/message_camera_view.dart +++ /dev/null @@ -1,184 +0,0 @@ -import 'dart:io'; - -import 'package:camera/camera.dart'; -import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; -import 'package:flutter/material.dart'; - -class MessageCameraView extends StatefulWidget { - const MessageCameraView({super.key}); - - @override - State createState() => _MessageCameraViewState(); - -} - -class _MessageCameraViewState extends State { - final List _cameras = []; - late final CameraController _cameraController; - int _cameraIndex = 0; - FlashMode _flashMode = FlashMode.off; - Future? _initializeControllerFuture; - - @override - void initState() { - super.initState(); - availableCameras().then((List cameras) { - _cameras.clear(); - _cameras.addAll(cameras); - if (cameras.isEmpty) { - _initializeControllerFuture = Future.error("Failed to initialize camera"); - } else { - _cameraController = CameraController(cameras.first, ResolutionPreset.high); - _cameraIndex = 0; - _initializeControllerFuture = _cameraController.initialize().whenComplete(() => _cameraController.setFlashMode(_flashMode)); - } - setState(() {}); - }); - } - - @override - void dispose() { - _cameraController.setFlashMode(FlashMode.off).whenComplete(() => _cameraController.dispose()); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("Take a picture"), - ), - body: FutureBuilder( - future: _initializeControllerFuture, - builder: (context, snapshot) { - // Can't use hasData since the future returns void. - if (snapshot.connectionState == ConnectionState.done) { - return Stack( - children: [ - Column( - children: [ - Expanded(child: CameraPreview(_cameraController)), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - onPressed: _cameras.isEmpty ? null : () async { - setState(() { - _cameraIndex = (_cameraIndex+1) % _cameras.length; - }); - _cameraController.setDescription(_cameras[_cameraIndex]); - }, - iconSize: 32, - icon: const Icon(Icons.switch_camera), - ), - const SizedBox(width: 64, height: 72,), - 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 (_flashMode) { - FlashMode.off => - IconButton( - key: const ValueKey("button-flash-off"), - iconSize: 32, - onPressed: () async { - _flashMode = FlashMode.auto; - await _cameraController.setFlashMode(_flashMode); - setState(() {}); - }, - icon: const Icon(Icons.flash_off), - ), - FlashMode.auto => - IconButton( - key: const ValueKey("button-flash-auto"), - iconSize: 32, - onPressed: () async { - _flashMode = FlashMode.always; - await _cameraController.setFlashMode(_flashMode); - setState(() {}); - }, - icon: const Icon(Icons.flash_auto), - ), - FlashMode.always => - IconButton( - key: const ValueKey("button-flash-always"), - iconSize: 32, - onPressed: () async { - _flashMode = FlashMode.torch; - await _cameraController.setFlashMode(_flashMode); - setState(() {}); - }, - icon: const Icon(Icons.flash_on), - ), - FlashMode.torch => - IconButton( - key: const ValueKey("button-flash-torch"), - iconSize: 32, - onPressed: () async { - _flashMode = FlashMode.off; - await _cameraController.setFlashMode(_flashMode); - setState(() {}); - }, - icon: const Icon(Icons.flashlight_on), - ), - }, - ), - ], - ) - ], - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - decoration: BoxDecoration( - color: Theme - .of(context) - .colorScheme - .surface, - borderRadius: BorderRadius.circular(64), - ), - margin: const EdgeInsets.all(16), - child: IconButton( - onPressed: () async { - final sMsgr = ScaffoldMessenger.of(context); - final nav = Navigator.of(context); - try { - await _initializeControllerFuture; - final image = await _cameraController.takePicture(); - nav.pop(File(image.path)); - } catch (e) { - sMsgr.showSnackBar(SnackBar(content: Text("Failed to capture image: $e"))); - } - }, - style: IconButton.styleFrom( - foregroundColor: Theme - .of(context) - .colorScheme - .primary, - ), - icon: const Icon(Icons.camera), - iconSize: 64, - ), - ), - ), - ], - ); - } else if (snapshot.hasError) { - return DefaultErrorWidget( - message: snapshot.error.toString(), - ); - } else { - return const Center(child: CircularProgressIndicator(),); - } - }, - ), - ); - } -} diff --git a/lib/widgets/messages/message_input_bar.dart b/lib/widgets/messages/message_input_bar.dart index d040d52..82c064f 100644 --- a/lib/widgets/messages/message_input_bar.dart +++ b/lib/widgets/messages/message_input_bar.dart @@ -10,10 +10,10 @@ import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/widgets/messages/message_attachment_list.dart'; -import 'package:contacts_plus_plus/widgets/messages/message_camera_view.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:record/record.dart'; @@ -21,9 +21,8 @@ import 'package:uuid/uuid.dart'; class MessageInputBar extends StatefulWidget { - const MessageInputBar({this.showShadow=true, this.disabled=false, required this.recipient, this.onMessageSent, super.key}); + const MessageInputBar({this.disabled=false, required this.recipient, this.onMessageSent, super.key}); - final bool showShadow; final bool disabled; final Friend recipient; final Function()? onMessageSent; @@ -36,6 +35,7 @@ class _MessageInputBarState extends State { final TextEditingController _messageTextController = TextEditingController(); final List<(FileType, File)> _loadedFiles = []; final Record _recorder = Record(); + final ImagePicker _imagePicker = ImagePicker(); DateTime? _recordingStartTime; @@ -47,7 +47,6 @@ class _MessageInputBarState extends State { set _isRecording(value) => _recordingStartTime = value ? DateTime.now() : null; bool _recordingCancelled = false; - Future sendTextMessage(ApiClient client, MessagingClient mClient, String content) async { if (content.isEmpty) return; final message = Message( @@ -204,24 +203,15 @@ class _MessageInputBarState extends State { } } }, - child: AnimatedContainer( + child: Container( decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - blurRadius: widget.showShadow ? 8 : 0, - color: Theme - .of(context) - .shadowColor, - offset: const Offset(0, 4), - ), - ], + border: const Border(top: BorderSide(width: 1, color: Colors.black38)), color: Theme .of(context) .colorScheme .background, ), padding: const EdgeInsets.symmetric(horizontal: 4), - duration: const Duration(milliseconds: 250), child: Column( children: [ if (_isSending && _sendProgress != null) @@ -262,13 +252,24 @@ class _MessageInputBarState extends State { ), TextButton.icon( onPressed: _isSending ? null : () async { - final picture = await Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const MessageCameraView())) as File?; - if (picture != null) { - setState(() { - _loadedFiles.add((FileType.image, picture)); - }); + 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"), diff --git a/lib/widgets/messages/message_record_button.dart b/lib/widgets/messages/message_record_button.dart deleted file mode 100644 index e2f7d48..0000000 --- a/lib/widgets/messages/message_record_button.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:record/record.dart'; - -class MessageRecordButton extends StatefulWidget { - const MessageRecordButton({required this.disabled, this.onRecordStart, this.onRecordEnd, super.key}); - - final bool disabled; - final Function()? onRecordStart; - final Function(File? recording)? onRecordEnd; - - @override - State createState() => _MessageRecordButtonState(); -} - -class _MessageRecordButtonState extends State { - - final Record _recorder = Record(); - - @override - void dispose() { - super.dispose(); - Future.delayed(Duration.zero, _recorder.stop); - Future.delayed(Duration.zero, _recorder.dispose); - } - - @override - Widget build(BuildContext context) { - return Material( - child: GestureDetector( - onTapDown: widget.disabled ? null : (_) async { - HapticFeedback.vibrate(); - /* - widget.onRecordStart?.call(); - final dir = await getTemporaryDirectory(); - await _recorder.start( - path: "${dir.path}/A-${const Uuid().v4()}.ogg", - encoder: AudioEncoder.opus, - samplingRate: 44100, - ); - */ - }, - onLongPressUp: () async { - /* - if (await _recorder.isRecording()) { - final recording = await _recorder.stop(); - widget.onRecordEnd?.call(recording == null ? null : File(recording)); - } - */ - }, - child: Padding( - padding: const EdgeInsets.all(8), - child: Icon(Icons.mic_outlined, size: 28, color: Theme.of(context).colorScheme.onSurface,), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart index 72f96d9..f0e61c0 100644 --- a/lib/widgets/messages/messages_list.dart +++ b/lib/widgets/messages/messages_list.dart @@ -21,9 +21,7 @@ class MessagesList extends StatefulWidget { class _MessagesListState extends State with SingleTickerProviderStateMixin { final ScrollController _sessionListScrollController = ScrollController(); - final ScrollController _messageScrollController = ScrollController(); - bool _showBottomBarShadow = false; bool _showSessionListScrollChevron = false; double get _shevronOpacity => _showSessionListScrollChevron ? 1.0 : 0.0; @@ -50,19 +48,6 @@ class _MessagesListState extends State with SingleTickerProviderSt }); } }); - _messageScrollController.addListener(() { - if (!_messageScrollController.hasClients) return; - if (_messageScrollController.position.atEdge && _messageScrollController.position.pixels == 0 && - _showBottomBarShadow) { - setState(() { - _showBottomBarShadow = false; - }); - } else if (!_showBottomBarShadow) { - setState(() { - _showBottomBarShadow = true; - }); - } - }); } @@ -189,7 +174,6 @@ class _MessagesListState extends State with SingleTickerProviderSt return Provider( create: (BuildContext context) => AudioCacheClient(), child: ListView.builder( - controller: _messageScrollController, reverse: true, itemCount: cache.messages.length, itemBuilder: (context, index) { @@ -212,7 +196,6 @@ class _MessagesListState extends State with SingleTickerProviderSt MessageInputBar( recipient: widget.friend, disabled: cache == null || cache.error != null, - showShadow: _showBottomBarShadow, onMessageSent: () { setState(() {}); }, diff --git a/pubspec.lock b/pubspec.lock index d173443..9899365 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -376,6 +376,46 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "9978d3510af4e6a902e545ce19229b926e6de6a1828d6134d3aab2e129a4d270" + url: "https://pub.dev" + source: hosted + version: "0.8.7+5" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: c2f3c66400649bd132f721c88218945d6406f693092b2f741b79ae9cdb046e59 + url: "https://pub.dev" + source: hosted + version: "0.8.6+16" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "98f50d6b9f294c8ba35e25cc0d13b04bfddd25dbc8d32fa9d566a6572f2c081c" + url: "https://pub.dev" + source: hosted + version: "2.1.12" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: d779210bda268a03b57e923fb1e410f32f5c5e708ad256348bcbf1f44f558fd0 + url: "https://pub.dev" + source: hosted + version: "0.8.7+4" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "1991219d9dbc42a99aff77e663af8ca51ced592cd6685c9485e3458302d3d4f8" + url: "https://pub.dev" + source: hosted + version: "2.6.3" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a17e11c..b2716d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: camera: ^0.10.5 path_provider: ^2.0.15 crypto: ^3.0.3 + image_picker: ^0.8.7+5 dev_dependencies: flutter_test: