diff --git a/assets/images/logo_gradient.svg b/assets/images/logo_gradient.svg new file mode 100644 index 0000000..0add3ad --- /dev/null +++ b/assets/images/logo_gradient.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/main.dart b/lib/main.dart index fb0f3ff..11d9f6b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ late ApiProvider apiProvider; void main() async { final settingsController = SettingsController(SettingsService()); - // Load the user's preferred theme while the splash screen is displayed. + // Load the user's peganreferred theme while the splash screen is displayed. // This prevents a sudden theme change when the app is first displayed. await settingsController.loadSettings(); diff --git a/lib/src/components/best_friends_selector.dart b/lib/src/components/best_friends_selector.dart new file mode 100644 index 0000000..6deb9b9 --- /dev/null +++ b/lib/src/components/best_friends_selector.dart @@ -0,0 +1,128 @@ +import 'dart:collection'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:twonly/src/components/headline.dart'; +import 'package:twonly/src/components/initialsavatar.dart'; +import 'package:twonly/src/model/contacts_model.dart'; + +class BestFriendsSelector extends StatelessWidget { + final List users; + final Function(Int64, bool) updateStatus; + final HashSet selectedUserIds; + + const BestFriendsSelector({ + super.key, + required this.users, + required this.updateStatus, + required this.selectedUserIds, + }); + + @override + Widget build(BuildContext context) { + if (users.isEmpty) { + return Container(); + } + + final limitedUsers = users.length > 8 ? users.sublist(0, 8) : users; + return Column( + children: [ + HeadLineComponent(AppLocalizations.of(context)!.shareImageBestFriends), + Column( + spacing: 8, + children: List.generate( + (limitedUsers.length + 1) ~/ 2, + (rowIndex) { + final firstUserIndex = rowIndex * 2; + final secondUserIndex = firstUserIndex + 1; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: UserCheckbox( + isChecked: selectedUserIds + .contains(limitedUsers[firstUserIndex].userId), + user: limitedUsers[firstUserIndex], + onChanged: updateStatus, + ), + ), + (secondUserIndex < limitedUsers.length) + ? Expanded( + child: UserCheckbox( + isChecked: selectedUserIds + .contains(limitedUsers[secondUserIndex].userId), + user: limitedUsers[secondUserIndex], + onChanged: updateStatus, + ), + ) + : Expanded( + child: Container(), + ), + ], + ); + }, + ), + ), + ], + ); + } +} + +class UserCheckbox extends StatelessWidget { + final Contact user; + final Function(Int64, bool) onChanged; + final bool isChecked; + + const UserCheckbox({ + super.key, + required this.user, + required this.onChanged, + required this.isChecked, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: + EdgeInsets.symmetric(horizontal: 3), // Padding inside the container + child: GestureDetector( + onTap: () { + onChanged(user.userId, !isChecked); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 0), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outline, + width: 1.0, + ), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + children: [ + InitialsAvatar( + fontSize: 15, + displayName: user.displayName, + ), + SizedBox(width: 8), + Expanded( + child: Text( + user.displayName.length > 10 + ? '${user.displayName.substring(0, 10)}...' + : user.displayName, + overflow: TextOverflow.ellipsis, + ), + ), + Checkbox( + value: isChecked, + onChanged: (bool? value) { + onChanged(user.userId, value ?? false); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/components/headline.dart b/lib/src/components/headline.dart new file mode 100644 index 0000000..4e6241f --- /dev/null +++ b/lib/src/components/headline.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class HeadLineComponent extends StatelessWidget { + final String text; + + const HeadLineComponent(this.text, {super.key}); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 10), + child: Text( + text, + style: TextStyle(fontSize: 20), + ), + ); + } +} diff --git a/lib/src/components/user_context_menu.dart b/lib/src/components/user_context_menu.dart new file mode 100644 index 0000000..c687a02 --- /dev/null +++ b/lib/src/components/user_context_menu.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:pie_menu/pie_menu.dart'; +import 'package:twonly/src/model/contacts_model.dart'; + +class UserContextMenu extends StatefulWidget { + final Widget child; + final Contact user; + + const UserContextMenu({super.key, required this.user, required this.child}); + + @override + State createState() => _UserContextMenuState(); +} + +class _UserContextMenuState extends State { + @override + Widget build(BuildContext context) { + return PieMenu( + onPressed: () => print('pressed'), + actions: [ + PieAction( + tooltip: const Text('Verify user'), + onSelect: () { + print('Verify user selected'); + // Add your verification logic here + }, + child: const Icon(Icons.gpp_maybe_rounded), // Can be any widget + ), + PieAction( + tooltip: const Text('Send image'), + onSelect: () { + print('Send image selected'); + // Add your image sending logic here + }, + child: const Icon(Icons.camera_alt_rounded), // Can be any widget + ), + ], + child: widget.child, + ); + } +} diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 16d8400..e0ed7e3 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -11,8 +11,10 @@ "shareImageTitle": "Share with", "shareImageBestFriends": "Best friends", "shareImagedEditorSendImage": "Send", + "shareImagedEditorShareWith": "Share with", "shareImagedEditorSaveImage": "Save", "shareImagedEditorSavedImage": "Saved", + "shareImageAllUsers": "All contacts", "searchUsernameInput": "Username", "searchUsernameTitle": "Search username", "searchUsernameNotFound": "Username not found", diff --git a/lib/src/model/contacts_model.dart b/lib/src/model/contacts_model.dart index 2d44f68..1d8fea8 100644 --- a/lib/src/model/contacts_model.dart +++ b/lib/src/model/contacts_model.dart @@ -53,6 +53,10 @@ class DbContacts extends CvModelBase { List get fields => [userId, displayName, accepted, requested, blocked, createdAt]; + static Future> getActiveUsers() async { + return (await getUsers()).where((u) => u.accepted).toList(); + } + static Future> getUsers() async { try { var users = await dbProvider.db!.query(tableName, diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 0fbd9a3..35dd79c 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -85,3 +85,36 @@ String errorCodeToText(BuildContext context, ErrorCode code) { return code.toString(); // Fallback for unrecognized keys } } + +String formatDuration(int seconds) { + if (seconds < 60) { + return '$seconds Sec.'; + } else if (seconds < 3600) { + int minutes = seconds ~/ 60; + return '$minutes Min.'; + } else if (seconds < 86400) { + int hours = seconds ~/ 3600; + return '$hours Hrs.'; // Assuming "Stu." is for hours + } else { + int days = seconds ~/ 86400; + return '$days Days'; + } +} + +InputDecoration getInputDecoration(context, hintText) { + final primaryColor = + Theme.of(context).colorScheme.primary; // Get the primary color + return InputDecoration( + hintText: hintText, + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(9.0), + borderSide: BorderSide(color: primaryColor, width: 1.0), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: + BorderSide(color: Theme.of(context).colorScheme.outline, width: 1.0), + ), + contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0), + ); +} diff --git a/lib/src/views/chat_item_details_view.dart b/lib/src/views/chat_item_details_view.dart index 1f04d37..ee20120 100644 --- a/lib/src/views/chat_item_details_view.dart +++ b/lib/src/views/chat_item_details_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:twonly/src/model/contacts_model.dart'; class AlignedTextBox extends StatelessWidget { const AlignedTextBox({super.key, required this.text, required this.right}); @@ -39,9 +40,9 @@ class AlignedTextBox extends StatelessWidget { /// Displays detailed information about a SampleItem. class SampleItemDetailsView extends StatelessWidget { - const SampleItemDetailsView({super.key, required this.userId}); + const SampleItemDetailsView({super.key, required this.user}); - final int userId; + final Contact user; @override Widget build(BuildContext context) { @@ -71,7 +72,7 @@ class SampleItemDetailsView extends StatelessWidget { messages = messages.reversed.toList(); return Scaffold( appBar: AppBar( - title: Text('Your Chat with $userId'), + title: Text('Your Chat with ${user.displayName}'), ), body: Column( children: [ diff --git a/lib/src/views/chat_list_view.dart b/lib/src/views/chat_list_view.dart index 0540481..da135ec 100644 --- a/lib/src/views/chat_list_view.dart +++ b/lib/src/views/chat_list_view.dart @@ -2,12 +2,14 @@ import 'package:provider/provider.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/message_send_state_icon.dart'; import 'package:twonly/src/components/notification_badge.dart'; +import 'package:twonly/src/components/user_context_menu.dart'; +import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/providers/notify_provider.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chat_item_details_view.dart'; import 'package:twonly/src/views/search_username_view.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'new_message_view.dart'; import 'package:flutter/material.dart'; -import 'chat_item_details_view.dart'; import 'dart:async'; class ChatItem { @@ -26,31 +28,7 @@ class ChatItem { /// Displays a list of SampleItems. class ChatListView extends StatefulWidget { - const ChatListView({ - super.key, - this.items = const [ - ChatItem( - userId: 0, - username: "Alisa", - lastMessageInSeconds: 10, - flames: 129, - state: MessageSendState.sending), - ChatItem( - userId: 1, - username: "Klaus", - lastMessageInSeconds: 20829, - flames: 0, - state: MessageSendState.received), - ChatItem( - userId: 2, - username: "Markus", - lastMessageInSeconds: 291829, - state: MessageSendState.opened, - flames: 38), - ], - }); - - final List items; + const ChatListView({super.key}); @override State createState() => _ChatListViewState(); @@ -59,11 +37,17 @@ class ChatListView extends StatefulWidget { class _ChatListViewState extends State { int _secondsSinceOpen = 0; late Timer _timer; + List _activeUsers = []; @override void initState() { super.initState(); _startTimer(); + _loadActiveUsers(); + } + + Future _loadActiveUsers() async { + _activeUsers = context.read().allContacts; } void _startTimer() { @@ -80,49 +64,6 @@ class _ChatListViewState extends State { super.dispose(); } - String formatDuration(int seconds) { - if (seconds < 60) { - return '$seconds Sec.'; - } else if (seconds < 3600) { - int minutes = seconds ~/ 60; - return '$minutes Min.'; - } else if (seconds < 86400) { - int hours = seconds ~/ 3600; - return '$hours Hrs.'; // Assuming "Stu." is for hours - } else { - int days = seconds ~/ 86400; - return '$days Days'; - } - } - - Widget getSubtitle(ChatItem item) { - return Row( - children: [ - MessageSendStateIcon( - state: item.state, - ), - Text("•"), - const SizedBox(width: 5), - Text(formatDuration(item.lastMessageInSeconds + _secondsSinceOpen), - style: TextStyle(fontSize: 12)), - if (item.flames > 0) - Row( - children: [ - const SizedBox(width: 5), - Text("•"), - const SizedBox(width: 5), - Text(item.flames.toString(), style: TextStyle(fontSize: 12)), - Icon( - Icons.local_fire_department_sharp, - color: const Color.fromARGB(255, 215, 73, 58), - size: 16, - ), - ], - ) - ], - ); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -147,37 +88,94 @@ class _ChatListViewState extends State { ), body: ListView.builder( restorationId: 'chat_list_view', - itemCount: widget.items.length, + itemCount: _activeUsers.length, itemBuilder: (BuildContext context, int index) { - final item = widget.items[index]; - return ListTile( - title: Text(item.username), - subtitle: getSubtitle(item), - leading: InitialsAvatar(displayName: item.username), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SampleItemDetailsView( - userId: item.userId, - ), - ), - ); - }, - ); + final user = _activeUsers[index]; + return UserListItem(user: user, secondsSinceOpen: _secondsSinceOpen); + }, + ), + ); + } +} + +class UserListItem extends StatefulWidget { + final Contact user; + final int secondsSinceOpen; + + const UserListItem({ + super.key, + required this.user, + required this.secondsSinceOpen, + }); + + @override + State createState() => _UserListItem(); +} + +class _UserListItem extends State { + int flames = 0; + int lastMessageInSeconds = 0; + + @override + void initState() { + super.initState(); + _loadAsync(); + } + + Future _loadAsync() async { + // flames = await widget.user.getFlames(); + // lastMessageInSeconds = await widget.user.getLastMessageInSeconds(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return UserContextMenu( + user: widget.user, + child: ListTile( + title: Text(widget.user.displayName), + subtitle: Row( + children: [ + // MessageSendStateIcon( + // state: widget.user.state, + // ), + Text("•"), + const SizedBox(width: 5), + Text( + formatDuration(lastMessageInSeconds + widget.secondsSinceOpen), + style: TextStyle(fontSize: 12), + ), + if (flames > 0) + Row( + children: [ + const SizedBox(width: 5), + Text("•"), + const SizedBox(width: 5), + Text( + flames.toString(), + style: TextStyle(fontSize: 12), + ), + Icon( + Icons.local_fire_department_sharp, + color: const Color.fromARGB(255, 215, 73, 58), + size: 16, + ), + ], + ), + ], + ), + leading: InitialsAvatar(displayName: widget.user.displayName), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SampleItemDetailsView( + user: widget.user, + ), + ), + ); }, ), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NewMessageView(), - ), - ); - }, - child: const Icon(Icons.edit), - ), ); } } diff --git a/lib/src/views/home_view.dart b/lib/src/views/home_view.dart index d1ce05e..740b064 100644 --- a/lib/src/views/home_view.dart +++ b/lib/src/views/home_view.dart @@ -1,3 +1,5 @@ +import 'package:pie_menu/pie_menu.dart'; + import 'camera_preview_view.dart'; import 'chat_list_view.dart'; import 'profile_view.dart'; @@ -17,42 +19,65 @@ class HomeViewState extends State { final PageController _pageController = PageController(initialPage: 0); @override Widget build(BuildContext context) { - return Scaffold( - body: PageView( - controller: _pageController, - onPageChanged: (index) { - setState(() { - _activePageIdx = index; - }); - }, - children: [ - ChatListView(), - CameraPreviewViewPermission(), - ProfileView(settingsController: widget.settingsController) - ], + return PieCanvas( + theme: PieTheme( + brightness: Theme.of(context).brightness, + rightClickShowsMenu: true, + radius: 70, + buttonTheme: PieButtonTheme( + backgroundColor: Theme.of(context).colorScheme.tertiary, + iconColor: Theme.of(context).colorScheme.surfaceBright, + ), + buttonThemeHovered: PieButtonTheme( + backgroundColor: Theme.of(context).colorScheme.primary, + iconColor: Theme.of(context).colorScheme.surfaceBright, + ), + tooltipPadding: EdgeInsets.all(20), + overlayColor: const Color.fromARGB(41, 0, 0, 0), + + // spacing: 0, + tooltipTextStyle: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w600, + ), ), - bottomNavigationBar: BottomNavigationBar( - showSelectedLabels: false, - showUnselectedLabels: false, - selectedIconTheme: - IconThemeData(color: const Color.fromARGB(255, 255, 255, 255)), - items: [ - BottomNavigationBarItem(icon: Icon(Icons.chat), label: ""), - BottomNavigationBarItem( - icon: Icon(Icons.camera_alt), - label: "", - ), - BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""), - ], - onTap: (int index) { - setState(() { - _activePageIdx = index; - _pageController.animateToPage(_activePageIdx, - duration: const Duration(milliseconds: 100), - curve: Curves.bounceIn); - }); - }, - currentIndex: _activePageIdx, + child: Scaffold( + body: PageView( + controller: _pageController, + onPageChanged: (index) { + setState(() { + _activePageIdx = index; + }); + }, + children: [ + ChatListView(), + CameraPreviewViewPermission(), + ProfileView(settingsController: widget.settingsController) + ], + ), + bottomNavigationBar: BottomNavigationBar( + showSelectedLabels: false, + showUnselectedLabels: false, + selectedIconTheme: + IconThemeData(color: const Color.fromARGB(255, 255, 255, 255)), + items: [ + BottomNavigationBarItem(icon: Icon(Icons.chat), label: ""), + BottomNavigationBarItem( + icon: Icon(Icons.camera_alt), + label: "", + ), + BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""), + ], + onTap: (int index) { + setState(() { + _activePageIdx = index; + _pageController.animateToPage(_activePageIdx, + duration: const Duration(milliseconds: 100), + curve: Curves.bounceIn); + }); + }, + currentIndex: _activePageIdx, + ), ), ); } diff --git a/lib/src/views/new_message_view.dart b/lib/src/views/new_message_view.dart deleted file mode 100644 index 69498bb..0000000 --- a/lib/src/views/new_message_view.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:twonly/src/components/initialsavatar.dart'; -import 'package:twonly/src/model/contacts_model.dart'; -import 'package:twonly/src/views/search_username_view.dart'; - -class NewMessageView extends StatefulWidget { - const NewMessageView({super.key}); - - @override - State createState() => _NewMessageView(); -} - -class _NewMessageView extends State { - List _knownUsers = []; - List _filteredUsers = []; - String _lastSearchQuery = ''; - final TextEditingController searchUserName = TextEditingController(); - - @override - void initState() { - super.initState(); - _loadUsers(); - } - - Future _loadUsers() async { - final users = - (await DbContacts.getUsers()).where((c) => c.accepted).toList(); - setState(() { - _knownUsers = users; - _filteredUsers = List.from(_knownUsers); - }); - } - - Future _filterUsers(String query) async { - if (query.isEmpty) { - _filteredUsers = List.from(_knownUsers); - return; - } - if (_lastSearchQuery.length < query.length) { - _filteredUsers = _knownUsers - .where((user) => - user.displayName.toLowerCase().contains(query.toLowerCase())) - .toList(); - } else { - _filteredUsers = _filteredUsers - .where((user) => - user.displayName.toLowerCase().contains(query.toLowerCase())) - .toList(); - } - _lastSearchQuery = query; - } - - @override - Widget build(BuildContext context) { - InputDecoration getInputDecoration(hintText) { - final primaryColor = - Theme.of(context).colorScheme.primary; // Get the primary color - return InputDecoration( - hintText: hintText, - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(9.0), - borderSide: BorderSide(color: primaryColor, width: 1.0), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: BorderSide( - color: Theme.of(context).colorScheme.outline, width: 1.0), - ), - contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0), - ); - } - - 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: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: TextField( - onChanged: _filterUsers, - decoration: getInputDecoration( - AppLocalizations.of(context)!.newMessageTitle))), - const SizedBox(height: 10), - // Step 4: Add buttons at the top - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - FilledButton( - onPressed: () { - // Handle Neue Gruppe button press - }, - child: Text('Neue Gruppe'), - ), - FilledButton( - onPressed: () { - // Handle Nach Nutzername suchen button press - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => SearchUsernameView()), - ); - }, - child: Text('Nach Nutzername suchen'), - ), - ], - ), - const SizedBox(height: 10), - Expanded( - child: UserList(_filteredUsers), - ) - ], - ), - ), - ); - } -} - -class UserList extends StatelessWidget { - const UserList(this._knownUsers, {super.key}); - final List _knownUsers; - - @override - Widget build(BuildContext context) { - // Step 1: Sort the users alphabetically - _knownUsers.sort((a, b) => a.displayName.compareTo(b.displayName)); - - // Step 2: Group users by their initials - Map> groupedUsers = {}; - for (var user in _knownUsers) { - String initial = user.displayName[0].toUpperCase(); - if (!groupedUsers.containsKey(initial)) { - groupedUsers[initial] = []; - } - groupedUsers[initial]!.add(user.displayName); - } - - // Step 3: Create a list of sections - List>> sections = - groupedUsers.entries.toList(); - - return ListView.builder( - restorationId: 'new_message_users_list', - itemCount: sections.length, - itemBuilder: (BuildContext context, int sectionIndex) { - final section = sections[sectionIndex]; - final initial = section.key; - final users = section.value; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header for the initial - Padding( - padding: - const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text( - initial, - style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18), - ), - ), - // List of users under this initial - ...users.map((username) { - return ListTile( - title: Text(username), - leading: InitialsAvatar(displayName: username), - onTap: () { - // Handle tap - }, - ); - }).toList(), - ], - ); - }, - ); - } -} diff --git a/lib/src/views/search_username_view.dart b/lib/src/views/search_username_view.dart index 2d84d68..8dbd3dd 100644 --- a/lib/src/views/search_username_view.dart +++ b/lib/src/views/search_username_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/providers/notify_provider.dart'; @@ -97,14 +98,8 @@ class _SearchUsernameView extends State { .allContacts .where((contact) => !contact.accepted) .isNotEmpty) - Container( - alignment: Alignment.centerLeft, - padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 10), - child: Text( - AppLocalizations.of(context)!.searchUsernameNewFollowerTitle, - style: TextStyle(fontSize: 20), - ), - ), + HeadLineComponent( + AppLocalizations.of(context)!.searchUsernameNewFollowerTitle), Expanded( child: ContactsListView(), ) diff --git a/lib/src/views/share_image_editor_view.dart b/lib/src/views/share_image_editor_view.dart index 004dee2..8cdaf5e 100644 --- a/lib/src/views/share_image_editor_view.dart +++ b/lib/src/views/share_image_editor_view.dart @@ -18,7 +18,6 @@ class _ShareImageEditorView extends State { @override void initState() { - // TODO: implement initState super.initState(); imageIsLoaded(); } @@ -130,7 +129,7 @@ class _ShareImageEditorView extends State { ), label: Text( AppLocalizations.of(context)! - .shareImagedEditorSendImage, + .shareImagedEditorShareWith, style: TextStyle(fontSize: 17), ), ), diff --git a/lib/src/views/share_image_view.dart b/lib/src/views/share_image_view.dart index d482feb..d6c4dfc 100644 --- a/lib/src/views/share_image_view.dart +++ b/lib/src/views/share_image_view.dart @@ -3,8 +3,11 @@ import 'dart:collection'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:twonly/src/components/best_friends_selector.dart'; +import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/utils/misc.dart'; class ShareImageView extends StatefulWidget { const ShareImageView({super.key, required this.image}); @@ -15,8 +18,11 @@ class ShareImageView extends StatefulWidget { } class _ShareImageView extends State { - List _knownUsers = []; + List _users = []; + List _usersFiltered = []; final HashSet _selectedUserIds = HashSet(); + String _lastSearchQuery = ''; + final TextEditingController searchUserName = TextEditingController(); @override void initState() { @@ -25,12 +31,32 @@ class _ShareImageView extends State { } Future _loadUsers() async { - final users = await DbContacts.getUsers(); + final users = await DbContacts.getActiveUsers(); setState(() { - _knownUsers = users; + _users = users; + _usersFiltered = _users; }); } + Future _filterUsers(String query) async { + if (query.isEmpty) { + _usersFiltered = _users; + return; + } + if (_lastSearchQuery.length < query.length) { + _usersFiltered = _users + .where((user) => + user.displayName.toLowerCase().contains(query.toLowerCase())) + .toList(); + } else { + _usersFiltered = _usersFiltered + .where((user) => + user.displayName.toLowerCase().contains(query.toLowerCase())) + .toList(); + } + _lastSearchQuery = query; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -41,177 +67,59 @@ class _ShareImageView extends State { padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), child: Column( children: [ - Expanded( - child: ListView( - children: [ - Container( - alignment: Alignment.centerLeft, - padding: - EdgeInsets.symmetric(horizontal: 4.0, vertical: 10), - child: Text( - AppLocalizations.of(context)!.shareImageBestFriends, - style: TextStyle(fontSize: 20), - ), - ), - UserCheckboxList( - users: _knownUsers, - onChanged: (userId, checkedId) { - setState(() { - if (checkedId) { - _selectedUserIds.add(userId); - } else { - _selectedUserIds.remove(userId); - } - }); - }, - ), - const SizedBox(height: 10), - // Expanded( - // child: UserList(_filteredUsers), - // ) - ], - ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: _filterUsers, + decoration: getInputDecoration(context, + AppLocalizations.of(context)!.searchUsernameInput))), + const SizedBox(height: 10), + BestFriendsSelector( + users: _usersFiltered, + selectedUserIds: _selectedUserIds, + updateStatus: (userId, checked) { + if (checked) { + _selectedUserIds.add(userId); + } else { + _selectedUserIds.remove(userId); + } + }, ), - SizedBox( - height: 120, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FilledButton.icon( - icon: Icon(Icons.send), - onPressed: () async { - print(_selectedUserIds); - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => - // ShareImageView(image: widget.image)), - // ); - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.symmetric(vertical: 10, horizontal: 30), - ), - ), - label: Text( - AppLocalizations.of(context)! - .shareImagedEditorSendImage, - style: TextStyle(fontSize: 17), - ), - ), - ], - ), - ), + const SizedBox(height: 10), + HeadLineComponent(AppLocalizations.of(context)!.shareImageAllUsers), + Expanded( + child: UserList(_usersFiltered), ) ], ), ), - ); - } -} - -class UserCheckboxList extends StatelessWidget { - final List users; - final Function(Int64, bool) onChanged; - - const UserCheckboxList( - {super.key, required this.users, required this.onChanged}); - - @override - Widget build(BuildContext context) { - // Limit the number of users to 8 - final limitedUsers = users.length > 8 ? users.sublist(0, 8) : users; - - return Column( - spacing: 8, - children: List.generate((limitedUsers.length + 1) ~/ 2, (rowIndex) { - final firstUserIndex = rowIndex * 2; - final secondUserIndex = firstUserIndex + 1; - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: UserCheckbox( - user: limitedUsers[firstUserIndex], onChanged: onChanged)), - (secondUserIndex < limitedUsers.length) - ? Expanded( - child: UserCheckbox( - user: limitedUsers[secondUserIndex], - onChanged: onChanged), - ) - : Expanded( - child: Container(), - ), - ], - ); - }), - ); - } -} - -class UserCheckbox extends StatefulWidget { - final Contact user; - final Function(Int64, bool) onChanged; - - const UserCheckbox({super.key, required this.user, required this.onChanged}); - - @override - State createState() => _UserCheckboxState(); -} - -class _UserCheckboxState extends State { - bool isChecked = false; - - @override - Widget build(BuildContext context) { - return Container( - padding: - EdgeInsets.symmetric(horizontal: 3), // Padding inside the container - child: GestureDetector( - onTap: () { - setState(() { - isChecked = !isChecked; - widget.onChanged(widget.user.userId, isChecked); - }); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 10, vertical: 0), // Padding inside the container - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outline, - // color: Colors.blue, // Border color - width: 1.0, // Border width - ), - borderRadius: - BorderRadius.circular(8.0), // Optional: Rounded corners - ), + floatingActionButton: SizedBox( + height: 120, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20), child: Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - InitialsAvatar( - fontSize: 15, - 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, - ), - ), - Checkbox( - value: isChecked, - onChanged: (bool? value) { - setState(() { - isChecked = value ?? false; - widget.onChanged(widget.user.userId, isChecked); - }); + FilledButton.icon( + icon: Icon(Icons.send), + onPressed: () async { + print(_selectedUserIds); + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => + // ShareImageView(image: widget.image)), + // ); }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + EdgeInsets.symmetric(vertical: 10, horizontal: 30), + ), + ), + label: Text( + AppLocalizations.of(context)!.shareImagedEditorSendImage, + style: TextStyle(fontSize: 17), + ), ), ], ), @@ -220,3 +128,66 @@ class _UserCheckboxState extends State { ); } } + +class UserList extends StatelessWidget { + const UserList(this._knownUsers, {super.key}); + final List _knownUsers; + + @override + Widget build(BuildContext context) { + // Step 1: Sort the users alphabetically + _knownUsers.sort((a, b) => a.displayName.compareTo(b.displayName)); + + // Step 2: Group users by their initials + Map> groupedUsers = {}; + for (var user in _knownUsers) { + String initial = user.displayName[0].toUpperCase(); + if (!groupedUsers.containsKey(initial)) { + groupedUsers[initial] = []; + } + groupedUsers[initial]!.add(user.displayName); + } + + // Step 3: Create a list of sections + List>> sections = + groupedUsers.entries.toList(); + + return ListView.builder( + restorationId: 'new_message_users_list', + itemCount: sections.length, + itemBuilder: (BuildContext context, int sectionIndex) { + final section = sections[sectionIndex]; + final initial = section.key; + final users = section.value; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header for the initial + // Padding( + // padding: + // const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + // child: Text( + // initial, + // style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18), + // ), + // ), + // List of users under this initial + ...users.map((username) { + return ListTile( + title: Text(username), + leading: InitialsAvatar( + displayName: username, + fontSize: 15, + ), + onTap: () { + // Handle tap + }, + ); + }).toList(), + ], + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 869764c..b60710e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -794,6 +794,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + pie_menu: + dependency: "direct main" + description: + name: pie_menu + sha256: "011ccddc6f71c51a766cd3daba445f9e0cba9a6003e8528340c3064a241a5a30" + url: "https://pub.dev" + source: hosted + version: "3.2.7" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 93e2861..c6a73d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: path: ^1.9.0 path_provider: ^2.1.5 permission_handler: ^11.3.1 + pie_menu: ^3.2.7 pro_image_editor: ^7.6.4 protobuf: ^2.1.0 provider: ^6.1.2