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