mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 07:48:40 +00:00
start with share image view
This commit is contained in:
parent
6502859cc1
commit
50efa6530f
7 changed files with 289 additions and 99 deletions
|
|
@ -46,7 +46,7 @@
|
|||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Why? What is the purpose of the app?</string>
|
||||
<string>To create photos that can be shared.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Explanation on why the microphone access is needed.</string>
|
||||
</dict>
|
||||
|
|
|
|||
96
lib/src/components/initialsavatar_component.dart
Normal file
96
lib/src/components/initialsavatar_component.dart
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class InitialsAvatar extends StatelessWidget {
|
||||
final String displayName;
|
||||
|
||||
const InitialsAvatar({super.key, required this.displayName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Extract initials from the displayName
|
||||
List<String> nameParts = displayName.split(' ');
|
||||
String initials = nameParts.map((part) => part[0]).join().toUpperCase();
|
||||
|
||||
if (initials.length > 2) {
|
||||
initials = initials[0] + initials[1];
|
||||
} else if (initials.length == 1) {
|
||||
initials = displayName[0] + displayName[1];
|
||||
}
|
||||
|
||||
initials = initials.toUpperCase();
|
||||
|
||||
// Generate a color based on the initials (you can customize this logic)
|
||||
Color avatarColor = _getColorFromUsername(
|
||||
displayName, Theme.of(context).brightness == Brightness.dark);
|
||||
|
||||
return CircleAvatar(
|
||||
backgroundColor: avatarColor,
|
||||
child: Text(
|
||||
initials,
|
||||
style: TextStyle(
|
||||
color: _getTextColor(avatarColor),
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getTextColor(Color color) {
|
||||
double value = 100.0;
|
||||
// Ensure the value does not exceed the RGB limits
|
||||
int newRed = ((color.r * 255) - value).clamp(0, 255).round();
|
||||
int newGreen = (color.g * 255 - value).clamp(0, 255).round();
|
||||
int newBlue = (color.b * 255 - value).clamp(0, 255).round();
|
||||
|
||||
return Color.fromARGB((color.a * 255).round(), newRed, newGreen, newBlue);
|
||||
}
|
||||
|
||||
Color _getColorFromUsername(String displayName, bool isDarkMode) {
|
||||
// Define color lists for light and dark themes
|
||||
List<Color> lightColors = [
|
||||
Colors.red,
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
Colors.teal,
|
||||
Colors.amber,
|
||||
Colors.indigo,
|
||||
Colors.cyan,
|
||||
Colors.lime,
|
||||
Colors.pink,
|
||||
Colors.brown,
|
||||
Colors.grey,
|
||||
];
|
||||
|
||||
List<Color> darkColors = [
|
||||
const Color.fromARGB(255, 246, 227, 254), // Light Lavender
|
||||
const Color.fromARGB(255, 246, 216, 215), // Light Pink
|
||||
const Color.fromARGB(255, 226, 236, 235), // Light Teal
|
||||
const Color.fromARGB(255, 255, 224, 178), // Light Yellow
|
||||
const Color.fromARGB(255, 255, 182, 193), // Light Pink (Hot Pink)
|
||||
const Color.fromARGB(255, 173, 216, 230), // Light Blue
|
||||
const Color.fromARGB(255, 221, 160, 221), // Plum
|
||||
const Color.fromARGB(255, 255, 228, 196), // Bisque
|
||||
const Color.fromARGB(255, 240, 230, 140), // Khaki
|
||||
const Color.fromARGB(255, 255, 192, 203), // Pink
|
||||
const Color.fromARGB(255, 255, 218, 185), // Peach Puff
|
||||
const Color.fromARGB(255, 255, 160, 122), // Light Salmon
|
||||
const Color.fromARGB(255, 135, 206, 250), // Light Sky Blue
|
||||
const Color.fromARGB(255, 255, 228, 225), // Misty Rose
|
||||
const Color.fromARGB(255, 240, 248, 255), // Alice Blue
|
||||
const Color.fromARGB(255, 255, 250, 205), // Lemon Chiffon
|
||||
const Color.fromARGB(255, 255, 218, 185), // Peach Puff
|
||||
];
|
||||
|
||||
// Simple logic to generate a hash from initials
|
||||
int hash = displayName.codeUnits.fold(0, (prev, element) => prev + element);
|
||||
|
||||
// Select the appropriate color list based on the current theme brightness
|
||||
List<Color> colors = isDarkMode ? darkColors : lightColors;
|
||||
|
||||
// Use the hash to select a color from the list
|
||||
return colors[hash % colors.length];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
|
|
@ -114,88 +113,3 @@ Future<Result> createNewUser(String username, String inviteCode) async {
|
|||
|
||||
return res;
|
||||
}
|
||||
|
||||
Widget createInitialsAvatar(String username, bool isDarkMode) {
|
||||
// Extract initials from the username
|
||||
List<String> nameParts = username.split(' ');
|
||||
String initials = nameParts.map((part) => part[0]).join().toUpperCase();
|
||||
if (initials.length > 2) {
|
||||
initials = initials[0] + initials[1];
|
||||
} else if (initials.length == 1) {
|
||||
initials = username[0] + username[1];
|
||||
}
|
||||
|
||||
initials = initials.toUpperCase();
|
||||
|
||||
// Generate a color based on the initials (you can customize this logic)
|
||||
Color avatarColor = _getColorFromUsername(username, isDarkMode);
|
||||
|
||||
return CircleAvatar(
|
||||
backgroundColor: avatarColor,
|
||||
child: Text(
|
||||
initials,
|
||||
style: TextStyle(
|
||||
color: _getTextColor(avatarColor),
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 20),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getTextColor(Color color) {
|
||||
double value = 100.0;
|
||||
// Ensure the value does not exceed the RGB limits
|
||||
int newRed = ((color.r * 255) - value).clamp(0, 255).round();
|
||||
int newGreen = (color.g * 255 - value).clamp(0, 255).round();
|
||||
int newBlue = (color.b * 255 - value).clamp(0, 255).round();
|
||||
|
||||
return Color.fromARGB((color.a * 255).round(), newRed, newGreen, newBlue);
|
||||
}
|
||||
|
||||
Color _getColorFromUsername(String username, bool isDarkMode) {
|
||||
// Define color lists for light and dark themes
|
||||
List<Color> lightColors = [
|
||||
Colors.red,
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
Colors.teal,
|
||||
Colors.amber,
|
||||
Colors.indigo,
|
||||
Colors.cyan,
|
||||
Colors.lime,
|
||||
Colors.pink,
|
||||
Colors.brown,
|
||||
Colors.grey,
|
||||
];
|
||||
|
||||
List<Color> darkColors = [
|
||||
const Color.fromARGB(255, 246, 227, 254), // Light Lavender
|
||||
const Color.fromARGB(255, 246, 216, 215), // Light Pink
|
||||
const Color.fromARGB(255, 226, 236, 235), // Light Teal
|
||||
const Color.fromARGB(255, 255, 224, 178), // Light Yellow
|
||||
const Color.fromARGB(255, 255, 182, 193), // Light Pink (Hot Pink)
|
||||
const Color.fromARGB(255, 173, 216, 230), // Light Blue
|
||||
const Color.fromARGB(255, 221, 160, 221), // Plum
|
||||
const Color.fromARGB(255, 255, 228, 196), // Bisque
|
||||
const Color.fromARGB(255, 240, 230, 140), // Khaki
|
||||
const Color.fromARGB(255, 255, 192, 203), // Pink
|
||||
const Color.fromARGB(255, 255, 218, 185), // Peach Puff
|
||||
const Color.fromARGB(255, 255, 160, 122), // Light Salmon
|
||||
const Color.fromARGB(255, 135, 206, 250), // Light Sky Blue
|
||||
const Color.fromARGB(255, 255, 228, 225), // Misty Rose
|
||||
const Color.fromARGB(255, 240, 248, 255), // Alice Blue
|
||||
const Color.fromARGB(255, 255, 250, 205), // Lemon Chiffon
|
||||
const Color.fromARGB(255, 255, 218, 185), // Peach Puff
|
||||
];
|
||||
|
||||
// Simple logic to generate a hash from initials
|
||||
int hash = username.codeUnits.fold(0, (prev, element) => prev + element);
|
||||
|
||||
// Select the appropriate color list based on the current theme brightness
|
||||
List<Color> colors = isDarkMode ? darkColors : lightColors;
|
||||
|
||||
// Use the hash to select a color from the list
|
||||
return colors[hash % colors.length];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
// import 'package:camera/camera.dart';
|
||||
// import 'camera_editor_view.dart';
|
||||
// import 'package:flutter/gestures.dart';
|
||||
import 'package:camerawesome/pigeon.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math';
|
||||
import 'package:camerawesome/camerawesome_plugin.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/views/share_image_view.dart';
|
||||
|
||||
class CameraPreviewView extends StatefulWidget {
|
||||
const CameraPreviewView({super.key});
|
||||
|
|
@ -30,6 +29,50 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
borderRadius: BorderRadius.circular(22),
|
||||
child: CameraAwesomeBuilder.custom(
|
||||
progressIndicator: Container(),
|
||||
onMediaCaptureEvent: (event) {
|
||||
switch ((event.status, event.isPicture, event.isVideo)) {
|
||||
case (MediaCaptureStatus.capturing, true, false):
|
||||
debugPrint('Capturing picture...');
|
||||
case (MediaCaptureStatus.success, true, false):
|
||||
event.captureRequest.when(
|
||||
single: (single) {
|
||||
final path = single.file?.path;
|
||||
if (path == null) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareImageView(image: path),
|
||||
),
|
||||
);
|
||||
debugPrint('Picture saved: ${path}');
|
||||
},
|
||||
multiple: (multiple) {
|
||||
multiple.fileBySensor.forEach((key, value) {
|
||||
debugPrint('multiple image taken: $key ${value?.path}');
|
||||
});
|
||||
},
|
||||
);
|
||||
case (MediaCaptureStatus.failure, true, false):
|
||||
debugPrint('Failed to capture picture: ${event.exception}');
|
||||
case (MediaCaptureStatus.capturing, false, true):
|
||||
debugPrint('Capturing video...');
|
||||
case (MediaCaptureStatus.success, false, true):
|
||||
event.captureRequest.when(
|
||||
single: (single) {
|
||||
debugPrint('Video saved: ${single.file?.path}');
|
||||
},
|
||||
multiple: (multiple) {
|
||||
multiple.fileBySensor.forEach((key, value) {
|
||||
debugPrint('multiple video taken: $key ${value?.path}');
|
||||
});
|
||||
},
|
||||
);
|
||||
case (MediaCaptureStatus.failure, false, true):
|
||||
debugPrint('Failed to capture video: ${event.exception}');
|
||||
default:
|
||||
debugPrint('Unknown event: $event');
|
||||
}
|
||||
},
|
||||
builder: (cameraState, preview) {
|
||||
return Container(
|
||||
child: Stack(
|
||||
|
|
@ -99,7 +142,25 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
),
|
||||
);
|
||||
},
|
||||
saveConfig: SaveConfig.photoAndVideo(),
|
||||
saveConfig: SaveConfig.photoAndVideo(
|
||||
photoPathBuilder: (sensors) async {
|
||||
final Directory extDir = await getTemporaryDirectory();
|
||||
final testDir = await Directory(
|
||||
'${extDir.path}/images',
|
||||
).create(recursive: true);
|
||||
final String filePath =
|
||||
'${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
return SingleCaptureRequest(filePath, sensors.first);
|
||||
// // Separate pictures taken with front and back camera
|
||||
// return MultipleCaptureRequest(
|
||||
// {
|
||||
// for (final sensor in sensors)
|
||||
// sensor:
|
||||
// '${testDir.path}/${sensor.position == SensorPosition.front ? 'front_' : "back_"}${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||||
// },
|
||||
// );
|
||||
},
|
||||
),
|
||||
previewPadding: const EdgeInsets.all(10),
|
||||
// onPreviewTapBuilder: (state) => OnPreviewTap(
|
||||
// onTap: (Offset position, PreviewSize flutterPreviewSize,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:twonly/src/utils.dart';
|
||||
import 'package:twonly/src/components/initialsavatar_component.dart';
|
||||
import 'package:twonly/src/views/search_username_view.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'new_message_view.dart';
|
||||
|
|
@ -193,8 +193,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
return ListTile(
|
||||
title: Text(item.username),
|
||||
subtitle: getSubtitle(item),
|
||||
leading: createInitialsAvatar(item.username,
|
||||
Theme.of(context).brightness == Brightness.dark),
|
||||
leading: InitialsAvatar(displayName: item.username),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:twonly/src/components/initialsavatar_component.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/utils.dart';
|
||||
import 'package:twonly/src/views/search_username_view.dart';
|
||||
|
||||
class NewMessageView extends StatefulWidget {
|
||||
|
|
@ -166,8 +166,7 @@ class UserList extends StatelessWidget {
|
|||
...users.map((username) {
|
||||
return ListTile(
|
||||
title: Text(username),
|
||||
leading: createInitialsAvatar(
|
||||
username, Theme.of(context).brightness == Brightness.dark),
|
||||
leading: InitialsAvatar(displayName: username),
|
||||
onTap: () {
|
||||
// Handle tap
|
||||
},
|
||||
|
|
|
|||
121
lib/src/views/share_image_view.dart
Normal file
121
lib/src/views/share_image_view.dart
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:twonly/src/components/initialsavatar_component.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
|
||||
class ShareImageView extends StatefulWidget {
|
||||
const ShareImageView({super.key, required this.image});
|
||||
final String image;
|
||||
|
||||
@override
|
||||
State<ShareImageView> createState() => _ShareImageView();
|
||||
}
|
||||
|
||||
class _ShareImageView extends State<ShareImageView> {
|
||||
List<Contact> _knownUsers = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUsers();
|
||||
}
|
||||
|
||||
Future<void> _loadUsers() async {
|
||||
final users = await DbContacts.getUsers();
|
||||
setState(() {
|
||||
_knownUsers = users;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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: [
|
||||
UserCheckboxList(users: _knownUsers),
|
||||
const SizedBox(height: 10),
|
||||
// Expanded(
|
||||
// child: UserList(_filteredUsers),
|
||||
// )
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UserCheckboxList extends StatelessWidget {
|
||||
final List<Contact> users;
|
||||
|
||||
UserCheckboxList({required this.users});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Limit the number of users to 8
|
||||
final limitedUsers = users.length > 8 ? users.sublist(0, 8) : users;
|
||||
|
||||
return Column(
|
||||
children: List.generate((limitedUsers.length + 1) ~/ 2, (rowIndex) {
|
||||
final firstUserIndex = rowIndex * 2;
|
||||
final secondUserIndex = firstUserIndex + 1;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(limitedUsers[firstUserIndex].displayName),
|
||||
if (secondUserIndex < limitedUsers.length)
|
||||
Text(limitedUsers[secondUserIndex].displayName),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UserCheckbox extends StatefulWidget {
|
||||
final Contact user;
|
||||
|
||||
UserCheckbox({required this.user});
|
||||
|
||||
@override
|
||||
_UserCheckboxState createState() => _UserCheckboxState();
|
||||
}
|
||||
|
||||
class _UserCheckboxState extends State<UserCheckbox> {
|
||||
bool isChecked = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: isChecked,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
isChecked = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
CircleAvatar(
|
||||
child: InitialsAvatar(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue