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 {
|
class FriendApi {
|
||||||
static Future<List<Friend>> getFriendsList(ApiClient client, {DateTime? lastStatusUpdate}) async {
|
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()}" : ""}");
|
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;
|
final data = jsonDecode(response.body) as List;
|
||||||
return data.map((e) => Friend.fromMap(e)).toList();
|
return data.map((e) => Friend.fromMap(e)).toList();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ class MessageApi {
|
||||||
"${userId.isEmpty ? "" : "&user=$userId"}"
|
"${userId.isEmpty ? "" : "&user=$userId"}"
|
||||||
"&unread=$unreadOnly"
|
"&unread=$unreadOnly"
|
||||||
);
|
);
|
||||||
ApiClient.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final data = jsonDecode(response.body) as List;
|
final data = jsonDecode(response.body) as List;
|
||||||
return data.map((e) => Message.fromMap(e)).toList();
|
return data.map((e) => Message.fromMap(e)).toList();
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import 'package:path/path.dart';
|
||||||
class RecordApi {
|
class RecordApi {
|
||||||
static Future<List<Record>> getRecordsAt(ApiClient client, {required String path}) async {
|
static Future<List<Record>> getRecordsAt(ApiClient client, {required String path}) async {
|
||||||
final response = await client.get("/users/${client.userId}/records?path=$path");
|
final response = await client.get("/users/${client.userId}/records?path=$path");
|
||||||
ApiClient.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final body = jsonDecode(response.body) as List;
|
final body = jsonDecode(response.body) as List;
|
||||||
return body.map((e) => Record.fromMap(e)).toList();
|
return body.map((e) => Record.fromMap(e)).toList();
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ class RecordApi {
|
||||||
final body = jsonEncode(record.toMap());
|
final body = jsonEncode(record.toMap());
|
||||||
final response = await client.post(
|
final response = await client.post(
|
||||||
"/users/${record.ownerId}/records/${record.id}/preprocess", body: body);
|
"/users/${record.ownerId}/records/${record.id}/preprocess", body: body);
|
||||||
ApiClient.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final resultBody = jsonDecode(response.body);
|
final resultBody = jsonDecode(response.body);
|
||||||
return PreprocessStatus.fromMap(resultBody);
|
return PreprocessStatus.fromMap(resultBody);
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ class RecordApi {
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
"/users/${preprocessStatus.ownerId}/records/${preprocessStatus.recordId}/preprocess/${preprocessStatus.id}"
|
"/users/${preprocessStatus.ownerId}/records/${preprocessStatus.recordId}/preprocess/${preprocessStatus.id}"
|
||||||
);
|
);
|
||||||
ApiClient.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final body = jsonDecode(response.body);
|
final body = jsonDecode(response.body);
|
||||||
return PreprocessStatus.fromMap(body);
|
return PreprocessStatus.fromMap(body);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ class RecordApi {
|
||||||
|
|
||||||
static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async {
|
static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async {
|
||||||
final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks");
|
final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks");
|
||||||
ApiClient.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final body = jsonDecode(response.body);
|
final body = jsonDecode(response.body);
|
||||||
final res = AssetUploadData.fromMap(body);
|
final res = AssetUploadData.fromMap(body);
|
||||||
if (res.uploadState == UploadState.failed) throw body;
|
if (res.uploadState == UploadState.failed) throw body;
|
||||||
|
@ -68,7 +68,7 @@ class RecordApi {
|
||||||
static Future<void> upsertRecord(ApiClient client, {required Record record}) async {
|
static Future<void> upsertRecord(ApiClient client, {required Record record}) async {
|
||||||
final body = jsonEncode(record.toMap());
|
final body = jsonEncode(record.toMap());
|
||||||
final response = await client.put("/users/${client.userId}/records/${record.id}", body: body);
|
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,
|
static Future<void> uploadAsset(ApiClient client,
|
||||||
|
@ -87,14 +87,14 @@ class RecordApi {
|
||||||
..headers.addAll(client.authorizationHeader);
|
..headers.addAll(client.authorizationHeader);
|
||||||
final response = await request.send();
|
final response = await request.send();
|
||||||
final bodyBytes = await response.stream.toBytes();
|
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);
|
progressCallback?.call(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> finishUpload(ApiClient client, {required NeosDBAsset asset}) async {
|
static Future<void> finishUpload(ApiClient client, {required NeosDBAsset asset}) async {
|
||||||
final response = await client.patch("/users/${client.userId}/assets/${asset.hash}/chunks");
|
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 {
|
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);
|
progressCallback?.call(0);
|
||||||
final imageDigest = await AssetDigest.fromData(await image.readAsBytes(), basename(image.path));
|
final imageDigest = await AssetDigest.fromData(await image.readAsBytes(), basename(image.path));
|
||||||
final imageData = await decodeImageFromList(imageDigest.data);
|
final imageData = await decodeImageFromList(imageDigest.data);
|
||||||
|
final filename = basenameWithoutExtension(image.path);
|
||||||
|
|
||||||
final objectJson = jsonEncode(
|
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 objectBytes = Uint8List.fromList(utf8.encode(objectJson));
|
||||||
|
|
||||||
final objectDigest = await AssetDigest.fromData(objectBytes, "${basenameWithoutExtension(image.path)}.json");
|
final objectDigest = await AssetDigest.fromData(objectBytes, "${basenameWithoutExtension(image.path)}.json");
|
||||||
|
|
||||||
final filename = basenameWithoutExtension(image.path);
|
|
||||||
final digests = [imageDigest, objectDigest];
|
final digests = [imageDigest, objectDigest];
|
||||||
|
|
||||||
final record = Record.fromRequiredData(
|
final record = Record.fromRequiredData(
|
||||||
|
|
|
@ -10,28 +10,28 @@ import 'package:package_info_plus/package_info_plus.dart';
|
||||||
class UserApi {
|
class UserApi {
|
||||||
static Future<Iterable<User>> searchUsers(ApiClient client, {required String needle}) async {
|
static Future<Iterable<User>> searchUsers(ApiClient client, {required String needle}) async {
|
||||||
final response = await client.get("/users?name=$needle");
|
final response = await client.get("/users?name=$needle");
|
||||||
ApiClient.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final data = jsonDecode(response.body) as List;
|
final data = jsonDecode(response.body) as List;
|
||||||
return data.map((e) => User.fromMap(e));
|
return data.map((e) => User.fromMap(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<User> getUser(ApiClient client, {required String userId}) async {
|
static Future<User> getUser(ApiClient client, {required String userId}) async {
|
||||||
final response = await client.get("/users/$userId/");
|
final response = await client.get("/users/$userId/");
|
||||||
ApiClient.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
return User.fromMap(data);
|
return User.fromMap(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<UserStatus> getUserStatus(ApiClient client, {required String userId}) async {
|
static Future<UserStatus> getUserStatus(ApiClient client, {required String userId}) async {
|
||||||
final response = await client.get("/users/$userId/status");
|
final response = await client.get("/users/$userId/status");
|
||||||
ApiClient.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
return UserStatus.fromMap(data);
|
return UserStatus.fromMap(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> notifyOnlineInstance(ApiClient client) async {
|
static Future<void> notifyOnlineInstance(ApiClient client) async {
|
||||||
final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineId.hashCode}");
|
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 {
|
static Future<void> setStatus(ApiClient client, {required UserStatus status}) async {
|
||||||
|
@ -42,12 +42,12 @@ class UserApi {
|
||||||
);
|
);
|
||||||
final body = jsonEncode(status.toMap(shallow: true));
|
final body = jsonEncode(status.toMap(shallow: true));
|
||||||
final response = await client.put("/users/${client.userId}/status", body: body);
|
final response = await client.put("/users/${client.userId}/status", body: body);
|
||||||
ApiClient.checkResponse(response);
|
client.checkResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<PersonalProfile> getPersonalProfile(ApiClient client) async {
|
static Future<PersonalProfile> getPersonalProfile(ApiClient client) async {
|
||||||
final response = await client.get("/users/${client.userId}");
|
final response = await client.get("/users/${client.userId}");
|
||||||
ApiClient.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
return PersonalProfile.fromMap(data);
|
return PersonalProfile.fromMap(data);
|
||||||
}
|
}
|
||||||
|
@ -64,11 +64,11 @@ class UserApi {
|
||||||
);
|
);
|
||||||
final body = jsonEncode(friend.toMap(shallow: true));
|
final body = jsonEncode(friend.toMap(shallow: true));
|
||||||
final response = await client.put("/users/${client.userId}/friends/${user.id}", body: body);
|
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 {
|
static Future<void> removeUserAsFriend(ApiClient client, {required User user}) async {
|
||||||
final response = await client.delete("/users/${client.userId}/friends/${user.id}");
|
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
|
@override
|
||||||
bool updateShouldNotify(covariant ClientHolder oldWidget) =>
|
bool updateShouldNotify(covariant ClientHolder oldWidget) =>
|
||||||
oldWidget.apiClient != apiClient
|
oldWidget.apiClient != apiClient
|
||||||
|| oldWidget.settingsClient != settingsClient;
|
|| oldWidget.settingsClient != settingsClient
|
||||||
|
|| oldWidget.notificationClient != notificationClient;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ class ApiClient {
|
||||||
required String username,
|
required String username,
|
||||||
required String password,
|
required String password,
|
||||||
bool rememberMe=true,
|
bool rememberMe=true,
|
||||||
bool rememberPass=false,
|
bool rememberPass=true,
|
||||||
String? oneTimePad,
|
String? oneTimePad,
|
||||||
}) async {
|
}) async {
|
||||||
final body = {
|
final body = {
|
||||||
|
@ -54,11 +54,13 @@ class ApiClient {
|
||||||
if (response.statusCode == 400) {
|
if (response.statusCode == 400) {
|
||||||
throw "Invalid Credentials";
|
throw "Invalid Credentials";
|
||||||
}
|
}
|
||||||
checkResponse(response);
|
checkResponseCode(response);
|
||||||
|
|
||||||
final authData = AuthenticationData.fromMap(jsonDecode(response.body));
|
final authData = AuthenticationData.fromMap(jsonDecode(response.body));
|
||||||
if (authData.isAuthenticated) {
|
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: userIdKey, value: authData.userId);
|
||||||
await storage.write(key: machineIdKey, value: authData.secretMachineId);
|
await storage.write(key: machineIdKey, value: authData.secretMachineId);
|
||||||
await storage.write(key: tokenKey, value: authData.token);
|
await storage.write(key: tokenKey, value: authData.token);
|
||||||
|
@ -68,7 +70,9 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<AuthenticationData> tryCachedLogin() async {
|
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? userId = await storage.read(key: userIdKey);
|
||||||
String? machineId = await storage.read(key: machineIdKey);
|
String? machineId = await storage.read(key: machineIdKey);
|
||||||
String? token = await storage.read(key: tokenKey);
|
String? token = await storage.read(key: tokenKey);
|
||||||
|
@ -79,7 +83,7 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
final response = await http.get(buildFullUri("/users/$userId"), headers: {
|
final response = await http.patch(buildFullUri("/userSessions"), headers: {
|
||||||
"Authorization": "neos $userId:$token"
|
"Authorization": "neos $userId:$token"
|
||||||
});
|
});
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
@ -100,7 +104,9 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout(BuildContext context) async {
|
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: userIdKey);
|
||||||
await storage.delete(key: machineIdKey);
|
await storage.delete(key: machineIdKey);
|
||||||
await storage.delete(key: tokenKey);
|
await storage.delete(key: tokenKey);
|
||||||
|
@ -117,28 +123,30 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void checkResponse(http.Response response) {
|
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";
|
|
||||||
}
|
|
||||||
if (response.statusCode == 403) {
|
if (response.statusCode == 403) {
|
||||||
tryCachedLogin();
|
tryCachedLogin().then((value) {
|
||||||
// TODO: Show the login screen again if cached login was unsuccessful.
|
if (!value.isAuthenticated) {
|
||||||
throw "You are not authorized to do that. $error";
|
// TODO: Turn api-client into a change notifier to present login screen when logged out
|
||||||
}
|
}
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
|
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;
|
Map<String, String> get authorizationHeader => _authenticationData.authorizationHeader;
|
||||||
|
|
|
@ -16,7 +16,7 @@ class AudioCacheClient {
|
||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
await file.create(recursive: true);
|
await file.create(recursive: true);
|
||||||
final response = await http.get(Uri.parse(Aux.neosDbToHttp(clip.assetUri)));
|
final response = await http.get(Uri.parse(Aux.neosDbToHttp(clip.assetUri)));
|
||||||
ApiClient.checkResponse(response);
|
ApiClient.checkResponseCode(response);
|
||||||
await file.writeAsBytes(response.bodyBytes);
|
await file.writeAsBytes(response.bodyBytes);
|
||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
|
|
|
@ -286,7 +286,7 @@ class MessagingClient extends ChangeNotifier {
|
||||||
Uri.parse("${Config.neosHubUrl}/negotiate"),
|
Uri.parse("${Config.neosHubUrl}/negotiate"),
|
||||||
headers: _apiClient.authorizationHeader,
|
headers: _apiClient.authorizationHeader,
|
||||||
);
|
);
|
||||||
ApiClient.checkResponse(response);
|
_apiClient.checkResponse(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw "Failed to acquire connection info from Neos API: $e";
|
throw "Failed to acquire connection info from Neos API: $e";
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,13 @@ class JsonTemplate {
|
||||||
|
|
||||||
JsonTemplate({required this.data});
|
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 texture2dUid = const Uuid().v4();
|
||||||
final quadMeshUid = const Uuid().v4();
|
final quadMeshUid = const Uuid().v4();
|
||||||
final quadMeshSizeUid = const Uuid().v4();
|
final quadMeshSizeUid = const Uuid().v4();
|
||||||
final materialId = const Uuid().v4();
|
final materialId = const Uuid().v4();
|
||||||
final boxColliderSizeUid = const Uuid().v4();
|
final boxColliderSizeUid = const Uuid().v4();
|
||||||
|
final ratio = height/width;
|
||||||
final data = {
|
final data = {
|
||||||
"Object": {
|
"Object": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
|
@ -508,8 +509,8 @@ class JsonTemplate {
|
||||||
"Size": {
|
"Size": {
|
||||||
"ID": quadMeshSizeUid,
|
"ID": quadMeshSizeUid,
|
||||||
"Data": [
|
"Data": [
|
||||||
1,
|
ratio > 1 ? ratio : 1,
|
||||||
height/width
|
ratio > 1 ? 1 : ratio
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"UVScale": {
|
"UVScale": {
|
||||||
|
@ -706,7 +707,7 @@ class JsonTemplate {
|
||||||
},
|
},
|
||||||
"Name": {
|
"Name": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
"Data": "alice"
|
"Data": filename
|
||||||
},
|
},
|
||||||
"Tag": {
|
"Tag": {
|
||||||
"ID": const Uuid().v4(),
|
"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 'dart:io';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
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:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
class MessageAttachmentList extends StatefulWidget {
|
class MessageAttachmentList extends StatefulWidget {
|
||||||
|
@ -22,7 +22,6 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
bool _showShadow = true;
|
bool _showShadow = true;
|
||||||
bool _popupIsOpen = false;
|
bool _popupIsOpen = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -88,7 +87,9 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
_loadedFiles.remove(file);
|
setState(() {
|
||||||
|
_loadedFiles.remove(file);
|
||||||
|
});
|
||||||
await widget.onChange(_loadedFiles);
|
await widget.onChange(_loadedFiles);
|
||||||
},
|
},
|
||||||
child: const Text("Yes"),
|
child: const Text("Yes"),
|
||||||
|
@ -191,11 +192,19 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final picture = await Navigator.of(context).push(
|
final picture = await ImagePicker().pickImage(source: ImageSource.camera);
|
||||||
MaterialPageRoute(builder: (context) => const MessageCameraView())) as File?;
|
|
||||||
if (picture != null) {
|
if (picture != null) {
|
||||||
_loadedFiles.add((FileType.image, picture));
|
final file = File(picture.path);
|
||||||
await widget.onChange(_loadedFiles);
|
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,),
|
icon: const Icon(Icons.camera,),
|
||||||
|
|
|
@ -19,7 +19,7 @@ class MessageAudioPlayer extends StatefulWidget {
|
||||||
State<MessageAudioPlayer> createState() => _MessageAudioPlayerState();
|
State<MessageAudioPlayer> createState() => _MessageAudioPlayerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBindingObserver {
|
class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
|
||||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||||
Future? _audioFileFuture;
|
Future? _audioFileFuture;
|
||||||
double _sliderValue = 0;
|
double _sliderValue = 0;
|
||||||
|
@ -82,6 +82,7 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
if (!Platform.isAndroid) {
|
if (!Platform.isAndroid) {
|
||||||
return _createErrorWidget("Sorry, audio-messages are not\n supported on this platform.");
|
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/friend.dart';
|
||||||
import 'package:contacts_plus_plus/models/message.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_attachment_list.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/message_camera_view.dart';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:record/record.dart';
|
import 'package:record/record.dart';
|
||||||
|
@ -21,9 +21,8 @@ import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
|
||||||
class MessageInputBar extends StatefulWidget {
|
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 bool disabled;
|
||||||
final Friend recipient;
|
final Friend recipient;
|
||||||
final Function()? onMessageSent;
|
final Function()? onMessageSent;
|
||||||
|
@ -36,6 +35,7 @@ class _MessageInputBarState extends State<MessageInputBar> {
|
||||||
final TextEditingController _messageTextController = TextEditingController();
|
final TextEditingController _messageTextController = TextEditingController();
|
||||||
final List<(FileType, File)> _loadedFiles = [];
|
final List<(FileType, File)> _loadedFiles = [];
|
||||||
final Record _recorder = Record();
|
final Record _recorder = Record();
|
||||||
|
final ImagePicker _imagePicker = ImagePicker();
|
||||||
|
|
||||||
DateTime? _recordingStartTime;
|
DateTime? _recordingStartTime;
|
||||||
|
|
||||||
|
@ -47,7 +47,6 @@ class _MessageInputBarState extends State<MessageInputBar> {
|
||||||
set _isRecording(value) => _recordingStartTime = value ? DateTime.now() : null;
|
set _isRecording(value) => _recordingStartTime = value ? DateTime.now() : null;
|
||||||
bool _recordingCancelled = false;
|
bool _recordingCancelled = false;
|
||||||
|
|
||||||
|
|
||||||
Future<void> sendTextMessage(ApiClient client, MessagingClient mClient, String content) async {
|
Future<void> sendTextMessage(ApiClient client, MessagingClient mClient, String content) async {
|
||||||
if (content.isEmpty) return;
|
if (content.isEmpty) return;
|
||||||
final message = Message(
|
final message = Message(
|
||||||
|
@ -204,24 +203,15 @@ class _MessageInputBarState extends State<MessageInputBar> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: AnimatedContainer(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
border: const Border(top: BorderSide(width: 1, color: Colors.black38)),
|
||||||
BoxShadow(
|
|
||||||
blurRadius: widget.showShadow ? 8 : 0,
|
|
||||||
color: Theme
|
|
||||||
.of(context)
|
|
||||||
.shadowColor,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
color: Theme
|
color: Theme
|
||||||
.of(context)
|
.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.background,
|
.background,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
duration: const Duration(milliseconds: 250),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (_isSending && _sendProgress != null)
|
if (_isSending && _sendProgress != null)
|
||||||
|
@ -262,13 +252,24 @@ class _MessageInputBarState extends State<MessageInputBar> {
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: _isSending ? null : () async {
|
onPressed: _isSending ? null : () async {
|
||||||
final picture = await Navigator.of(context).push(
|
final picture = await _imagePicker.pickImage(source: ImageSource.camera);
|
||||||
MaterialPageRoute(builder: (context) => const MessageCameraView())) as File?;
|
if (picture == null) {
|
||||||
if (picture != null) {
|
if (context.mounted) {
|
||||||
setState(() {
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to get image path")));
|
||||||
_loadedFiles.add((FileType.image, picture));
|
}
|
||||||
});
|
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),
|
icon: const Icon(Icons.camera),
|
||||||
label: const Text("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 {
|
class _MessagesListState extends State<MessagesList> with SingleTickerProviderStateMixin {
|
||||||
final ScrollController _sessionListScrollController = ScrollController();
|
final ScrollController _sessionListScrollController = ScrollController();
|
||||||
final ScrollController _messageScrollController = ScrollController();
|
|
||||||
|
|
||||||
bool _showBottomBarShadow = false;
|
|
||||||
bool _showSessionListScrollChevron = false;
|
bool _showSessionListScrollChevron = false;
|
||||||
|
|
||||||
double get _shevronOpacity => _showSessionListScrollChevron ? 1.0 : 0.0;
|
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(
|
return Provider(
|
||||||
create: (BuildContext context) => AudioCacheClient(),
|
create: (BuildContext context) => AudioCacheClient(),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: _messageScrollController,
|
|
||||||
reverse: true,
|
reverse: true,
|
||||||
itemCount: cache.messages.length,
|
itemCount: cache.messages.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
@ -212,7 +196,6 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
MessageInputBar(
|
MessageInputBar(
|
||||||
recipient: widget.friend,
|
recipient: widget.friend,
|
||||||
disabled: cache == null || cache.error != null,
|
disabled: cache == null || cache.error != null,
|
||||||
showShadow: _showBottomBarShadow,
|
|
||||||
onMessageSent: () {
|
onMessageSent: () {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
|
|
40
pubspec.lock
40
pubspec.lock
|
@ -376,6 +376,46 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
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:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -61,6 +61,7 @@ dependencies:
|
||||||
camera: ^0.10.5
|
camera: ^0.10.5
|
||||||
path_provider: ^2.0.15
|
path_provider: ^2.0.15
|
||||||
crypto: ^3.0.3
|
crypto: ^3.0.3
|
||||||
|
image_picker: ^0.8.7+5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue