Fix some auth and attachment issues
This commit is contained in:
parent
358e8490bc
commit
730de37b78
18 changed files with 211 additions and 343 deletions
|
@ -6,7 +6,7 @@ import 'package:contacts_plus_plus/models/friend.dart';
|
|||
class FriendApi {
|
||||
static Future<List<Friend>> getFriendsList(ApiClient client, {DateTime? lastStatusUpdate}) async {
|
||||
final response = await client.get("/users/${client.userId}/friends${lastStatusUpdate != null ? "?lastStatusUpdate=${lastStatusUpdate.toUtc().toIso8601String()}" : ""}");
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
final data = jsonDecode(response.body) as List;
|
||||
return data.map((e) => Friend.fromMap(e)).toList();
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ class MessageApi {
|
|||
"${userId.isEmpty ? "" : "&user=$userId"}"
|
||||
"&unread=$unreadOnly"
|
||||
);
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
final data = jsonDecode(response.body) as List;
|
||||
return data.map((e) => Message.fromMap(e)).toList();
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import 'package:path/path.dart';
|
|||
class RecordApi {
|
||||
static Future<List<Record>> getRecordsAt(ApiClient client, {required String path}) async {
|
||||
final response = await client.get("/users/${client.userId}/records?path=$path");
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
final body = jsonDecode(response.body) as List;
|
||||
return body.map((e) => Record.fromMap(e)).toList();
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class RecordApi {
|
|||
final body = jsonEncode(record.toMap());
|
||||
final response = await client.post(
|
||||
"/users/${record.ownerId}/records/${record.id}/preprocess", body: body);
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
final resultBody = jsonDecode(response.body);
|
||||
return PreprocessStatus.fromMap(resultBody);
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ class RecordApi {
|
|||
final response = await client.get(
|
||||
"/users/${preprocessStatus.ownerId}/records/${preprocessStatus.recordId}/preprocess/${preprocessStatus.id}"
|
||||
);
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
final body = jsonDecode(response.body);
|
||||
return PreprocessStatus.fromMap(body);
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ class RecordApi {
|
|||
|
||||
static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async {
|
||||
final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks");
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
final body = jsonDecode(response.body);
|
||||
final res = AssetUploadData.fromMap(body);
|
||||
if (res.uploadState == UploadState.failed) throw body;
|
||||
|
@ -68,7 +68,7 @@ class RecordApi {
|
|||
static Future<void> upsertRecord(ApiClient client, {required Record record}) async {
|
||||
final body = jsonEncode(record.toMap());
|
||||
final response = await client.put("/users/${client.userId}/records/${record.id}", body: body);
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
}
|
||||
|
||||
static Future<void> uploadAsset(ApiClient client,
|
||||
|
@ -87,14 +87,14 @@ class RecordApi {
|
|||
..headers.addAll(client.authorizationHeader);
|
||||
final response = await request.send();
|
||||
final bodyBytes = await response.stream.toBytes();
|
||||
ApiClient.checkResponse(http.Response.bytes(bodyBytes, response.statusCode));
|
||||
client.checkResponse(http.Response.bytes(bodyBytes, response.statusCode));
|
||||
progressCallback?.call(1);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> finishUpload(ApiClient client, {required NeosDBAsset asset}) async {
|
||||
final response = await client.patch("/users/${client.userId}/assets/${asset.hash}/chunks");
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
}
|
||||
|
||||
static Future<void> uploadAssets(ApiClient client, {required List<AssetDigest> assets, void Function(double progress)? progressCallback}) async {
|
||||
|
@ -123,14 +123,14 @@ class RecordApi {
|
|||
progressCallback?.call(0);
|
||||
final imageDigest = await AssetDigest.fromData(await image.readAsBytes(), basename(image.path));
|
||||
final imageData = await decodeImageFromList(imageDigest.data);
|
||||
final filename = basenameWithoutExtension(image.path);
|
||||
|
||||
final objectJson = jsonEncode(
|
||||
JsonTemplate.image(imageUri: imageDigest.dbUri, width: imageData.width, height: imageData.height).data);
|
||||
JsonTemplate.image(imageUri: imageDigest.dbUri, filename: filename, width: imageData.width, height: imageData.height).data);
|
||||
final objectBytes = Uint8List.fromList(utf8.encode(objectJson));
|
||||
|
||||
final objectDigest = await AssetDigest.fromData(objectBytes, "${basenameWithoutExtension(image.path)}.json");
|
||||
|
||||
final filename = basenameWithoutExtension(image.path);
|
||||
final digests = [imageDigest, objectDigest];
|
||||
|
||||
final record = Record.fromRequiredData(
|
||||
|
|
|
@ -10,28 +10,28 @@ import 'package:package_info_plus/package_info_plus.dart';
|
|||
class UserApi {
|
||||
static Future<Iterable<User>> searchUsers(ApiClient client, {required String needle}) async {
|
||||
final response = await client.get("/users?name=$needle");
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
final data = jsonDecode(response.body) as List;
|
||||
return data.map((e) => User.fromMap(e));
|
||||
}
|
||||
|
||||
static Future<User> getUser(ApiClient client, {required String userId}) async {
|
||||
final response = await client.get("/users/$userId/");
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
final data = jsonDecode(response.body);
|
||||
return User.fromMap(data);
|
||||
}
|
||||
|
||||
static Future<UserStatus> getUserStatus(ApiClient client, {required String userId}) async {
|
||||
final response = await client.get("/users/$userId/status");
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
final data = jsonDecode(response.body);
|
||||
return UserStatus.fromMap(data);
|
||||
}
|
||||
|
||||
static Future<void> notifyOnlineInstance(ApiClient client) async {
|
||||
final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineId.hashCode}");
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
}
|
||||
|
||||
static Future<void> setStatus(ApiClient client, {required UserStatus status}) async {
|
||||
|
@ -42,12 +42,12 @@ class UserApi {
|
|||
);
|
||||
final body = jsonEncode(status.toMap(shallow: true));
|
||||
final response = await client.put("/users/${client.userId}/status", body: body);
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
}
|
||||
|
||||
static Future<PersonalProfile> getPersonalProfile(ApiClient client) async {
|
||||
final response = await client.get("/users/${client.userId}");
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
final data = jsonDecode(response.body);
|
||||
return PersonalProfile.fromMap(data);
|
||||
}
|
||||
|
@ -64,11 +64,11 @@ class UserApi {
|
|||
);
|
||||
final body = jsonEncode(friend.toMap(shallow: true));
|
||||
final response = await client.put("/users/${client.userId}/friends/${user.id}", body: body);
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
}
|
||||
|
||||
static Future<void> removeUserAsFriend(ApiClient client, {required User user}) async {
|
||||
final response = await client.delete("/users/${client.userId}/friends/${user.id}");
|
||||
ApiClient.checkResponse(response);
|
||||
client.checkResponse(response);
|
||||
}
|
||||
}
|
|
@ -30,5 +30,6 @@ class ClientHolder extends InheritedWidget {
|
|||
@override
|
||||
bool updateShouldNotify(covariant ClientHolder oldWidget) =>
|
||||
oldWidget.apiClient != apiClient
|
||||
|| oldWidget.settingsClient != settingsClient;
|
||||
|| oldWidget.settingsClient != settingsClient
|
||||
|| oldWidget.notificationClient != notificationClient;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ class ApiClient {
|
|||
required String username,
|
||||
required String password,
|
||||
bool rememberMe=true,
|
||||
bool rememberPass=false,
|
||||
bool rememberPass=true,
|
||||
String? oneTimePad,
|
||||
}) async {
|
||||
final body = {
|
||||
|
@ -54,11 +54,13 @@ class ApiClient {
|
|||
if (response.statusCode == 400) {
|
||||
throw "Invalid Credentials";
|
||||
}
|
||||
checkResponse(response);
|
||||
checkResponseCode(response);
|
||||
|
||||
final authData = AuthenticationData.fromMap(jsonDecode(response.body));
|
||||
if (authData.isAuthenticated) {
|
||||
const FlutterSecureStorage storage = FlutterSecureStorage();
|
||||
const FlutterSecureStorage storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
await storage.write(key: userIdKey, value: authData.userId);
|
||||
await storage.write(key: machineIdKey, value: authData.secretMachineId);
|
||||
await storage.write(key: tokenKey, value: authData.token);
|
||||
|
@ -68,7 +70,9 @@ class ApiClient {
|
|||
}
|
||||
|
||||
static Future<AuthenticationData> tryCachedLogin() async {
|
||||
const FlutterSecureStorage storage = FlutterSecureStorage();
|
||||
const FlutterSecureStorage storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
String? userId = await storage.read(key: userIdKey);
|
||||
String? machineId = await storage.read(key: machineIdKey);
|
||||
String? token = await storage.read(key: tokenKey);
|
||||
|
@ -79,7 +83,7 @@ class ApiClient {
|
|||
}
|
||||
|
||||
if (token != null) {
|
||||
final response = await http.get(buildFullUri("/users/$userId"), headers: {
|
||||
final response = await http.patch(buildFullUri("/userSessions"), headers: {
|
||||
"Authorization": "neos $userId:$token"
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
|
@ -100,7 +104,9 @@ class ApiClient {
|
|||
}
|
||||
|
||||
Future<void> logout(BuildContext context) async {
|
||||
const FlutterSecureStorage storage = FlutterSecureStorage();
|
||||
const FlutterSecureStorage storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
await storage.delete(key: userIdKey);
|
||||
await storage.delete(key: machineIdKey);
|
||||
await storage.delete(key: tokenKey);
|
||||
|
@ -117,28 +123,30 @@ class ApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
static void checkResponse(http.Response response) {
|
||||
final error = "(${response.statusCode}${kDebugMode ? "|${response.body}" : ""})";
|
||||
if (response.statusCode >= 300) {
|
||||
FlutterError.reportError(FlutterErrorDetails(exception: error));
|
||||
}
|
||||
if (response.statusCode == 429) {
|
||||
throw "Sorry, you are being rate limited. $error";
|
||||
}
|
||||
void checkResponse(http.Response response) {
|
||||
if (response.statusCode == 403) {
|
||||
tryCachedLogin();
|
||||
// TODO: Show the login screen again if cached login was unsuccessful.
|
||||
throw "You are not authorized to do that. $error";
|
||||
}
|
||||
if (response.statusCode == 404) {
|
||||
throw "Resource not found. $error";
|
||||
}
|
||||
if (response.statusCode == 500) {
|
||||
throw "Internal server error. $error";
|
||||
}
|
||||
if (response.statusCode >= 300) {
|
||||
throw "Unknown Error. $error";
|
||||
tryCachedLogin().then((value) {
|
||||
if (!value.isAuthenticated) {
|
||||
// TODO: Turn api-client into a change notifier to present login screen when logged out
|
||||
}
|
||||
});
|
||||
}
|
||||
checkResponseCode(response);
|
||||
}
|
||||
|
||||
static void checkResponseCode(http.Response response) {
|
||||
if (response.statusCode < 300) return;
|
||||
|
||||
final error = "${switch (response.statusCode) {
|
||||
429 => "Sorry, you are being rate limited.",
|
||||
403 => "You are not authorized to do that.",
|
||||
404 => "Resource not found.",
|
||||
500 => "Internal server error.",
|
||||
_ => "Unknown Error."
|
||||
}} (${response.statusCode}${kDebugMode ? "|${response.body}" : ""})";
|
||||
|
||||
FlutterError.reportError(FlutterErrorDetails(exception: error));
|
||||
throw error;
|
||||
}
|
||||
|
||||
Map<String, String> get authorizationHeader => _authenticationData.authorizationHeader;
|
||||
|
|
|
@ -16,7 +16,7 @@ class AudioCacheClient {
|
|||
if (!await file.exists()) {
|
||||
await file.create(recursive: true);
|
||||
final response = await http.get(Uri.parse(Aux.neosDbToHttp(clip.assetUri)));
|
||||
ApiClient.checkResponse(response);
|
||||
ApiClient.checkResponseCode(response);
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
return file;
|
||||
|
|
|
@ -286,7 +286,7 @@ class MessagingClient extends ChangeNotifier {
|
|||
Uri.parse("${Config.neosHubUrl}/negotiate"),
|
||||
headers: _apiClient.authorizationHeader,
|
||||
);
|
||||
ApiClient.checkResponse(response);
|
||||
_apiClient.checkResponse(response);
|
||||
} catch (e) {
|
||||
throw "Failed to acquire connection info from Neos API: $e";
|
||||
}
|
||||
|
|
|
@ -7,12 +7,13 @@ class JsonTemplate {
|
|||
|
||||
JsonTemplate({required this.data});
|
||||
|
||||
factory JsonTemplate.image({required String imageUri, required int width, required int height}) {
|
||||
factory JsonTemplate.image({required String imageUri, required String filename, required int width, required int height}) {
|
||||
final texture2dUid = const Uuid().v4();
|
||||
final quadMeshUid = const Uuid().v4();
|
||||
final quadMeshSizeUid = const Uuid().v4();
|
||||
final materialId = const Uuid().v4();
|
||||
final boxColliderSizeUid = const Uuid().v4();
|
||||
final ratio = height/width;
|
||||
final data = {
|
||||
"Object": {
|
||||
"ID": const Uuid().v4(),
|
||||
|
@ -508,8 +509,8 @@ class JsonTemplate {
|
|||
"Size": {
|
||||
"ID": quadMeshSizeUid,
|
||||
"Data": [
|
||||
1,
|
||||
height/width
|
||||
ratio > 1 ? ratio : 1,
|
||||
ratio > 1 ? 1 : ratio
|
||||
]
|
||||
},
|
||||
"UVScale": {
|
||||
|
@ -706,7 +707,7 @@ class JsonTemplate {
|
|||
},
|
||||
"Name": {
|
||||
"ID": const Uuid().v4(),
|
||||
"Data": "alice"
|
||||
"Data": filename
|
||||
},
|
||||
"Tag": {
|
||||
"ID": const Uuid().v4(),
|
||||
|
|
63
lib/widgets/messages/camera_image_view.dart
Normal file
63
lib/widgets/messages/camera_image_view.dart
Normal file
|
@ -0,0 +1,63 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
class CameraImageView extends StatelessWidget {
|
||||
const CameraImageView({required this.file, super.key});
|
||||
|
||||
final File file;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Stack(
|
||||
children: [
|
||||
PhotoView(
|
||||
imageProvider: FileImage(
|
||||
file,
|
||||
),
|
||||
initialScale: PhotoViewComputedScale.covered,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 32),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
side: BorderSide(width: 1, color: Theme.of(context).colorScheme.error)
|
||||
),
|
||||
icon: const Icon(Icons.close),
|
||||
label: const Text("Cancel",),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
side: BorderSide(width: 1, color: Theme.of(context).colorScheme.primary)
|
||||
),
|
||||
icon: const Icon(Icons.check),
|
||||
label: const Text("Okay"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:contacts_plus_plus/widgets/messages/message_camera_view.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class MessageAttachmentList extends StatefulWidget {
|
||||
|
@ -22,7 +22,6 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
|
|||
final ScrollController _scrollController = ScrollController();
|
||||
bool _showShadow = true;
|
||||
bool _popupIsOpen = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -88,7 +87,9 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
|
|||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
_loadedFiles.remove(file);
|
||||
setState(() {
|
||||
_loadedFiles.remove(file);
|
||||
});
|
||||
await widget.onChange(_loadedFiles);
|
||||
},
|
||||
child: const Text("Yes"),
|
||||
|
@ -191,11 +192,19 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
|
|||
),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () async {
|
||||
final picture = await Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const MessageCameraView())) as File?;
|
||||
final picture = await ImagePicker().pickImage(source: ImageSource.camera);
|
||||
if (picture != null) {
|
||||
_loadedFiles.add((FileType.image, picture));
|
||||
await widget.onChange(_loadedFiles);
|
||||
final file = File(picture.path);
|
||||
if (await file.exists()) {
|
||||
setState(() {
|
||||
_loadedFiles.add((FileType.image, file));
|
||||
});
|
||||
await widget.onChange(_loadedFiles);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to load image file")));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.camera,),
|
||||
|
|
|
@ -19,7 +19,7 @@ class MessageAudioPlayer extends StatefulWidget {
|
|||
State<MessageAudioPlayer> createState() => _MessageAudioPlayerState();
|
||||
}
|
||||
|
||||
class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBindingObserver {
|
||||
class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
Future? _audioFileFuture;
|
||||
double _sliderValue = 0;
|
||||
|
@ -82,6 +82,7 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
if (!Platform.isAndroid) {
|
||||
return _createErrorWidget("Sorry, audio-messages are not\n supported on this platform.");
|
||||
}
|
||||
|
@ -220,4 +221,8 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement wantKeepAlive
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
|
@ -1,184 +0,0 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageCameraView extends StatefulWidget {
|
||||
const MessageCameraView({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _MessageCameraViewState();
|
||||
|
||||
}
|
||||
|
||||
class _MessageCameraViewState extends State<MessageCameraView> {
|
||||
final List<CameraDescription> _cameras = [];
|
||||
late final CameraController _cameraController;
|
||||
int _cameraIndex = 0;
|
||||
FlashMode _flashMode = FlashMode.off;
|
||||
Future? _initializeControllerFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
availableCameras().then((List<CameraDescription> cameras) {
|
||||
_cameras.clear();
|
||||
_cameras.addAll(cameras);
|
||||
if (cameras.isEmpty) {
|
||||
_initializeControllerFuture = Future.error("Failed to initialize camera");
|
||||
} else {
|
||||
_cameraController = CameraController(cameras.first, ResolutionPreset.high);
|
||||
_cameraIndex = 0;
|
||||
_initializeControllerFuture = _cameraController.initialize().whenComplete(() => _cameraController.setFlashMode(_flashMode));
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cameraController.setFlashMode(FlashMode.off).whenComplete(() => _cameraController.dispose());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Take a picture"),
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: _initializeControllerFuture,
|
||||
builder: (context, snapshot) {
|
||||
// Can't use hasData since the future returns void.
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Expanded(child: CameraPreview(_cameraController)),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _cameras.isEmpty ? null : () async {
|
||||
setState(() {
|
||||
_cameraIndex = (_cameraIndex+1) % _cameras.length;
|
||||
});
|
||||
_cameraController.setDescription(_cameras[_cameraIndex]);
|
||||
},
|
||||
iconSize: 32,
|
||||
icon: const Icon(Icons.switch_camera),
|
||||
),
|
||||
const SizedBox(width: 64, height: 72,),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) =>
|
||||
FadeTransition(
|
||||
opacity: animation,
|
||||
child: RotationTransition(
|
||||
turns: Tween<double>(begin: 0.6, end: 1).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: switch (_flashMode) {
|
||||
FlashMode.off =>
|
||||
IconButton(
|
||||
key: const ValueKey("button-flash-off"),
|
||||
iconSize: 32,
|
||||
onPressed: () async {
|
||||
_flashMode = FlashMode.auto;
|
||||
await _cameraController.setFlashMode(_flashMode);
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.flash_off),
|
||||
),
|
||||
FlashMode.auto =>
|
||||
IconButton(
|
||||
key: const ValueKey("button-flash-auto"),
|
||||
iconSize: 32,
|
||||
onPressed: () async {
|
||||
_flashMode = FlashMode.always;
|
||||
await _cameraController.setFlashMode(_flashMode);
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.flash_auto),
|
||||
),
|
||||
FlashMode.always =>
|
||||
IconButton(
|
||||
key: const ValueKey("button-flash-always"),
|
||||
iconSize: 32,
|
||||
onPressed: () async {
|
||||
_flashMode = FlashMode.torch;
|
||||
await _cameraController.setFlashMode(_flashMode);
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.flash_on),
|
||||
),
|
||||
FlashMode.torch =>
|
||||
IconButton(
|
||||
key: const ValueKey("button-flash-torch"),
|
||||
iconSize: 32,
|
||||
onPressed: () async {
|
||||
_flashMode = FlashMode.off;
|
||||
await _cameraController.setFlashMode(_flashMode);
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.flashlight_on),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.surface,
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
final sMsgr = ScaffoldMessenger.of(context);
|
||||
final nav = Navigator.of(context);
|
||||
try {
|
||||
await _initializeControllerFuture;
|
||||
final image = await _cameraController.takePicture();
|
||||
nav.pop(File(image.path));
|
||||
} catch (e) {
|
||||
sMsgr.showSnackBar(SnackBar(content: Text("Failed to capture image: $e")));
|
||||
}
|
||||
},
|
||||
style: IconButton.styleFrom(
|
||||
foregroundColor: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
icon: const Icon(Icons.camera),
|
||||
iconSize: 64,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return DefaultErrorWidget(
|
||||
message: snapshot.error.toString(),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator(),);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,10 +10,10 @@ import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
|||
import 'package:contacts_plus_plus/models/friend.dart';
|
||||
import 'package:contacts_plus_plus/models/message.dart';
|
||||
import 'package:contacts_plus_plus/widgets/messages/message_attachment_list.dart';
|
||||
import 'package:contacts_plus_plus/widgets/messages/message_camera_view.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
@ -21,9 +21,8 @@ import 'package:uuid/uuid.dart';
|
|||
|
||||
|
||||
class MessageInputBar extends StatefulWidget {
|
||||
const MessageInputBar({this.showShadow=true, this.disabled=false, required this.recipient, this.onMessageSent, super.key});
|
||||
const MessageInputBar({this.disabled=false, required this.recipient, this.onMessageSent, super.key});
|
||||
|
||||
final bool showShadow;
|
||||
final bool disabled;
|
||||
final Friend recipient;
|
||||
final Function()? onMessageSent;
|
||||
|
@ -36,6 +35,7 @@ class _MessageInputBarState extends State<MessageInputBar> {
|
|||
final TextEditingController _messageTextController = TextEditingController();
|
||||
final List<(FileType, File)> _loadedFiles = [];
|
||||
final Record _recorder = Record();
|
||||
final ImagePicker _imagePicker = ImagePicker();
|
||||
|
||||
DateTime? _recordingStartTime;
|
||||
|
||||
|
@ -47,7 +47,6 @@ class _MessageInputBarState extends State<MessageInputBar> {
|
|||
set _isRecording(value) => _recordingStartTime = value ? DateTime.now() : null;
|
||||
bool _recordingCancelled = false;
|
||||
|
||||
|
||||
Future<void> sendTextMessage(ApiClient client, MessagingClient mClient, String content) async {
|
||||
if (content.isEmpty) return;
|
||||
final message = Message(
|
||||
|
@ -204,24 +203,15 @@ class _MessageInputBarState extends State<MessageInputBar> {
|
|||
}
|
||||
}
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: widget.showShadow ? 8 : 0,
|
||||
color: Theme
|
||||
.of(context)
|
||||
.shadowColor,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
border: const Border(top: BorderSide(width: 1, color: Colors.black38)),
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.background,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isSending && _sendProgress != null)
|
||||
|
@ -262,13 +252,24 @@ class _MessageInputBarState extends State<MessageInputBar> {
|
|||
),
|
||||
TextButton.icon(
|
||||
onPressed: _isSending ? null : () async {
|
||||
final picture = await Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const MessageCameraView())) as File?;
|
||||
if (picture != null) {
|
||||
setState(() {
|
||||
_loadedFiles.add((FileType.image, picture));
|
||||
});
|
||||
final picture = await _imagePicker.pickImage(source: ImageSource.camera);
|
||||
if (picture == null) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to get image path")));
|
||||
}
|
||||
return;
|
||||
}
|
||||
final file = File(picture.path);
|
||||
if (await file.exists()) {
|
||||
setState(() {
|
||||
_loadedFiles.add((FileType.image, file));
|
||||
});
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to load image file")));
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
icon: const Icon(Icons.camera),
|
||||
label: const Text("Camera"),
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
class MessageRecordButton extends StatefulWidget {
|
||||
const MessageRecordButton({required this.disabled, this.onRecordStart, this.onRecordEnd, super.key});
|
||||
|
||||
final bool disabled;
|
||||
final Function()? onRecordStart;
|
||||
final Function(File? recording)? onRecordEnd;
|
||||
|
||||
@override
|
||||
State<MessageRecordButton> createState() => _MessageRecordButtonState();
|
||||
}
|
||||
|
||||
class _MessageRecordButtonState extends State<MessageRecordButton> {
|
||||
|
||||
final Record _recorder = Record();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
Future.delayed(Duration.zero, _recorder.stop);
|
||||
Future.delayed(Duration.zero, _recorder.dispose);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: GestureDetector(
|
||||
onTapDown: widget.disabled ? null : (_) async {
|
||||
HapticFeedback.vibrate();
|
||||
/*
|
||||
widget.onRecordStart?.call();
|
||||
final dir = await getTemporaryDirectory();
|
||||
await _recorder.start(
|
||||
path: "${dir.path}/A-${const Uuid().v4()}.ogg",
|
||||
encoder: AudioEncoder.opus,
|
||||
samplingRate: 44100,
|
||||
);
|
||||
*/
|
||||
},
|
||||
onLongPressUp: () async {
|
||||
/*
|
||||
if (await _recorder.isRecording()) {
|
||||
final recording = await _recorder.stop();
|
||||
widget.onRecordEnd?.call(recording == null ? null : File(recording));
|
||||
}
|
||||
*/
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(Icons.mic_outlined, size: 28, color: Theme.of(context).colorScheme.onSurface,),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -21,9 +21,7 @@ class MessagesList extends StatefulWidget {
|
|||
|
||||
class _MessagesListState extends State<MessagesList> with SingleTickerProviderStateMixin {
|
||||
final ScrollController _sessionListScrollController = ScrollController();
|
||||
final ScrollController _messageScrollController = ScrollController();
|
||||
|
||||
bool _showBottomBarShadow = false;
|
||||
bool _showSessionListScrollChevron = false;
|
||||
|
||||
double get _shevronOpacity => _showSessionListScrollChevron ? 1.0 : 0.0;
|
||||
|
@ -50,19 +48,6 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
|||
});
|
||||
}
|
||||
});
|
||||
_messageScrollController.addListener(() {
|
||||
if (!_messageScrollController.hasClients) return;
|
||||
if (_messageScrollController.position.atEdge && _messageScrollController.position.pixels == 0 &&
|
||||
_showBottomBarShadow) {
|
||||
setState(() {
|
||||
_showBottomBarShadow = false;
|
||||
});
|
||||
} else if (!_showBottomBarShadow) {
|
||||
setState(() {
|
||||
_showBottomBarShadow = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -189,7 +174,6 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
|||
return Provider(
|
||||
create: (BuildContext context) => AudioCacheClient(),
|
||||
child: ListView.builder(
|
||||
controller: _messageScrollController,
|
||||
reverse: true,
|
||||
itemCount: cache.messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
|
@ -212,7 +196,6 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
|||
MessageInputBar(
|
||||
recipient: widget.friend,
|
||||
disabled: cache == null || cache.error != null,
|
||||
showShadow: _showBottomBarShadow,
|
||||
onMessageSent: () {
|
||||
setState(() {});
|
||||
},
|
||||
|
|
40
pubspec.lock
40
pubspec.lock
|
@ -376,6 +376,46 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "9978d3510af4e6a902e545ce19229b926e6de6a1828d6134d3aab2e129a4d270"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.7+5"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: c2f3c66400649bd132f721c88218945d6406f693092b2f741b79ae9cdb046e59
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.6+16"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "98f50d6b9f294c8ba35e25cc0d13b04bfddd25dbc8d32fa9d566a6572f2c081c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.12"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: d779210bda268a03b57e923fb1e410f32f5c5e708ad256348bcbf1f44f558fd0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.7+4"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "1991219d9dbc42a99aff77e663af8ca51ced592cd6685c9485e3458302d3d4f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.3"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -61,6 +61,7 @@ dependencies:
|
|||
camera: ^0.10.5
|
||||
path_provider: ^2.0.15
|
||||
crypto: ^3.0.3
|
||||
image_picker: ^0.8.7+5
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in a new issue