mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
uploads of files does work
This commit is contained in:
parent
e42b68e8ee
commit
2ab0ad3daa
13 changed files with 324 additions and 107 deletions
|
|
@ -88,7 +88,7 @@ class TextContent extends MessageContent {
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class ImageContent extends MessageContent {
|
class ImageContent extends MessageContent {
|
||||||
final String imageToken;
|
final List<int> imageToken;
|
||||||
|
|
||||||
ImageContent(this.imageToken);
|
ImageContent(this.imageToken);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,9 @@ Map<String, dynamic> _$TextContentToJson(TextContent instance) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageContent _$ImageContentFromJson(Map<String, dynamic> json) => ImageContent(
|
ImageContent _$ImageContentFromJson(Map<String, dynamic> json) => ImageContent(
|
||||||
json['imageToken'] as String,
|
(json['imageToken'] as List<dynamic>)
|
||||||
|
.map((e) => (e as num).toInt())
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ImageContentToJson(ImageContent instance) =>
|
Map<String, dynamic> _$ImageContentToJson(ImageContent instance) =>
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,8 @@ class ApiProvider {
|
||||||
DbContacts.acceptUser(fromUserId.toInt());
|
DbContacts.acceptUser(fromUserId.toInt());
|
||||||
updateNotifier();
|
updateNotifier();
|
||||||
break;
|
break;
|
||||||
|
case MessageKind.image:
|
||||||
|
log.info("Got image: ${message.content}");
|
||||||
default:
|
default:
|
||||||
log.shout("Got unknown MessageKind $message");
|
log.shout("Got unknown MessageKind $message");
|
||||||
}
|
}
|
||||||
|
|
@ -360,6 +362,43 @@ class ApiProvider {
|
||||||
return _asResult(resp);
|
return _asResult(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Result> getUploadToken(int size) async {
|
||||||
|
var get = ApplicationData_GetUploadToken()..len = size;
|
||||||
|
var appData = ApplicationData()..getuploadtoken = get;
|
||||||
|
var req = createClientToServerFromApplicationData(appData);
|
||||||
|
final resp = await _sendRequestV0(req);
|
||||||
|
if (resp == null) {
|
||||||
|
return Result.error(ErrorCode.InternalError);
|
||||||
|
}
|
||||||
|
return _asResult(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<int>?> uploadData(Uint8List data) async {
|
||||||
|
Result res = await getUploadToken(data.length);
|
||||||
|
|
||||||
|
if (res.isError || !res.value.hasUploadtoken()) {
|
||||||
|
Logger("api.dart").shout("Error getting upload token!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<int> uploadToken = res.value.uploadtoken;
|
||||||
|
log.info("Got token: $uploadToken");
|
||||||
|
|
||||||
|
log.shout("fragmentate the data");
|
||||||
|
|
||||||
|
var get = ApplicationData_UploadData()
|
||||||
|
..uploadToken = uploadToken
|
||||||
|
..data = data
|
||||||
|
..offset = 0;
|
||||||
|
|
||||||
|
var appData = ApplicationData()..uploaddata = get;
|
||||||
|
var req = createClientToServerFromApplicationData(appData);
|
||||||
|
final resp = await _sendRequestV0(req);
|
||||||
|
if (resp == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return _asResult(resp).isSuccess ? uploadToken : null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Result> getUserData(String username) async {
|
Future<Result> getUserData(String username) async {
|
||||||
var get = ApplicationData_GetUserByUsername()..username = username;
|
var get = ApplicationData_GetUserByUsername()..username = username;
|
||||||
var appData = ApplicationData()..getuserbyusername = get;
|
var appData = ApplicationData()..getuserbyusername = get;
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,29 @@ import 'package:twonly/src/model/contacts_model.dart';
|
||||||
/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool
|
/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool
|
||||||
// ignore: prefer_mixin
|
// ignore: prefer_mixin
|
||||||
class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
|
// The page index of the HomeView widget
|
||||||
|
int _activePageIdx = 0;
|
||||||
|
|
||||||
int _newContactRequests = 0;
|
int _newContactRequests = 0;
|
||||||
List<Contact> _allContacts = [];
|
List<Contact> _allContacts = [];
|
||||||
|
|
||||||
|
List<Contact> _sendingCurrentlyTo = [];
|
||||||
|
|
||||||
int get newContactRequests => _newContactRequests;
|
int get newContactRequests => _newContactRequests;
|
||||||
List<Contact> get allContacts => _allContacts;
|
List<Contact> get allContacts => _allContacts;
|
||||||
|
List<Contact> get sendingCurrentlyTo => _sendingCurrentlyTo;
|
||||||
|
|
||||||
|
int get activePageIdx => _activePageIdx;
|
||||||
|
|
||||||
|
void setActivePageIdx(int idx) {
|
||||||
|
_activePageIdx = idx;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addSendingTo(List<Contact> users) {
|
||||||
|
_sendingCurrentlyTo.addAll(users);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void update() async {
|
void update() async {
|
||||||
_allContacts = await DbContacts.getUsers();
|
_allContacts = await DbContacts.getUsers();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/main.dart';
|
import 'package:twonly/main.dart';
|
||||||
import 'package:twonly/src/model/contacts_model.dart';
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:twonly/src/proto/api/error.pb.dart';
|
import 'package:twonly/src/proto/api/error.pb.dart';
|
||||||
import 'package:twonly/src/providers/api_provider.dart';
|
import 'package:twonly/src/providers/api_provider.dart';
|
||||||
|
import 'package:twonly/src/providers/notify_provider.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
// ignore: library_prefixes
|
// ignore: library_prefixes
|
||||||
import 'package:twonly/src/utils/signal.dart' as SignalHelper;
|
import 'package:twonly/src/utils/signal.dart' as SignalHelper;
|
||||||
|
|
@ -55,6 +59,9 @@ Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
|
||||||
|
|
||||||
Result resp = await apiProvider.sendTextMessage(userId, bytes);
|
Result resp = await apiProvider.sendTextMessage(userId, bytes);
|
||||||
|
|
||||||
|
Logger("encryptAndSendMessage")
|
||||||
|
.shout("handle errors here and store them in the database");
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,3 +93,43 @@ Future<Result> createNewUser(String username, String inviteCode) async {
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future sendImage(
|
||||||
|
BuildContext context, List<Contact> users, String imagePath) async {
|
||||||
|
// 1. set notifier provider
|
||||||
|
|
||||||
|
context.read<NotifyProvider>().addSendingTo(users);
|
||||||
|
|
||||||
|
File imageFile = File(imagePath);
|
||||||
|
|
||||||
|
Uint8List? imageBytes = await getCompressedImage(imageFile);
|
||||||
|
if (imageBytes == null) {
|
||||||
|
Logger("api.dart").shout("Error compressing image!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < users.length; i++) {
|
||||||
|
Int64 target = users[i].userId;
|
||||||
|
Uint8List? encryptedImage =
|
||||||
|
await SignalHelper.encryptBytes(imageBytes, target);
|
||||||
|
if (encryptedImage == null) {
|
||||||
|
Logger("api.dart").shout("Error encrypting image!");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int>? imageToken = await apiProvider.uploadData(encryptedImage);
|
||||||
|
if (imageToken == null) {
|
||||||
|
Logger("api.dart").shout("handle error uploading like saving...");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message msg = Message(
|
||||||
|
kind: MessageKind.image,
|
||||||
|
content: ImageContent(imageToken),
|
||||||
|
timestamp: DateTime.timestamp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
print("Send image to $target");
|
||||||
|
encryptAndSendMessage(target, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
|
import 'package:image/image.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
@ -118,3 +121,13 @@ InputDecoration getInputDecoration(context, hintText) {
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
|
contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Uint8List?> getCompressedImage(File file) async {
|
||||||
|
var result = await FlutterImageCompress.compressWithFile(
|
||||||
|
file.absolute.path,
|
||||||
|
quality: 90,
|
||||||
|
);
|
||||||
|
print(file.lengthSync());
|
||||||
|
print(result!.length);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,27 @@ List<Uint8List>? removeLastFourBytes(Uint8List original) {
|
||||||
return [newList, lastFourBytes];
|
return [newList, lastFourBytes];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Uint8List?> encryptBytes(Uint8List bytes, Int64 target) async {
|
||||||
|
try {
|
||||||
|
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
||||||
|
|
||||||
|
SessionCipher session = SessionCipher.fromStore(
|
||||||
|
signalStore, SignalProtocolAddress(target.toString(), defaultDeviceId));
|
||||||
|
|
||||||
|
final ciphertext =
|
||||||
|
await session.encrypt(Uint8List.fromList(gzip.encode(bytes)));
|
||||||
|
|
||||||
|
var b = BytesBuilder();
|
||||||
|
b.add(ciphertext.serialize());
|
||||||
|
b.add(intToBytes(ciphertext.getType()));
|
||||||
|
|
||||||
|
return b.takeBytes();
|
||||||
|
} catch (e) {
|
||||||
|
Logger("utils/signal").shout(e.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Uint8List?> encryptMessage(Message msg, Int64 target) async {
|
Future<Uint8List?> encryptMessage(Message msg, Int64 target) async {
|
||||||
try {
|
try {
|
||||||
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
||||||
|
|
|
||||||
|
|
@ -77,17 +77,17 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
PageRouteBuilder(
|
PageRouteBuilder(
|
||||||
opaque: false,
|
opaque: false,
|
||||||
pageBuilder: (context, a1, a2) =>
|
pageBuilder: (context, a1, a2) =>
|
||||||
ShareImageEditorView(image: path),
|
ShareImageEditorView(image: path),
|
||||||
transitionsBuilder:
|
transitionsBuilder:
|
||||||
(context, animation, secondaryAnimation, child) {
|
(context, animation, secondaryAnimation, child) {
|
||||||
return child;
|
return child;
|
||||||
},
|
},
|
||||||
transitionDuration: Duration.zero,
|
transitionDuration: Duration.zero,
|
||||||
reverseTransitionDuration: Duration.zero),
|
reverseTransitionDuration: Duration.zero,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
debugPrint('Picture saved: ${path}');
|
|
||||||
},
|
},
|
||||||
multiple: (multiple) {
|
multiple: (multiple) {
|
||||||
multiple.fileBySensor.forEach((key, value) {
|
multiple.fileBySensor.forEach((key, value) {
|
||||||
|
|
|
||||||
|
|
@ -66,35 +66,69 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
List<Contact> sendingCurrentlyTo =
|
||||||
|
context.watch<NotifyProvider>().sendingCurrentlyTo;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(AppLocalizations.of(context)!.chatsTitle),
|
title: Text(AppLocalizations.of(context)!.chatsTitle),
|
||||||
actions: [
|
actions: [
|
||||||
NotificationBadge(
|
NotificationBadge(
|
||||||
count: context.watch<NotifyProvider>().newContactRequests,
|
count: context.watch<NotifyProvider>().newContactRequests,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(Icons.person_add),
|
icon: Icon(Icons.person_add),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => SearchUsernameView(),
|
builder: (context) => SearchUsernameView(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
if (sendingCurrentlyTo.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Stack(
|
||||||
|
// child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
|
strokeWidth: 1,
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.send, // Replace with your desired icon
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
size: 20, // Adjust the size as needed
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// ),
|
||||||
),
|
),
|
||||||
);
|
title: Text(sendingCurrentlyTo
|
||||||
},
|
.map((e) => e.displayName)
|
||||||
),
|
.toList()
|
||||||
)
|
.join(", ")),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
Expanded(
|
||||||
restorationId: 'chat_list_view',
|
child: ListView.builder(
|
||||||
itemCount: _activeUsers.length,
|
restorationId: 'chat_list_view',
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemCount: _activeUsers.length,
|
||||||
final user = _activeUsers[index];
|
itemBuilder: (BuildContext context, int index) {
|
||||||
return UserListItem(user: user, secondsSinceOpen: _secondsSinceOpen);
|
final user = _activeUsers[index];
|
||||||
},
|
return UserListItem(
|
||||||
),
|
user: user, secondsSinceOpen: _secondsSinceOpen);
|
||||||
);
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:pie_menu/pie_menu.dart';
|
import 'package:pie_menu/pie_menu.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:twonly/src/providers/notify_provider.dart';
|
||||||
|
|
||||||
import 'camera_preview_view.dart';
|
import 'camera_preview_view.dart';
|
||||||
import 'chat_list_view.dart';
|
import 'chat_list_view.dart';
|
||||||
|
|
@ -6,6 +8,8 @@ import 'profile_view.dart';
|
||||||
import '../settings/settings_controller.dart';
|
import '../settings/settings_controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final PageController homeViewPageController = PageController(initialPage: 0);
|
||||||
|
|
||||||
class HomeView extends StatefulWidget {
|
class HomeView extends StatefulWidget {
|
||||||
const HomeView({super.key, required this.settingsController});
|
const HomeView({super.key, required this.settingsController});
|
||||||
final SettingsController settingsController;
|
final SettingsController settingsController;
|
||||||
|
|
@ -15,8 +19,6 @@ class HomeView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeViewState extends State<HomeView> {
|
class HomeViewState extends State<HomeView> {
|
||||||
int _activePageIdx = 0;
|
|
||||||
final PageController _pageController = PageController(initialPage: 0);
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PieCanvas(
|
return PieCanvas(
|
||||||
|
|
@ -43,11 +45,9 @@ class HomeViewState extends State<HomeView> {
|
||||||
),
|
),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: PageView(
|
body: PageView(
|
||||||
controller: _pageController,
|
controller: homeViewPageController,
|
||||||
onPageChanged: (index) {
|
onPageChanged: (index) {
|
||||||
setState(() {
|
context.read<NotifyProvider>().setActivePageIdx(index);
|
||||||
_activePageIdx = index;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
ChatListView(),
|
ChatListView(),
|
||||||
|
|
@ -69,14 +69,14 @@ class HomeViewState extends State<HomeView> {
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""),
|
BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""),
|
||||||
],
|
],
|
||||||
onTap: (int index) {
|
onTap: (int index) {
|
||||||
|
context.read<NotifyProvider>().setActivePageIdx(index);
|
||||||
setState(() {
|
setState(() {
|
||||||
_activePageIdx = index;
|
homeViewPageController.animateToPage(index,
|
||||||
_pageController.animateToPage(_activePageIdx,
|
|
||||||
duration: const Duration(milliseconds: 100),
|
duration: const Duration(milliseconds: 100),
|
||||||
curve: Curves.bounceIn);
|
curve: Curves.bounceIn);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
currentIndex: _activePageIdx,
|
currentIndex: context.watch<NotifyProvider>().activePageIdx,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import 'dart:collection';
|
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:provider/provider.dart';
|
||||||
import 'package:twonly/src/components/best_friends_selector.dart';
|
import 'package:twonly/src/components/best_friends_selector.dart';
|
||||||
import 'package:twonly/src/components/headline.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/utils/api.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/views/home_view.dart';
|
||||||
|
|
||||||
class ShareImageView extends StatefulWidget {
|
class ShareImageView extends StatefulWidget {
|
||||||
const ShareImageView({super.key, required this.image});
|
const ShareImageView({super.key, required this.image});
|
||||||
|
|
@ -86,9 +89,14 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
HeadLineComponent(AppLocalizations.of(context)!.shareImageAllUsers),
|
if (_usersFiltered.isNotEmpty)
|
||||||
|
HeadLineComponent(
|
||||||
|
AppLocalizations.of(context)!.shareImageAllUsers),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: UserList(_usersFiltered),
|
child: UserList(
|
||||||
|
List.from(_usersFiltered),
|
||||||
|
selectedUserIds: _selectedUserIds,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -103,13 +111,17 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
icon: Icon(Icons.send),
|
icon: Icon(Icons.send),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
print(_selectedUserIds);
|
sendImage(
|
||||||
// Navigator.push(
|
context,
|
||||||
// context,
|
_users
|
||||||
// MaterialPageRoute(
|
.where((c) => _selectedUserIds.contains(c.userId))
|
||||||
// builder: (context) =>
|
.toList(),
|
||||||
// ShareImageView(image: widget.image)),
|
widget.image);
|
||||||
// );
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.read<NotifyProvider>().setActivePageIdx(0);
|
||||||
|
homeViewPageController.jumpToPage(0);
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||||
|
|
@ -130,62 +142,44 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserList extends StatelessWidget {
|
class UserList extends StatelessWidget {
|
||||||
const UserList(this._knownUsers, {super.key});
|
const UserList(this.users, {super.key, required this.selectedUserIds});
|
||||||
final List<Contact> _knownUsers;
|
final List<Contact> users;
|
||||||
|
final HashSet<Int64> selectedUserIds;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Step 1: Sort the users alphabetically
|
// Step 1: Sort the users alphabetically
|
||||||
_knownUsers.sort((a, b) => a.displayName.compareTo(b.displayName));
|
users.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(
|
return ListView.builder(
|
||||||
restorationId: 'new_message_users_list',
|
restorationId: 'new_message_users_list',
|
||||||
itemCount: sections.length,
|
itemCount: users.length,
|
||||||
itemBuilder: (BuildContext context, int sectionIndex) {
|
itemBuilder: (BuildContext context, int i) {
|
||||||
final section = sections[sectionIndex];
|
Contact user = users[i];
|
||||||
final initial = section.key;
|
return ListTile(
|
||||||
final users = section.value;
|
title: Text(user.displayName),
|
||||||
|
leading: InitialsAvatar(
|
||||||
return Column(
|
displayName: user.displayName,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
fontSize: 15,
|
||||||
children: [
|
),
|
||||||
// Header for the initial
|
trailing: Checkbox(
|
||||||
// Padding(
|
value: selectedUserIds.contains(user.userId),
|
||||||
// padding:
|
onChanged: (bool? value) {
|
||||||
// const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
if (value == null) return;
|
||||||
// child: Text(
|
if (value) {
|
||||||
// initial,
|
selectedUserIds.add(user.userId);
|
||||||
// style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18),
|
} else {
|
||||||
// ),
|
selectedUserIds.remove(user.userId);
|
||||||
// ),
|
}
|
||||||
// List of users under this initial
|
},
|
||||||
...users.map((username) {
|
),
|
||||||
return ListTile(
|
onTap: () {
|
||||||
title: Text(username),
|
if (!selectedUserIds.contains(user.userId)) {
|
||||||
leading: InitialsAvatar(
|
selectedUserIds.add(user.userId);
|
||||||
displayName: username,
|
} else {
|
||||||
fontSize: 15,
|
selectedUserIds.remove(user.userId);
|
||||||
),
|
}
|
||||||
onTap: () {
|
},
|
||||||
// Handle tap
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
48
pubspec.lock
48
pubspec.lock
|
|
@ -315,6 +315,54 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_image_compress:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress
|
||||||
|
sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
flutter_image_compress_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_common
|
||||||
|
sha256: "7f79bc6c8a363063620b4e372fa86bc691e1cb28e58048cd38e030692fbd99ee"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
|
flutter_image_compress_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_macos
|
||||||
|
sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
|
flutter_image_compress_ohos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_ohos
|
||||||
|
sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.3"
|
||||||
|
flutter_image_compress_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_platform_interface
|
||||||
|
sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
|
flutter_image_compress_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_web
|
||||||
|
sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.5"
|
||||||
flutter_keyboard_visibility:
|
flutter_keyboard_visibility:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ dependencies:
|
||||||
fixnum: ^1.1.1
|
fixnum: ^1.1.1
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_image_compress: ^2.4.0
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_secure_storage: ^9.2.2
|
flutter_secure_storage: ^9.2.2
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue