mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:48:41 +00:00
add context menu
This commit is contained in:
parent
34d588a0d1
commit
e42b68e8ee
17 changed files with 548 additions and 494 deletions
10
assets/images/logo_gradient.svg
Normal file
10
assets/images/logo_gradient.svg
Normal 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 |
|
|
@ -16,7 +16,7 @@ late ApiProvider apiProvider;
|
||||||
void main() async {
|
void main() async {
|
||||||
final settingsController = SettingsController(SettingsService());
|
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.
|
// This prevents a sudden theme change when the app is first displayed.
|
||||||
await settingsController.loadSettings();
|
await settingsController.loadSettings();
|
||||||
|
|
||||||
|
|
|
||||||
128
lib/src/components/best_friends_selector.dart
Normal file
128
lib/src/components/best_friends_selector.dart
Normal 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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/src/components/headline.dart
Normal file
19
lib/src/components/headline.dart
Normal 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
lib/src/components/user_context_menu.dart
Normal file
41
lib/src/components/user_context_menu.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,8 +11,10 @@
|
||||||
"shareImageTitle": "Share with",
|
"shareImageTitle": "Share with",
|
||||||
"shareImageBestFriends": "Best friends",
|
"shareImageBestFriends": "Best friends",
|
||||||
"shareImagedEditorSendImage": "Send",
|
"shareImagedEditorSendImage": "Send",
|
||||||
|
"shareImagedEditorShareWith": "Share with",
|
||||||
"shareImagedEditorSaveImage": "Save",
|
"shareImagedEditorSaveImage": "Save",
|
||||||
"shareImagedEditorSavedImage": "Saved",
|
"shareImagedEditorSavedImage": "Saved",
|
||||||
|
"shareImageAllUsers": "All contacts",
|
||||||
"searchUsernameInput": "Username",
|
"searchUsernameInput": "Username",
|
||||||
"searchUsernameTitle": "Search username",
|
"searchUsernameTitle": "Search username",
|
||||||
"searchUsernameNotFound": "Username not found",
|
"searchUsernameNotFound": "Username not found",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@ class DbContacts extends CvModelBase {
|
||||||
List<CvField> get fields =>
|
List<CvField> get fields =>
|
||||||
[userId, displayName, accepted, requested, blocked, createdAt];
|
[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 {
|
static Future<List<Contact>> getUsers() async {
|
||||||
try {
|
try {
|
||||||
var users = await dbProvider.db!.query(tableName,
|
var users = await dbProvider.db!.query(tableName,
|
||||||
|
|
|
||||||
|
|
@ -85,3 +85,36 @@ String errorCodeToText(BuildContext context, ErrorCode code) {
|
||||||
return code.toString(); // Fallback for unrecognized keys
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
|
|
||||||
class AlignedTextBox extends StatelessWidget {
|
class AlignedTextBox extends StatelessWidget {
|
||||||
const AlignedTextBox({super.key, required this.text, required this.right});
|
const AlignedTextBox({super.key, required this.text, required this.right});
|
||||||
|
|
@ -39,9 +40,9 @@ class AlignedTextBox extends StatelessWidget {
|
||||||
|
|
||||||
/// Displays detailed information about a SampleItem.
|
/// Displays detailed information about a SampleItem.
|
||||||
class SampleItemDetailsView extends StatelessWidget {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -71,7 +72,7 @@ class SampleItemDetailsView extends StatelessWidget {
|
||||||
messages = messages.reversed.toList();
|
messages = messages.reversed.toList();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Your Chat with $userId'),
|
title: Text('Your Chat with ${user.displayName}'),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@ import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/src/components/initialsavatar.dart';
|
import 'package:twonly/src/components/initialsavatar.dart';
|
||||||
import 'package:twonly/src/components/message_send_state_icon.dart';
|
import 'package:twonly/src/components/message_send_state_icon.dart';
|
||||||
import 'package:twonly/src/components/notification_badge.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/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:twonly/src/views/search_username_view.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'new_message_view.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'chat_item_details_view.dart';
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
class ChatItem {
|
class ChatItem {
|
||||||
|
|
@ -26,31 +28,7 @@ class ChatItem {
|
||||||
|
|
||||||
/// Displays a list of SampleItems.
|
/// Displays a list of SampleItems.
|
||||||
class ChatListView extends StatefulWidget {
|
class ChatListView extends StatefulWidget {
|
||||||
const ChatListView({
|
const ChatListView({super.key});
|
||||||
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;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatListView> createState() => _ChatListViewState();
|
State<ChatListView> createState() => _ChatListViewState();
|
||||||
|
|
@ -59,11 +37,17 @@ class ChatListView extends StatefulWidget {
|
||||||
class _ChatListViewState extends State<ChatListView> {
|
class _ChatListViewState extends State<ChatListView> {
|
||||||
int _secondsSinceOpen = 0;
|
int _secondsSinceOpen = 0;
|
||||||
late Timer _timer;
|
late Timer _timer;
|
||||||
|
List<Contact> _activeUsers = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_startTimer();
|
_startTimer();
|
||||||
|
_loadActiveUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _loadActiveUsers() async {
|
||||||
|
_activeUsers = context.read<NotifyProvider>().allContacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startTimer() {
|
void _startTimer() {
|
||||||
|
|
@ -80,49 +64,6 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
super.dispose();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -147,37 +88,94 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
restorationId: 'chat_list_view',
|
restorationId: 'chat_list_view',
|
||||||
itemCount: widget.items.length,
|
itemCount: _activeUsers.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final item = widget.items[index];
|
final user = _activeUsers[index];
|
||||||
return ListTile(
|
return UserListItem(user: user, secondsSinceOpen: _secondsSinceOpen);
|
||||||
title: Text(item.username),
|
},
|
||||||
subtitle: getSubtitle(item),
|
),
|
||||||
leading: InitialsAvatar(displayName: item.username),
|
);
|
||||||
onTap: () {
|
}
|
||||||
Navigator.push(
|
}
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
class UserListItem extends StatefulWidget {
|
||||||
builder: (context) => SampleItemDetailsView(
|
final Contact user;
|
||||||
userId: item.userId,
|
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),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:pie_menu/pie_menu.dart';
|
||||||
|
|
||||||
import 'camera_preview_view.dart';
|
import 'camera_preview_view.dart';
|
||||||
import 'chat_list_view.dart';
|
import 'chat_list_view.dart';
|
||||||
import 'profile_view.dart';
|
import 'profile_view.dart';
|
||||||
|
|
@ -17,42 +19,65 @@ class HomeViewState extends State<HomeView> {
|
||||||
final PageController _pageController = PageController(initialPage: 0);
|
final PageController _pageController = PageController(initialPage: 0);
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return PieCanvas(
|
||||||
body: PageView(
|
theme: PieTheme(
|
||||||
controller: _pageController,
|
brightness: Theme.of(context).brightness,
|
||||||
onPageChanged: (index) {
|
rightClickShowsMenu: true,
|
||||||
setState(() {
|
radius: 70,
|
||||||
_activePageIdx = index;
|
buttonTheme: PieButtonTheme(
|
||||||
});
|
backgroundColor: Theme.of(context).colorScheme.tertiary,
|
||||||
},
|
iconColor: Theme.of(context).colorScheme.surfaceBright,
|
||||||
children: [
|
),
|
||||||
ChatListView(),
|
buttonThemeHovered: PieButtonTheme(
|
||||||
CameraPreviewViewPermission(),
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
ProfileView(settingsController: widget.settingsController)
|
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(
|
child: Scaffold(
|
||||||
showSelectedLabels: false,
|
body: PageView(
|
||||||
showUnselectedLabels: false,
|
controller: _pageController,
|
||||||
selectedIconTheme:
|
onPageChanged: (index) {
|
||||||
IconThemeData(color: const Color.fromARGB(255, 255, 255, 255)),
|
setState(() {
|
||||||
items: [
|
_activePageIdx = index;
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.chat), label: ""),
|
});
|
||||||
BottomNavigationBarItem(
|
},
|
||||||
icon: Icon(Icons.camera_alt),
|
children: [
|
||||||
label: "",
|
ChatListView(),
|
||||||
),
|
CameraPreviewViewPermission(),
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""),
|
ProfileView(settingsController: widget.settingsController)
|
||||||
],
|
],
|
||||||
onTap: (int index) {
|
),
|
||||||
setState(() {
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
_activePageIdx = index;
|
showSelectedLabels: false,
|
||||||
_pageController.animateToPage(_activePageIdx,
|
showUnselectedLabels: false,
|
||||||
duration: const Duration(milliseconds: 100),
|
selectedIconTheme:
|
||||||
curve: Curves.bounceIn);
|
IconThemeData(color: const Color.fromARGB(255, 255, 255, 255)),
|
||||||
});
|
items: [
|
||||||
},
|
BottomNavigationBarItem(icon: Icon(Icons.chat), label: ""),
|
||||||
currentIndex: _activePageIdx,
|
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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:twonly/src/components/headline.dart';
|
||||||
import 'package:twonly/src/components/initialsavatar.dart';
|
import 'package:twonly/src/components/initialsavatar.dart';
|
||||||
import 'package:twonly/src/model/contacts_model.dart';
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
import 'package:twonly/src/providers/notify_provider.dart';
|
import 'package:twonly/src/providers/notify_provider.dart';
|
||||||
|
|
@ -97,14 +98,8 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
||||||
.allContacts
|
.allContacts
|
||||||
.where((contact) => !contact.accepted)
|
.where((contact) => !contact.accepted)
|
||||||
.isNotEmpty)
|
.isNotEmpty)
|
||||||
Container(
|
HeadLineComponent(
|
||||||
alignment: Alignment.centerLeft,
|
AppLocalizations.of(context)!.searchUsernameNewFollowerTitle),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 10),
|
|
||||||
child: Text(
|
|
||||||
AppLocalizations.of(context)!.searchUsernameNewFollowerTitle,
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ContactsListView(),
|
child: ContactsListView(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
// TODO: implement initState
|
|
||||||
super.initState();
|
super.initState();
|
||||||
imageIsLoaded();
|
imageIsLoaded();
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +129,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
AppLocalizations.of(context)!
|
AppLocalizations.of(context)!
|
||||||
.shareImagedEditorSendImage,
|
.shareImagedEditorShareWith,
|
||||||
style: TextStyle(fontSize: 17),
|
style: TextStyle(fontSize: 17),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@ import 'dart:collection';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.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/components/initialsavatar.dart';
|
||||||
import 'package:twonly/src/model/contacts_model.dart';
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
class ShareImageView extends StatefulWidget {
|
class ShareImageView extends StatefulWidget {
|
||||||
const ShareImageView({super.key, required this.image});
|
const ShareImageView({super.key, required this.image});
|
||||||
|
|
@ -15,8 +18,11 @@ class ShareImageView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ShareImageView extends State<ShareImageView> {
|
class _ShareImageView extends State<ShareImageView> {
|
||||||
List<Contact> _knownUsers = [];
|
List<Contact> _users = [];
|
||||||
|
List<Contact> _usersFiltered = [];
|
||||||
final HashSet<Int64> _selectedUserIds = HashSet<Int64>();
|
final HashSet<Int64> _selectedUserIds = HashSet<Int64>();
|
||||||
|
String _lastSearchQuery = '';
|
||||||
|
final TextEditingController searchUserName = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -25,12 +31,32 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadUsers() async {
|
Future<void> _loadUsers() async {
|
||||||
final users = await DbContacts.getUsers();
|
final users = await DbContacts.getActiveUsers();
|
||||||
setState(() {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -41,177 +67,59 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10),
|
padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Padding(
|
||||||
child: ListView(
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
children: [
|
child: TextField(
|
||||||
Container(
|
onChanged: _filterUsers,
|
||||||
alignment: Alignment.centerLeft,
|
decoration: getInputDecoration(context,
|
||||||
padding:
|
AppLocalizations.of(context)!.searchUsernameInput))),
|
||||||
EdgeInsets.symmetric(horizontal: 4.0, vertical: 10),
|
const SizedBox(height: 10),
|
||||||
child: Text(
|
BestFriendsSelector(
|
||||||
AppLocalizations.of(context)!.shareImageBestFriends,
|
users: _usersFiltered,
|
||||||
style: TextStyle(fontSize: 20),
|
selectedUserIds: _selectedUserIds,
|
||||||
),
|
updateStatus: (userId, checked) {
|
||||||
),
|
if (checked) {
|
||||||
UserCheckboxList(
|
_selectedUserIds.add(userId);
|
||||||
users: _knownUsers,
|
} else {
|
||||||
onChanged: (userId, checkedId) {
|
_selectedUserIds.remove(userId);
|
||||||
setState(() {
|
}
|
||||||
if (checkedId) {
|
},
|
||||||
_selectedUserIds.add(userId);
|
|
||||||
} else {
|
|
||||||
_selectedUserIds.remove(userId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
// Expanded(
|
|
||||||
// child: UserList(_filteredUsers),
|
|
||||||
// )
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(height: 10),
|
||||||
height: 120,
|
HeadLineComponent(AppLocalizations.of(context)!.shareImageAllUsers),
|
||||||
child: Padding(
|
Expanded(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
child: UserList(_usersFiltered),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
floatingActionButton: SizedBox(
|
||||||
}
|
height: 120,
|
||||||
}
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||||
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
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
InitialsAvatar(
|
FilledButton.icon(
|
||||||
fontSize: 15,
|
icon: Icon(Icons.send),
|
||||||
displayName: widget
|
onPressed: () async {
|
||||||
.user.displayName), // Display first letter of the name
|
print(_selectedUserIds);
|
||||||
SizedBox(width: 8),
|
// Navigator.push(
|
||||||
Expanded(
|
// context,
|
||||||
child: Text(
|
// MaterialPageRoute(
|
||||||
widget.user.displayName.length > 10
|
// builder: (context) =>
|
||||||
? '${widget.user.displayName.substring(0, 10)}...' // Trim if too long
|
// ShareImageView(image: widget.image)),
|
||||||
: widget.user.displayName,
|
// );
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Checkbox(
|
|
||||||
value: isChecked,
|
|
||||||
onChanged: (bool? value) {
|
|
||||||
setState(() {
|
|
||||||
isChecked = value ?? false;
|
|
||||||
widget.onChanged(widget.user.userId, isChecked);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
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(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -794,6 +794,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.2"
|
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:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ dependencies:
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
permission_handler: ^11.3.1
|
permission_handler: ^11.3.1
|
||||||
|
pie_menu: ^3.2.7
|
||||||
pro_image_editor: ^7.6.4
|
pro_image_editor: ^7.6.4
|
||||||
protobuf: ^2.1.0
|
protobuf: ^2.1.0
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue