diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f03ccf5..4642b9b 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -46,7 +46,7 @@ UIApplicationSupportsIndirectInputEvents NSCameraUsageDescription - Why? What is the purpose of the app? + To create photos that can be shared. NSMicrophoneUsageDescription Explanation on why the microphone access is needed. diff --git a/lib/src/components/initialsavatar_component.dart b/lib/src/components/initialsavatar_component.dart new file mode 100644 index 0000000..ef86654 --- /dev/null +++ b/lib/src/components/initialsavatar_component.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +class InitialsAvatar extends StatelessWidget { + final String displayName; + + const InitialsAvatar({super.key, required this.displayName}); + + @override + Widget build(BuildContext context) { + // Extract initials from the displayName + List nameParts = displayName.split(' '); + String initials = nameParts.map((part) => part[0]).join().toUpperCase(); + + if (initials.length > 2) { + initials = initials[0] + initials[1]; + } else if (initials.length == 1) { + initials = displayName[0] + displayName[1]; + } + + initials = initials.toUpperCase(); + + // Generate a color based on the initials (you can customize this logic) + Color avatarColor = _getColorFromUsername( + displayName, Theme.of(context).brightness == Brightness.dark); + + return CircleAvatar( + backgroundColor: avatarColor, + child: Text( + initials, + style: TextStyle( + color: _getTextColor(avatarColor), + fontWeight: FontWeight.normal, + fontSize: 20, + ), + ), + ); + } + + Color _getTextColor(Color color) { + double value = 100.0; + // Ensure the value does not exceed the RGB limits + int newRed = ((color.r * 255) - value).clamp(0, 255).round(); + int newGreen = (color.g * 255 - value).clamp(0, 255).round(); + int newBlue = (color.b * 255 - value).clamp(0, 255).round(); + + return Color.fromARGB((color.a * 255).round(), newRed, newGreen, newBlue); + } + + Color _getColorFromUsername(String displayName, bool isDarkMode) { + // Define color lists for light and dark themes + List lightColors = [ + Colors.red, + Colors.green, + Colors.blue, + Colors.orange, + Colors.purple, + Colors.teal, + Colors.amber, + Colors.indigo, + Colors.cyan, + Colors.lime, + Colors.pink, + Colors.brown, + Colors.grey, + ]; + + List darkColors = [ + const Color.fromARGB(255, 246, 227, 254), // Light Lavender + const Color.fromARGB(255, 246, 216, 215), // Light Pink + const Color.fromARGB(255, 226, 236, 235), // Light Teal + const Color.fromARGB(255, 255, 224, 178), // Light Yellow + const Color.fromARGB(255, 255, 182, 193), // Light Pink (Hot Pink) + const Color.fromARGB(255, 173, 216, 230), // Light Blue + const Color.fromARGB(255, 221, 160, 221), // Plum + const Color.fromARGB(255, 255, 228, 196), // Bisque + const Color.fromARGB(255, 240, 230, 140), // Khaki + const Color.fromARGB(255, 255, 192, 203), // Pink + const Color.fromARGB(255, 255, 218, 185), // Peach Puff + const Color.fromARGB(255, 255, 160, 122), // Light Salmon + const Color.fromARGB(255, 135, 206, 250), // Light Sky Blue + const Color.fromARGB(255, 255, 228, 225), // Misty Rose + const Color.fromARGB(255, 240, 248, 255), // Alice Blue + const Color.fromARGB(255, 255, 250, 205), // Lemon Chiffon + const Color.fromARGB(255, 255, 218, 185), // Peach Puff + ]; + + // Simple logic to generate a hash from initials + int hash = displayName.codeUnits.fold(0, (prev, element) => prev + element); + + // Select the appropriate color list based on the current theme brightness + List colors = isDarkMode ? darkColors : lightColors; + + // Use the hash to select a color from the list + return colors[hash % colors.length]; + } +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 95a66bb..2b1b1cd 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; -import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:twonly/main.dart'; import 'package:twonly/src/model/contacts_model.dart'; @@ -114,88 +113,3 @@ Future createNewUser(String username, String inviteCode) async { return res; } - -Widget createInitialsAvatar(String username, bool isDarkMode) { - // Extract initials from the username - List nameParts = username.split(' '); - String initials = nameParts.map((part) => part[0]).join().toUpperCase(); - if (initials.length > 2) { - initials = initials[0] + initials[1]; - } else if (initials.length == 1) { - initials = username[0] + username[1]; - } - - initials = initials.toUpperCase(); - - // Generate a color based on the initials (you can customize this logic) - Color avatarColor = _getColorFromUsername(username, isDarkMode); - - return CircleAvatar( - backgroundColor: avatarColor, - child: Text( - initials, - style: TextStyle( - color: _getTextColor(avatarColor), - fontWeight: FontWeight.normal, - fontSize: 20), - ), - ); -} - -Color _getTextColor(Color color) { - double value = 100.0; - // Ensure the value does not exceed the RGB limits - int newRed = ((color.r * 255) - value).clamp(0, 255).round(); - int newGreen = (color.g * 255 - value).clamp(0, 255).round(); - int newBlue = (color.b * 255 - value).clamp(0, 255).round(); - - return Color.fromARGB((color.a * 255).round(), newRed, newGreen, newBlue); -} - -Color _getColorFromUsername(String username, bool isDarkMode) { - // Define color lists for light and dark themes - List lightColors = [ - Colors.red, - Colors.green, - Colors.blue, - Colors.orange, - Colors.purple, - Colors.teal, - Colors.amber, - Colors.indigo, - Colors.cyan, - Colors.lime, - Colors.pink, - Colors.brown, - Colors.grey, - ]; - - List darkColors = [ - const Color.fromARGB(255, 246, 227, 254), // Light Lavender - const Color.fromARGB(255, 246, 216, 215), // Light Pink - const Color.fromARGB(255, 226, 236, 235), // Light Teal - const Color.fromARGB(255, 255, 224, 178), // Light Yellow - const Color.fromARGB(255, 255, 182, 193), // Light Pink (Hot Pink) - const Color.fromARGB(255, 173, 216, 230), // Light Blue - const Color.fromARGB(255, 221, 160, 221), // Plum - const Color.fromARGB(255, 255, 228, 196), // Bisque - const Color.fromARGB(255, 240, 230, 140), // Khaki - const Color.fromARGB(255, 255, 192, 203), // Pink - const Color.fromARGB(255, 255, 218, 185), // Peach Puff - const Color.fromARGB(255, 255, 160, 122), // Light Salmon - const Color.fromARGB(255, 135, 206, 250), // Light Sky Blue - const Color.fromARGB(255, 255, 228, 225), // Misty Rose - const Color.fromARGB(255, 240, 248, 255), // Alice Blue - const Color.fromARGB(255, 255, 250, 205), // Lemon Chiffon - const Color.fromARGB(255, 255, 218, 185), // Peach Puff - ]; - - // Simple logic to generate a hash from initials - int hash = username.codeUnits.fold(0, (prev, element) => prev + element); - - // Select the appropriate color list based on the current theme brightness - List colors = isDarkMode ? darkColors : lightColors; - - // Use the hash to select a color from the list - return colors[hash % colors.length]; -} diff --git a/lib/src/views/camera_preview_view.dart b/lib/src/views/camera_preview_view.dart index dbcdb79..92cbd45 100644 --- a/lib/src/views/camera_preview_view.dart +++ b/lib/src/views/camera_preview_view.dart @@ -1,10 +1,9 @@ -// import 'package:camera/camera.dart'; -// import 'camera_editor_view.dart'; -// import 'package:flutter/gestures.dart'; -import 'package:camerawesome/pigeon.dart'; +import 'dart:io'; + import 'package:flutter/material.dart'; -import 'dart:math'; import 'package:camerawesome/camerawesome_plugin.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:twonly/src/views/share_image_view.dart'; class CameraPreviewView extends StatefulWidget { const CameraPreviewView({super.key}); @@ -30,6 +29,50 @@ class _CameraPreviewViewState extends State { borderRadius: BorderRadius.circular(22), child: CameraAwesomeBuilder.custom( progressIndicator: Container(), + onMediaCaptureEvent: (event) { + switch ((event.status, event.isPicture, event.isVideo)) { + case (MediaCaptureStatus.capturing, true, false): + debugPrint('Capturing picture...'); + case (MediaCaptureStatus.success, true, false): + event.captureRequest.when( + single: (single) { + final path = single.file?.path; + if (path == null) return; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShareImageView(image: path), + ), + ); + debugPrint('Picture saved: ${path}'); + }, + multiple: (multiple) { + multiple.fileBySensor.forEach((key, value) { + debugPrint('multiple image taken: $key ${value?.path}'); + }); + }, + ); + case (MediaCaptureStatus.failure, true, false): + debugPrint('Failed to capture picture: ${event.exception}'); + case (MediaCaptureStatus.capturing, false, true): + debugPrint('Capturing video...'); + case (MediaCaptureStatus.success, false, true): + event.captureRequest.when( + single: (single) { + debugPrint('Video saved: ${single.file?.path}'); + }, + multiple: (multiple) { + multiple.fileBySensor.forEach((key, value) { + debugPrint('multiple video taken: $key ${value?.path}'); + }); + }, + ); + case (MediaCaptureStatus.failure, false, true): + debugPrint('Failed to capture video: ${event.exception}'); + default: + debugPrint('Unknown event: $event'); + } + }, builder: (cameraState, preview) { return Container( child: Stack( @@ -99,7 +142,25 @@ class _CameraPreviewViewState extends State { ), ); }, - saveConfig: SaveConfig.photoAndVideo(), + saveConfig: SaveConfig.photoAndVideo( + photoPathBuilder: (sensors) async { + final Directory extDir = await getTemporaryDirectory(); + final testDir = await Directory( + '${extDir.path}/images', + ).create(recursive: true); + final String filePath = + '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg'; + return SingleCaptureRequest(filePath, sensors.first); + // // Separate pictures taken with front and back camera + // return MultipleCaptureRequest( + // { + // for (final sensor in sensors) + // sensor: + // '${testDir.path}/${sensor.position == SensorPosition.front ? 'front_' : "back_"}${DateTime.now().millisecondsSinceEpoch}.jpg', + // }, + // ); + }, + ), previewPadding: const EdgeInsets.all(10), // onPreviewTapBuilder: (state) => OnPreviewTap( // onTap: (Offset position, PreviewSize flutterPreviewSize, diff --git a/lib/src/views/chat_list_view.dart b/lib/src/views/chat_list_view.dart index 8cde652..1a41078 100644 --- a/lib/src/views/chat_list_view.dart +++ b/lib/src/views/chat_list_view.dart @@ -1,4 +1,4 @@ -import 'package:twonly/src/utils.dart'; +import 'package:twonly/src/components/initialsavatar_component.dart'; import 'package:twonly/src/views/search_username_view.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'new_message_view.dart'; @@ -193,8 +193,7 @@ class _ChatListViewState extends State { return ListTile( title: Text(item.username), subtitle: getSubtitle(item), - leading: createInitialsAvatar(item.username, - Theme.of(context).brightness == Brightness.dark), + leading: InitialsAvatar(displayName: item.username), onTap: () { Navigator.push( context, diff --git a/lib/src/views/new_message_view.dart b/lib/src/views/new_message_view.dart index 3f3292f..063d2eb 100644 --- a/lib/src/views/new_message_view.dart +++ b/lib/src/views/new_message_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:twonly/src/components/initialsavatar_component.dart'; import 'package:twonly/src/model/contacts_model.dart'; -import 'package:twonly/src/utils.dart'; import 'package:twonly/src/views/search_username_view.dart'; class NewMessageView extends StatefulWidget { @@ -166,8 +166,7 @@ class UserList extends StatelessWidget { ...users.map((username) { return ListTile( title: Text(username), - leading: createInitialsAvatar( - username, Theme.of(context).brightness == Brightness.dark), + leading: InitialsAvatar(displayName: username), onTap: () { // Handle tap }, diff --git a/lib/src/views/share_image_view.dart b/lib/src/views/share_image_view.dart new file mode 100644 index 0000000..b840ec6 --- /dev/null +++ b/lib/src/views/share_image_view.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:twonly/src/components/initialsavatar_component.dart'; +import 'package:twonly/src/model/contacts_model.dart'; + +class ShareImageView extends StatefulWidget { + const ShareImageView({super.key, required this.image}); + final String image; + + @override + State createState() => _ShareImageView(); +} + +class _ShareImageView extends State { + List _knownUsers = []; + + @override + void initState() { + super.initState(); + _loadUsers(); + } + + Future _loadUsers() async { + final users = await DbContacts.getUsers(); + setState(() { + _knownUsers = users; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.newMessageTitle), + ), + body: Padding( + padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), + child: Column( + children: [ + UserCheckboxList(users: _knownUsers), + const SizedBox(height: 10), + // Expanded( + // child: UserList(_filteredUsers), + // ) + ], + ), + ), + ); + } +} + +class UserCheckboxList extends StatelessWidget { + final List users; + + UserCheckboxList({required this.users}); + + @override + Widget build(BuildContext context) { + // Limit the number of users to 8 + final limitedUsers = users.length > 8 ? users.sublist(0, 8) : users; + + return Column( + children: List.generate((limitedUsers.length + 1) ~/ 2, (rowIndex) { + final firstUserIndex = rowIndex * 2; + final secondUserIndex = firstUserIndex + 1; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text(limitedUsers[firstUserIndex].displayName), + if (secondUserIndex < limitedUsers.length) + Text(limitedUsers[secondUserIndex].displayName), + ], + ); + }), + ); + } +} + +class UserCheckbox extends StatefulWidget { + final Contact user; + + UserCheckbox({required this.user}); + + @override + _UserCheckboxState createState() => _UserCheckboxState(); +} + +class _UserCheckboxState extends State { + bool isChecked = false; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Checkbox( + value: isChecked, + onChanged: (bool? value) { + setState(() { + isChecked = value ?? false; + }); + }, + ), + CircleAvatar( + child: InitialsAvatar( + displayName: + widget.user.displayName), // Display first letter of the name + ), + SizedBox(width: 8), + Expanded( + child: Text( + widget.user.displayName.length > 10 + ? '${widget.user.displayName.substring(0, 10)}...' // Trim if too long + : widget.user.displayName, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } +}