diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index d0ce8d4..f24ce14 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -3,6 +3,9 @@
+
+
+
> 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();
}
diff --git a/lib/apis/message_api.dart b/lib/apis/message_api.dart
index ab79e33..335d748 100644
--- a/lib/apis/message_api.dart
+++ b/lib/apis/message_api.dart
@@ -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();
}
diff --git a/lib/apis/record_api.dart b/lib/apis/record_api.dart
new file mode 100644
index 0000000..f8c07b3
--- /dev/null
+++ b/lib/apis/record_api.dart
@@ -0,0 +1,225 @@
+import 'dart:convert';
+import 'dart:io';
+import 'dart:math';
+import 'dart:typed_data';
+import 'package:collection/collection.dart';
+import 'package:contacts_plus_plus/models/records/asset_digest.dart';
+import 'package:contacts_plus_plus/models/records/json_template.dart';
+import 'package:http/http.dart' as http;
+import 'package:flutter/material.dart';
+
+import 'package:contacts_plus_plus/clients/api_client.dart';
+import 'package:contacts_plus_plus/models/records/asset_upload_data.dart';
+import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
+import 'package:contacts_plus_plus/models/records/preprocess_status.dart';
+import 'package:contacts_plus_plus/models/records/record.dart';
+import 'package:http_parser/http_parser.dart';
+import 'package:path/path.dart';
+
+class RecordApi {
+ static Future> getRecordsAt(ApiClient client, {required String path}) async {
+ final response = await client.get("/users/${client.userId}/records?path=$path");
+ client.checkResponse(response);
+ final body = jsonDecode(response.body) as List;
+ return body.map((e) => Record.fromMap(e)).toList();
+ }
+
+ static Future preprocessRecord(ApiClient client, {required Record record}) async {
+ final body = jsonEncode(record.toMap());
+ final response = await client.post(
+ "/users/${record.ownerId}/records/${record.id}/preprocess", body: body);
+ client.checkResponse(response);
+ final resultBody = jsonDecode(response.body);
+ return PreprocessStatus.fromMap(resultBody);
+ }
+
+ static Future getPreprocessStatus(ApiClient client,
+ {required PreprocessStatus preprocessStatus}) async {
+ final response = await client.get(
+ "/users/${preprocessStatus.ownerId}/records/${preprocessStatus.recordId}/preprocess/${preprocessStatus.id}"
+ );
+ client.checkResponse(response);
+ final body = jsonDecode(response.body);
+ return PreprocessStatus.fromMap(body);
+ }
+
+ static Future tryPreprocessRecord(ApiClient client, {required Record record}) async {
+ var status = await preprocessRecord(client, record: record);
+ while (status.state == RecordPreprocessState.preprocessing) {
+ await Future.delayed(const Duration(seconds: 1));
+ status = await getPreprocessStatus(client, preprocessStatus: status);
+ }
+
+ if (status.state != RecordPreprocessState.success) {
+ throw "Record Preprocessing failed: ${status.failReason}";
+ }
+ return status;
+ }
+
+ static Future beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async {
+ final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks");
+ client.checkResponse(response);
+ final body = jsonDecode(response.body);
+ final res = AssetUploadData.fromMap(body);
+ if (res.uploadState == UploadState.failed) throw body;
+ return res;
+ }
+
+ static Future 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);
+ client.checkResponse(response);
+ }
+
+ static Future uploadAsset(ApiClient client,
+ {required AssetUploadData uploadData, required String filename, required NeosDBAsset asset, required Uint8List data, void Function(double number)? progressCallback}) async {
+ for (int i = 0; i < uploadData.totalChunks; i++) {
+ progressCallback?.call(i/uploadData.totalChunks);
+ final offset = i * uploadData.chunkSize;
+ final end = (i + 1) * uploadData.chunkSize;
+ final request = http.MultipartRequest(
+ "POST",
+ ApiClient.buildFullUri("/users/${client.userId}/assets/${asset.hash}/chunks/$i"),
+ )
+ ..files.add(http.MultipartFile.fromBytes(
+ "file", data.getRange(offset, min(end, data.length)).toList(), filename: filename,
+ contentType: MediaType.parse("multipart/form-data")))
+ ..headers.addAll(client.authorizationHeader);
+ final response = await request.send();
+ final bodyBytes = await response.stream.toBytes();
+ client.checkResponse(http.Response.bytes(bodyBytes, response.statusCode));
+ progressCallback?.call(1);
+ }
+ }
+
+ static Future finishUpload(ApiClient client, {required NeosDBAsset asset}) async {
+ final response = await client.patch("/users/${client.userId}/assets/${asset.hash}/chunks");
+ client.checkResponse(response);
+ }
+
+ static Future uploadAssets(ApiClient client, {required List assets, void Function(double progress)? progressCallback}) async {
+ progressCallback?.call(0);
+ for (int i = 0; i < assets.length; i++) {
+ final totalProgress = i/assets.length;
+ progressCallback?.call(totalProgress);
+ final entry = assets[i];
+ final uploadData = await beginUploadAsset(client, asset: entry.asset);
+ if (uploadData.uploadState == UploadState.failed) {
+ throw "Asset upload failed: ${uploadData.uploadState.name}";
+ }
+ await uploadAsset(client,
+ uploadData: uploadData,
+ asset: entry.asset,
+ data: entry.data,
+ filename: entry.name,
+ progressCallback: (progress) => progressCallback?.call(totalProgress + progress * 1/assets.length),
+ );
+ await finishUpload(client, asset: entry.asset);
+ }
+ progressCallback?.call(1);
+ }
+
+ static Future uploadImage(ApiClient client, {required File image, required String machineId, void Function(double progress)? progressCallback}) async {
+ 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, 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 digests = [imageDigest, objectDigest];
+
+ final record = Record.fromRequiredData(
+ recordType: RecordType.texture,
+ userId: client.userId,
+ machineId: machineId,
+ assetUri: objectDigest.dbUri,
+ filename: filename,
+ thumbnailUri: imageDigest.dbUri,
+ digests: digests,
+ extraTags: ["image"],
+ );
+ progressCallback?.call(.1);
+ final status = await tryPreprocessRecord(client, record: record);
+ final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded);
+ progressCallback?.call(.2);
+
+ await uploadAssets(
+ client,
+ assets: digests.where((digest) => toUpload.any((diff) => digest.asset.hash == diff.hash)).toList(),
+ progressCallback: (progress) => progressCallback?.call(.2 + progress * .6));
+ await upsertRecord(client, record: record);
+ progressCallback?.call(1);
+ return record;
+ }
+
+ static Future uploadVoiceClip(ApiClient client, {required File voiceClip, required String machineId, void Function(double progress)? progressCallback}) async {
+ progressCallback?.call(0);
+ final voiceDigest = await AssetDigest.fromData(await voiceClip.readAsBytes(), basename(voiceClip.path));
+
+ final filename = basenameWithoutExtension(voiceClip.path);
+ final digests = [voiceDigest];
+
+ final record = Record.fromRequiredData(
+ recordType: RecordType.audio,
+ userId: client.userId,
+ machineId: machineId,
+ assetUri: voiceDigest.dbUri,
+ filename: filename,
+ thumbnailUri: "",
+ digests: digests,
+ extraTags: ["voice", "message"],
+ );
+ progressCallback?.call(.1);
+ final status = await tryPreprocessRecord(client, record: record);
+ final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded);
+ progressCallback?.call(.2);
+
+ await uploadAssets(
+ client,
+ assets: digests.where((digest) => toUpload.any((diff) => digest.asset.hash == diff.hash)).toList(),
+ progressCallback: (progress) => progressCallback?.call(.2 + progress * .6));
+ await upsertRecord(client, record: record);
+ progressCallback?.call(1);
+ return record;
+ }
+
+ static Future uploadRawFile(ApiClient client, {required File file, required String machineId, void Function(double progress)? progressCallback}) async {
+ progressCallback?.call(0);
+ final fileDigest = await AssetDigest.fromData(await file.readAsBytes(), basename(file.path));
+
+ final objectJson = jsonEncode(JsonTemplate.rawFile(assetUri: fileDigest.dbUri, filename: fileDigest.name).data);
+ final objectBytes = Uint8List.fromList(utf8.encode(objectJson));
+
+ final objectDigest = await AssetDigest.fromData(objectBytes, "${basenameWithoutExtension(file.path)}.json");
+
+ final digests = [fileDigest, objectDigest];
+
+ final record = Record.fromRequiredData(
+ recordType: RecordType.texture,
+ userId: client.userId,
+ machineId: machineId,
+ assetUri: objectDigest.dbUri,
+ filename: fileDigest.name,
+ thumbnailUri: JsonTemplate.thumbUrl,
+ digests: digests,
+ extraTags: ["document"],
+ );
+ progressCallback?.call(.1);
+ final status = await tryPreprocessRecord(client, record: record);
+ final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded);
+ progressCallback?.call(.2);
+
+ await uploadAssets(
+ client,
+ assets: digests.where((digest) => toUpload.any((diff) => digest.asset.hash == diff.hash)).toList(),
+ progressCallback: (progress) => progressCallback?.call(.2 + progress * .6));
+ await upsertRecord(client, record: record);
+ progressCallback?.call(1);
+ return record;
+ }
+}
diff --git a/lib/apis/user_api.dart b/lib/apis/user_api.dart
index 5424579..e85d5a5 100644
--- a/lib/apis/user_api.dart
+++ b/lib/apis/user_api.dart
@@ -10,43 +10,44 @@ import 'package:package_info_plus/package_info_plus.dart';
class UserApi {
static Future> 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 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 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 notifyOnlineInstance(ApiClient client) async {
final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineId.hashCode}");
- ApiClient.checkResponse(response);
+ client.checkResponse(response);
}
static Future setStatus(ApiClient client, {required UserStatus status}) async {
final pkginfo = await PackageInfo.fromPlatform();
status = status.copyWith(
neosVersion: "${pkginfo.version} of ${pkginfo.appName}",
+ isMobile: true,
);
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 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);
}
@@ -63,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 removeUserAsFriend(ApiClient client, {required User user}) async {
final response = await client.delete("/users/${client.userId}/friends/${user.id}");
- ApiClient.checkResponse(response);
+ client.checkResponse(response);
}
}
\ No newline at end of file
diff --git a/lib/auxiliary.dart b/lib/auxiliary.dart
index 93d0e50..6f792d9 100644
--- a/lib/auxiliary.dart
+++ b/lib/auxiliary.dart
@@ -1,4 +1,5 @@
import 'package:contacts_plus_plus/config.dart';
+import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:html/parser.dart' as htmlparser;
@@ -85,4 +86,19 @@ extension Format on Duration {
return "$hh:$mm:$ss";
}
}
+}
+
+extension DateTimeX on DateTime {
+ static DateTime epoch = DateTime.fromMillisecondsSinceEpoch(0);
+ static DateTime one = DateTime(1);
+}
+
+extension ColorX on Color {
+ Color invert() {
+ final r = 255 - red;
+ final g = 255 - green;
+ final b = 255 - blue;
+
+ return Color.fromARGB((opacity * 255).round(), r, g, b);
+ }
}
\ No newline at end of file
diff --git a/lib/client_holder.dart b/lib/client_holder.dart
index 988d578..3c1a5e0 100644
--- a/lib/client_holder.dart
+++ b/lib/client_holder.dart
@@ -14,8 +14,9 @@ class ClientHolder extends InheritedWidget {
super.key,
required AuthenticationData authenticationData,
required this.settingsClient,
- required super.child
- }) : apiClient = ApiClient(authenticationData: authenticationData);
+ required super.child,
+ required Function() onLogout,
+ }) : apiClient = ApiClient(authenticationData: authenticationData, onLogout: onLogout);
static ClientHolder? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType();
@@ -30,5 +31,6 @@ class ClientHolder extends InheritedWidget {
@override
bool updateShouldNotify(covariant ClientHolder oldWidget) =>
oldWidget.apiClient != apiClient
- || oldWidget.settingsClient != settingsClient;
+ || oldWidget.settingsClient != settingsClient
+ || oldWidget.notificationClient != notificationClient;
}
diff --git a/lib/clients/api_client.dart b/lib/clients/api_client.dart
index 4bdfb1e..cc2472d 100644
--- a/lib/clients/api_client.dart
+++ b/lib/clients/api_client.dart
@@ -1,8 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_phoenix/flutter_phoenix.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:contacts_plus_plus/models/authentication_data.dart';
@@ -18,10 +16,12 @@ class ApiClient {
static const String tokenKey = "token";
static const String passwordKey = "password";
- ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData;
+ ApiClient({required AuthenticationData authenticationData, required this.onLogout}) : _authenticationData = authenticationData;
final AuthenticationData _authenticationData;
final Logger _logger = Logger("API");
+ // Saving the context here feels kinda cringe ngl
+ final Function() onLogout;
AuthenticationData get authenticationData => _authenticationData;
String get userId => _authenticationData.userId;
@@ -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 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) {
@@ -99,15 +103,15 @@ class ApiClient {
return AuthenticationData.unauthenticated();
}
- Future logout(BuildContext context) async {
- const FlutterSecureStorage storage = FlutterSecureStorage();
+ Future logout() async {
+ const FlutterSecureStorage storage = FlutterSecureStorage(
+ aOptions: AndroidOptions(encryptedSharedPreferences: true),
+ );
await storage.delete(key: userIdKey);
await storage.delete(key: machineIdKey);
await storage.delete(key: tokenKey);
await storage.delete(key: passwordKey);
- if (context.mounted) {
- Phoenix.rebirth(context);
- }
+ onLogout();
}
Future extendSession() async {
@@ -117,22 +121,30 @@ class ApiClient {
}
}
- static void checkResponse(http.Response response) {
- final error = "(${response.statusCode}${kDebugMode ? "|${response.body}" : ""})";
- 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 == 500) {
- throw "Internal server error. $error";
- }
- if (response.statusCode >= 300) {
- throw "Unknown Error. $error";
+ tryCachedLogin().then((value) {
+ if (!value.isAuthenticated) {
+ onLogout();
+ }
+ });
}
+ checkResponseCode(response);
+ }
+
+ static void checkResponseCode(http.Response response) {
+ if (response.statusCode < 300) return;
+
+ final error = "${switch (response.statusCode) {
+ 429 => "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 get authorizationHeader => _authenticationData.authorizationHeader;
diff --git a/lib/clients/audio_cache_client.dart b/lib/clients/audio_cache_client.dart
new file mode 100644
index 0000000..d097732
--- /dev/null
+++ b/lib/clients/audio_cache_client.dart
@@ -0,0 +1,24 @@
+import 'dart:io';
+
+import 'package:contacts_plus_plus/auxiliary.dart';
+import 'package:contacts_plus_plus/clients/api_client.dart';
+import 'package:http/http.dart' as http;
+import 'package:contacts_plus_plus/models/message.dart';
+import 'package:path/path.dart';
+import 'package:path_provider/path_provider.dart';
+
+class AudioCacheClient {
+ final Future _directoryFuture = getTemporaryDirectory();
+
+ Future cachedNetworkAudioFile(AudioClipContent clip) async {
+ final directory = await _directoryFuture;
+ final file = File("${directory.path}/${basename(clip.assetUri)}");
+ if (!await file.exists()) {
+ await file.create(recursive: true);
+ final response = await http.get(Uri.parse(Aux.neosDbToHttp(clip.assetUri)));
+ ApiClient.checkResponseCode(response);
+ await file.writeAsBytes(response.bodyBytes);
+ }
+ return file;
+ }
+}
\ No newline at end of file
diff --git a/lib/clients/messaging_client.dart b/lib/clients/messaging_client.dart
index 96868a6..0108ac5 100644
--- a/lib/clients/messaging_client.dart
+++ b/lib/clients/messaging_client.dart
@@ -72,6 +72,7 @@ class MessagingClient extends ChangeNotifier {
box.delete(_lastUpdateKey);
await refreshFriendsListWithErrorHandler();
await _refreshUnreads();
+ _unreadSafeguard = Timer.periodic(_unreadSafeguardDuration, (timer) => _refreshUnreads());
});
_startWebsocket();
_notifyOnlineTimer = Timer.periodic(const Duration(seconds: 60), (timer) async {
@@ -85,6 +86,7 @@ class MessagingClient extends ChangeNotifier {
void dispose() {
_autoRefresh?.cancel();
_notifyOnlineTimer?.cancel();
+ _unreadSafeguard?.cancel();
_wsChannel?.close();
super.dispose();
}
@@ -142,7 +144,7 @@ class MessagingClient extends ChangeNotifier {
};
_sendData(data);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
- cache.messages.add(message);
+ cache.addMessage(message);
notifyListeners();
}
@@ -217,12 +219,10 @@ class MessagingClient extends ChangeNotifier {
}
Future _refreshUnreads() async {
- _unreadSafeguard?.cancel();
try {
final unreadMessages = await MessageApi.getUserMessages(_apiClient, unreadOnly: true);
updateAllUnreads(unreadMessages.toList());
} catch (_) {}
- _unreadSafeguard = Timer(_unreadSafeguardDuration, _refreshUnreads);
}
void _sortFriendsCache() {
@@ -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";
}
diff --git a/lib/clients/settings_client.dart b/lib/clients/settings_client.dart
index 0f7666e..5b5df56 100644
--- a/lib/clients/settings_client.dart
+++ b/lib/clients/settings_client.dart
@@ -15,7 +15,6 @@ class SettingsClient {
final data = await _storage.read(key: _settingsKey);
if (data == null) return;
_currentSettings = Settings.fromMap(jsonDecode(data));
-
}
Future changeSettings(Settings newSettings) async {
diff --git a/lib/main.dart b/lib/main.dart
index 9073b58..ef86464 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,8 +1,8 @@
import 'dart:developer';
-import 'dart:io' show Platform;
import 'package:contacts_plus_plus/apis/github_api.dart';
import 'package:contacts_plus_plus/client_holder.dart';
+import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/clients/settings_client.dart';
import 'package:contacts_plus_plus/models/sem_ver.dart';
@@ -27,13 +27,20 @@ void main() async {
Logger.root.onRecord.listen((event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time));
final settingsClient = SettingsClient();
await settingsClient.loadSettings();
- runApp(Phoenix(child: ContactsPlusPlus(settingsClient: settingsClient,)));
+ final newSettings = settingsClient.currentSettings.copyWith(machineId: settingsClient.currentSettings.machineId.valueOrDefault);
+ await settingsClient.changeSettings(newSettings); // Save generated machineId to disk
+ AuthenticationData cachedAuth = AuthenticationData.unauthenticated();
+ try {
+ cachedAuth = await ApiClient.tryCachedLogin();
+ } catch (_) {}
+ runApp(ContactsPlusPlus(settingsClient: settingsClient, cachedAuthentication: cachedAuth));
}
class ContactsPlusPlus extends StatefulWidget {
- const ContactsPlusPlus({required this.settingsClient, super.key});
+ const ContactsPlusPlus({required this.settingsClient, required this.cachedAuthentication, super.key});
final SettingsClient settingsClient;
+ final AuthenticationData cachedAuthentication;
@override
State createState() => _ContactsPlusPlusState();
@@ -41,7 +48,7 @@ class ContactsPlusPlus extends StatefulWidget {
class _ContactsPlusPlusState extends State {
final Typography _typography = Typography.material2021(platform: TargetPlatform.android);
- AuthenticationData _authData = AuthenticationData.unauthenticated();
+ late AuthenticationData _authData = widget.cachedAuthentication;
bool _checkedForUpdate = false;
void showUpdateDialogOnFirstBuild(BuildContext context) {
@@ -95,43 +102,61 @@ class _ContactsPlusPlusState extends State {
@override
Widget build(BuildContext context) {
- return ClientHolder(
- settingsClient: widget.settingsClient,
- authenticationData: _authData,
- child: DynamicColorBuilder(
- builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
- debugShowCheckedModeBanner: false,
- title: 'Contacts++',
- theme: ThemeData(
- useMaterial3: true,
- textTheme: _typography.white,
- colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark),
- ),
- home: Builder( // Builder is necessary here since we need a context which has access to the ClientHolder
- builder: (context) {
- showUpdateDialogOnFirstBuild(context);
- final clientHolder = ClientHolder.of(context);
- return _authData.isAuthenticated ?
- ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime.
- create: (context) =>
- MessagingClient(
- apiClient: clientHolder.apiClient,
- notificationClient: clientHolder.notificationClient,
- ),
- child: const FriendsList(),
- ) :
- LoginScreen(
- onLoginSuccessful: (AuthenticationData authData) async {
- if (authData.isAuthenticated) {
- setState(() {
- _authData = authData;
- });
+ return Phoenix(
+ child: Builder(
+ builder: (context) {
+ return ClientHolder(
+ settingsClient: widget.settingsClient,
+ authenticationData: _authData,
+ onLogout: () {
+ setState(() {
+ _authData = AuthenticationData.unauthenticated();
+ });
+ Phoenix.rebirth(context);
+ },
+ child: DynamicColorBuilder(
+ builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
+ debugShowCheckedModeBanner: false,
+ title: 'Contacts++',
+ theme: ThemeData(
+ useMaterial3: true,
+ textTheme: _typography.black,
+ colorScheme: lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light),
+ ),
+ darkTheme: ThemeData(
+ useMaterial3: true,
+ textTheme: _typography.white,
+ colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark),
+ ),
+ themeMode: ThemeMode.values[widget.settingsClient.currentSettings.themeMode.valueOrDefault],
+ home: Builder( // Builder is necessary here since we need a context which has access to the ClientHolder
+ builder: (context) {
+ showUpdateDialogOnFirstBuild(context);
+ final clientHolder = ClientHolder.of(context);
+ return _authData.isAuthenticated ?
+ ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime.
+ create: (context) =>
+ MessagingClient(
+ apiClient: clientHolder.apiClient,
+ notificationClient: clientHolder.notificationClient,
+ ),
+ child: const FriendsList(),
+ ) :
+ LoginScreen(
+ onLoginSuccessful: (AuthenticationData authData) async {
+ if (authData.isAuthenticated) {
+ setState(() {
+ _authData = authData;
+ });
+ }
+ },
+ );
}
- },
- );
- }
- )
- ),
+ )
+ ),
+ ),
+ );
+ }
),
);
}
diff --git a/lib/models/friend.dart b/lib/models/friend.dart
index cc8c89f..15ec426 100644
--- a/lib/models/friend.dart
+++ b/lib/models/friend.dart
@@ -93,14 +93,14 @@ enum OnlineStatus {
online;
static final List _colors = [
- Colors.white54,
- Colors.white54,
+ Colors.transparent,
+ Colors.transparent,
Colors.yellow,
Colors.red,
Colors.green,
];
- Color get color => _colors[index];
+ Color color(BuildContext context) => this == OnlineStatus.offline || this == OnlineStatus.invisible ? Theme.of(context).colorScheme.onSurface : _colors[index];
factory OnlineStatus.fromString(String? text) {
return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
diff --git a/lib/models/message.dart b/lib/models/message.dart
index 4afd900..ad06399 100644
--- a/lib/models/message.dart
+++ b/lib/models/message.dart
@@ -49,7 +49,7 @@ class Message implements Comparable {
final MessageState state;
Message({required this.id, required this.recipientId, required this.senderId, required this.type,
- required this.content, required DateTime sendTime, this.state=MessageState.local})
+ required this.content, required DateTime sendTime, required this.state})
: formattedContent = FormatNode.fromText(content), sendTime = sendTime.toUtc();
factory Message.fromMap(Map map, {MessageState? withState}) {
@@ -65,7 +65,7 @@ class Message implements Comparable {
type: type,
content: map["content"],
sendTime: DateTime.parse(map["sendTime"]),
- state: withState ?? (map["readTime"] != null ? MessageState.read : MessageState.local)
+ state: withState ?? (map["readTime"] != null ? MessageState.read : MessageState.sent)
);
}
@@ -125,7 +125,7 @@ class MessageCache {
bool addMessage(Message message) {
final existingIdx = _messages.indexWhere((element) => element.id == message.id);
if (existingIdx == -1) {
- _messages.add(message);
+ _messages.insert(0, message);
_ensureIntegrity();
} else {
_messages[existingIdx] = message;
@@ -175,7 +175,7 @@ class AudioClipContent {
final String id;
final String assetUri;
- AudioClipContent({required this.id, required this.assetUri});
+ const AudioClipContent({required this.id, required this.assetUri});
factory AudioClipContent.fromMap(Map map) {
return AudioClipContent(
@@ -190,7 +190,7 @@ class MarkReadBatch {
final List ids;
final DateTime readTime;
- MarkReadBatch({required this.senderId, required this.ids, required this.readTime});
+ const MarkReadBatch({required this.senderId, required this.ids, required this.readTime});
Map toMap() {
return {
diff --git a/lib/models/records/asset_diff.dart b/lib/models/records/asset_diff.dart
new file mode 100644
index 0000000..bb0a2ce
--- /dev/null
+++ b/lib/models/records/asset_diff.dart
@@ -0,0 +1,34 @@
+
+import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
+
+class AssetDiff extends NeosDBAsset{
+ final Diff state;
+ final bool isUploaded;
+
+ const AssetDiff({required hash, required bytes, required this.state, required this.isUploaded}) : super(hash: hash, bytes: bytes);
+
+ factory AssetDiff.fromMap(Map map) {
+ return AssetDiff(
+ hash: map["hash"],
+ bytes: map["bytes"],
+ state: Diff.fromInt(map["state"]),
+ isUploaded: map["isUploaded"],
+ );
+ }
+}
+
+enum Diff {
+ added,
+ unchanged,
+ removed;
+
+ factory Diff.fromInt(int? idx) {
+ return Diff.values[idx ?? 1];
+ }
+
+ factory Diff.fromString(String? text) {
+ return Diff.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
+ orElse: () => Diff.unchanged,
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/models/records/asset_digest.dart b/lib/models/records/asset_digest.dart
new file mode 100644
index 0000000..33b7332
--- /dev/null
+++ b/lib/models/records/asset_digest.dart
@@ -0,0 +1,25 @@
+
+import 'dart:typed_data';
+
+import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
+import 'package:path/path.dart';
+
+class AssetDigest {
+ final Uint8List data;
+ final NeosDBAsset asset;
+ final String name;
+ final String dbUri;
+
+ AssetDigest({required this.data, required this.asset, required this.name, required this.dbUri});
+
+ static Future fromData(Uint8List data, String filename) async {
+ final asset = NeosDBAsset.fromData(data);
+
+ return AssetDigest(
+ data: data,
+ asset: asset,
+ name: basenameWithoutExtension(filename),
+ dbUri: "neosdb:///${asset.hash}${extension(filename)}",
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/models/records/asset_upload_data.dart b/lib/models/records/asset_upload_data.dart
new file mode 100644
index 0000000..6df0555
--- /dev/null
+++ b/lib/models/records/asset_upload_data.dart
@@ -0,0 +1,46 @@
+
+enum UploadState {
+ uploadingChunks,
+ finalizing,
+ uploaded,
+ failed,
+ unknown;
+
+ factory UploadState.fromString(String? text) {
+ return UploadState.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
+ orElse: () => UploadState.unknown,
+ );
+ }
+}
+
+class AssetUploadData {
+ final String signature;
+ final String variant;
+ final String ownerId;
+ final int totalBytes;
+ final int chunkSize;
+ final int totalChunks;
+ final UploadState uploadState;
+
+ const AssetUploadData({
+ required this.signature,
+ required this.variant,
+ required this.ownerId,
+ required this.totalBytes,
+ required this.chunkSize,
+ required this.totalChunks,
+ required this.uploadState,
+ });
+
+ factory AssetUploadData.fromMap(Map map) {
+ return AssetUploadData(
+ signature: map["signature"],
+ variant: map["variant"] ?? "",
+ ownerId: map["ownerId"] ?? "",
+ totalBytes: map["totalBytes"] ?? -1,
+ chunkSize: map["chunkSize"] ?? -1,
+ totalChunks: map["totalChunks"] ?? -1,
+ uploadState: UploadState.fromString(map["uploadStat"]),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/models/records/json_template.dart b/lib/models/records/json_template.dart
new file mode 100644
index 0000000..4a13bbd
--- /dev/null
+++ b/lib/models/records/json_template.dart
@@ -0,0 +1,2800 @@
+import 'package:path/path.dart';
+import 'package:uuid/uuid.dart';
+
+class JsonTemplate {
+ static const String thumbUrl = "neosdb:///8ed80703e48c3d1556093927b67298f3d5e10315e9f782ec56fc49d6366f09b7.webp";
+ final Map data;
+
+ JsonTemplate({required this.data});
+
+ 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(),
+ "Components": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "Type": "FrooxEngine.Grabbable",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "ReparentOnRelease": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "PreserveUserSpace": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "DestroyOnRelease": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "GrabPriority": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "GrabPriorityWhenGrabbed": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "CustomCanGrabCheck": {
+ "ID": const Uuid().v4(),
+ "Data": {
+ "Target": null
+ }
+ },
+ "EditModeOnly": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "AllowSteal": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "DropOnDisable": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "ActiveUserFilter": {
+ "ID": const Uuid().v4(),
+ "Data": "Disabled"
+ },
+ "OnlyUsers": {
+ "ID": const Uuid().v4(),
+ "Data": []
+ },
+ "Scalable": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Receivable": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "AllowOnlyPhysicalGrab": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "_grabber": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "_lastParent": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "_lastParentIsUserSpace": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "__legacyActiveUserRootOnly-ID": const Uuid().v4()
+ }
+ },
+ {
+ "Type": "FrooxEngine.StaticTexture2D",
+ "Data": {
+ "ID": texture2dUid,
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "URL": {
+ "ID": const Uuid().v4(),
+ "Data": "@$imageUri"
+ },
+ "FilterMode": {
+ "ID": const Uuid().v4(),
+ "Data": "Anisotropic"
+ },
+ "AnisotropicLevel": {
+ "ID": const Uuid().v4(),
+ "Data": 16
+ },
+ "Uncompressed": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "DirectLoad": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "ForceExactVariant": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "PreferredFormat": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "MipMapBias": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "IsNormalMap": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "WrapModeU": {
+ "ID": const Uuid().v4(),
+ "Data": "Repeat"
+ },
+ "WrapModeV": {
+ "ID": const Uuid().v4(),
+ "Data": "Repeat"
+ },
+ "PowerOfTwoAlignThreshold": {
+ "ID": const Uuid().v4(),
+ "Data": 0.05
+ },
+ "CrunchCompressed": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "MaxSize": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "MipMaps": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "MipMapFilter": {
+ "ID": const Uuid().v4(),
+ "Data": "Box"
+ },
+ "Readable": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.ItemTextureThumbnailSource",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Texture": {
+ "ID": const Uuid().v4(),
+ "Data": texture2dUid
+ },
+ "Crop": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.SnapPlane",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Normal": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "SnapParent": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.ReferenceProxy",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Reference": {
+ "ID": const Uuid().v4(),
+ "Data": texture2dUid
+ },
+ "SpawnInstanceOnTrigger": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.AssetProxy`1[[FrooxEngine.Texture2D, FrooxEngine, Version=2022.1.28.1335, Culture=neutral, PublicKeyToken=null]]",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "AssetReference": {
+ "ID": const Uuid().v4(),
+ "Data": texture2dUid
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.UnlitMaterial",
+ "Data": {
+ "ID": materialId,
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "HighPriorityIntegration": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "TintColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "Texture": {
+ "ID": const Uuid().v4(),
+ "Data": texture2dUid
+ },
+ "TextureScale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0
+ ]
+ },
+ "TextureOffset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0
+ ]
+ },
+ "MaskTexture": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "MaskScale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0
+ ]
+ },
+ "MaskOffset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0
+ ]
+ },
+ "MaskMode": {
+ "ID": const Uuid().v4(),
+ "Data": "MultiplyAlpha"
+ },
+ "BlendMode": {
+ "ID": const Uuid().v4(),
+ "Data": "Alpha"
+ },
+ "AlphaCutoff": {
+ "ID": const Uuid().v4(),
+ "Data": 0.5
+ },
+ "UseVertexColors": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Sidedness": {
+ "ID": const Uuid().v4(),
+ "Data": "Double"
+ },
+ "ZWrite": {
+ "ID": const Uuid().v4(),
+ "Data": "Auto"
+ },
+ "OffsetTexture": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "OffsetMagnitude": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0
+ ]
+ },
+ "OffsetTextureScale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0
+ ]
+ },
+ "OffsetTextureOffset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0
+ ]
+ },
+ "PolarUVmapping": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "PolarPower": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "StereoTextureTransform": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "RightEyeTextureScale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0
+ ]
+ },
+ "RightEyeTextureOffset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0
+ ]
+ },
+ "DecodeAsNormalMap": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "UseBillboardGeometry": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "UsePerBillboardScale": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "UsePerBillboardRotation": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "UsePerBillboardUV": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "BillboardSize": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.005,
+ 0.005
+ ]
+ },
+ "OffsetFactor": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "OffsetUnits": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "RenderQueue": {
+ "ID": const Uuid().v4(),
+ "Data": -1
+ },
+ "_unlit-ID": const Uuid().v4(),
+ "_unlitBillboard-ID": const Uuid().v4()
+ }
+ },
+ {
+ "Type": "FrooxEngine.QuadMesh",
+ "Data": {
+ "ID": quadMeshUid,
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "HighPriorityIntegration": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "OverrideBoundingBox": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "OverridenBoundingBox": {
+ "ID": const Uuid().v4(),
+ "Data": {
+ "Min": [
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Max": [
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ }
+ },
+ "Rotation": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "Size": {
+ "ID": quadMeshSizeUid,
+ "Data": [
+ ratio > 1 ? ratio : 1,
+ ratio > 1 ? 1 : ratio
+ ]
+ },
+ "UVScale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0
+ ]
+ },
+ "ScaleUVWithSize": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "UVOffset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0
+ ]
+ },
+ "DualSided": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "UseVertexColors": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "UpperLeftColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "LowerLeftColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "LowerRightColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "UpperRightColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.MeshRenderer",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Mesh": {
+ "ID": const Uuid().v4(),
+ "Data": quadMeshUid
+ },
+ "Materials": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "ID": const Uuid().v4(),
+ "Data": materialId
+ }
+ ]
+ },
+ "MaterialPropertyBlocks": {
+ "ID": const Uuid().v4(),
+ "Data": []
+ },
+ "ShadowCastMode": {
+ "ID": const Uuid().v4(),
+ "Data": "On"
+ },
+ "MotionVectorMode": {
+ "ID": const Uuid().v4(),
+ "Data": "Object"
+ },
+ "SortingOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.BoxCollider",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 1000000
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Offset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ },
+ "Type": {
+ "ID": const Uuid().v4(),
+ "Data": "NoCollision"
+ },
+ "Mass": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "CharacterCollider": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "IgnoreRaycasts": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "Size": {
+ "ID": boxColliderSizeUid,
+ "Data": [
+ 0.7071067,
+ 0.7071067,
+ 0.0
+ ]
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.Float2ToFloat3SwizzleDriver",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Source": {
+ "ID": const Uuid().v4(),
+ "Data": quadMeshSizeUid
+ },
+ "Target": {
+ "ID": const Uuid().v4(),
+ "Data": boxColliderSizeUid
+ },
+ "X": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Y": {
+ "ID": const Uuid().v4(),
+ "Data": 1
+ },
+ "Z": {
+ "ID": const Uuid().v4(),
+ "Data": -1
+ }
+ }
+ }
+ ]
+ },
+ "Name": {
+ "ID": const Uuid().v4(),
+ "Data": filename
+ },
+ "Tag": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "Active": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Persistent-ID": const Uuid().v4(),
+ "Position": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.8303015,
+ 1.815294,
+ 0.494639724
+ ]
+ },
+ "Rotation": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.05315749E-07,
+ 0.0222634021,
+ -1.08297385E-07,
+ 0.999752164
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.9999994,
+ 0.999999464,
+ 0.99999994
+ ]
+ },
+ "OrderOffset": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "ParentReference": const Uuid().v4(),
+ "Children": []
+ },
+ "TypeVersions": {
+ "FrooxEngine.Grabbable": 2,
+ "FrooxEngine.QuadMesh": 1,
+ "FrooxEngine.BoxCollider": 1
+ }
+ };
+ return JsonTemplate(data: data);
+ }
+
+ factory JsonTemplate.rawFile({required String assetUri, required String filename}) {
+ final var20 = const Uuid().v4();
+ final var19 = const Uuid().v4();
+ final var18 = const Uuid().v4();
+ final var17 = const Uuid().v4();
+ final var16 = const Uuid().v4();
+ final var15 = const Uuid().v4();
+ final var14 = const Uuid().v4();
+ final var13 = const Uuid().v4();
+ final var12 = const Uuid().v4();
+ final var11 = const Uuid().v4();
+ final var10 = const Uuid().v4();
+ final var9 = const Uuid().v4();
+ final var8 = const Uuid().v4();
+ final var7 = const Uuid().v4();
+ final var6 = const Uuid().v4();
+ final var5 = const Uuid().v4();
+ final var4 = const Uuid().v4();
+ final var3 = const Uuid().v4();
+ final var2 = const Uuid().v4();
+ final var1 = const Uuid().v4();
+ final var0 = const Uuid().v4();
+ final data = {
+ "Object": {
+ "ID": const Uuid().v4(),
+ "Components": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "Type": "FrooxEngine.ObjectRoot",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.StaticBinary",
+ "Data": {
+ "ID": var0,
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "URL": {
+ "ID": const Uuid().v4(),
+ "Data": "@$assetUri"
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.BinaryExportable",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Binary": {
+ "ID": const Uuid().v4(),
+ "Data": var0
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.FileMetadata",
+ "Data": {
+ "ID": var1,
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Filename": {
+ "ID": const Uuid().v4(),
+ "Data": filename
+ },
+ "MIME": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "IsProcessing-ID": const Uuid().v4()
+ }
+ },
+ {
+ "Type": "FrooxEngine.FileVisual",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "MetadataSource": {
+ "ID": const Uuid().v4(),
+ "Data": var1
+ },
+ "TypeLabel": {
+ "ID": const Uuid().v4(),
+ "Data": var2
+ },
+ "NameLabel": {
+ "ID": const Uuid().v4(),
+ "Data": var3
+ },
+ "FillMaterial": {
+ "ID": const Uuid().v4(),
+ "Data": var4
+ },
+ "OutlineMaterial": {
+ "ID": const Uuid().v4(),
+ "Data": var5
+ },
+ "TypeMaterial": {
+ "ID": const Uuid().v4(),
+ "Data": var6
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.Grabbable",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "ReparentOnRelease": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "PreserveUserSpace": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "DestroyOnRelease": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "GrabPriority": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "GrabPriorityWhenGrabbed": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "CustomCanGrabCheck": {
+ "ID": const Uuid().v4(),
+ "Data": {
+ "Target": null
+ }
+ },
+ "EditModeOnly": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "AllowSteal": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "DropOnDisable": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "ActiveUserFilter": {
+ "ID": const Uuid().v4(),
+ "Data": "Disabled"
+ },
+ "OnlyUsers": {
+ "ID": const Uuid().v4(),
+ "Data": []
+ },
+ "Scalable": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Receivable": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "AllowOnlyPhysicalGrab": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "_grabber": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "_lastParent": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "_lastParentIsUserSpace": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "__legacyActiveUserRootOnly-ID": const Uuid().v4()
+ }
+ }
+ ]
+ },
+ "Name": {
+ "ID": const Uuid().v4(),
+ "Data": filename
+ },
+ "Tag": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "Active": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Persistent-ID": const Uuid().v4(),
+ "Position": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.12835562,
+ 1.54872811,
+ -2.16048574
+ ]
+ },
+ "Rotation": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0814014,
+ 0.69532,
+ -0.07976244,
+ 0.7096068
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.00000036,
+ 0.99999994,
+ 1.00000036
+ ]
+ },
+ "OrderOffset": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "ParentReference": const Uuid().v4(),
+ "Children": [
+ {
+ "ID": const Uuid().v4(),
+ "Components": {
+ "ID": const Uuid().v4(),
+ "Data": []
+ },
+ "Name": {
+ "ID": const Uuid().v4(),
+ "Data": "FileVisual"
+ },
+ "Tag": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "Active": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Persistent-ID": const Uuid().v4(),
+ "Position": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ },
+ "Rotation": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "OrderOffset": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "ParentReference": const Uuid().v4(),
+ "Children": [
+ {
+ "ID": const Uuid().v4(),
+ "Components": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "Type": "FrooxEngine.MeshRenderer",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Mesh": {
+ "ID": const Uuid().v4(),
+ "Data": var7
+ },
+ "Materials": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "ID": const Uuid().v4(),
+ "Data": var4
+ },
+ {
+ "ID": const Uuid().v4(),
+ "Data": var5
+ },
+ {
+ "ID": const Uuid().v4(),
+ "Data": var6
+ }
+ ]
+ },
+ "MaterialPropertyBlocks": {
+ "ID": const Uuid().v4(),
+ "Data": []
+ },
+ "ShadowCastMode": {
+ "ID": const Uuid().v4(),
+ "Data": "On"
+ },
+ "MotionVectorMode": {
+ "ID": const Uuid().v4(),
+ "Data": "Object"
+ },
+ "SortingOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.BoxCollider",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Offset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.180121541,
+ 0.0,
+ 0.0669048056
+ ]
+ },
+ "Type": {
+ "ID": const Uuid().v4(),
+ "Data": "Static"
+ },
+ "Mass": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "CharacterCollider": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "IgnoreRaycasts": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "Size": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 2.360243,
+ 2.5,
+ 0.1516055
+ ]
+ }
+ }
+ }
+ ]
+ },
+ "Name": {
+ "ID": const Uuid().v4(),
+ "Data": "File Mesh"
+ },
+ "Tag": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "Active": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Persistent-ID": const Uuid().v4(),
+ "Position": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 5.96046448E-08,
+ 0.0,
+ 0.0
+ ]
+ },
+ "Rotation": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ -1.19209275E-07,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.04071409,
+ 0.0407139659,
+ 0.0407141037
+ ]
+ },
+ "OrderOffset": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "ParentReference": const Uuid().v4(),
+ "Children": []
+ },
+ {
+ "ID": const Uuid().v4(),
+ "Components": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "Type": "FrooxEngine.TextRenderer",
+ "Data": {
+ "ID": var3,
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "HighPriorityIntegration": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "OverrideBoundingBox": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "OverridenBoundingBox": {
+ "ID": const Uuid().v4(),
+ "Data": {
+ "Min": [
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Max": [
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ }
+ },
+ "Font": {
+ "ID": const Uuid().v4(),
+ "Data": var8
+ },
+ "Text": {
+ "ID": const Uuid().v4(),
+ "Data": basenameWithoutExtension(filename)
+ },
+ "ParseRichText": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "NullText": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "Size": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "HorizontalAlign": {
+ "ID": const Uuid().v4(),
+ "Data": "Center"
+ },
+ "VerticalAlign": {
+ "ID": const Uuid().v4(),
+ "Data": "Top"
+ },
+ "AlignmentMode": {
+ "ID": const Uuid().v4(),
+ "Data": "Geometric"
+ },
+ "Color": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "Materials": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "ID": const Uuid().v4(),
+ "Data": var9
+ }
+ ]
+ },
+ "LineHeight": {
+ "ID": const Uuid().v4(),
+ "Data": 0.8
+ },
+ "Bounded": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "BoundsSize": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.5,
+ 0.2
+ ]
+ },
+ "BoundsAlignment": {
+ "ID": const Uuid().v4(),
+ "Data": "MiddleCenter"
+ },
+ "MaskPattern": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "HorizontalAutoSize": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "VerticalAutoSize": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "CaretPosition": {
+ "ID": const Uuid().v4(),
+ "Data": -1
+ },
+ "SelectionStart": {
+ "ID": const Uuid().v4(),
+ "Data": -1
+ },
+ "CaretColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "SelectionColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.5,
+ 0.2,
+ 0.5
+ ]
+ },
+ "_legacyFontMaterial-ID": const Uuid().v4(),
+ "_legacyAlign-ID": const Uuid().v4()
+ }
+ },
+ {
+ "Type": "FrooxEngine.BoxCollider",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Offset": {
+ "ID": var10,
+ "Data": [
+ 0.0,
+ 0.0590983443,
+ 0.0
+ ]
+ },
+ "Type": {
+ "ID": const Uuid().v4(),
+ "Data": "Static"
+ },
+ "Mass": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "CharacterCollider": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "IgnoreRaycasts": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "Size": {
+ "ID": var11,
+ "Data": [
+ 0.5113616,
+ 0.09316488,
+ 0.0
+ ]
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.BoundingBoxDriver",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "BoundedSource": {
+ "ID": const Uuid().v4(),
+ "Data": var3
+ },
+ "Size": {
+ "ID": const Uuid().v4(),
+ "Data": var11
+ },
+ "Center": {
+ "ID": const Uuid().v4(),
+ "Data": var10
+ },
+ "Padding": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ }
+ }
+ }
+ ]
+ },
+ "Name": {
+ "ID": const Uuid().v4(),
+ "Data": "NameLabel"
+ },
+ "Tag": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "Active": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Persistent-ID": const Uuid().v4(),
+ "Position": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0009058714,
+ -0.08701205,
+ 0.00394916534
+ ]
+ },
+ "Rotation": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0009555904,
+ 0.999872863,
+ 0.000245468284,
+ 0.01591436
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.3075354,
+ 0.307534128,
+ 0.307536483
+ ]
+ },
+ "OrderOffset": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "ParentReference": const Uuid().v4(),
+ "Children": []
+ },
+ {
+ "ID": const Uuid().v4(),
+ "Components": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "Type": "FrooxEngine.TextRenderer",
+ "Data": {
+ "ID": var2,
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "HighPriorityIntegration": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "OverrideBoundingBox": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "OverridenBoundingBox": {
+ "ID": const Uuid().v4(),
+ "Data": {
+ "Min": [
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Max": [
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ }
+ },
+ "Font": {
+ "ID": const Uuid().v4(),
+ "Data": var8
+ },
+ "Text": {
+ "ID": const Uuid().v4(),
+ "Data": extension(filename).toUpperCase()
+ },
+ "ParseRichText": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "NullText": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "Size": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "HorizontalAlign": {
+ "ID": const Uuid().v4(),
+ "Data": "Center"
+ },
+ "VerticalAlign": {
+ "ID": const Uuid().v4(),
+ "Data": "Middle"
+ },
+ "AlignmentMode": {
+ "ID": const Uuid().v4(),
+ "Data": "Geometric"
+ },
+ "Color": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "Materials": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "ID": const Uuid().v4(),
+ "Data": var9
+ }
+ ]
+ },
+ "LineHeight": {
+ "ID": const Uuid().v4(),
+ "Data": 0.8
+ },
+ "Bounded": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "BoundsSize": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.24,
+ 1.0
+ ]
+ },
+ "BoundsAlignment": {
+ "ID": const Uuid().v4(),
+ "Data": "MiddleCenter"
+ },
+ "MaskPattern": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "HorizontalAutoSize": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "VerticalAutoSize": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "CaretPosition": {
+ "ID": const Uuid().v4(),
+ "Data": -1
+ },
+ "SelectionStart": {
+ "ID": const Uuid().v4(),
+ "Data": -1
+ },
+ "CaretColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "SelectionColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.5,
+ 0.2,
+ 0.5
+ ]
+ },
+ "_legacyFontMaterial-ID": const Uuid().v4(),
+ "_legacyAlign-ID": const Uuid().v4()
+ }
+ },
+ {
+ "Type": "FrooxEngine.BoxCollider",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Offset": {
+ "ID": var12,
+ "Data": [
+ -3.7252903E-09,
+ 0.0,
+ 0.0
+ ]
+ },
+ "Type": {
+ "ID": const Uuid().v4(),
+ "Data": "Static"
+ },
+ "Mass": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "CharacterCollider": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "IgnoreRaycasts": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "Size": {
+ "ID": var13,
+ "Data": [
+ 0.1862,
+ 0.08590001,
+ 0.0
+ ]
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.BoundingBoxDriver",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "BoundedSource": {
+ "ID": const Uuid().v4(),
+ "Data": var2
+ },
+ "Size": {
+ "ID": const Uuid().v4(),
+ "Data": var13
+ },
+ "Center": {
+ "ID": const Uuid().v4(),
+ "Data": var12
+ },
+ "Padding": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ }
+ }
+ }
+ ]
+ },
+ "Name": {
+ "ID": const Uuid().v4(),
+ "Data": "TypeLabel"
+ },
+ "Tag": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "Active": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Persistent-ID": const Uuid().v4(),
+ "Position": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.02074349,
+ 0.02509594,
+ 0.00547504425
+ ]
+ },
+ "Rotation": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 3.05048379E-05,
+ 0.9999975,
+ -0.000117197917,
+ -0.0022352722
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.268987477,
+ 0.2689861,
+ 0.268988162
+ ]
+ },
+ "OrderOffset": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "ParentReference": const Uuid().v4(),
+ "Children": []
+ },
+ {
+ "ID": const Uuid().v4(),
+ "Components": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "Type": "FrooxEngine.Panner2D",
+ "Data": {
+ "ID": const Uuid().v4(),
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "_target": {
+ "ID": const Uuid().v4(),
+ "Data": var14
+ },
+ "_offset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0
+ ]
+ },
+ "_preOffset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0
+ ]
+ },
+ "_speed": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 0.0
+ ]
+ },
+ "_repeat": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0
+ ]
+ },
+ "PingPong": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.PBS_DualSidedMetallic",
+ "Data": {
+ "ID": var5,
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "HighPriorityIntegration": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "TextureScale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0
+ ]
+ },
+ "TextureOffset": {
+ "ID": var14,
+ "Data": [
+ 0.399169922,
+ 0.0
+ ]
+ },
+ "AlbedoColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.25,
+ 0.25,
+ 0.25,
+ 1.0
+ ]
+ },
+ "AlbedoTexture": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "EmissiveColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "EmissiveMap": {
+ "ID": const Uuid().v4(),
+ "Data": var15
+ },
+ "NormalMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "NormalScale": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "OcclusionMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "Culling": {
+ "ID": const Uuid().v4(),
+ "Data": "Off"
+ },
+ "AlphaHandling": {
+ "ID": const Uuid().v4(),
+ "Data": "Opaque"
+ },
+ "AlphaClip": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "OffsetFactor": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "OffsetUnits": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "RenderQueue": {
+ "ID": const Uuid().v4(),
+ "Data": -1
+ },
+ "Metallic": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "Smoothness": {
+ "ID": const Uuid().v4(),
+ "Data": 0.9
+ },
+ "MetallicMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "_regular-ID": const Uuid().v4(),
+ "_transparent-ID": const Uuid().v4()
+ }
+ }
+ ]
+ },
+ "Name": {
+ "ID": const Uuid().v4(),
+ "Data": "OutlineMaterial"
+ },
+ "Tag": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "Active": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Persistent-ID": const Uuid().v4(),
+ "Position": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ },
+ "Rotation": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.000407140964,
+ 0.000407139567,
+ 0.000407140964
+ ]
+ },
+ "OrderOffset": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "ParentReference": const Uuid().v4(),
+ "Children": []
+ },
+ {
+ "ID": const Uuid().v4(),
+ "Components": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "Type": "FrooxEngine.PBS_DualSidedMetallic",
+ "Data": {
+ "ID": var4,
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "HighPriorityIntegration": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "TextureScale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0
+ ]
+ },
+ "TextureOffset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0
+ ]
+ },
+ "AlbedoColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "AlbedoTexture": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "EmissiveColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "EmissiveMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "NormalMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "NormalScale": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "OcclusionMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "Culling": {
+ "ID": const Uuid().v4(),
+ "Data": "Off"
+ },
+ "AlphaHandling": {
+ "ID": const Uuid().v4(),
+ "Data": "Opaque"
+ },
+ "AlphaClip": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "OffsetFactor": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "OffsetUnits": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "RenderQueue": {
+ "ID": const Uuid().v4(),
+ "Data": -1
+ },
+ "Metallic": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "Smoothness": {
+ "ID": const Uuid().v4(),
+ "Data": 0.75
+ },
+ "MetallicMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "_regular-ID": const Uuid().v4(),
+ "_transparent-ID": const Uuid().v4()
+ }
+ }
+ ]
+ },
+ "Name": {
+ "ID": const Uuid().v4(),
+ "Data": "FillMaterial"
+ },
+ "Tag": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "Active": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Persistent-ID": const Uuid().v4(),
+ "Position": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ },
+ "Rotation": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.000407140964,
+ 0.000407139567,
+ 0.000407140964
+ ]
+ },
+ "OrderOffset": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "ParentReference": const Uuid().v4(),
+ "Children": []
+ },
+ {
+ "ID": const Uuid().v4(),
+ "Components": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "Type": "FrooxEngine.PBS_DualSidedMetallic",
+ "Data": {
+ "ID": var6,
+ "persistent-ID": const Uuid().v4(),
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "HighPriorityIntegration": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "TextureScale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0
+ ]
+ },
+ "TextureOffset": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0
+ ]
+ },
+ "AlbedoColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.25,
+ 0.25,
+ 0.25,
+ 1.0
+ ]
+ },
+ "AlbedoTexture": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "EmissiveColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "EmissiveMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "NormalMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "NormalScale": {
+ "ID": const Uuid().v4(),
+ "Data": 1.0
+ },
+ "OcclusionMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "Culling": {
+ "ID": const Uuid().v4(),
+ "Data": "Off"
+ },
+ "AlphaHandling": {
+ "ID": const Uuid().v4(),
+ "Data": "Opaque"
+ },
+ "AlphaClip": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "OffsetFactor": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "OffsetUnits": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "RenderQueue": {
+ "ID": const Uuid().v4(),
+ "Data": -1
+ },
+ "Metallic": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "Smoothness": {
+ "ID": const Uuid().v4(),
+ "Data": 0.8
+ },
+ "MetallicMap": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "_regular-ID": const Uuid().v4(),
+ "_transparent-ID": const Uuid().v4()
+ }
+ }
+ ]
+ },
+ "Name": {
+ "ID": const Uuid().v4(),
+ "Data": "TypeMaterial"
+ },
+ "Tag": {
+ "ID": const Uuid().v4(),
+ "Data": ""
+ },
+ "Active": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "Persistent-ID": const Uuid().v4(),
+ "Position": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ },
+ "Rotation": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "Scale": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.000407140964,
+ 0.000407139567,
+ 0.000407140964
+ ]
+ },
+ "OrderOffset": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "ParentReference": const Uuid().v4(),
+ "Children": []
+ }
+ ]
+ }
+ ]
+ },
+ "Assets": [
+ {
+ "Type": "FrooxEngine.StaticMesh",
+ "Data": {
+ "ID": var7,
+ "persistent": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "URL": {
+ "ID": const Uuid().v4(),
+ "Data": "@neosdb:///3738bf6fc560f7d08d872ce12b06f4d9337ac5da415b6de6008a49ca128658ec"
+ },
+ "Readable": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.FontChain",
+ "Data": {
+ "ID": var8,
+ "persistent": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "HighPriorityIntegration": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "MainFont": {
+ "ID": const Uuid().v4(),
+ "Data": var16
+ },
+ "FallbackFonts": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ {
+ "ID": const Uuid().v4(),
+ "Data": var17
+ },
+ {
+ "ID": const Uuid().v4(),
+ "Data": var18
+ },
+ {
+ "ID": const Uuid().v4(),
+ "Data": var19
+ },
+ {
+ "ID": const Uuid().v4(),
+ "Data": var20
+ }
+ ]
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.StaticFont",
+ "Data": {
+ "ID": var16,
+ "persistent": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "URL": {
+ "ID": const Uuid().v4(),
+ "Data": "@neosdb:///c801b8d2522fb554678f17f4597158b1af3f9be3abd6ce35d5a3112a81e2bf39"
+ },
+ "Padding": {
+ "ID": const Uuid().v4(),
+ "Data": 1
+ },
+ "PixelRange": {
+ "ID": const Uuid().v4(),
+ "Data": 4
+ },
+ "GlyphEmSize": {
+ "ID": const Uuid().v4(),
+ "Data": 32
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.StaticFont",
+ "Data": {
+ "ID": var17,
+ "persistent": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "URL": {
+ "ID": const Uuid().v4(),
+ "Data": "@neosdb:///4cac521169034ddd416c6deffe2eb16234863761837df677a910697ec5babd25"
+ },
+ "Padding": {
+ "ID": const Uuid().v4(),
+ "Data": 1
+ },
+ "PixelRange": {
+ "ID": const Uuid().v4(),
+ "Data": 4
+ },
+ "GlyphEmSize": {
+ "ID": const Uuid().v4(),
+ "Data": 32
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.StaticFont",
+ "Data": {
+ "ID": var18,
+ "persistent": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "URL": {
+ "ID": const Uuid().v4(),
+ "Data": "@neosdb:///23e7ad7cb0a5a4cf75e07c9e0848b1eb06bba15e8fa9b8cb0579fc823c532927"
+ },
+ "Padding": {
+ "ID": const Uuid().v4(),
+ "Data": 1
+ },
+ "PixelRange": {
+ "ID": const Uuid().v4(),
+ "Data": 4
+ },
+ "GlyphEmSize": {
+ "ID": const Uuid().v4(),
+ "Data": 32
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.StaticFont",
+ "Data": {
+ "ID": var19,
+ "persistent": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "URL": {
+ "ID": const Uuid().v4(),
+ "Data": "@neosdb:///415dc6290378574135b64c808dc640c1df7531973290c4970c51fdeb849cb0c5"
+ },
+ "Padding": {
+ "ID": const Uuid().v4(),
+ "Data": 1
+ },
+ "PixelRange": {
+ "ID": const Uuid().v4(),
+ "Data": 4
+ },
+ "GlyphEmSize": {
+ "ID": const Uuid().v4(),
+ "Data": 32
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.StaticFont",
+ "Data": {
+ "ID": var20,
+ "persistent": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "URL": {
+ "ID": const Uuid().v4(),
+ "Data": "@neosdb:///bcda0bcc22bab28ea4fedae800bfbf9ec76d71cc3b9f851779a35b7e438a839d"
+ },
+ "Padding": {
+ "ID": const Uuid().v4(),
+ "Data": 1
+ },
+ "PixelRange": {
+ "ID": const Uuid().v4(),
+ "Data": 4
+ },
+ "GlyphEmSize": {
+ "ID": const Uuid().v4(),
+ "Data": 32
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.TextUnlitMaterial",
+ "Data": {
+ "ID": var9,
+ "persistent": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "HighPriorityIntegration": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "_shader-ID": const Uuid().v4(),
+ "FontAtlas": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "TintColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ },
+ "OutlineColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "BackgroundColor": {
+ "ID": const Uuid().v4(),
+ "Data": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ "AutoBackgroundColor": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "GlyphRenderMethod": {
+ "ID": const Uuid().v4(),
+ "Data": "MSDF"
+ },
+ "PixelRange": {
+ "ID": const Uuid().v4(),
+ "Data": 4.0
+ },
+ "FaceDilate": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "OutlineThickness": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "FaceSoftness": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "BlendMode": {
+ "ID": const Uuid().v4(),
+ "Data": "Alpha"
+ },
+ "Sidedness": {
+ "ID": const Uuid().v4(),
+ "Data": "Double"
+ },
+ "ZWrite": {
+ "ID": const Uuid().v4(),
+ "Data": "Auto"
+ },
+ "ZTest": {
+ "ID": const Uuid().v4(),
+ "Data": "LessOrEqual"
+ },
+ "OffsetFactor": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "OffsetUnits": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "RenderQueue": {
+ "ID": const Uuid().v4(),
+ "Data": -1
+ }
+ }
+ },
+ {
+ "Type": "FrooxEngine.StaticTexture2D",
+ "Data": {
+ "ID": var15,
+ "persistent": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "UpdateOrder": {
+ "ID": const Uuid().v4(),
+ "Data": 0
+ },
+ "Enabled": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "URL": {
+ "ID": const Uuid().v4(),
+ "Data": "@neosdb:///274f0d4ea4bce93abc224c9ae9f9a97a9a396b382c5338f71c738d1591dd5c35.webp"
+ },
+ "FilterMode": {
+ "ID": const Uuid().v4(),
+ "Data": "Anisotropic"
+ },
+ "AnisotropicLevel": {
+ "ID": const Uuid().v4(),
+ "Data": 8
+ },
+ "Uncompressed": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "DirectLoad": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "ForceExactVariant": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "PreferredFormat": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "MipMapBias": {
+ "ID": const Uuid().v4(),
+ "Data": 0.0
+ },
+ "IsNormalMap": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ },
+ "WrapModeU": {
+ "ID": const Uuid().v4(),
+ "Data": "Repeat"
+ },
+ "WrapModeV": {
+ "ID": const Uuid().v4(),
+ "Data": "Repeat"
+ },
+ "PowerOfTwoAlignThreshold": {
+ "ID": const Uuid().v4(),
+ "Data": 0.05
+ },
+ "CrunchCompressed": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "MaxSize": {
+ "ID": const Uuid().v4(),
+ "Data": null
+ },
+ "MipMaps": {
+ "ID": const Uuid().v4(),
+ "Data": true
+ },
+ "MipMapFilter": {
+ "ID": const Uuid().v4(),
+ "Data": "Box"
+ },
+ "Readable": {
+ "ID": const Uuid().v4(),
+ "Data": false
+ }
+ }
+ }
+ ],
+ "TypeVersions": {
+ "FrooxEngine.Grabbable": 2,
+ "FrooxEngine.BoxCollider": 1,
+ "FrooxEngine.TextRenderer": 5
+ }
+ };
+ return JsonTemplate(data: data);
+ }
+}
\ No newline at end of file
diff --git a/lib/models/records/neos_db_asset.dart b/lib/models/records/neos_db_asset.dart
new file mode 100644
index 0000000..8b0c64e
--- /dev/null
+++ b/lib/models/records/neos_db_asset.dart
@@ -0,0 +1,26 @@
+import 'dart:typed_data';
+
+import 'package:crypto/crypto.dart';
+
+class NeosDBAsset {
+ final String hash;
+ final int bytes;
+
+ const NeosDBAsset({required this.hash, required this.bytes});
+
+ factory NeosDBAsset.fromMap(Map map) {
+ return NeosDBAsset(hash: map["hash"] ?? "", bytes: map["bytes"] ?? -1);
+ }
+
+ factory NeosDBAsset.fromData(Uint8List data) {
+ final digest = sha256.convert(data);
+ return NeosDBAsset(hash: digest.toString().replaceAll("-", "").toLowerCase(), bytes: data.length);
+ }
+
+ Map toMap() {
+ return {
+ "hash": hash,
+ "bytes": bytes,
+ };
+ }
+}
\ No newline at end of file
diff --git a/lib/models/records/preprocess_status.dart b/lib/models/records/preprocess_status.dart
new file mode 100644
index 0000000..9b25d03
--- /dev/null
+++ b/lib/models/records/preprocess_status.dart
@@ -0,0 +1,41 @@
+import 'package:contacts_plus_plus/models/records/asset_diff.dart';
+
+enum RecordPreprocessState
+{
+ preprocessing,
+ success,
+ failed;
+
+ factory RecordPreprocessState.fromString(String? text) {
+ return RecordPreprocessState.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
+ orElse: () => RecordPreprocessState.failed,
+ );
+ }
+}
+
+
+class PreprocessStatus {
+ final String id;
+ final String ownerId;
+ final String recordId;
+ final RecordPreprocessState state;
+ final num progress;
+ final String failReason;
+ final List resultDiffs;
+
+ const PreprocessStatus({required this.id, required this.ownerId, required this.recordId, required this.state,
+ required this.progress, required this.failReason, required this.resultDiffs,
+ });
+
+ factory PreprocessStatus.fromMap(Map map) {
+ return PreprocessStatus(
+ id: map["id"],
+ ownerId: map["ownerId"],
+ recordId: map["recordId"],
+ state: RecordPreprocessState.fromString(map["state"]),
+ progress: map["progress"],
+ failReason: map["failReason"] ?? "",
+ resultDiffs: (map["resultDiffs"] as List? ?? []).map((e) => AssetDiff.fromMap(e)).toList(),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/models/records/record.dart b/lib/models/records/record.dart
new file mode 100644
index 0000000..125a0c6
--- /dev/null
+++ b/lib/models/records/record.dart
@@ -0,0 +1,303 @@
+import 'package:contacts_plus_plus/auxiliary.dart';
+import 'package:contacts_plus_plus/models/message.dart';
+import 'package:contacts_plus_plus/models/records/asset_digest.dart';
+import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
+import 'package:contacts_plus_plus/string_formatter.dart';
+import 'package:flutter/material.dart';
+import 'package:uuid/uuid.dart';
+
+enum RecordType {
+ unknown,
+ link,
+ object,
+ directory,
+ texture,
+ audio;
+
+ factory RecordType.fromName(String? name) {
+ return RecordType.values.firstWhere((element) => element.name.toLowerCase() == name?.toLowerCase().trim(), orElse: () => RecordType.unknown);
+ }
+}
+
+class RecordId {
+ final String? id;
+ final String? ownerId;
+ final bool isValid;
+
+ const RecordId({required this.id, required this.ownerId, required this.isValid});
+
+ factory RecordId.fromMap(Map? map) {
+ return RecordId(id: map?["id"], ownerId: map?["ownerId"], isValid: map?["isValid"] ?? false);
+ }
+
+ Map toMap() {
+ return {
+ "id": id,
+ "ownerId": ownerId,
+ "isValid": isValid,
+ };
+ }
+}
+
+class Record {
+ final String id;
+ final RecordId combinedRecordId;
+ final String ownerId;
+ final String assetUri;
+ final int globalVersion;
+ final int localVersion;
+ final String lastModifyingUserId;
+ final String lastModifyingMachineId;
+ final bool isSynced;
+ final DateTime fetchedOn;
+ final String name;
+ final FormatNode formattedName;
+ final String description;
+ final RecordType recordType;
+ final List tags;
+ final String path;
+ final String thumbnailUri;
+ final bool isPublic;
+ final bool isForPatreons;
+ final bool isListed;
+ final DateTime lastModificationTime;
+ final DateTime creationTime;
+ final int visits;
+ final int rating;
+ final int randomOrder;
+ final List manifest;
+ final List neosDBManifest;
+ final String url;
+ final bool isValidOwnerId;
+ final bool isValidRecordId;
+
+ Record({
+ required this.id,
+ required this.combinedRecordId,
+ required this.isSynced,
+ required this.fetchedOn,
+ required this.path,
+ required this.ownerId,
+ required this.assetUri,
+ required this.name,
+ required this.description,
+ required this.tags,
+ required this.recordType,
+ required this.thumbnailUri,
+ required this.isPublic,
+ required this.isListed,
+ required this.isForPatreons,
+ required this.lastModificationTime,
+ required this.neosDBManifest,
+ required this.lastModifyingUserId,
+ required this.lastModifyingMachineId,
+ required this.creationTime,
+ required this.manifest,
+ required this.url,
+ required this.isValidOwnerId,
+ required this.isValidRecordId,
+ required this.globalVersion,
+ required this.localVersion,
+ required this.visits,
+ required this.rating,
+ required this.randomOrder,
+ }) : formattedName = FormatNode.fromText(name);
+
+ factory Record.fromRequiredData({
+ required RecordType recordType,
+ required String userId,
+ required String machineId,
+ required String assetUri,
+ required String filename,
+ required String thumbnailUri,
+ required List digests,
+ List? extraTags,
+ }) {
+ final combinedRecordId = RecordId(id: Record.generateId(), ownerId: userId, isValid: true);
+ return Record(
+ id: combinedRecordId.id.toString(),
+ combinedRecordId: combinedRecordId,
+ assetUri: assetUri,
+ name: filename,
+ tags: ([
+ filename,
+ "message_item",
+ "message_id:${Message.generateId()}",
+ "contacts-plus-plus"
+ ] + (extraTags ?? [])).unique(),
+ recordType: recordType,
+ thumbnailUri: thumbnailUri,
+ isPublic: false,
+ isForPatreons: false,
+ isListed: false,
+ neosDBManifest: digests.map((e) => e.asset).toList(),
+ globalVersion: 0,
+ localVersion: 1,
+ lastModifyingUserId: userId,
+ lastModifyingMachineId: machineId,
+ lastModificationTime: DateTime.now().toUtc(),
+ creationTime: DateTime.now().toUtc(),
+ ownerId: userId,
+ isSynced: false,
+ fetchedOn: DateTimeX.one,
+ path: '',
+ description: '',
+ manifest: digests.map((e) => e.dbUri).toList(),
+ url: "neosrec:///$userId/${combinedRecordId.id}",
+ isValidOwnerId: true,
+ isValidRecordId: true,
+ visits: 0,
+ rating: 0,
+ randomOrder: 0,
+ );
+ }
+
+ factory Record.fromMap(Map map) {
+ return Record(
+ id: map["id"] ?? "0",
+ combinedRecordId: RecordId.fromMap(map["combinedRecordId"]),
+ ownerId: map["ownerId"] ?? "",
+ assetUri: map["assetUri"] ?? "",
+ globalVersion: map["globalVersion"] ?? 0,
+ localVersion: map["localVersion"] ?? 0,
+ name: map["name"] ?? "",
+ description: map["description"] ?? "",
+ tags: (map["tags"] as List? ?? []).map((e) => e.toString()).toList(),
+ recordType: RecordType.fromName(map["recordType"]),
+ thumbnailUri: map["thumbnailUri"] ?? "",
+ isPublic: map["isPublic"] ?? false,
+ isForPatreons: map["isForPatreons"] ?? false,
+ isListed: map["isListed"] ?? false,
+ lastModificationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
+ neosDBManifest: (map["neosDBManifest"] as List? ?? []).map((e) => NeosDBAsset.fromMap(e)).toList(),
+ lastModifyingUserId: map["lastModifyingUserId"] ?? "",
+ lastModifyingMachineId: map["lastModifyingMachineId"] ?? "",
+ creationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
+ isSynced: map["isSynced"] ?? false,
+ fetchedOn: DateTime.tryParse(map["fetchedOn"]) ?? DateTimeX.epoch,
+ path: map["path"] ?? "",
+ manifest: (map["neosDBManifest"] as List? ?? []).map((e) => e.toString()).toList(),
+ url: map["url"] ?? "",
+ isValidOwnerId: map["isValidOwnerId"] ?? "",
+ isValidRecordId: map["isValidRecordId"] ?? "",
+ visits: map["visits"] ?? 0,
+ rating: map["rating"] ?? 0,
+ randomOrder: map["randomOrder"] ?? 0
+ );
+ }
+
+ Record copyWith({
+ String? id,
+ String? ownerId,
+ String? recordId,
+ String? assetUri,
+ int? globalVersion,
+ int? localVersion,
+ String? name,
+ TextSpan? formattedName,
+ String? description,
+ List? tags,
+ RecordType? recordType,
+ String? thumbnailUri,
+ bool? isPublic,
+ bool? isForPatreons,
+ bool? isListed,
+ bool? isDeleted,
+ DateTime? lastModificationTime,
+ List? neosDBManifest,
+ String? lastModifyingUserId,
+ String? lastModifyingMachineId,
+ DateTime? creationTime,
+ RecordId? combinedRecordId,
+ bool? isSynced,
+ DateTime? fetchedOn,
+ String? path,
+ List? manifest,
+ String? url,
+ bool? isValidOwnerId,
+ bool? isValidRecordId,
+ int? visits,
+ int? rating,
+ int? randomOrder,
+ }) {
+ return Record(
+ id: id ?? this.id,
+ ownerId: ownerId ?? this.ownerId,
+ assetUri: assetUri ?? this.assetUri,
+ globalVersion: globalVersion ?? this.globalVersion,
+ localVersion: localVersion ?? this.localVersion,
+ name: name ?? this.name,
+ description: description ?? this.description,
+ tags: tags ?? this.tags,
+ recordType: recordType ?? this.recordType,
+ thumbnailUri: thumbnailUri ?? this.thumbnailUri,
+ isPublic: isPublic ?? this.isPublic,
+ isForPatreons: isForPatreons ?? this.isForPatreons,
+ isListed: isListed ?? this.isListed,
+ lastModificationTime: lastModificationTime ?? this.lastModificationTime,
+ neosDBManifest: neosDBManifest ?? this.neosDBManifest,
+ lastModifyingUserId: lastModifyingUserId ?? this.lastModifyingUserId,
+ lastModifyingMachineId: lastModifyingMachineId ?? this.lastModifyingMachineId,
+ creationTime: creationTime ?? this.creationTime,
+ combinedRecordId: combinedRecordId ?? this.combinedRecordId,
+ isSynced: isSynced ?? this.isSynced,
+ fetchedOn: fetchedOn ?? this.fetchedOn,
+ path: path ?? this.path,
+ manifest: manifest ?? this.manifest,
+ url: url ?? this.url,
+ isValidOwnerId: isValidOwnerId ?? this.isValidOwnerId,
+ isValidRecordId: isValidRecordId ?? this.isValidRecordId,
+ visits: visits ?? this.visits,
+ rating: rating ?? this.rating,
+ randomOrder: randomOrder ?? this.randomOrder,
+ );
+ }
+
+ Map toMap() {
+ return {
+ "id": id,
+ "ownerId": ownerId,
+ "assetUri": assetUri,
+ "globalVersion": globalVersion,
+ "localVersion": localVersion,
+ "name": name,
+ "description": description.asNullable,
+ "tags": tags,
+ "recordType": recordType.name,
+ "thumbnailUri": thumbnailUri.asNullable,
+ "isPublic": isPublic,
+ "isForPatreons": isForPatreons,
+ "isListed": isListed,
+ "lastModificationTime": lastModificationTime.toUtc().toIso8601String(),
+ "neosDBManifest": neosDBManifest.map((e) => e.toMap()).toList(),
+ "lastModifyingUserId": lastModifyingUserId,
+ "lastModifyingMachineId": lastModifyingMachineId,
+ "creationTime": creationTime.toUtc().toIso8601String(),
+ "combinedRecordId": combinedRecordId.toMap(),
+ "isSynced": isSynced,
+ "fetchedOn": fetchedOn.toUtc().toIso8601String(),
+ "path": path.asNullable,
+ "manifest": manifest,
+ "url": url,
+ "isValidOwnerId": isValidOwnerId,
+ "isValidRecordId": isValidRecordId,
+ "visits": visits,
+ "rating": rating,
+ "randomOrder": randomOrder,
+ };
+ }
+
+ static String generateId() {
+ return "R-${const Uuid().v4()}";
+ }
+
+ String? extractMessageId() {
+ const key = "message_id:";
+ for (final tag in tags) {
+ if (tag.startsWith(key)) {
+ return tag.replaceFirst(key, "");
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/lib/models/settings.dart b/lib/models/settings.dart
index 9f4ee9a..97484d7 100644
--- a/lib/models/settings.dart
+++ b/lib/models/settings.dart
@@ -2,6 +2,8 @@ import 'dart:convert';
import 'package:contacts_plus_plus/models/friend.dart';
import 'package:contacts_plus_plus/models/sem_ver.dart';
+import 'package:flutter/material.dart';
+import 'package:uuid/uuid.dart';
class SettingsEntry {
final T? value;
@@ -36,22 +38,29 @@ class Settings {
final SettingsEntry notificationsDenied;
final SettingsEntry lastOnlineStatus;
final SettingsEntry lastDismissedVersion;
+ final SettingsEntry machineId;
+ final SettingsEntry themeMode;
Settings({
SettingsEntry? notificationsDenied,
SettingsEntry? lastOnlineStatus,
- SettingsEntry? lastDismissedVersion
+ SettingsEntry? themeMode,
+ SettingsEntry? lastDismissedVersion,
+ SettingsEntry? machineId
})
: notificationsDenied = notificationsDenied ?? const SettingsEntry(deflt: false),
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry(deflt: OnlineStatus.online.index),
- lastDismissedVersion = lastDismissedVersion ?? SettingsEntry(deflt: SemVer.zero().toString())
- ;
+ themeMode = themeMode ?? SettingsEntry(deflt: ThemeMode.dark.index),
+ lastDismissedVersion = lastDismissedVersion ?? SettingsEntry(deflt: SemVer.zero().toString()),
+ machineId = machineId ?? SettingsEntry(deflt: const Uuid().v4());
factory Settings.fromMap(Map map) {
return Settings(
notificationsDenied: retrieveEntryOrNull(map["notificationsDenied"]),
lastOnlineStatus: retrieveEntryOrNull(map["lastOnlineStatus"]),
- lastDismissedVersion: retrieveEntryOrNull(map["lastDismissedVersion"])
+ themeMode: retrieveEntryOrNull(map["themeMode"]),
+ lastDismissedVersion: retrieveEntryOrNull(map["lastDismissedVersion"]),
+ machineId: retrieveEntryOrNull(map["machineId"]),
);
}
@@ -68,7 +77,9 @@ class Settings {
return {
"notificationsDenied": notificationsDenied.toMap(),
"lastOnlineStatus": lastOnlineStatus.toMap(),
+ "themeMode": themeMode.toMap(),
"lastDismissedVersion": lastDismissedVersion.toMap(),
+ "machineId": machineId.toMap(),
};
}
@@ -76,14 +87,17 @@ class Settings {
Settings copyWith({
bool? notificationsDenied,
- int? unreadCheckIntervalMinutes,
int? lastOnlineStatus,
+ int? themeMode,
String? lastDismissedVersion,
+ String? machineId,
}) {
return Settings(
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
lastOnlineStatus: this.lastOnlineStatus.passThrough(lastOnlineStatus),
+ themeMode: this.themeMode.passThrough(themeMode),
lastDismissedVersion: this.lastDismissedVersion.passThrough(lastDismissedVersion),
+ machineId: this.machineId.passThrough(machineId),
);
}
}
\ No newline at end of file
diff --git a/lib/widgets/friends/friend_online_status_indicator.dart b/lib/widgets/friends/friend_online_status_indicator.dart
index 6e3328b..98b8ac8 100644
--- a/lib/widgets/friends/friend_online_status_indicator.dart
+++ b/lib/widgets/friends/friend_online_status_indicator.dart
@@ -15,11 +15,11 @@ class FriendOnlineStatusIndicator extends StatelessWidget {
child: Image.asset(
"assets/images/logo-white.png",
alignment: Alignment.center,
- color: userStatus.onlineStatus.color,
+ color: userStatus.onlineStatus.color(context),
),
) : Icon(
userStatus.onlineStatus == OnlineStatus.offline ? Icons.circle_outlined : Icons.circle,
- color: userStatus.onlineStatus.color,
+ color: userStatus.onlineStatus.color(context),
size: 10,
);
}
diff --git a/lib/widgets/friends/friends_list.dart b/lib/widgets/friends/friends_list.dart
index 7af63df..2c70ba0 100644
--- a/lib/widgets/friends/friends_list.dart
+++ b/lib/widgets/friends/friends_list.dart
@@ -4,7 +4,6 @@ import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/models/friend.dart';
-import 'package:contacts_plus_plus/models/personal_profile.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.dart';
import 'package:contacts_plus_plus/widgets/friends/friend_list_tile.dart';
@@ -42,7 +41,6 @@ class _FriendsListState extends State {
final clientHolder = ClientHolder.of(context);
if (_clientHolder != clientHolder) {
_clientHolder = clientHolder;
- final apiClient = _clientHolder!.apiClient;
_refreshUserStatus();
}
}
@@ -79,7 +77,7 @@ class _FriendsListState extends State {
children: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
- child: Icon(Icons.circle, size: 16, color: userStatus.onlineStatus.color,),
+ child: Icon(Icons.circle, size: 16, color: userStatus.onlineStatus.color(context),),
),
Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"),
],
@@ -114,7 +112,7 @@ class _FriendsListState extends State {
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
- Icon(Icons.circle, size: 16, color: item.color,),
+ Icon(Icons.circle, size: 16, color: item.color(context),),
const SizedBox(width: 8,),
Text(toBeginningOfSentenceCase(item.name)!),
],
@@ -254,6 +252,7 @@ class _FriendsListState extends State {
friends.sort((a, b) => a.username.length.compareTo(b.username.length));
}
return ListView.builder(
+ physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
itemCount: friends.length,
itemBuilder: (context, index) {
final friend = friends[index];
diff --git a/lib/widgets/generic_avatar.dart b/lib/widgets/generic_avatar.dart
index fa30337..552b1cf 100644
--- a/lib/widgets/generic_avatar.dart
+++ b/lib/widgets/generic_avatar.dart
@@ -13,14 +13,14 @@ class GenericAvatar extends StatelessWidget {
Widget build(BuildContext context) {
return imageUri.isEmpty ? CircleAvatar(
radius: radius,
- foregroundColor: foregroundColor,
- backgroundColor: Colors.transparent,
+ foregroundColor: foregroundColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
+ backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Icon(placeholderIcon, color: foregroundColor,),
) : CachedNetworkImage(
imageBuilder: (context, imageProvider) {
return CircleAvatar(
foregroundImage: imageProvider,
- foregroundColor: foregroundColor,
+ foregroundColor: Colors.transparent,
backgroundColor: Colors.transparent,
radius: radius,
);
@@ -28,20 +28,20 @@ class GenericAvatar extends StatelessWidget {
imageUrl: imageUri,
placeholder: (context, url) {
return CircleAvatar(
- backgroundColor: Colors.white54,
- foregroundColor: foregroundColor,
+ backgroundColor: Theme.of(context).colorScheme.primaryContainer,
+ foregroundColor: foregroundColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
radius: radius,
child: Padding(
padding: const EdgeInsets.all(8.0),
- child: CircularProgressIndicator(color: foregroundColor, strokeWidth: 2),
+ child: CircularProgressIndicator(color: foregroundColor ?? Theme.of(context).colorScheme.onPrimaryContainer, strokeWidth: 2),
),
);
},
errorWidget: (context, error, what) => CircleAvatar(
radius: radius,
- foregroundColor: foregroundColor,
- backgroundColor: Colors.transparent,
- child: Icon(placeholderIcon, color: foregroundColor,),
+ foregroundColor: foregroundColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
+ backgroundColor: Theme.of(context).colorScheme.primaryContainer,
+ child: Icon(placeholderIcon, color: foregroundColor ?? Theme.of(context).colorScheme.onPrimaryContainer,),
),
);
}
diff --git a/lib/widgets/login_screen.dart b/lib/widgets/login_screen.dart
index 753ed18..541caf9 100644
--- a/lib/widgets/login_screen.dart
+++ b/lib/widgets/login_screen.dart
@@ -19,12 +19,7 @@ class _LoginScreenState extends State {
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _totpController = TextEditingController();
final ScrollController _scrollController = ScrollController();
- late final Future _cachedLoginFuture = ApiClient.tryCachedLogin().then((value) async {
- if (value.isAuthenticated) {
- await loginSuccessful(value);
- }
- return value;
- });
+
late final FocusNode _passwordFocusNode;
late final FocusNode _totpFocusNode;
@@ -150,102 +145,94 @@ class _LoginScreenState extends State {
appBar: AppBar(
title: const Text("Contacts++"),
),
- body: FutureBuilder(
- future: _cachedLoginFuture,
- builder: (context, snapshot) {
- if (snapshot.hasData || snapshot.hasError) {
- final authData = snapshot.data;
- if (authData?.isAuthenticated ?? false) {
- return const SizedBox.shrink();
- }
- return ListView(
- controller: _scrollController,
- children: [
- Padding(
- padding: const EdgeInsets.symmetric(vertical: 64),
- child: Center(
- child: Text("Sign In", style: Theme
- .of(context)
- .textTheme
- .headlineMedium),
+ body: Builder(
+ builder: (context) {
+ return ListView(
+ controller: _scrollController,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 64),
+ child: Center(
+ child: Text("Sign In", style: Theme
+ .of(context)
+ .textTheme
+ .headlineMedium),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
+ child: TextField(
+ autofocus: true,
+ controller: _usernameController,
+ onEditingComplete: () => _passwordFocusNode.requestFocus(),
+ decoration: InputDecoration(
+ contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(32),
+ ),
+ labelText: 'Username',
),
),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
+ child: TextField(
+ controller: _passwordController,
+ focusNode: _passwordFocusNode,
+ onEditingComplete: submit,
+ obscureText: true,
+ decoration: InputDecoration(
+ contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(32)
+ ),
+ labelText: 'Password',
+ ),
+ ),
+ ),
+ if (_needsTotp)
Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
child: TextField(
- autofocus: true,
- controller: _usernameController,
- onEditingComplete: () => _passwordFocusNode.requestFocus(),
+ controller: _totpController,
+ focusNode: _totpFocusNode,
+ onEditingComplete: submit,
+ obscureText: false,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32),
),
- labelText: 'Username',
+ labelText: '2FA Code',
),
),
),
- Padding(
- padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
- child: TextField(
- controller: _passwordController,
- focusNode: _passwordFocusNode,
- onEditingComplete: submit,
- obscureText: true,
- decoration: InputDecoration(
- contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(32)
- ),
- labelText: 'Password',
- ),
- ),
+ Padding(
+ padding: const EdgeInsets.only(top: 16),
+ child: _isLoading ?
+ const Center(child: CircularProgressIndicator()) :
+ TextButton.icon(
+ onPressed: submit,
+ icon: const Icon(Icons.login),
+ label: const Text("Login"),
),
- if (_needsTotp)
- Padding(
+ ),
+ Center(
+ child: AnimatedOpacity(
+ opacity: _errorOpacity,
+ duration: const Duration(milliseconds: 200),
+ child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
- child: TextField(
- controller: _totpController,
- focusNode: _totpFocusNode,
- onEditingComplete: submit,
- obscureText: false,
- decoration: InputDecoration(
- contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(32),
- ),
- labelText: '2FA Code',
- ),
- ),
- ),
- Padding(
- padding: const EdgeInsets.only(top: 16),
- child: _isLoading ?
- const Center(child: CircularProgressIndicator()) :
- TextButton.icon(
- onPressed: submit,
- icon: const Icon(Icons.login),
- label: const Text("Login"),
+ child: Text(_error, style: Theme
+ .of(context)
+ .textTheme
+ .labelMedium
+ ?.copyWith(color: Colors.red)),
),
),
- Center(
- child: AnimatedOpacity(
- opacity: _errorOpacity,
- duration: const Duration(milliseconds: 200),
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
- child: Text(_error, style: Theme
- .of(context)
- .textTheme
- .labelMedium
- ?.copyWith(color: Colors.red)),
- ),
- ),
- )
- ],
- );
- }
- return const LinearProgressIndicator();
+ )
+ ],
+ );
}
),
);
diff --git a/lib/widgets/messages/camera_image_view.dart b/lib/widgets/messages/camera_image_view.dart
new file mode 100644
index 0000000..de20b04
--- /dev/null
+++ b/lib/widgets/messages/camera_image_view.dart
@@ -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"),
+ )
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/widgets/messages/message_asset.dart b/lib/widgets/messages/message_asset.dart
index f1f0fba..e1f95ef 100644
--- a/lib/widgets/messages/message_asset.dart
+++ b/lib/widgets/messages/message_asset.dart
@@ -20,38 +20,46 @@ class MessageAsset extends StatelessWidget {
@override
Widget build(BuildContext context) {
final content = jsonDecode(message.content);
- PhotoAsset? photoAsset;
- try {
- photoAsset = PhotoAsset.fromTags((content["tags"] as List).map((e) => "$e").toList());
- } catch (_) {}
final formattedName = FormatNode.fromText(content["name"]);
return Container(
constraints: const BoxConstraints(maxWidth: 300),
child: Column(
children: [
- CachedNetworkImage(
- imageUrl: Aux.neosDbToHttp(content["thumbnailUri"]),
- imageBuilder: (context, image) {
- return InkWell(
- onTap: () async {
- await Navigator.push(
- context, MaterialPageRoute(builder: (context) =>
- PhotoView(
- minScale: PhotoViewComputedScale.contained,
- imageProvider: photoAsset == null
- ? image
- : CachedNetworkImageProvider(Aux.neosDbToHttp(photoAsset.imageUri)),
- heroAttributes: PhotoViewHeroAttributes(tag: message.id),
- ),
- ),);
- },
- child: Hero(
- tag: message.id,
- child: ClipRRect(borderRadius: BorderRadius.circular(16), child: Image(image: image,)),
- ),
- );
- },
- placeholder: (context, uri) => const CircularProgressIndicator(),
+ SizedBox(
+ height: 256,
+ width: double.infinity,
+ child: CachedNetworkImage(
+ imageUrl: Aux.neosDbToHttp(content["thumbnailUri"]),
+ imageBuilder: (context, image) {
+ return InkWell(
+ onTap: () async {
+ PhotoAsset? photoAsset;
+ try {
+ photoAsset = PhotoAsset.fromTags((content["tags"] as List).map((e) => "$e").toList());
+ } catch (_) {}
+ await Navigator.push(
+ context, MaterialPageRoute(builder: (context) =>
+ PhotoView(
+ minScale: PhotoViewComputedScale.contained,
+ imageProvider: photoAsset == null
+ ? image
+ : CachedNetworkImageProvider(Aux.neosDbToHttp(photoAsset.imageUri)),
+ heroAttributes: PhotoViewHeroAttributes(tag: message.id),
+ ),
+ ),);
+ },
+ child: Hero(
+ tag: message.id,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(16),
+ child: Image(image: image, fit: BoxFit.cover,),
+ ),
+ ),
+ );
+ },
+ errorWidget: (context, url, error) => const Icon(Icons.broken_image, size: 64,),
+ placeholder: (context, uri) => const Center(child: CircularProgressIndicator()),
+ ),
),
const SizedBox(height: 8,),
Row(
diff --git a/lib/widgets/messages/message_attachment_list.dart b/lib/widgets/messages/message_attachment_list.dart
new file mode 100644
index 0000000..21480ca
--- /dev/null
+++ b/lib/widgets/messages/message_attachment_list.dart
@@ -0,0 +1,304 @@
+import 'dart:io';
+
+import 'package:collection/collection.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 {
+ const MessageAttachmentList({required this.onChange, required this.disabled, this.initialFiles, super.key});
+
+ final List<(FileType, File)>? initialFiles;
+ final Function(List<(FileType, File)> files) onChange;
+ final bool disabled;
+
+ @override
+ State createState() => _MessageAttachmentListState();
+}
+
+class _MessageAttachmentListState extends State {
+ final List<(FileType, File)> _loadedFiles = [];
+ final ScrollController _scrollController = ScrollController();
+ bool _showShadow = true;
+ bool _popupIsOpen = false;
+ @override
+ void initState() {
+ super.initState();
+ _loadedFiles.clear();
+ _loadedFiles.addAll(widget.initialFiles ?? []);
+ _scrollController.addListener(() {
+ if (_scrollController.position.maxScrollExtent > 0 && !_showShadow) {
+ setState(() {
+ _showShadow = true;
+ });
+ }
+ if (_scrollController.position.atEdge && _scrollController.position.pixels > 0
+ && _showShadow) {
+ setState(() {
+ _showShadow = false;
+ });
+ }
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Expanded(
+ child: ShaderMask(
+ shaderCallback: (Rect bounds) {
+ return LinearGradient(
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ colors: [Colors.transparent, Colors.transparent, Colors.transparent, Theme
+ .of(context)
+ .colorScheme
+ .background
+ ],
+ stops: [0.0, 0.0, _showShadow ? 0.90 : 1.0, 1.0], // 10% purple, 80% transparent, 10% purple
+ ).createShader(bounds);
+ },
+ blendMode: BlendMode.dstOut,
+ child: SingleChildScrollView(
+ controller: _scrollController,
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children: _loadedFiles.map((file) =>
+ Padding(
+ padding: const EdgeInsets.only(left: 4.0, right: 4.0, top: 4.0),
+ child: TextButton.icon(
+ onPressed: widget.disabled ? null : () {
+ showDialog(context: context, builder: (context) =>
+ AlertDialog(
+ title: const Text("Remove attachment"),
+ content: Text(
+ "This will remove attachment '${basename(
+ file.$2.path)}', are you sure?"),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: const Text("No"),
+ ),
+ TextButton(
+ onPressed: () async {
+ Navigator.of(context).pop();
+ setState(() {
+ _loadedFiles.remove(file);
+ });
+ await widget.onChange(_loadedFiles);
+ },
+ child: const Text("Yes"),
+ )
+ ],
+ ),
+ );
+ },
+ style: TextButton.styleFrom(
+ foregroundColor: Theme
+ .of(context)
+ .colorScheme
+ .onBackground,
+ side: BorderSide(
+ color: Theme
+ .of(context)
+ .colorScheme
+ .primary,
+ width: 1
+ ),
+ ),
+ label: Text(basename(file.$2.path)),
+ icon: switch (file.$1) {
+ FileType.image => const Icon(Icons.image),
+ _ => const Icon(Icons.attach_file)
+ }
+ ),
+ ),
+ ).toList()
+ ),
+ ),
+ ),
+ ),
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 200),
+ switchInCurve: Curves.decelerate,
+ transitionBuilder: (child, animation) => FadeTransition(
+ opacity: animation,
+ child: SizeTransition(
+ sizeFactor: animation,
+ axis: Axis.horizontal,
+ //position: Tween(begin: const Offset(1, 0), end: const Offset(0, 0)).animate(animation),
+ child: child,
+ ),
+ ),
+ child: _popupIsOpen ? Row(
+ key: const ValueKey("popup-buttons"),
+ children: [
+ IconButton(
+ iconSize: 24,
+ style: IconButton.styleFrom(
+ backgroundColor: Theme
+ .of(context)
+ .colorScheme
+ .surface,
+ foregroundColor: Theme
+ .of(context)
+ .colorScheme
+ .onSurface,
+ side: BorderSide(
+ width: 1,
+ color: Theme
+ .of(context)
+ .colorScheme
+ .secondary,
+ )
+ ),
+ padding: EdgeInsets.zero,
+ onPressed: () async {
+ final result = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true);
+ if (result != null) {
+ setState(() {
+ _loadedFiles.addAll(
+ result.files.map((e) => e.path != null ? (FileType.image, File(e.path!)) : null)
+ .whereNotNull());
+ });
+ }
+
+ },
+ icon: const Icon(Icons.image,),
+ ),
+ IconButton(
+ iconSize: 24,
+ style: IconButton.styleFrom(
+ backgroundColor: Theme
+ .of(context)
+ .colorScheme
+ .surface,
+ foregroundColor: Theme
+ .of(context)
+ .colorScheme
+ .onSurface,
+ side: BorderSide(
+ width: 1,
+ color: Theme
+ .of(context)
+ .colorScheme
+ .secondary,
+ )
+ ),
+ padding: EdgeInsets.zero,
+ onPressed: () async {
+ final picture = await ImagePicker().pickImage(source: ImageSource.camera);
+ if (picture != null) {
+ 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,),
+ ),
+ IconButton(
+ iconSize: 24,
+ style: IconButton.styleFrom(
+ backgroundColor: Theme
+ .of(context)
+ .colorScheme
+ .surface,
+ foregroundColor: Theme
+ .of(context)
+ .colorScheme
+ .onSurface,
+ side: BorderSide(
+ width: 1,
+ color: Theme
+ .of(context)
+ .colorScheme
+ .secondary,
+ )
+ ),
+ padding: EdgeInsets.zero,
+ onPressed: () async {
+ final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: true);
+ if (result != null) {
+ setState(() {
+ _loadedFiles.addAll(
+ result.files.map((e) => e.path != null ? (FileType.any, File(e.path!)) : null)
+ .whereNotNull());
+ });
+ }
+ },
+ icon: const Icon(Icons.file_present_rounded,),
+ ),
+ ],
+ ) : const SizedBox.shrink(),
+ ),
+ Container(
+ color: Theme.of(context).colorScheme.surface,
+ child: IconButton(onPressed: () {
+ setState(() {
+ _popupIsOpen = !_popupIsOpen;
+ });
+ }, icon: AnimatedRotation(
+ duration: const Duration(milliseconds: 200),
+ turns: _popupIsOpen ? 3/8 : 0,
+ child: const Icon(Icons.add),
+ )),
+ )
+ ],
+ );
+ }
+}
+
+enum DocumentType {
+ gallery,
+ camera,
+ rawFile;
+}
+
+class PopupMenuIcon extends PopupMenuEntry {
+ const PopupMenuIcon({this.radius=24, this.value, required this.icon, this.onPressed, super.key});
+
+ final T? value;
+ final double radius;
+ final Widget icon;
+ final void Function()? onPressed;
+
+ @override
+ State createState() => _PopupMenuIconState();
+
+ @override
+ double get height => radius;
+
+ @override
+ bool represents(T? value) => this.value == value;
+
+}
+
+class _PopupMenuIconState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(128),
+ child: Container(
+ color: Theme.of(context).colorScheme.surface,
+ padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2),
+ margin: const EdgeInsets.all(1),
+ child: InkWell(
+ child: widget.icon,
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/widgets/messages/message_audio_player.dart b/lib/widgets/messages/message_audio_player.dart
index 5a864a9..34c99b4 100644
--- a/lib/widgets/messages/message_audio_player.dart
+++ b/lib/widgets/messages/message_audio_player.dart
@@ -2,11 +2,12 @@ import 'dart:convert';
import 'dart:io' show Platform;
import 'package:contacts_plus_plus/auxiliary.dart';
+import 'package:contacts_plus_plus/clients/audio_cache_client.dart';
import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart';
-import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
+import 'package:provider/provider.dart';
class MessageAudioPlayer extends StatefulWidget {
const MessageAudioPlayer({required this.message, this.foregroundColor, super.key});
@@ -18,44 +19,76 @@ class MessageAudioPlayer extends StatefulWidget {
State createState() => _MessageAudioPlayerState();
}
-class _MessageAudioPlayerState extends State {
+class _MessageAudioPlayerState extends State with WidgetsBindingObserver {
final AudioPlayer _audioPlayer = AudioPlayer();
+ Future? _audioFileFuture;
double _sliderValue = 0;
@override
void initState() {
super.initState();
- if (Platform.isAndroid) {
- _audioPlayer.setUrl(
- Aux.neosDbToHttp(AudioClipContent
- .fromMap(jsonDecode(widget.message.content))
- .assetUri),
- preload: true).whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off));
+ WidgetsBinding.instance.addObserver(this);
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (state == AppLifecycleState.paused) {
+ _audioPlayer.stop();
}
}
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ final audioCache = Provider.of(context);
+ _audioFileFuture = audioCache
+ .cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content)))
+ .then((value) => _audioPlayer.setFilePath(value.path))
+ .whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off));
+ }
+
+ @override
+ void didUpdateWidget(covariant MessageAudioPlayer oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.message.id == widget.message.id) return;
+ final audioCache = Provider.of(context);
+ _audioFileFuture = audioCache
+ .cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content)))
+ .then((value) async {
+ final path = _audioPlayer.setFilePath(value.path);
+ await _audioPlayer.setLoopMode(LoopMode.off);
+ await _audioPlayer.pause();
+ await _audioPlayer.seek(Duration.zero);
+ return path;
+ });
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ _audioPlayer.dispose().onError((error, stackTrace) {});
+ super.dispose();
+ }
+
Widget _createErrorWidget(String error) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
- Icon(Icons.error_outline, color: Theme
- .of(context)
- .colorScheme
- .error,),
- const SizedBox(height: 4,),
- Text(error, textAlign: TextAlign.center,
+ Icon(
+ Icons.error_outline,
+ color: Theme.of(context).colorScheme.error,
+ ),
+ const SizedBox(
+ height: 4,
+ ),
+ Text(
+ error,
+ textAlign: TextAlign.center,
softWrap: true,
maxLines: 3,
- style: Theme
- .of(context)
- .textTheme
- .bodySmall
- ?.copyWith(color: Theme
- .of(context)
- .colorScheme
- .error),
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.error),
),
],
),
@@ -67,116 +100,137 @@ class _MessageAudioPlayerState extends State {
if (!Platform.isAndroid) {
return _createErrorWidget("Sorry, audio-messages are not\n supported on this platform.");
}
+
return IntrinsicWidth(
child: StreamBuilder(
- stream: _audioPlayer.playerStateStream,
- builder: (context, snapshot) {
- if (snapshot.hasData) {
- final playerState = snapshot.data as PlayerState;
- return Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- mainAxisAlignment: MainAxisAlignment.center,
+ stream: _audioPlayer.playerStateStream,
+ builder: (context, snapshot) {
+ if (snapshot.hasError) {
+ FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
+ return _createErrorWidget("Failed to load audio-message.");
+ }
+ final playerState = snapshot.data;
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Row(
- mainAxisSize: MainAxisSize.max,
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- IconButton(
- onPressed: () {
- switch (playerState.processingState) {
- case ProcessingState.idle:
- case ProcessingState.loading:
- case ProcessingState.buffering:
- break;
- case ProcessingState.ready:
- if (playerState.playing) {
- _audioPlayer.pause();
- } else {
- _audioPlayer.play();
- }
- break;
- case ProcessingState.completed:
- _audioPlayer.seek(Duration.zero);
- _audioPlayer.play();
- break;
- }
- },
- color: widget.foregroundColor,
- icon: SizedBox(
- width: 24,
- height: 24,
- child: playerState.processingState == ProcessingState.loading
- ? const Center(child: CircularProgressIndicator(),)
- : Icon(((_audioPlayer.duration ?? Duration.zero) - _audioPlayer.position).inMilliseconds <
- 10 ? Icons.replay
- : (playerState.playing ? Icons.pause : Icons.play_arrow)),
- ),
- ),
- StreamBuilder(
- stream: _audioPlayer.positionStream,
- builder: (context, snapshot) {
- _sliderValue = (_audioPlayer.position.inMilliseconds /
- (_audioPlayer.duration?.inMilliseconds ?? 0)).clamp(0, 1);
- return StatefulBuilder( // Not sure if this makes sense here...
- builder: (context, setState) {
- return SliderTheme(
- data: SliderThemeData(
- inactiveTrackColor: widget.foregroundColor?.withAlpha(100),
- ),
- child: Slider(
- thumbColor: widget.foregroundColor,
- value: _sliderValue,
- min: 0.0,
- max: 1.0,
- onChanged: (value) async {
- _audioPlayer.pause();
- setState(() {
- _sliderValue = value;
- });
- _audioPlayer.seek(Duration(
- milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(),
- ));
- },
- ),
- );
+ FutureBuilder(
+ future: _audioFileFuture,
+ builder: (context, fileSnapshot) {
+ if (fileSnapshot.hasError) {
+ return const IconButton(
+ icon: Icon(Icons.warning),
+ onPressed: null,
+ );
+ }
+ return IconButton(
+ onPressed: fileSnapshot.hasData &&
+ snapshot.hasData &&
+ playerState != null &&
+ playerState.processingState != ProcessingState.loading
+ ? () {
+ switch (playerState.processingState) {
+ case ProcessingState.idle:
+ case ProcessingState.loading:
+ case ProcessingState.buffering:
+ break;
+ case ProcessingState.ready:
+ if (playerState.playing) {
+ _audioPlayer.pause();
+ } else {
+ _audioPlayer.play();
+ }
+ break;
+ case ProcessingState.completed:
+ _audioPlayer.seek(Duration.zero);
+ _audioPlayer.play();
+ break;
}
- );
- }
- )
- ],
+ }
+ : null,
+ color: widget.foregroundColor,
+ icon: Icon(
+ ((_audioPlayer.duration ?? const Duration(days: 9999)) - _audioPlayer.position)
+ .inMilliseconds <
+ 10
+ ? Icons.replay
+ : ((playerState?.playing ?? false) ? Icons.pause : Icons.play_arrow),
+ ),
+ );
+ },
),
- Row(
- mainAxisSize: MainAxisSize.max,
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- const SizedBox(width: 4,),
- StreamBuilder(
- stream: _audioPlayer.positionStream,
- builder: (context, snapshot) {
- return Text("${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ??
- "??"}",
- style: Theme
- .of(context)
- .textTheme
- .bodySmall
- ?.copyWith(color: widget.foregroundColor?.withAlpha(150)),
- );
- }
- ),
- const Spacer(),
- MessageStateIndicator(message: widget.message, foregroundColor: widget.foregroundColor,),
- ],
+ StreamBuilder(
+ stream: _audioPlayer.positionStream,
+ builder: (context, snapshot) {
+ _sliderValue = _audioPlayer.duration == null
+ ? 0
+ : (_audioPlayer.position.inMilliseconds / (_audioPlayer.duration!.inMilliseconds))
+ .clamp(0, 1);
+ return StatefulBuilder(
+ // Not sure if this makes sense here...
+ builder: (context, setState) {
+ return SliderTheme(
+ data: SliderThemeData(
+ inactiveTrackColor: widget.foregroundColor?.withAlpha(100),
+ ),
+ child: Slider(
+ thumbColor: widget.foregroundColor,
+ value: _sliderValue,
+ min: 0.0,
+ max: 1.0,
+ onChanged: (value) async {
+ _audioPlayer.pause();
+ setState(() {
+ _sliderValue = value;
+ });
+ _audioPlayer.seek(
+ Duration(
+ milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(),
+ ),
+ );
+ },
+ ),
+ );
+ },
+ );
+ },
)
],
- );
- } else if (snapshot.hasError) {
- FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
- return _createErrorWidget("Failed to load audio-message.");
- } else {
- return const Center(child: CircularProgressIndicator(),);
- }
- }
+ ),
+ Row(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ const SizedBox(
+ width: 4,
+ ),
+ StreamBuilder(
+ stream: _audioPlayer.positionStream,
+ builder: (context, snapshot) {
+ return Text(
+ "${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ?? "??"}",
+ style: Theme.of(context)
+ .textTheme
+ .bodySmall
+ ?.copyWith(color: widget.foregroundColor?.withAlpha(150)),
+ );
+ },
+ ),
+ const Spacer(),
+ MessageStateIndicator(
+ message: widget.message,
+ foregroundColor: widget.foregroundColor,
+ ),
+ ],
+ ),
+ ],
+ );
+ },
),
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/widgets/messages/message_bubble.dart b/lib/widgets/messages/message_bubble.dart
index e1f9ea9..8f07a6e 100644
--- a/lib/widgets/messages/message_bubble.dart
+++ b/lib/widgets/messages/message_bubble.dart
@@ -29,8 +29,7 @@ class MessageBubble extends StatelessWidget {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: switch (message.type) {
- MessageType.sessionInvite =>
- MessageSessionInvite(message: message, foregroundColor: foregroundColor,),
+ MessageType.sessionInvite => MessageSessionInvite(message: message, foregroundColor: foregroundColor,),
MessageType.object => MessageAsset(message: message, foregroundColor: foregroundColor,),
MessageType.sound => MessageAudioPlayer(message: message, foregroundColor: foregroundColor,),
MessageType.unknown || MessageType.text => MessageText(message: message, foregroundColor: foregroundColor,)
diff --git a/lib/widgets/messages/message_input_bar.dart b/lib/widgets/messages/message_input_bar.dart
new file mode 100644
index 0000000..6fffaa5
--- /dev/null
+++ b/lib/widgets/messages/message_input_bar.dart
@@ -0,0 +1,570 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:collection/collection.dart';
+import 'package:contacts_plus_plus/apis/record_api.dart';
+import 'package:contacts_plus_plus/auxiliary.dart';
+import 'package:contacts_plus_plus/client_holder.dart';
+import 'package:contacts_plus_plus/clients/api_client.dart';
+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: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:permission_handler/permission_handler.dart';
+import 'package:provider/provider.dart';
+import 'package:record/record.dart';
+import 'package:uuid/uuid.dart';
+
+
+class MessageInputBar extends StatefulWidget {
+ const MessageInputBar({this.disabled=false, required this.recipient, this.onMessageSent, super.key});
+
+ final bool disabled;
+ final Friend recipient;
+ final Function()? onMessageSent;
+
+ @override
+ State createState() => _MessageInputBarState();
+}
+
+class _MessageInputBarState extends State {
+ final TextEditingController _messageTextController = TextEditingController();
+ final List<(FileType, File)> _loadedFiles = [];
+ final Record _recorder = Record();
+ final ImagePicker _imagePicker = ImagePicker();
+
+ DateTime? _recordingStartTime;
+
+ bool _isSending = false;
+ bool _attachmentPickerOpen = false;
+ String _currentText = "";
+ double? _sendProgress;
+ bool get _isRecording => _recordingStartTime != null;
+ set _isRecording(value) => _recordingStartTime = value ? DateTime.now() : null;
+ bool _recordingCancelled = false;
+
+ @override
+ void dispose() {
+ _recorder.dispose();
+ _messageTextController.dispose();
+ super.dispose();
+ }
+
+ Future sendTextMessage(ApiClient client, MessagingClient mClient, String content) async {
+ if (content.isEmpty) return;
+ final message = Message(
+ id: Message.generateId(),
+ recipientId: widget.recipient.id,
+ senderId: client.userId,
+ type: MessageType.text,
+ content: content,
+ sendTime: DateTime.now().toUtc(),
+ state: MessageState.local,
+ );
+ mClient.sendMessage(message);
+ }
+
+ Future sendImageMessage(ApiClient client, MessagingClient mClient, File file, String machineId,
+ void Function(double progress) progressCallback) async {
+ final record = await RecordApi.uploadImage(
+ client,
+ image: file,
+ machineId: machineId,
+ progressCallback: progressCallback,
+ );
+ final message = Message(
+ id: record.extractMessageId() ?? Message.generateId(),
+ recipientId: widget.recipient.id,
+ senderId: client.userId,
+ type: MessageType.object,
+ content: jsonEncode(record.toMap()),
+ sendTime: DateTime.now().toUtc(),
+ state: MessageState.local
+ );
+ mClient.sendMessage(message);
+ }
+
+ Future sendVoiceMessage(ApiClient client, MessagingClient mClient, File file, String machineId,
+ void Function(double progress) progressCallback) async {
+ final record = await RecordApi.uploadVoiceClip(
+ client,
+ voiceClip: file,
+ machineId: machineId,
+ progressCallback: progressCallback,
+ );
+ final message = Message(
+ id: record.extractMessageId() ?? Message.generateId(),
+ recipientId: widget.recipient.id,
+ senderId: client.userId,
+ type: MessageType.sound,
+ content: jsonEncode(record.toMap()),
+ sendTime: DateTime.now().toUtc(),
+ state: MessageState.local,
+ );
+ mClient.sendMessage(message);
+ }
+
+ Future sendRawFileMessage(ApiClient client, MessagingClient mClient, File file, String machineId,
+ void Function(double progress) progressCallback) async {
+ final record = await RecordApi.uploadRawFile(
+ client,
+ file: file,
+ machineId: machineId,
+ progressCallback: progressCallback,
+ );
+ final message = Message(
+ id: record.extractMessageId() ?? Message.generateId(),
+ recipientId: widget.recipient.id,
+ senderId: client.userId,
+ type: MessageType.object,
+ content: jsonEncode(record.toMap()),
+ sendTime: DateTime.now().toUtc(),
+ state: MessageState.local,
+ );
+ mClient.sendMessage(message);
+ }
+
+ void _pointerMoveEventHandler(PointerMoveEvent event) {
+ if (!_isRecording) return;
+ final width = MediaQuery.of(context).size.width;
+
+ if (event.localPosition.dx < width - width/4) {
+ if (!_recordingCancelled) {
+ HapticFeedback.vibrate();
+ setState(() {
+ _recordingCancelled = true;
+ });
+ }
+ } else {
+ if (_recordingCancelled) {
+ HapticFeedback.vibrate();
+ setState(() {
+ _recordingCancelled = false;
+ });
+ }
+ }
+ }
+
+ Stream _recordingDurationStream() async* {
+ while (_isRecording) {
+ yield DateTime.now().difference(_recordingStartTime!);
+ await Future.delayed(const Duration(milliseconds: 100));
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final mClient = Provider.of(context, listen: false);
+ return Listener(
+ onPointerMove: _pointerMoveEventHandler,
+ onPointerUp: (_) async {
+ // Do this here as the pointerUp event of the gesture detector on the mic button can be unreliable
+ final cHolder = ClientHolder.of(context);
+ if (_isRecording) {
+ if (_recordingCancelled) {
+ setState(() {
+ _isRecording = false;
+ });
+ final recording = await _recorder.stop();
+ if (recording == null) return;
+ final file = File(recording);
+ if (await file.exists()) {
+ await file.delete();
+ }
+ }
+ setState(() {
+ _recordingCancelled = false;
+ _isRecording = false;
+ });
+
+ if (await _recorder.isRecording()) {
+ final recording = await _recorder.stop();
+ if (recording == null) return;
+
+ final file = File(recording);
+ setState(() {
+ _isSending = true;
+ _sendProgress = 0;
+ });
+ final apiClient = cHolder.apiClient;
+ await sendVoiceMessage(
+ apiClient,
+ mClient,
+ file,
+ cHolder.settingsClient.currentSettings.machineId.valueOrDefault,
+ (progress) {
+ setState(() {
+ _sendProgress = progress;
+ });
+ }
+ );
+ setState(() {
+ _isSending = false;
+ _sendProgress = null;
+ });
+ }
+ }
+ },
+ child: Container(
+ decoration: BoxDecoration(
+ border: const Border(top: BorderSide(width: 1, color: Colors.black)),
+ color: Theme
+ .of(context)
+ .colorScheme
+ .background,
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 4),
+ child: Column(
+ children: [
+ if (_isSending && _sendProgress != null)
+ LinearProgressIndicator(value: _sendProgress),
+ Container(
+ decoration: BoxDecoration(
+ color: Theme
+ .of(context)
+ .colorScheme
+ .background,
+ ),
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 200),
+ switchInCurve: Curves.easeOut,
+ switchOutCurve: Curves.easeOut,
+ transitionBuilder: (Widget child, animation) =>
+ SizeTransition(sizeFactor: animation, child: child,),
+ child: switch ((_attachmentPickerOpen, _loadedFiles)) {
+ (true, []) =>
+ Row(
+ key: const ValueKey("attachment-picker"),
+ children: [
+ TextButton.icon(
+ onPressed: _isSending ? null : () async {
+ final result = await FilePicker.platform.pickFiles(
+ type: FileType.image, allowMultiple: true);
+ if (result != null) {
+ setState(() {
+ _loadedFiles.addAll(
+ result.files.map((e) =>
+ e.path != null ? (FileType.image, File(e.path!)) : null)
+ .whereNotNull());
+ });
+ }
+ },
+ icon: const Icon(Icons.image),
+ label: const Text("Gallery"),
+ ),
+ TextButton.icon(
+ onPressed: _isSending ? null : () async {
+ 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"),
+ ),
+ TextButton.icon(
+ onPressed: _isSending ? null : () async {
+ final result = await FilePicker.platform.pickFiles(
+ type: FileType.any, allowMultiple: true);
+ if (result != null) {
+ setState(() {
+ _loadedFiles.addAll(
+ result.files.map((e) =>
+ e.path != null ? (FileType.any, File(e.path!)) : null)
+ .whereNotNull());
+ });
+ }
+ },
+ icon: const Icon(Icons.file_present_rounded),
+ label: const Text("Document"),
+ ),
+ ],
+ ),
+ (false, []) => null,
+ (_, _) =>
+ MessageAttachmentList(
+ disabled: _isSending,
+ initialFiles: _loadedFiles,
+ onChange: (List<(FileType, File)> loadedFiles) => setState(() {
+ _loadedFiles.clear();
+ _loadedFiles.addAll(loadedFiles);
+ }),
+ ),
+ },
+ ),
+ ),
+ Row(
+ children: [
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 200),
+ transitionBuilder: (Widget child, Animation animation) =>
+ FadeTransition(
+ opacity: animation,
+ child: RotationTransition(
+ turns: Tween(begin: 0.6, end: 1).animate(animation),
+ child: child,
+ ),
+ ),
+ child: switch((_attachmentPickerOpen, _isRecording)) {
+ (_, true) => IconButton(
+ onPressed: () {
+
+ },
+ icon: Icon(Icons.delete, color: _recordingCancelled ? Theme.of(context).colorScheme.error : null,),
+ ),
+ (false, _) => IconButton(
+ key: const ValueKey("add-attachment-icon"),
+ onPressed: _isSending ? null : () {
+ setState(() {
+ _attachmentPickerOpen = true;
+ });
+ },
+ icon: const Icon(Icons.attach_file,),
+ ),
+ (true, _) => IconButton(
+ key: const ValueKey("remove-attachment-icon"),
+ onPressed: _isSending ? null : () async {
+ if (_loadedFiles.isNotEmpty) {
+ await showDialog(context: context, builder: (context) =>
+ AlertDialog(
+ title: const Text("Remove all attachments"),
+ content: const Text("This will remove all attachments, are you sure?"),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: const Text("No"),
+ ),
+ TextButton(
+ onPressed: () {
+ setState(() {
+ _loadedFiles.clear();
+ _attachmentPickerOpen = false;
+ });
+ Navigator.of(context).pop();
+ },
+ child: const Text("Yes"),
+ )
+ ],
+ ));
+ } else {
+ setState(() {
+ _attachmentPickerOpen = false;
+ });
+ }
+ },
+ icon: const Icon(Icons.close,),
+ ),
+ },
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
+ child: Stack(
+ children: [
+ TextField(
+ enabled: (!widget.disabled) && !_isSending,
+ autocorrect: true,
+ controller: _messageTextController,
+ showCursor: !_isRecording,
+ maxLines: 4,
+ minLines: 1,
+ onChanged: (text) {
+ if (text.isEmpty != _currentText.isEmpty) {
+ setState(() {
+ _currentText = text;
+ });
+ return;
+ }
+ _currentText = text;
+ },
+ style: Theme.of(context).textTheme.bodyLarge,
+ decoration: InputDecoration(
+ isDense: true,
+ hintText: _isRecording ? "" : "Message ${widget.recipient
+ .username}...",
+ hintMaxLines: 1,
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ fillColor: Colors.black26,
+ filled: true,
+ border: OutlineInputBorder(
+ borderSide: BorderSide.none,
+ borderRadius: BorderRadius.circular(24),
+ )
+ ),
+ ),
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 200),
+ transitionBuilder: (Widget child, Animation animation) =>
+ FadeTransition(
+ opacity: animation,
+ child: SlideTransition(
+ position: Tween(
+ begin: const Offset(0, .2),
+ end: const Offset(0, 0),
+ ).animate(animation),
+ child: child,
+ ),
+ ),
+ child: _isRecording ? Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12.0),
+ child: _recordingCancelled ? Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ const SizedBox(width: 8,),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 8.0),
+ child: Icon(Icons.cancel, color: Colors.red, size: 16,),
+ ),
+ Text("Cancel Recording", style: Theme.of(context).textTheme.titleMedium),
+ ],
+ ) : Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ const SizedBox(width: 8,),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 8.0),
+ child: Icon(Icons.circle, color: Colors.red, size: 16,),
+ ),
+ StreamBuilder(
+ stream: _recordingDurationStream(),
+ builder: (context, snapshot) {
+ return Text("Recording: ${snapshot.data?.format()}", style: Theme.of(context).textTheme.titleMedium);
+ }
+ ),
+ ],
+ ),
+ ) : const SizedBox.shrink(),
+ ),
+ ],
+ ),
+ ),
+ ),
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 200),
+ transitionBuilder: (Widget child, Animation animation) =>
+ FadeTransition(opacity: animation, child: RotationTransition(
+ turns: Tween(begin: 0.5, end: 1).animate(animation), child: child,),),
+ child: _currentText.isNotEmpty || _loadedFiles.isNotEmpty ? IconButton(
+ key: const ValueKey("send-button"),
+ splashRadius: 24,
+ padding: EdgeInsets.zero,
+ onPressed: _isSending ? null : () async {
+ final cHolder = ClientHolder.of(context);
+ final sMsgnr = ScaffoldMessenger.of(context);
+ final settings = cHolder.settingsClient.currentSettings;
+ final toSend = List<(FileType, File)>.from(_loadedFiles);
+ setState(() {
+ _isSending = true;
+ _sendProgress = 0;
+ _attachmentPickerOpen = false;
+ _loadedFiles.clear();
+ });
+ try {
+ for (int i = 0; i < toSend.length; i++) {
+ final totalProgress = i / toSend.length;
+ final file = toSend[i];
+ if (file.$1 == FileType.image) {
+ await sendImageMessage(
+ cHolder.apiClient, mClient, file.$2, settings.machineId.valueOrDefault,
+ (progress) =>
+ setState(() {
+ _sendProgress = totalProgress + progress * 1 / toSend.length;
+ }),
+ );
+ } else {
+ await sendRawFileMessage(
+ cHolder.apiClient, mClient, file.$2, settings.machineId.valueOrDefault, (progress) =>
+ setState(() =>
+ _sendProgress = totalProgress + progress * 1 / toSend.length));
+ }
+ }
+ setState(() {
+ _sendProgress = null;
+ });
+
+ if (_currentText.isNotEmpty) {
+ await sendTextMessage(cHolder.apiClient, mClient, _messageTextController.text);
+ }
+ _messageTextController.clear();
+ _currentText = "";
+ _loadedFiles.clear();
+ _attachmentPickerOpen = false;
+ } catch (e, s) {
+ FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
+ sMsgnr.showSnackBar(SnackBar(content: Text("Failed to send a message: $e")));
+ }
+ setState(() {
+ _isSending = false;
+ _sendProgress = null;
+ });
+ widget.onMessageSent?.call();
+ },
+ icon: const Icon(Icons.send),
+ ) : GestureDetector(
+ onTapUp: (_) {
+ _recordingCancelled = true;
+ },
+ onTapDown: widget.disabled ? null : (_) async {
+ HapticFeedback.vibrate();
+ final hadToAsk = await Permission.microphone.isDenied;
+ final hasPermission = !await _recorder.hasPermission();
+ if (hasPermission) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+ content: Text("No permission to record audio."),
+ ));
+ }
+ return;
+ }
+ if (hadToAsk) {
+ // We had to ask for permissions so the user removed their finger from the record button.
+ return;
+ }
+
+ final dir = await getTemporaryDirectory();
+ await _recorder.start(
+ path: "${dir.path}/A-${const Uuid().v4()}.wav",
+ encoder: AudioEncoder.wav,
+ samplingRate: 44100
+ );
+ setState(() {
+ _isRecording = true;
+ });
+ },
+ child: IconButton(
+ icon: const Icon(Icons.mic_outlined),
+ onPressed: _isSending ? null : () {
+ // Empty onPressed for that sweet sweet ripple effect
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart
index 69c5c4c..de04404 100644
--- a/lib/widgets/messages/messages_list.dart
+++ b/lib/widgets/messages/messages_list.dart
@@ -1,9 +1,9 @@
-import 'package:contacts_plus_plus/client_holder.dart';
+import 'package:contacts_plus_plus/clients/audio_cache_client.dart';
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/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart';
+import 'package:contacts_plus_plus/widgets/messages/message_input_bar.dart';
import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -20,20 +20,15 @@ class MessagesList extends StatefulWidget {
}
class _MessagesListState extends State with SingleTickerProviderStateMixin {
- final TextEditingController _messageTextController = TextEditingController();
final ScrollController _sessionListScrollController = ScrollController();
- final ScrollController _messageScrollController = ScrollController();
- bool _isSendable = false;
bool _showSessionListScrollChevron = false;
- bool _showBottomBarShadow = false;
+ bool _sessionListOpen = false;
double get _shevronOpacity => _showSessionListScrollChevron ? 1.0 : 0.0;
-
@override
void dispose() {
- _messageTextController.dispose();
_sessionListScrollController.dispose();
super.dispose();
}
@@ -47,262 +42,204 @@ class _MessagesListState extends State with SingleTickerProviderSt
_showSessionListScrollChevron = true;
});
}
- if (_sessionListScrollController.position.atEdge && _sessionListScrollController.position.pixels > 0
- && _showSessionListScrollChevron) {
+ if (_sessionListScrollController.position.atEdge &&
+ _sessionListScrollController.position.pixels > 0 &&
+ _showSessionListScrollChevron) {
setState(() {
_showSessionListScrollChevron = false;
});
}
});
- _messageScrollController.addListener(() {
- if (!_messageScrollController.hasClients) return;
- if (_messageScrollController.position.atEdge && _messageScrollController.position.pixels == 0 &&
- _showBottomBarShadow) {
- setState(() {
- _showBottomBarShadow = false;
- });
- } else if (!_showBottomBarShadow) {
- setState(() {
- _showBottomBarShadow = true;
- });
- }
- });
}
@override
Widget build(BuildContext context) {
- final apiClient = ClientHolder
- .of(context)
- .apiClient;
- var sessions = widget.friend.userStatus.activeSessions;
- final appBarColor = Theme
- .of(context)
- .colorScheme
- .surfaceVariant;
- return Consumer(
- builder: (context, mClient, _) {
- final cache = mClient.getUserMessageCache(widget.friend.id);
- return Scaffold(
- appBar: AppBar(
- title: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- FriendOnlineStatusIndicator(userStatus: widget.friend.userStatus),
- const SizedBox(width: 8,),
- Text(widget.friend.username),
- if (widget.friend.isHeadless) Padding(
- padding: const EdgeInsets.only(left: 12),
- child: Icon(Icons.dns, size: 18, color: Theme
- .of(context)
- .colorScheme
- .onSecondaryContainer
- .withAlpha(150),),
- ),
- ],
- ),
- scrolledUnderElevation: 0.0,
- backgroundColor: appBarColor,
- ),
- body: Column(
+ final sessions = widget.friend.userStatus.activeSessions;
+ final appBarColor = Theme.of(context).colorScheme.surfaceVariant;
+ return Consumer(builder: (context, mClient, _) {
+ final cache = mClient.getUserMessageCache(widget.friend.id);
+ return Scaffold(
+ appBar: AppBar(
+ title: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- if (sessions.isNotEmpty) Container(
- constraints: const BoxConstraints(maxHeight: 64),
- decoration: BoxDecoration(
- color: appBarColor,
- border: const Border(top: BorderSide(width: 1, color: Colors.black26),)
+ FriendOnlineStatusIndicator(userStatus: widget.friend.userStatus),
+ const SizedBox(
+ width: 8,
+ ),
+ Text(widget.friend.username),
+ if (widget.friend.isHeadless)
+ Padding(
+ padding: const EdgeInsets.only(left: 12),
+ child: Icon(
+ Icons.dns,
+ size: 18,
+ color: Theme.of(context).colorScheme.onSecondaryContainer.withAlpha(150),
+ ),
),
- child: Stack(
- children: [
- ListView.builder(
- controller: _sessionListScrollController,
- scrollDirection: Axis.horizontal,
- itemCount: sessions.length,
- itemBuilder: (context, index) => SessionTile(session: sessions[index]),
- ),
- AnimatedOpacity(
- opacity: _shevronOpacity,
- curve: Curves.easeOut,
- duration: const Duration(milliseconds: 200),
- child: Align(
- alignment: Alignment.centerRight,
- child: Container(
- padding: const EdgeInsets.only(left: 16, right: 4, top: 1, bottom: 1),
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.centerLeft,
- end: Alignment.centerRight,
- colors: [
- appBarColor.withOpacity(0),
- appBarColor,
- appBarColor,
- ],
- ),
- ),
- height: double.infinity,
- child: const Icon(Icons.chevron_right),
- ),
- ),
- )
- ],
+ ],
+ ),
+ bottom: sessions.isNotEmpty && _sessionListOpen
+ ? null
+ : PreferredSize(
+ preferredSize: const Size.fromHeight(1),
+ child: Container(
+ height: 1,
+ color: Colors.black,
+ ),
+ ),
+ actions: [
+ if (sessions.isNotEmpty)
+ AnimatedRotation(
+ turns: _sessionListOpen ? -1 / 4 : 1 / 4,
+ duration: const Duration(milliseconds: 200),
+ child: IconButton(
+ onPressed: () {
+ setState(() {
+ _sessionListOpen = !_sessionListOpen;
+ });
+ },
+ icon: const Icon(Icons.chevron_right),
),
),
- Expanded(
- child: Builder(
- builder: (context) {
- if (cache == null) {
- return const Column(
- mainAxisAlignment: MainAxisAlignment.start,
+ const SizedBox(
+ width: 4,
+ )
+ ],
+ scrolledUnderElevation: 0.0,
+ backgroundColor: appBarColor,
+ surfaceTintColor: Colors.transparent,
+ shadowColor: Colors.transparent,
+ ),
+ body: Column(
+ children: [
+ if (sessions.isNotEmpty)
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 200),
+ transitionBuilder: (child, animation) =>
+ SizeTransition(sizeFactor: animation, axis: Axis.vertical, child: child),
+ child: sessions.isEmpty || !_sessionListOpen
+ ? null
+ : Container(
+ constraints: const BoxConstraints(maxHeight: 64),
+ decoration: BoxDecoration(
+ color: appBarColor,
+ border: const Border(
+ bottom: BorderSide(width: 1, color: Colors.black),
+ )),
+ child: Stack(
children: [
- LinearProgressIndicator()
- ],
- );
- }
- if (cache.error != null) {
- return DefaultErrorWidget(
- message: cache.error.toString(),
- onRetry: () {
- setState(() {
- mClient.deleteUserMessageCache(widget.friend.id);
- });
- mClient.loadUserMessageCache(widget.friend.id);
- },
- );
- }
- if (cache.messages.isEmpty) {
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Icon(Icons.message_outlined),
- Padding(
- padding: const EdgeInsets.symmetric(vertical: 24),
- child: Text(
- "There are no messages here\nWhy not say hello?",
- textAlign: TextAlign.center,
- style: Theme
- .of(context)
- .textTheme
- .titleMedium,
+ ListView.builder(
+ controller: _sessionListScrollController,
+ scrollDirection: Axis.horizontal,
+ itemCount: sessions.length,
+ itemBuilder: (context, index) => SessionTile(session: sessions[index]),
+ ),
+ AnimatedOpacity(
+ opacity: _shevronOpacity,
+ curve: Curves.easeOut,
+ duration: const Duration(milliseconds: 200),
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Container(
+ padding: const EdgeInsets.only(left: 16, right: 4, top: 1, bottom: 1),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ colors: [
+ appBarColor.withOpacity(0),
+ appBarColor,
+ appBarColor,
+ ],
+ ),
+ ),
+ height: double.infinity,
+ child: const Icon(Icons.chevron_right),
+ ),
),
)
],
),
- );
- }
- return ListView.builder(
- controller: _messageScrollController,
- reverse: true,
- itemCount: cache.messages.length,
- itemBuilder: (context, index) {
- final entry = cache.messages[index];
- if (index == cache.messages.length - 1) {
- return Padding(
- padding: const EdgeInsets.only(top: 12),
- child: MessageBubble(message: entry,),
- );
- }
- return MessageBubble(message: entry,);
- },
- );
- },
- ),
+ ),
),
- AnimatedContainer(
- decoration: BoxDecoration(
- boxShadow: [
- BoxShadow(
- blurRadius: _showBottomBarShadow ? 8 : 0,
- color: Theme.of(context).shadowColor,
- offset: const Offset(0, 4),
- ),
- ],
- color: Theme.of(context).colorScheme.background,
- ),
- padding: const EdgeInsets.symmetric(horizontal: 4),
- duration: const Duration(milliseconds: 250),
- child: Row(
- children: [
- Expanded(
- child: Padding(
- padding: const EdgeInsets.all(8),
- child: TextField(
- enabled: cache != null && cache.error == null,
- autocorrect: true,
- controller: _messageTextController,
- maxLines: 4,
- minLines: 1,
- onChanged: (text) {
- if (text.isNotEmpty && !_isSendable) {
- setState(() {
- _isSendable = true;
- });
- } else if (text.isEmpty && _isSendable) {
- setState(() {
- _isSendable = false;
- });
- }
+ Expanded(
+ child: Stack(
+ children: [
+ Builder(
+ builder: (context) {
+ if (cache == null) {
+ return const Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [LinearProgressIndicator()],
+ );
+ }
+ if (cache.error != null) {
+ return DefaultErrorWidget(
+ message: cache.error.toString(),
+ onRetry: () {
+ setState(() {
+ mClient.deleteUserMessageCache(widget.friend.id);
+ });
+ mClient.loadUserMessageCache(widget.friend.id);
},
- decoration: InputDecoration(
- isDense: true,
- hintText: "Message ${widget.friend
- .username}...",
- hintMaxLines: 1,
- contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(24)
+ );
+ }
+ if (cache.messages.isEmpty) {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.message_outlined),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 24),
+ child: Text(
+ "There are no messages here\nWhy not say hello?",
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
)
+ ],
),
- ),
- ),
- ),
- Padding(
- padding: const EdgeInsets.only(left: 8, right: 4.0),
- child: Consumer(
- builder: (context, mClient, _) {
- return IconButton(
- splashRadius: 24,
- onPressed: _isSendable ? () async {
- setState(() {
- _isSendable = false;
- });
- final message = Message(
- id: Message.generateId(),
- recipientId: widget.friend.id,
- senderId: apiClient.userId,
- type: MessageType.text,
- content: _messageTextController.text,
- sendTime: DateTime.now().toUtc(),
+ );
+ }
+ return Provider(
+ create: (BuildContext context) => AudioCacheClient(),
+ child: ListView.builder(
+ reverse: true,
+ physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
+ itemCount: cache.messages.length,
+ itemBuilder: (context, index) {
+ final entry = cache.messages[index];
+ if (index == cache.messages.length - 1) {
+ return Padding(
+ padding: const EdgeInsets.only(top: 12),
+ child: MessageBubble(
+ message: entry,
+ ),
);
- try {
- mClient.sendMessage(message);
- _messageTextController.clear();
- setState(() {});
- } catch (e) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text("Failed to send message\n$e",
- maxLines: null,
- ),
- ),
- );
- setState(() {
- _isSendable = true;
- });
- }
- } : null,
- iconSize: 28,
- icon: const Icon(Icons.send),
- );
- },
- ),
- )
- ],
- ),
+ }
+ return MessageBubble(
+ message: entry,
+ );
+ },
+ ),
+ );
+ },
+ ),
+ ],
),
- ],
- ),
- );
- }
- );
+ ),
+ MessageInputBar(
+ recipient: widget.friend,
+ disabled: cache == null || cache.error != null,
+ onMessageSent: () {
+ setState(() {});
+ },
+ ),
+ ],
+ ),
+ );
+ });
}
}
diff --git a/lib/widgets/my_profile_dialog.dart b/lib/widgets/my_profile_dialog.dart
index 8674eec..0929376 100644
--- a/lib/widgets/my_profile_dialog.dart
+++ b/lib/widgets/my_profile_dialog.dart
@@ -56,7 +56,7 @@ class _MyProfileDialogState extends State {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(profile.username, style: tt.titleLarge),
- Text(profile.email, style: tt.labelMedium?.copyWith(color: Colors.white54),)
+ Text(profile.email, style: tt.labelMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withAlpha(150)),)
],
),
GenericAvatar(imageUri: Aux.neosDbToHttp(profile.userProfile.iconUrl), radius: 24,)
diff --git a/lib/widgets/settings_page.dart b/lib/widgets/settings_page.dart
index 3a77812..8a07882 100644
--- a/lib/widgets/settings_page.dart
+++ b/lib/widgets/settings_page.dart
@@ -1,5 +1,7 @@
import 'package:contacts_plus_plus/client_holder.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_phoenix/flutter_phoenix.dart';
+import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -27,6 +29,31 @@ class SettingsPage extends StatelessWidget {
initialState: !sClient.currentSettings.notificationsDenied.valueOrDefault,
onChanged: (value) async => await sClient.changeSettings(sClient.currentSettings.copyWith(notificationsDenied: !value)),
),
+ const ListSectionHeader(name: "Appearance"),
+ ListTile(
+ trailing: StatefulBuilder(
+ builder: (context, setState) {
+ return DropdownButton(
+ items: ThemeMode.values.map((mode) => DropdownMenuItem(
+ value: mode,
+ child: Text("${toBeginningOfSentenceCase(mode.name)}",),
+ )).toList(),
+ value: ThemeMode.values[sClient.currentSettings.themeMode.valueOrDefault],
+ onChanged: (ThemeMode? value) async {
+ final currentSetting = sClient.currentSettings.themeMode.value;
+ if (currentSetting != value?.index) {
+ await sClient.changeSettings(sClient.currentSettings.copyWith(themeMode: value?.index));
+ if (context.mounted) {
+ Phoenix.rebirth(context);
+ }
+ }
+ setState(() {});
+ },
+ );
+ }
+ ),
+ title: const Text("Theme Mode"),
+ ),
const ListSectionHeader(name: "Other"),
ListTile(
trailing: const Icon(Icons.logout),
@@ -44,7 +71,7 @@ class SettingsPage extends StatelessWidget {
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("No")),
TextButton(
onPressed: () async {
- await ClientHolder.of(context).apiClient.logout(context);
+ await ClientHolder.of(context).apiClient.logout();
},
child: const Text("Yes"),
),
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index 075ecba..9750187 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -8,6 +8,7 @@
#include
#include
+#include
#include
void fl_register_plugins(FlPluginRegistry* registry) {
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
+ g_autoptr(FlPluginRegistrar) record_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
+ record_linux_plugin_register_with_registrar(record_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 6fd458b..0238680 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
flutter_secure_storage_linux
+ record_linux
url_launcher_linux
)
diff --git a/pubspec.lock b/pubspec.lock
index 8c30c56..4d7300d 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -57,6 +57,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
+ camera:
+ dependency: "direct main"
+ description:
+ name: camera
+ sha256: "309b823e61f15ff6b5b2e4c0ff2e1512ea661cad5355f71fc581e510ae5b26bb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.10.5"
+ camera_android:
+ dependency: transitive
+ description:
+ name: camera_android
+ sha256: "61bbae4af0204b9bbfd82182e313d405abf5a01bdb057ff6675f2269a5cab4fd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.10.8+1"
+ camera_avfoundation:
+ dependency: transitive
+ description:
+ name: camera_avfoundation
+ sha256: "7ac8b950672716722af235eed7a7c37896853669800b7da706bb0a9fd41d3737"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.13+1"
+ camera_platform_interface:
+ dependency: transitive
+ description:
+ name: camera_platform_interface
+ sha256: "525017018d116c5db8c4c43ec2d9b1663216b369c9f75149158280168a7ce472"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.0"
+ camera_web:
+ dependency: transitive
+ description:
+ name: camera_web
+ sha256: d77965f32479ee6d8f48205dcf10f845d7210595c6c00faa51eab265d1cae993
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.1+3"
characters:
dependency: transitive
description:
@@ -89,8 +129,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
- crypto:
+ cross_file:
dependency: transitive
+ description:
+ name: cross_file
+ sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.3+4"
+ crypto:
+ dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
@@ -153,6 +201,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.4"
+ file_picker:
+ dependency: "direct main"
+ description:
+ name: file_picker
+ sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.3.0"
flutter:
dependency: "direct main"
description: flutter
@@ -214,6 +270,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.14"
flutter_secure_storage:
dependency: "direct main"
description:
@@ -305,13 +369,53 @@ packages:
source: hosted
version: "0.13.6"
http_parser:
- dependency: transitive
+ dependency: "direct main"
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
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:
@@ -441,7 +545,7 @@ packages:
source: hosted
version: "1.8.3"
path_provider:
- dependency: transitive
+ dependency: "direct main"
description:
name: path_provider
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
@@ -496,6 +600,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.1"
+ permission_handler:
+ dependency: "direct main"
+ description:
+ name: permission_handler
+ sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.2.0"
+ permission_handler_android:
+ dependency: transitive
+ description:
+ name: permission_handler_android
+ sha256: d8cc6a62ded6d0f49c6eac337e080b066ee3bce4d405bd9439a61e1f1927bfe8
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.2.1"
+ permission_handler_apple:
+ dependency: transitive
+ description:
+ name: permission_handler_apple
+ sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.0.8"
+ permission_handler_platform_interface:
+ dependency: transitive
+ description:
+ name: permission_handler_platform_interface
+ sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.9.0"
+ permission_handler_windows:
+ dependency: transitive
+ description:
+ name: permission_handler_windows
+ sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.2"
petitparser:
dependency: transitive
description:
@@ -544,6 +688,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.5"
+ quiver:
+ dependency: transitive
+ description:
+ name: quiver
+ sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.1"
+ record:
+ dependency: "direct main"
+ description:
+ name: record
+ sha256: f703397f5a60d9b2b655b3acc94ba079b2d9a67dc0725bdb90ef2fee2441ebf7
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.4.4"
+ record_linux:
+ dependency: transitive
+ description:
+ name: record_linux
+ sha256: "348db92c4ec1b67b1b85d791381c8c99d7c6908de141e7c9edc20dad399b15ce"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.1"
+ record_macos:
+ dependency: transitive
+ description:
+ name: record_macos
+ sha256: d1d0199d1395f05e218207e8cacd03eb9dc9e256ddfe2cfcbbb90e8edea06057
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2"
+ record_platform_interface:
+ dependency: transitive
+ description:
+ name: record_platform_interface
+ sha256: "7a2d4ce7ac3752505157e416e4e0d666a54b1d5d8601701b7e7e5e30bec181b4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.0"
+ record_web:
+ dependency: transitive
+ description:
+ name: record_web
+ sha256: "219ffb4ca59b4338117857db56d3ffadbde3169bcaf1136f5f4d4656f4a2372d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.0"
+ record_windows:
+ dependency: transitive
+ description:
+ name: record_windows
+ sha256: "42d545155a26b20d74f5107648dbb3382dbbc84dc3f1adc767040359e57a1345"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.1"
rxdart:
dependency: transitive
description:
@@ -613,6 +813,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
+ stream_transform:
+ dependency: transitive
+ description:
+ name: stream_transform
+ sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
string_scanner:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index e402518..12cc480 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
-version: 1.2.3+1
+version: 1.3.0+1
environment:
sdk: '>=3.0.0'
@@ -31,11 +31,9 @@ dependencies:
flutter:
sdk: flutter
-
- # The following adds the Cupertino Icons font to your application.
- # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
http: ^0.13.5
+ http_parser: ^4.0.2
uuid: ^3.0.7
flutter_secure_storage: ^8.0.0
intl: ^0.18.1
@@ -58,6 +56,13 @@ dependencies:
dynamic_color: ^1.6.5
hive: ^2.2.3
hive_flutter: ^1.1.0
+ file_picker: ^5.3.0
+ record: ^4.4.4
+ camera: ^0.10.5
+ path_provider: ^2.0.15
+ crypto: ^3.0.3
+ image_picker: ^0.8.7+5
+ permission_handler: ^10.2.0
dev_dependencies:
flutter_test:
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index 7d8bb4d..a728785 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -8,6 +8,8 @@
#include
#include
+#include
+#include
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -15,6 +17,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
+ PermissionHandlerWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
+ RecordWindowsPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 2d0eeb9..3016adf 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -5,6 +5,8 @@
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
flutter_secure_storage_windows
+ permission_handler_windows
+ record_windows
url_launcher_windows
)