add context menu

This commit is contained in:
otsmr 2025-01-25 23:30:22 +01:00
parent 34d588a0d1
commit e42b68e8ee
17 changed files with 548 additions and 494 deletions

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="100%" height="100%">
<defs>
<radialGradient id="gradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="0%" style="stop-color: rgb(107, 255, 191); stop-opacity: 1" />
<stop offset="100%" style="stop-color: rgb(255, 255, 255); stop-opacity: 1" />
</radialGradient>
</defs>
<rect width="100%" height="100%" fill="url(#gradient)" />
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#fff" d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -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();

View file

@ -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<Contact> users;
final Function(Int64, bool) updateStatus;
final HashSet<Int64> 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);
},
),
],
),
),
),
);
}
}

View file

@ -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),
),
);
}
}

View file

@ -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<UserContextMenu> createState() => _UserContextMenuState();
}
class _UserContextMenuState extends State<UserContextMenu> {
@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,
);
}
}

View file

@ -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",

View file

@ -53,6 +53,10 @@ class DbContacts extends CvModelBase {
List<CvField> get fields =>
[userId, displayName, accepted, requested, blocked, createdAt];
static Future<List<Contact>> getActiveUsers() async {
return (await getUsers()).where((u) => u.accepted).toList();
}
static Future<List<Contact>> getUsers() async {
try {
var users = await dbProvider.db!.query(tableName,

View file

@ -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),
);
}

View file

@ -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: [

View file

@ -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<ChatItem> items;
const ChatListView({super.key});
@override
State<ChatListView> createState() => _ChatListViewState();
@ -59,11 +37,17 @@ class ChatListView extends StatefulWidget {
class _ChatListViewState extends State<ChatListView> {
int _secondsSinceOpen = 0;
late Timer _timer;
List<Contact> _activeUsers = [];
@override
void initState() {
super.initState();
_startTimer();
_loadActiveUsers();
}
Future _loadActiveUsers() async {
_activeUsers = context.read<NotifyProvider>().allContacts;
}
void _startTimer() {
@ -80,49 +64,6 @@ class _ChatListViewState extends State<ChatListView> {
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<ChatListView> {
),
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<UserListItem> createState() => _UserListItem();
}
class _UserListItem extends State<UserListItem> {
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),
),
);
}
}

View file

@ -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<HomeView> {
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,
),
),
);
}

View file

@ -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<NewMessageView> createState() => _NewMessageView();
}
class _NewMessageView extends State<NewMessageView> {
List<Contact> _knownUsers = [];
List<Contact> _filteredUsers = [];
String _lastSearchQuery = '';
final TextEditingController searchUserName = TextEditingController();
@override
void initState() {
super.initState();
_loadUsers();
}
Future<void> _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<Contact> _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<String, List<String>> 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<MapEntry<String, List<String>>> 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(),
],
);
},
);
}
}

View file

@ -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<SearchUsernameView> {
.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(),
)

View file

@ -18,7 +18,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
@override
void initState() {
// TODO: implement initState
super.initState();
imageIsLoaded();
}
@ -130,7 +129,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
),
label: Text(
AppLocalizations.of(context)!
.shareImagedEditorSendImage,
.shareImagedEditorShareWith,
style: TextStyle(fontSize: 17),
),
),

View file

@ -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<ShareImageView> {
List<Contact> _knownUsers = [];
List<Contact> _users = [];
List<Contact> _usersFiltered = [];
final HashSet<Int64> _selectedUserIds = HashSet<Int64>();
String _lastSearchQuery = '';
final TextEditingController searchUserName = TextEditingController();
@override
void initState() {
@ -25,12 +31,32 @@ class _ShareImageView extends State<ShareImageView> {
}
Future<void> _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<ShareImageView> {
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>(
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<Contact> 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<UserCheckbox> createState() => _UserCheckboxState();
}
class _UserCheckboxState extends State<UserCheckbox> {
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>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
),
label: Text(
AppLocalizations.of(context)!.shareImagedEditorSendImage,
style: TextStyle(fontSize: 17),
),
),
],
),
@ -220,3 +128,66 @@ class _UserCheckboxState extends State<UserCheckbox> {
);
}
}
class UserList extends StatelessWidget {
const UserList(this._knownUsers, {super.key});
final List<Contact> _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<String, List<String>> 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<MapEntry<String, List<String>>> 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(),
],
);
},
);
}
}

View file

@ -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:

View file

@ -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