From 69f2d92405f5da9acf4405f4a6b5893cff68ed15 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Sun, 27 Apr 2025 12:23:56 +0200 Subject: [PATCH 1/5] Adjust record models and endpints to new asset upload API --- lib/apis/record_api.dart | 319 +++++++++++++----- lib/models/records/asset_chunk.dart | 13 + lib/models/records/asset_diff.dart | 18 +- lib/models/records/asset_manifest.dart | 33 ++ lib/models/records/asset_upload_data.dart | 88 +++-- .../records/cloudflare_chunk_result.dart | 11 + lib/models/records/preprocess_status.dart | 33 +- lib/models/records/record.dart | 311 +++++++---------- lib/models/records/record_version.dart | 27 ++ .../inventory/inventory_browser_app_bar.dart | 52 +-- lib/widgets/messages/message_input_bar.dart | 274 ++++++++------- lib/widgets/worlds/world_view.dart | 4 +- 12 files changed, 729 insertions(+), 454 deletions(-) create mode 100644 lib/models/records/asset_chunk.dart create mode 100644 lib/models/records/asset_manifest.dart create mode 100644 lib/models/records/cloudflare_chunk_result.dart create mode 100644 lib/models/records/record_version.dart diff --git a/lib/apis/record_api.dart b/lib/apis/record_api.dart index 3678803..d18d362 100644 --- a/lib/apis/record_api.dart +++ b/lib/apis/record_api.dart @@ -1,20 +1,25 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; + import 'package:collection/collection.dart'; -import 'package:recon/models/records/asset_digest.dart'; -import 'package:recon/models/records/json_template.dart'; -import 'package:http/http.dart' as http; import 'package:flutter/material.dart'; - +import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; +import 'package:path/path.dart'; import 'package:recon/clients/api_client.dart'; +import 'package:recon/models/records/asset_chunk.dart'; +import 'package:recon/models/records/asset_diff.dart'; +import 'package:recon/models/records/asset_digest.dart'; +import 'package:recon/models/records/asset_manifest.dart'; import 'package:recon/models/records/asset_upload_data.dart'; -import 'package:recon/models/records/resonite_db_asset.dart'; +import 'package:recon/models/records/cloudflare_chunk_result.dart'; +import 'package:recon/models/records/json_template.dart'; import 'package:recon/models/records/preprocess_status.dart'; import 'package:recon/models/records/record.dart'; -import 'package:http_parser/http_parser.dart'; -import 'package:path/path.dart'; +import 'package:recon/models/records/resonite_db_asset.dart'; import 'package:recon/models/records/search_sort.dart'; class RecordApi { @@ -61,7 +66,11 @@ class RecordApi { return body.map((e) => Record.fromMap(e)).toList(); } - static Future> getGroupRecordsAt(ApiClient client, {required String path, required String groupId}) async { + static Future> getGroupRecordsAt( + ApiClient client, { + required String path, + required String groupId, + }) async { final response = await client.get("/groups/$groupId/records?path=$path"); client.checkResponse(response); final body = jsonDecode(response.body) as List; @@ -81,13 +90,85 @@ class RecordApi { 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}"); + 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 _beginAssetUpload(ApiClient client, {required AssetManifest manifest}) async { + final response = await client.post("/users/${client.userId}/assets/${manifest.hash}/upload?size=${manifest.bytes}"); + client.checkResponse(response); + final body = jsonDecode(response.body); + return AssetUploadData.fromMap(body); + } + + static Future _directAssetUpload({required AssetUploadData uploadData, required Uint8List assetData}) async { + final request = http.MultipartRequest( + "PUT", + Uri.parse(uploadData.uploadEndpoint), + ) + ..files.add( + http.MultipartFile.fromBytes( + "file", + assetData, + filename: uploadData.hash, + contentType: MediaType.parse("multipart/form-data"), + ), + ) + ..headers.addAll( + { + "Upload-Key": uploadData.uploadKey, + "Upload-Timestamp": uploadData.createdOn.toIso8601String(), + }, + ); + final response = await request.send(); + final bodyBytes = await response.stream.toBytes(); + ApiClient.checkResponseCode(http.Response.bytes(bodyBytes, response.statusCode)); + } + + static Future _chunkedAssetUpload({ + required AssetUploadData uploadData, + required int chunkIndex, + required Uint8List chunkData, + }) async { + final response = await http.put( + Uri.parse(uploadData.uploadEndpoint), + headers: { + "Upload-Key": uploadData.uploadKey, + "Part-Number": chunkIndex.toString(), + }, + body: chunkData, + ); + ApiClient.checkResponseCode(response); + final body = jsonDecode(response.body); + return CloudflareChunkResult.fromMap(body); + } + + static Future _finalizeUpload(ApiClient client, {required AssetUploadData uploadData}) async { + final response = await client.patch( + "/users/${client.userId}/assets/${uploadData.hash}/upload/${uploadData.id}", + body: jsonEncode(uploadData.toMap()), + ); + client.checkResponse(response); + } + + static Future _getUploadInfo(ApiClient client, {required AssetUploadData uploadData}) async { + final response = await client.patch( + "/users/${client.userId}/assets/${uploadData.hash}/upload/${uploadData.id}", + body: jsonEncode(uploadData.toMap()), + ); + client.checkResponse(response); + final body = jsonDecode(response.body); + return AssetUploadData.fromMap(body); + } + static Future tryPreprocessRecord(ApiClient client, {required Record record}) async { var status = await preprocessRecord(client, record: record); while (status.state == RecordPreprocessState.preprocessing) { @@ -110,19 +191,25 @@ class RecordApi { 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); + static Future upsertRecord(ApiClient client, {required Record record, bool ensureFolder = false}) async { + final response = await client.put( + "/users/${client.userId}/records/${record.id}?ensureFolder=$ensureFolder", + body: jsonEncode(record.toMap()), + ); client.checkResponse(response); + final body = jsonDecode(response.body); + return Record.fromMap(body); } - static Future uploadAsset(ApiClient client, - {required AssetUploadData uploadData, - required String filename, - required ResoniteDBAsset asset, - required Uint8List data, - void Function(double number)? progressCallback}) async { - for (int i = 0; i < uploadData.totalChunks; i++) { + static Future uploadAsset( + ApiClient client, { + required AssetUploadData uploadData, + required String filename, + required ResoniteDBAsset asset, + required Uint8List data, + void Function(double number)? progressCallback, + }) async { + for (var i = 0; i < uploadData.totalChunks; i++) { progressCallback?.call(i / uploadData.totalChunks); final offset = i * uploadData.chunkSize; final end = (i + 1) * uploadData.chunkSize; @@ -131,7 +218,13 @@ class RecordApi { 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"))) + 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(); @@ -145,9 +238,13 @@ class RecordApi { client.checkResponse(response); } - static Future uploadAssets(ApiClient client, {required List assets, void Function(double progress)? progressCallback}) async { + 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++) { + for (var i = 0; i < assets.length; i++) { final totalProgress = i / assets.length; progressCallback?.call(totalProgress); final entry = assets[i]; @@ -168,103 +265,145 @@ class RecordApi { progressCallback?.call(1); } - static Future uploadImage(ApiClient client, {required File image, required String machineId, void Function(double progress)? progressCallback}) async { + 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 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"], - ); + final record = Record.inventoryRoot(); progressCallback?.call(.1); final status = await tryPreprocessRecord(client, record: record); - final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded); + final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded ?? false); 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 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 { + static Future> _uploadAsset( + ApiClient client, { + required AssetManifest manifest, + required Uint8List data, + void Function(double progress)? progressCallback, + }) async { + final chunkedUploads = []; + final uploadData = await _beginAssetUpload(client, manifest: manifest); + if (uploadData.isDirectUpload) { + await _directAssetUpload(uploadData: uploadData, assetData: data); + } else { + final chunks = data.slices(uploadData.chunkSize); + for (final (cIdx, chunk) in chunks.indexed) { + final chunkResult = await _chunkedAssetUpload( + uploadData: uploadData, + chunkIndex: cIdx + 1, + chunkData: Uint8List.fromList(chunk), + ); + final chunkInfo = AssetChunk(index: cIdx, key: chunkResult.eTag); + uploadData.chunks.add(chunkInfo); + progressCallback?.call(cIdx / chunks.length); + } + chunkedUploads.add(uploadData); + } + await _finalizeUpload(client, uploadData: uploadData); + progressCallback?.call(1); + return chunkedUploads; + } + + 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); + final bytes = voiceClip.readAsBytesSync(); + final voiceManifest = AssetManifest.fromData(bytes); + final assetManifests = {voiceManifest: bytes}; + + final record = Record.local( + name: "Voice Message", + recordType: RecordType.audio, + ownerId: client.userId, + assetManifest: assetManifests.keys.toList(), + assetUri: "resdb:///${voiceManifest.hash}.${extension(voiceClip.path)}"); + + var preproc = await preprocessRecord(client, record: record); + while (preproc.state != RecordPreprocessState.success) { + preproc = await getPreprocessStatus(client, preprocessStatus: preproc); + if (preproc.state == RecordPreprocessState.failed) { + throw "Failed to upload asset: ${preproc.failReason}"; + } + progressCallback?.call(preproc.progress * 0.2); + await Future.delayed(const Duration(seconds: 1)); + } + final chunkedUploads = []; + for (final diff in preproc.resultDiffs.where( + (element) => element.state == Diff.added && !(element.isUploaded ?? false), + )) { + final dataKey = assetManifests.keys.firstWhere( + (element) => element.hash == diff.hash, + ); + final chunked = await _uploadAsset( + client, + manifest: diff, + data: assetManifests[dataKey]!, + progressCallback: (progress) => progressCallback?.call(0.2 + progress * 0.9), + ); + chunkedUploads.addAll(chunked); + } + + for (var uploadData in chunkedUploads) { + while (uploadData.uploadState != UploadState.uploaded) { + uploadData = await _getUploadInfo(client, uploadData: uploadData); + if (uploadData.uploadState == UploadState.failed) { + throw "Upload failed: Unknown cloud error when combining asset chunks"; + } + await Future.delayed(const Duration(milliseconds: 1500)); + } + } - 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 { + 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; + return Record.inventoryRoot(); } } diff --git a/lib/models/records/asset_chunk.dart b/lib/models/records/asset_chunk.dart new file mode 100644 index 0000000..107935f --- /dev/null +++ b/lib/models/records/asset_chunk.dart @@ -0,0 +1,13 @@ +class AssetChunk { + final int index; + final String key; + + const AssetChunk({required this.index, required this.key}); + + factory AssetChunk.fromMap(Map map) => AssetChunk(index: map["index"], key: map["key"]); + + Map toMap() => { + "index": index, + "key": key, + }; +} diff --git a/lib/models/records/asset_diff.dart b/lib/models/records/asset_diff.dart index 49e89c5..5a588c7 100644 --- a/lib/models/records/asset_diff.dart +++ b/lib/models/records/asset_diff.dart @@ -1,9 +1,8 @@ +import 'package:recon/models/records/asset_manifest.dart'; -import 'package:recon/models/records/resonite_db_asset.dart'; - -class AssetDiff extends ResoniteDBAsset{ +class AssetDiff extends AssetManifest { final Diff state; - final bool isUploaded; + final bool? isUploaded; const AssetDiff({required super.hash, required super.bytes, required this.state, required this.isUploaded}); @@ -15,6 +14,12 @@ class AssetDiff extends ResoniteDBAsset{ isUploaded: map["isUploaded"], ); } + + @override + bool operator ==(Object other) => identical(this, other) || super == other; + + @override + int get hashCode => hash.hashCode; } enum Diff { @@ -27,8 +32,9 @@ enum Diff { } factory Diff.fromString(String? text) { - return Diff.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(), + 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_manifest.dart b/lib/models/records/asset_manifest.dart new file mode 100644 index 0000000..79d11d2 --- /dev/null +++ b/lib/models/records/asset_manifest.dart @@ -0,0 +1,33 @@ +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; + +class AssetManifest { + final String hash; + final int bytes; + + const AssetManifest({required this.hash, required this.bytes}); + + factory AssetManifest.fromMap(Map map) { + return AssetManifest(hash: map["hash"] ?? "", bytes: map["bytes"] ?? -1); + } + + factory AssetManifest.fromData(Uint8List data) { + final digest = sha256.convert(data); + return AssetManifest(hash: digest.toString().replaceAll("-", "").toLowerCase(), bytes: data.length); + } + + Map toMap() { + return { + "hash": hash, + "bytes": bytes, + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || other is AssetManifest && runtimeType == other.runtimeType && hash == other.hash; + + @override + int get hashCode => hash.hashCode; +} diff --git a/lib/models/records/asset_upload_data.dart b/lib/models/records/asset_upload_data.dart index 6df0555..ce5aef3 100644 --- a/lib/models/records/asset_upload_data.dart +++ b/lib/models/records/asset_upload_data.dart @@ -1,3 +1,5 @@ +import 'package:intl/intl.dart'; +import 'package:recon/models/records/asset_chunk.dart'; enum UploadState { uploadingChunks, @@ -7,40 +9,82 @@ enum UploadState { unknown; factory UploadState.fromString(String? text) { - return UploadState.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(), + return UploadState.values.firstWhere( + (element) => element.name.toLowerCase() == text?.toLowerCase(), orElse: () => UploadState.unknown, ); } + + @override + String toString() => toBeginningOfSentenceCase(super.toString()); } class AssetUploadData { - final String signature; - final String variant; + final String hash; + final String? variant; + final String id; final String ownerId; final int totalBytes; final int chunkSize; final int totalChunks; final UploadState uploadState; + final String uploadKey; + final String uploadEndpoint; + final bool isDirectUpload; + final int maxUploadConcurrency; + final List chunks; + final DateTime createdOn; const AssetUploadData({ - required this.signature, - required this.variant, - required this.ownerId, - required this.totalBytes, - required this.chunkSize, - required this.totalChunks, - required this.uploadState, + required this.hash, + required this.variant, + required this.id, + required this.ownerId, + required this.totalBytes, + required this.chunkSize, + required this.totalChunks, + required this.uploadState, + required this.uploadKey, + required this.uploadEndpoint, + required this.isDirectUpload, + required this.maxUploadConcurrency, + required this.chunks, + required this.createdOn, }); - 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 + factory AssetUploadData.fromMap(Map map) => AssetUploadData( + hash: map["hash"], + variant: map["variant"], + id: map["id"], + ownerId: map["ownerId"], + totalBytes: map["totalBytes"], + chunkSize: map["chunkSize"], + totalChunks: map["totalChunks"], + uploadState: UploadState.fromString(map["uploadState"]), + uploadKey: map["uploadKey"], + uploadEndpoint: map["uploadEndpoint"], + isDirectUpload: map["isDirectUpload"], + maxUploadConcurrency: map["maxUploadConcurrency"], + chunks: (map["chunks"] as List?)?.cast>().map(AssetChunk.fromMap).toList() ?? [], + createdOn: DateTime.parse(map["createdOn"]).toUtc(), + ); + + Map toMap() => { + "hash": hash, + "variant": variant, + "id": id, + "ownerId": ownerId, + "totalBytes": totalBytes, + "chunkSize": chunkSize, + "totalChunks": totalChunks, + "uploadState": uploadState.toString(), + "uploadKey": uploadKey, + "uploadEndpoint": uploadEndpoint, + "isDirectUpload": isDirectUpload, + "maxUploadConcurrency": maxUploadConcurrency, + "chunks": chunks.map( + (e) => e.toMap(), + ), + "createdOn": createdOn, + }; +} diff --git a/lib/models/records/cloudflare_chunk_result.dart b/lib/models/records/cloudflare_chunk_result.dart new file mode 100644 index 0000000..be99d5f --- /dev/null +++ b/lib/models/records/cloudflare_chunk_result.dart @@ -0,0 +1,11 @@ +class CloudflareChunkResult { + final String eTag; + final String checksumCRC32; + + const CloudflareChunkResult({required this.eTag, required this.checksumCRC32}); + + factory CloudflareChunkResult.fromMap(Map map) => CloudflareChunkResult( + eTag: map["ETag"], + checksumCRC32: map["ChecksumCRC32"], + ); +} diff --git a/lib/models/records/preprocess_status.dart b/lib/models/records/preprocess_status.dart index 675b1c8..0d08013 100644 --- a/lib/models/records/preprocess_status.dart +++ b/lib/models/records/preprocess_status.dart @@ -1,30 +1,39 @@ import 'package:recon/models/records/asset_diff.dart'; -enum RecordPreprocessState -{ +enum RecordPreprocessState { preprocessing, success, failed; factory RecordPreprocessState.fromString(String? text) { - return RecordPreprocessState.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(), + 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 double progress; + final String? failReason; final List resultDiffs; + final int attempts; + final int ttl; - const PreprocessStatus({required this.id, required this.ownerId, required this.recordId, required this.state, - required this.progress, required this.failReason, required this.resultDiffs, + const PreprocessStatus({ + required this.id, + required this.ownerId, + required this.recordId, + required this.state, + required this.progress, + required this.failReason, + required this.resultDiffs, + required this.attempts, + required this.ttl, }); factory PreprocessStatus.fromMap(Map map) { @@ -33,9 +42,11 @@ class PreprocessStatus { ownerId: map["ownerId"], recordId: map["recordId"], state: RecordPreprocessState.fromString(map["state"]), - progress: map["progress"], - failReason: map["failReason"] ?? "", + progress: (map["progress"] as num).toDouble(), + failReason: map["failReason"], resultDiffs: (map["resultDiffs"] as List? ?? []).map((e) => AssetDiff.fromMap(e)).toList(), + attempts: map["attempts"], + ttl: map["ttl"], ); } -} \ No newline at end of file +} diff --git a/lib/models/records/record.dart b/lib/models/records/record.dart index 32cc467..0dfcc46 100644 --- a/lib/models/records/record.dart +++ b/lib/models/records/record.dart @@ -1,7 +1,6 @@ import 'package:recon/auxiliary.dart'; -import 'package:recon/models/message.dart'; -import 'package:recon/models/records/asset_digest.dart'; -import 'package:recon/models/records/resonite_db_asset.dart'; +import 'package:recon/models/records/asset_manifest.dart'; +import 'package:recon/models/records/record_version.dart'; import 'package:recon/string_formatter.dart'; import 'package:uuid/uuid.dart'; @@ -14,40 +13,26 @@ enum RecordType { 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({this.id, 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, - }; + return RecordType.values.firstWhere( + (element) => element.name.toLowerCase() == name?.toLowerCase().trim(), + orElse: () => RecordType.unknown, + ); } } class Record { static final _rootRecord = Record( id: "0", - combinedRecordId: const RecordId(isValid: false), - isSynced: true, - fetchedOn: DateTimeX.epoch, path: "Inventory", ownerId: "", assetUri: "", name: "Inventory", + version: RecordVersion( + globalVersion: 1, + localVersion: 1, + lastModifyingUserId: null, + lastModifyingMachineId: null, + ), description: "", tags: [], recordType: RecordType.directory, @@ -56,58 +41,45 @@ class Record { isListed: false, isForPatreons: false, lastModificationTime: DateTimeX.epoch, - resoniteDBManifest: [], - lastModifyingUserId: "", - lastModifyingMachineId: "", creationTime: DateTimeX.epoch, - manifest: [], - url: "", - isValidOwnerId: true, - isValidRecordId: true, - globalVersion: 1, - localVersion: 1, + assetManifest: [], visits: 0, rating: 0, randomOrder: 0, + ownerName: null, + isDeleted: false, + isReadOnly: true, + firstPublishTime: DateTimeX.epoch, ); 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 RecordVersion version; final String name; final FormatNode formattedName; - final String description; - final FormatNode formattedDescription; + final String? description; + final FormatNode? formattedDescription; final RecordType recordType; + final String? ownerName; final List tags; - final String path; - final String thumbnailUri; + final String? path; + final String? thumbnailUri; final bool isPublic; final bool isForPatreons; final bool isListed; + final bool isDeleted; + final bool isReadOnly; final DateTime lastModificationTime; final DateTime creationTime; + final DateTime? firstPublishTime; final int visits; final int rating; final int randomOrder; - final List manifest; - final List resoniteDBManifest; - final String url; - final bool isValidOwnerId; - final bool isValidRecordId; + final List assetManifest; Record({ required this.id, - required this.combinedRecordId, - required this.isSynced, - required this.fetchedOn, required this.path, required this.ownerId, required this.assetUri, @@ -120,75 +92,27 @@ class Record { required this.isListed, required this.isForPatreons, required this.lastModificationTime, - required this.resoniteDBManifest, - 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.assetManifest, + required this.version, required this.visits, required this.rating, required this.randomOrder, + required this.ownerName, + required this.isDeleted, + required this.isReadOnly, + required this.firstPublishTime, }) : formattedName = FormatNode.fromText(name), formattedDescription = FormatNode.fromText(description); - 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()}", "recon"] + (extraTags ?? [])).unique(), - recordType: recordType, - thumbnailUri: thumbnailUri, - isPublic: false, - isForPatreons: false, - isListed: false, - resoniteDBManifest: 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: "resrec:///$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"] ?? "", + version: RecordVersion.fromMap(map["version"]), description: map["description"] ?? "", tags: (map["tags"] as List? ?? []).map((e) => e.toString()).toList(), recordType: RecordType.fromName(map["recordType"]), @@ -196,21 +120,57 @@ class Record { isPublic: map["isPublic"] ?? false, isForPatreons: map["isForPatreons"] ?? false, isListed: map["isListed"] ?? false, - lastModificationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch, - resoniteDBManifest: (map["resoniteDBManifest"] as List? ?? []).map((e) => ResoniteDBAsset.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, + lastModificationTime: DateTime.tryParse(map["lastModificationTime"] ?? "") ?? DateTimeX.epoch, + assetManifest: (map["assetManifest"] as List? ?? []).map((e) => AssetManifest.fromMap(e)).toList(), + creationTime: DateTime.tryParse(map["lastModificationTime"] ?? "") ?? DateTimeX.epoch, path: map["path"] ?? "", - manifest: (map["resoniteDBManifest"] as List? ?? []).map((e) => e.toString()).toList(), - url: map["url"] ?? "", - isValidOwnerId: map["isValidOwnerId"] == "true", - isValidRecordId: map["isValidRecordId"] == "true", visits: map["visits"] ?? 0, rating: map["rating"] ?? 0, randomOrder: map["randomOrder"] ?? 0, + ownerName: map["ownerName"], + isDeleted: map["isDeleted"] ?? false, + isReadOnly: map["isReadOnly"] ?? false, + firstPublishTime: DateTime.tryParse(map["firstPublishTime"] ?? ""), + ); + } + + factory Record.local({ + required String name, + required RecordType recordType, + required String ownerId, + required List assetManifest, + String assetUri = "", + }) { + final now = DateTime.now(); + return Record( + id: generateId(), + path: null, + ownerId: ownerId, + assetUri: assetUri, + name: name, + description: null, + tags: [], + recordType: recordType, + thumbnailUri: null, + isPublic: true, + isListed: true, + isForPatreons: false, + lastModificationTime: now, + creationTime: now, + assetManifest: assetManifest, + version: RecordVersion( + globalVersion: 1, + localVersion: 1, + lastModifyingUserId: ownerId, + lastModifyingMachineId: null, + ), + visits: 0, + rating: 0, + randomOrder: 0, + ownerName: null, + isDeleted: false, + isReadOnly: false, + firstPublishTime: null, ); } @@ -255,100 +215,79 @@ class Record { Record copyWith({ String? id, String? ownerId, - String? recordId, String? assetUri, - int? globalVersion, - int? localVersion, + RecordVersion? version, String? name, - String? description, - List? tags, + String? Function()? description, RecordType? recordType, - String? thumbnailUri, + String? Function()? ownerName, + List? tags, + String? Function()? path, + String? Function()? thumbnailUri, bool? isPublic, bool? isForPatreons, bool? isListed, bool? isDeleted, + bool? isReadOnly, DateTime? lastModificationTime, - List? resoniteDBManifest, - String? lastModifyingUserId, - String? lastModifyingMachineId, DateTime? creationTime, - RecordId? combinedRecordId, - bool? isSynced, - DateTime? fetchedOn, - String? path, - List? manifest, - String? url, - bool? isValidOwnerId, - bool? isValidRecordId, + DateTime? Function()? firstPublishTime, 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, - resoniteDBManifest: resoniteDBManifest ?? this.resoniteDBManifest, - 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, - ); - } + List? assetManifest, + }) => + Record( + id: id ?? this.id, + path: path == null ? this.path : path(), + ownerId: ownerId ?? this.ownerId, + assetUri: assetUri ?? this.assetUri, + name: name ?? this.name, + description: description == null ? this.description : description(), + tags: tags ?? this.tags, + recordType: recordType ?? this.recordType, + thumbnailUri: thumbnailUri == null ? this.thumbnailUri : thumbnailUri(), + isPublic: isPublic ?? this.isPublic, + isListed: isListed ?? this.isListed, + isForPatreons: isForPatreons ?? this.isForPatreons, + lastModificationTime: lastModificationTime ?? this.lastModificationTime, + creationTime: creationTime ?? this.creationTime, + assetManifest: assetManifest ?? this.assetManifest, + version: version ?? this.version, + visits: visits ?? this.visits, + rating: rating ?? this.rating, + randomOrder: randomOrder ?? this.randomOrder, + ownerName: ownerName == null ? this.ownerName : ownerName(), + isDeleted: isDeleted ?? this.isDeleted, + isReadOnly: isReadOnly ?? this.isReadOnly, + firstPublishTime: firstPublishTime == null ? this.firstPublishTime : firstPublishTime(), + ); Map toMap() { return { "id": id, "ownerId": ownerId, "assetUri": assetUri, - "globalVersion": globalVersion, - "localVersion": localVersion, + "version": version.toMap(), "name": name, - "description": description.asNullable, + "description": description?.asNullable, "tags": tags, "recordType": recordType.name, - "thumbnailUri": thumbnailUri.asNullable, + "thumbnailUri": thumbnailUri?.asNullable, "isPublic": isPublic, "isForPatreons": isForPatreons, "isListed": isListed, "lastModificationTime": lastModificationTime.toUtc().toIso8601String(), - "resoniteDBManifest": resoniteDBManifest.map((e) => e.toMap()).toList(), - "lastModifyingUserId": lastModifyingUserId, - "lastModifyingMachineId": lastModifyingMachineId, + "assetManifest": assetManifest.map((e) => e.toMap()).toList(), "creationTime": creationTime.toUtc().toIso8601String(), - "combinedRecordId": combinedRecordId.toMap(), - "isSynced": isSynced, - "fetchedOn": fetchedOn.toUtc().toIso8601String(), - "path": path.asNullable, - "manifest": manifest, - "url": url, - "isValidOwnerId": isValidOwnerId, - "isValidRecordId": isValidRecordId, + "path": path?.asNullable, "visits": visits, "rating": rating, "randomOrder": randomOrder, + "ownerName": ownerName, + "isDeleted": isDeleted, + "isReadOnly": isReadOnly, + "firstPublishTime": firstPublishTime?.toUtc().toIso8601String(), }; } diff --git a/lib/models/records/record_version.dart b/lib/models/records/record_version.dart new file mode 100644 index 0000000..a0a98d7 --- /dev/null +++ b/lib/models/records/record_version.dart @@ -0,0 +1,27 @@ +class RecordVersion { + final int globalVersion; + final int localVersion; + final String? lastModifyingUserId; + final String? lastModifyingMachineId; + + const RecordVersion({ + required this.globalVersion, + required this.localVersion, + required this.lastModifyingUserId, + required this.lastModifyingMachineId, + }); + + factory RecordVersion.fromMap(Map map) => RecordVersion( + globalVersion: map['globalVersion'], + localVersion: map['localVersion'], + lastModifyingUserId: map['lastModifyingUserId'], + lastModifyingMachineId: map['lastModifyingMachineId'], + ); + + Map toMap() => { + "globalVersion": globalVersion, + "localVersion": localVersion, + "lastModifyingUserId": lastModifyingUserId, + "lastModifyingMachineId": lastModifyingMachineId, + }; +} diff --git a/lib/widgets/inventory/inventory_browser_app_bar.dart b/lib/widgets/inventory/inventory_browser_app_bar.dart index f2ec14e..b1f176c 100644 --- a/lib/widgets/inventory/inventory_browser_app_bar.dart +++ b/lib/widgets/inventory/inventory_browser_app_bar.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:isolate'; import 'dart:ui'; import 'package:background_downloader/background_downloader.dart'; @@ -28,7 +27,7 @@ class _InventoryBrowserAppBarState extends State { final id = event.task.taskId; final status = event is TaskStatusUpdate ? event.status : null; final progress = event is TaskProgressUpdate ? event.progress : null; - final SendPort? send = IsolateNameServer.lookupPortByName('downloader_send_port'); + final send = IsolateNameServer.lookupPortByName('downloader_send_port'); send?.send([id, status, progress]); } @@ -36,7 +35,7 @@ class _InventoryBrowserAppBarState extends State { @override Widget build(BuildContext context) { return Consumer( - builder: (BuildContext context, InventoryClient iClient, Widget? _) { + builder: (context, iClient, _) { return AnimatedSwitcher( duration: const Duration(milliseconds: 350), transitionBuilder: (child, animation) => FadeTransition( @@ -61,7 +60,7 @@ class _InventoryBrowserAppBarState extends State { children: [ Icon( Icons.arrow_upward, - color: iClient.sortReverse == false + color: !iClient.sortReverse ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), @@ -71,7 +70,7 @@ class _InventoryBrowserAppBarState extends State { Text( "Ascending", style: TextStyle( - color: iClient.sortReverse == false + color: !iClient.sortReverse ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), @@ -83,24 +82,26 @@ class _InventoryBrowserAppBarState extends State { value: true, child: Row( children: [ - Icon(Icons.arrow_downward, - color: iClient.sortReverse == true - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface), + Icon( + Icons.arrow_downward, + color: iClient.sortReverse + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface, + ), const SizedBox( width: 8, ), Text( "Descending", style: TextStyle( - color: iClient.sortReverse == true + color: iClient.sortReverse ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), ), ], ), - ) + ), ]; }, ), @@ -134,7 +135,7 @@ class _InventoryBrowserAppBarState extends State { ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), - ) + ), ], ), ), @@ -192,7 +193,7 @@ class _InventoryBrowserAppBarState extends State { }, icon: const Icon(Icons.data_object), label: Text( - "Asset${iClient.selectedRecordCount != 1 ? "s" : ""} (${assetUris.map((e) => extension(e)).toList().unique().join(", ")})", + "Asset${iClient.selectedRecordCount != 1 ? "s" : ""} (${assetUris.map(extension).toList().unique().join(", ")})", ), ), TextButton.icon( @@ -201,7 +202,7 @@ class _InventoryBrowserAppBarState extends State { }, icon: const Icon(Icons.image), label: Text( - "Thumbnail${iClient.selectedRecordCount != 1 ? "s" : ""} (${thumbUris.map((e) => extension(e)).toList().unique().join(", ")})", + "Thumbnail${iClient.selectedRecordCount != 1 ? "s" : ""} (${thumbUris.map((e) => extension(e ?? "")).toList().unique().join(", ")})", ), ), ], @@ -233,10 +234,10 @@ class _InventoryBrowserAppBarState extends State { return; } - for (var record in selectedRecords) { + for (final record in selectedRecords) { final uri = selectedUris == thumbUris ? record.thumbnailUri : record.assetUri; final filename = - "${record.id.split("-")[1]}-${record.formattedName.toString()}${extension(uri)}"; + "${record.id.split("-")[1]}-${record.formattedName}${extension(uri ?? "")}"; try { final downloadTask = DownloadTask( url: Aux.resdbToHttp(uri), @@ -249,8 +250,9 @@ class _InventoryBrowserAppBarState extends State { if (downloadStatus.status == TaskStatus.complete) { final tempDirectory = await _tempDirectoryFuture; final file = File( - "${tempDirectory.path}/${record.id.split("-")[1]}-${record.formattedName.toString()}${extension(uri)}"); - if (await file.exists()) { + "${tempDirectory.path}/${record.id.split("-")[1]}-${record.formattedName}${extension(uri ?? "")}", + ); + if (file.existsSync()) { final newFile = File("$directory/$filename"); await file.copy(newFile.absolute.path); await file.delete(); @@ -258,7 +260,7 @@ class _InventoryBrowserAppBarState extends State { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("Downloaded ${record.formattedName.toString()}"), + content: Text("Downloaded ${record.formattedName}"), ), ); } else { @@ -271,7 +273,7 @@ class _InventoryBrowserAppBarState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - "Failed to download '${record.formattedName.toString()}':\n$e", + "Failed to download '${record.formattedName}':\n$e", ), ), ); @@ -295,9 +297,11 @@ class _InventoryBrowserAppBarState extends State { builder: (context, setState) { return AlertDialog( icon: const Icon(Icons.delete), - title: Text(iClient.selectedRecordCount == 1 - ? "Really delete this Record?" - : "Really delete ${iClient.selectedRecordCount} Records?"), + title: Text( + iClient.selectedRecordCount == 1 + ? "Really delete this Record?" + : "Really delete ${iClient.selectedRecordCount} Records?", + ), content: const Text("This action cannot be undone!"), actionsAlignment: MainAxisAlignment.spaceBetween, actions: [ @@ -344,7 +348,7 @@ class _InventoryBrowserAppBarState extends State { if (context.mounted) { Navigator.of(context).pop(true); } - iClient.reloadCurrentDirectory(); + await iClient.reloadCurrentDirectory(); }, style: TextButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.error, diff --git a/lib/widgets/messages/message_input_bar.dart b/lib/widgets/messages/message_input_bar.dart index 6ddec45..090a1d6 100644 --- a/lib/widgets/messages/message_input_bar.dart +++ b/lib/widgets/messages/message_input_bar.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -5,6 +6,8 @@ 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:recon/apis/record_api.dart'; import 'package:recon/auxiliary.dart'; @@ -15,6 +18,7 @@ import 'package:recon/models/message.dart'; import 'package:recon/models/users/friend.dart'; import 'package:recon/widgets/messages/message_attachment_list.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}); @@ -39,7 +43,9 @@ class _MessageInputBarState extends State { bool _attachmentPickerOpen = false; String _currentText = ""; double? _sendProgress; + bool get _isRecording => _recordingStartTime != null; + set _isRecording(value) => _recordingStartTime = value ? DateTime.now() : null; bool _recordingCancelled = false; @@ -64,8 +70,13 @@ class _MessageInputBarState extends State { mClient.sendMessage(message); } - Future sendImageMessage(ApiClient client, MessagingClient mClient, File file, String machineId, - void Function(double progress) progressCallback) async { + Future sendImageMessage( + ApiClient client, + MessagingClient mClient, + File file, + String machineId, + void Function(double progress) progressCallback, + ) async { final record = await RecordApi.uploadImage( client, image: file, @@ -73,18 +84,24 @@ class _MessageInputBarState extends State { 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); + 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 { + Future sendVoiceMessage( + ApiClient client, + MessagingClient mClient, + File file, + String machineId, + void Function(double progress) progressCallback, + ) async { final record = await RecordApi.uploadVoiceClip( client, voiceClip: file, @@ -103,8 +120,13 @@ class _MessageInputBarState extends State { mClient.sendMessage(message); } - Future sendRawFileMessage(ApiClient client, MessagingClient mClient, File file, String machineId, - void Function(double progress) progressCallback) async { + Future sendRawFileMessage( + ApiClient client, + MessagingClient mClient, + File file, + String machineId, + void Function(double progress) progressCallback, + ) async { final record = await RecordApi.uploadRawFile( client, file: file, @@ -167,7 +189,7 @@ class _MessageInputBarState extends State { final recording = await _recorder.stop(); if (recording == null) return; final file = File(recording); - if (await file.exists()) { + if (file.existsSync()) { await file.delete(); } } @@ -186,12 +208,29 @@ class _MessageInputBarState extends State { _sendProgress = 0; }); final apiClient = cHolder.apiClient; - await sendVoiceMessage( - apiClient, mClient, file, cHolder.settingsClient.currentSettings.machineId.valueOrDefault, (progress) { + try { + await sendVoiceMessage( + apiClient, mClient, file, cHolder.settingsClient.currentSettings.machineId.valueOrDefault, + (progress) { + setState(() { + _sendProgress = progress; + }); + }); + } catch (e, s) { + FlutterError.reportError( + FlutterErrorDetails( + exception: e, + stack: s, + ), + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Failed to send voice message: $e"))); + } setState(() { - _sendProgress = progress; + _sendProgress = null; + _isSending = false; }); - }); + } setState(() { _isSending = false; _sendProgress = null; @@ -210,7 +249,7 @@ class _MessageInputBarState extends State { child: Column( children: [ if (_isSending && _sendProgress != null) LinearProgressIndicator(value: _sendProgress), - Container( + DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, ), @@ -218,7 +257,7 @@ class _MessageInputBarState extends State { duration: const Duration(milliseconds: 200), switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeOut, - transitionBuilder: (Widget child, animation) => SizeTransition( + transitionBuilder: (child, animation) => SizeTransition( sizeFactor: animation, child: child, ), @@ -234,9 +273,11 @@ class _MessageInputBarState extends State { 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) - .nonNulls); + _loadedFiles.addAll( + result.files + .map((e) => e.path != null ? (FileType.image, File(e.path!)) : null) + .nonNulls, + ); }); } }, @@ -256,7 +297,7 @@ class _MessageInputBarState extends State { return; } final file = File(picture.path); - if (await file.exists()) { + if (file.existsSync()) { setState(() { _loadedFiles.add((FileType.image, file)); }); @@ -278,9 +319,11 @@ class _MessageInputBarState extends State { 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) - .nonNulls); + _loadedFiles.addAll( + result.files + .map((e) => e.path != null ? (FileType.any, File(e.path!)) : null) + .nonNulls, + ); }); } }, @@ -293,9 +336,10 @@ class _MessageInputBarState extends State { (_, _) => MessageAttachmentList( disabled: _isSending, initialFiles: _loadedFiles, - onChange: (List<(FileType, File)> loadedFiles) => setState(() { - _loadedFiles.clear(); - _loadedFiles.addAll(loadedFiles); + onChange: (loadedFiles) => setState(() { + _loadedFiles + ..clear() + ..addAll(loadedFiles); }), ), }, @@ -305,7 +349,7 @@ class _MessageInputBarState extends State { children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 200), - transitionBuilder: (Widget child, Animation animation) => FadeTransition( + transitionBuilder: (child, animation) => FadeTransition( opacity: animation, child: RotationTransition( turns: Tween(begin: 0.6, end: 1).animate(animation), @@ -326,7 +370,8 @@ class _MessageInputBarState extends State { ? null : () { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Sorry, this feature is not yet available"))); + const SnackBar(content: Text("Sorry, this feature is not yet available")), + ); return; // setState(() { // _attachmentPickerOpen = true; @@ -343,29 +388,30 @@ class _MessageInputBarState extends State { : () 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"), - ) - ], - )); + 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; @@ -401,25 +447,26 @@ class _MessageInputBarState extends State { }, 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), - )), + 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( + transitionBuilder: (child, animation) => FadeTransition( opacity: animation, child: SlideTransition( position: Tween( begin: const Offset(0, .2), - end: const Offset(0, 0), + end: Offset.zero, ).animate(animation), child: child, ), @@ -460,11 +507,14 @@ class _MessageInputBarState extends State { ), ), StreamBuilder( - stream: _recordingDurationStream(), - builder: (context, snapshot) { - return Text("Recording: ${snapshot.data?.format()}", - style: Theme.of(context).textTheme.titleMedium); - }), + stream: _recordingDurationStream(), + builder: (context, snapshot) { + return Text( + "Recording: ${snapshot.data?.format()}", + style: Theme.of(context).textTheme.titleMedium, + ); + }, + ), ], ), ) @@ -476,7 +526,7 @@ class _MessageInputBarState extends State { ), AnimatedSwitcher( duration: const Duration(milliseconds: 200), - transitionBuilder: (Widget child, Animation animation) => FadeTransition( + transitionBuilder: (child, animation) => FadeTransition( opacity: animation, child: RotationTransition( turns: Tween(begin: 0.5, end: 1).animate(animation), @@ -502,7 +552,7 @@ class _MessageInputBarState extends State { _loadedFiles.clear(); }); try { - for (int i = 0; i < toSend.length; i++) { + for (var i = 0; i < toSend.length; i++) { final totalProgress = i / toSend.length; final file = toSend[i]; if (file.$1 == FileType.image) { @@ -517,12 +567,14 @@ class _MessageInputBarState extends State { ); } else { await sendRawFileMessage( - cHolder.apiClient, - mClient, - file.$2, - settings.machineId.valueOrDefault, - (progress) => setState( - () => _sendProgress = totalProgress + progress * 1 / toSend.length)); + cHolder.apiClient, + mClient, + file.$2, + settings.machineId.valueOrDefault, + (progress) => setState( + () => _sendProgress = totalProgress + progress * 1 / toSend.length, + ), + ); } } setState(() { @@ -555,39 +607,35 @@ class _MessageInputBarState extends State { onTapDown: widget.disabled ? null : (_) async { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Sorry, this feature is not yet available"))); - return; - // 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", - // const RecordConfig( - // numChannels: 1, - // sampleRate: 44100, - // encoder: AudioEncoder.wav)); - // setState(() { - // _isRecording = true; - // }); + unawaited(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", + const RecordConfig( + numChannels: 1, + sampleRate: 44100, + encoder: AudioEncoder.wav, + ), + ); + setState(() { + _isRecording = true; + }); }, child: IconButton( icon: const Icon(Icons.mic_outlined), diff --git a/lib/widgets/worlds/world_view.dart b/lib/widgets/worlds/world_view.dart index 0afcff2..b4b2c22 100644 --- a/lib/widgets/worlds/world_view.dart +++ b/lib/widgets/worlds/world_view.dart @@ -114,10 +114,10 @@ class _WorldViewState extends State { ), Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8), - child: widget.world.formattedDescription.isEmpty + child: widget.world.formattedDescription?.isEmpty ?? true ? Text("No description", style: Theme.of(context).textTheme.labelLarge) : FormattedText( - widget.world.formattedDescription, + widget.world.formattedDescription!, style: Theme.of(context).textTheme.labelLarge?.apply(fontStyle: FontStyle.italic), ), ), From 32970e0ee5fdcc49d95158a8cc0d38f88f99aa50 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Sun, 27 Apr 2025 12:55:07 +0200 Subject: [PATCH 2/5] Rename and move some things around --- lib/apis/record_api.dart | 244 ++++++++------------------------- lib/models/records/record.dart | 4 +- 2 files changed, 62 insertions(+), 186 deletions(-) diff --git a/lib/apis/record_api.dart b/lib/apis/record_api.dart index d18d362..8c08404 100644 --- a/lib/apis/record_api.dart +++ b/lib/apis/record_api.dart @@ -102,56 +102,14 @@ class RecordApi { return PreprocessStatus.fromMap(body); } - static Future _beginAssetUpload(ApiClient client, {required AssetManifest manifest}) async { + static Future announceAssetUpload(ApiClient client, {required AssetManifest manifest}) async { final response = await client.post("/users/${client.userId}/assets/${manifest.hash}/upload?size=${manifest.bytes}"); client.checkResponse(response); final body = jsonDecode(response.body); return AssetUploadData.fromMap(body); } - static Future _directAssetUpload({required AssetUploadData uploadData, required Uint8List assetData}) async { - final request = http.MultipartRequest( - "PUT", - Uri.parse(uploadData.uploadEndpoint), - ) - ..files.add( - http.MultipartFile.fromBytes( - "file", - assetData, - filename: uploadData.hash, - contentType: MediaType.parse("multipart/form-data"), - ), - ) - ..headers.addAll( - { - "Upload-Key": uploadData.uploadKey, - "Upload-Timestamp": uploadData.createdOn.toIso8601String(), - }, - ); - final response = await request.send(); - final bodyBytes = await response.stream.toBytes(); - ApiClient.checkResponseCode(http.Response.bytes(bodyBytes, response.statusCode)); - } - - static Future _chunkedAssetUpload({ - required AssetUploadData uploadData, - required int chunkIndex, - required Uint8List chunkData, - }) async { - final response = await http.put( - Uri.parse(uploadData.uploadEndpoint), - headers: { - "Upload-Key": uploadData.uploadKey, - "Part-Number": chunkIndex.toString(), - }, - body: chunkData, - ); - ApiClient.checkResponseCode(response); - final body = jsonDecode(response.body); - return CloudflareChunkResult.fromMap(body); - } - - static Future _finalizeUpload(ApiClient client, {required AssetUploadData uploadData}) async { + static Future finalizeUpload(ApiClient client, {required AssetUploadData uploadData}) async { final response = await client.patch( "/users/${client.userId}/assets/${uploadData.hash}/upload/${uploadData.id}", body: jsonEncode(uploadData.toMap()), @@ -159,7 +117,7 @@ class RecordApi { client.checkResponse(response); } - static Future _getUploadInfo(ApiClient client, {required AssetUploadData uploadData}) async { + static Future getUploadInfo(ApiClient client, {required AssetUploadData uploadData}) async { final response = await client.patch( "/users/${client.userId}/assets/${uploadData.hash}/upload/${uploadData.id}", body: jsonEncode(uploadData.toMap()), @@ -169,141 +127,34 @@ class RecordApi { return AssetUploadData.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 ResoniteDBAsset 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, bool ensureFolder = false}) async { - final response = await client.put( - "/users/${client.userId}/records/${record.id}?ensureFolder=$ensureFolder", - body: jsonEncode(record.toMap()), + static Future _directAssetUpload({required AssetUploadData uploadData, required Uint8List assetData}) async { + final response = await http.put( + Uri.parse(uploadData.uploadEndpoint), + headers: { + "Upload-Key": uploadData.uploadKey, + "Upload-Timestamp": uploadData.createdOn.toIso8601String(), + }, + body: assetData, ); - client.checkResponse(response); - final body = jsonDecode(response.body); - return Record.fromMap(body); + ApiClient.checkResponseCode(response); } - static Future uploadAsset( - ApiClient client, { + static Future _chunkedAssetUpload({ required AssetUploadData uploadData, - required String filename, - required ResoniteDBAsset asset, - required Uint8List data, - void Function(double number)? progressCallback, - }) async { - for (var 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 ResoniteDBAsset 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 (var 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, + required int chunkIndex, + required Uint8List chunkData, }) 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.inventoryRoot(); - progressCallback?.call(.1); - final status = await tryPreprocessRecord(client, record: record); - final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded ?? false); - 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), + final response = await http.put( + Uri.parse(uploadData.uploadEndpoint), + headers: { + "Upload-Key": uploadData.uploadKey, + "Part-Number": chunkIndex.toString(), + }, + body: chunkData, ); - await upsertRecord(client, record: record); - progressCallback?.call(1); - return record; + ApiClient.checkResponseCode(response); + final body = jsonDecode(response.body); + return CloudflareChunkResult.fromMap(body); } static Future> _uploadAsset( @@ -313,7 +164,7 @@ class RecordApi { void Function(double progress)? progressCallback, }) async { final chunkedUploads = []; - final uploadData = await _beginAssetUpload(client, manifest: manifest); + final uploadData = await announceAssetUpload(client, manifest: manifest); if (uploadData.isDirectUpload) { await _directAssetUpload(uploadData: uploadData, assetData: data); } else { @@ -330,11 +181,35 @@ class RecordApi { } chunkedUploads.add(uploadData); } - await _finalizeUpload(client, uploadData: uploadData); + await finalizeUpload(client, uploadData: uploadData); progressCallback?.call(1); return chunkedUploads; } + static Future createRecord(ApiClient client, {required Record record, bool ensureFolder = false}) async { + final response = await client.put( + "/users/${client.userId}/records/${record.id}?ensureFolder=$ensureFolder", + body: jsonEncode(record.toMap()), + ); + client.checkResponse(response); + final body = jsonDecode(response.body); + return Record.fromMap(body); + } + + 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); + + progressCallback?.call(1); + return Record.inventoryRoot(); + } + static Future uploadVoiceClip( ApiClient client, { required File voiceClip, @@ -343,17 +218,18 @@ class RecordApi { }) async { progressCallback?.call(0); - final filename = basenameWithoutExtension(voiceClip.path); final bytes = voiceClip.readAsBytesSync(); final voiceManifest = AssetManifest.fromData(bytes); final assetManifests = {voiceManifest: bytes}; final record = Record.local( - name: "Voice Message", - recordType: RecordType.audio, - ownerId: client.userId, - assetManifest: assetManifests.keys.toList(), - assetUri: "resdb:///${voiceManifest.hash}.${extension(voiceClip.path)}"); + name: "Voice Message", + recordType: RecordType.audio, + ownerId: client.userId, + assetManifest: assetManifests.keys.toList(), + assetUri: "resdb:///${voiceManifest.hash}.${extension(voiceClip.path)}", + tags: ["message_item"], + ); var preproc = await preprocessRecord(client, record: record); while (preproc.state != RecordPreprocessState.success) { @@ -382,7 +258,7 @@ class RecordApi { for (var uploadData in chunkedUploads) { while (uploadData.uploadState != UploadState.uploaded) { - uploadData = await _getUploadInfo(client, uploadData: uploadData); + uploadData = await getUploadInfo(client, uploadData: uploadData); if (uploadData.uploadState == UploadState.failed) { throw "Upload failed: Unknown cloud error when combining asset chunks"; } @@ -390,7 +266,7 @@ class RecordApi { } } - await upsertRecord(client, record: record); + await createRecord(client, record: record); progressCallback?.call(1); return record; diff --git a/lib/models/records/record.dart b/lib/models/records/record.dart index 0dfcc46..9151abf 100644 --- a/lib/models/records/record.dart +++ b/lib/models/records/record.dart @@ -105,7 +105,6 @@ class Record { }) : formattedName = FormatNode.fromText(name), formattedDescription = FormatNode.fromText(description); - factory Record.fromMap(Map map) { return Record( id: map["id"] ?? "0", @@ -140,6 +139,7 @@ class Record { required String ownerId, required List assetManifest, String assetUri = "", + List tags = const [], }) { final now = DateTime.now(); return Record( @@ -149,7 +149,7 @@ class Record { assetUri: assetUri, name: name, description: null, - tags: [], + tags: tags, recordType: recordType, thumbnailUri: null, isPublic: true, From 9668a1049a7d312de86e5d8bf66fffaa03a7f687 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Sat, 4 Jul 2026 15:37:34 +0200 Subject: [PATCH 3/5] Fix invalid upload timestamp causing signature error on asset upload --- lib/apis/record_api.dart | 15 ++-- lib/models/records/asset_upload_data.dart | 8 +- lib/models/records/record.dart | 22 ++++- lib/widgets/messages/message_input_bar.dart | 89 ++++++++++++--------- pubspec.lock | 74 ++++++++--------- pubspec.yaml | 3 +- 6 files changed, 119 insertions(+), 92 deletions(-) diff --git a/lib/apis/record_api.dart b/lib/apis/record_api.dart index dbb0403..40a2ef9 100644 --- a/lib/apis/record_api.dart +++ b/lib/apis/record_api.dart @@ -1,13 +1,11 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:math'; import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'package:http_parser/http_parser.dart'; import 'package:path/path.dart'; import 'package:recon/clients/api_client.dart'; import 'package:recon/models/records/asset_chunk.dart'; @@ -16,10 +14,8 @@ import 'package:recon/models/records/asset_digest.dart'; import 'package:recon/models/records/asset_manifest.dart'; import 'package:recon/models/records/asset_upload_data.dart'; import 'package:recon/models/records/cloudflare_chunk_result.dart'; -import 'package:recon/models/records/json_template.dart'; import 'package:recon/models/records/preprocess_status.dart'; import 'package:recon/models/records/record.dart'; -import 'package:recon/models/records/resonite_db_asset.dart'; import 'package:recon/models/records/search_sort.dart'; class RecordApi { @@ -133,7 +129,7 @@ class RecordApi { Uri.parse(uploadData.uploadEndpoint), headers: { "Upload-Key": uploadData.uploadKey, - "Upload-Timestamp": uploadData.createdOn.toIso8601String(), + "Upload-Timestamp": uploadData.createdOn, }, body: assetData, ); @@ -215,6 +211,7 @@ class RecordApi { ApiClient client, { required File voiceClip, required String machineId, + String? messageId, void Function(double progress)? progressCallback, }) async { progressCallback?.call(0); @@ -228,8 +225,12 @@ class RecordApi { recordType: RecordType.audio, ownerId: client.userId, assetManifest: assetManifests.keys.toList(), - assetUri: "resdb:///${voiceManifest.hash}.${extension(voiceClip.path)}", - tags: ["message_item"], + assetUri: "resdb:///${voiceManifest.hash}${extension(voiceClip.path)}", + tags: [ + "message_item", + if (messageId != null) "message_id:$messageId", + ], + lastModifyingMachineId: machineId, ); var preproc = await preprocessRecord(client, record: record); diff --git a/lib/models/records/asset_upload_data.dart b/lib/models/records/asset_upload_data.dart index ce5aef3..6514526 100644 --- a/lib/models/records/asset_upload_data.dart +++ b/lib/models/records/asset_upload_data.dart @@ -33,7 +33,7 @@ class AssetUploadData { final bool isDirectUpload; final int maxUploadConcurrency; final List chunks; - final DateTime createdOn; + final String createdOn; // This is a string to prevent truncation of millisecond data which breaks asset upload HMAC checks const AssetUploadData({ required this.hash, @@ -66,7 +66,7 @@ class AssetUploadData { isDirectUpload: map["isDirectUpload"], maxUploadConcurrency: map["maxUploadConcurrency"], chunks: (map["chunks"] as List?)?.cast>().map(AssetChunk.fromMap).toList() ?? [], - createdOn: DateTime.parse(map["createdOn"]).toUtc(), + createdOn: map["createdOn"], ); Map toMap() => { @@ -82,9 +82,7 @@ class AssetUploadData { "uploadEndpoint": uploadEndpoint, "isDirectUpload": isDirectUpload, "maxUploadConcurrency": maxUploadConcurrency, - "chunks": chunks.map( - (e) => e.toMap(), - ), + "chunks": chunks.map((e) => e.toMap()).toList(), "createdOn": createdOn, }; } diff --git a/lib/models/records/record.dart b/lib/models/records/record.dart index 9151abf..6c76c7a 100644 --- a/lib/models/records/record.dart +++ b/lib/models/records/record.dart @@ -50,6 +50,7 @@ class Record { isDeleted: false, isReadOnly: true, firstPublishTime: DateTimeX.epoch, + rootRecordId: null, ); final String id; @@ -76,6 +77,7 @@ class Record { final int visits; final int rating; final int randomOrder; + final String? rootRecordId; final List assetManifest; Record({ @@ -102,6 +104,7 @@ class Record { required this.isDeleted, required this.isReadOnly, required this.firstPublishTime, + required this.rootRecordId, }) : formattedName = FormatNode.fromText(name), formattedDescription = FormatNode.fromText(description); @@ -130,6 +133,7 @@ class Record { isDeleted: map["isDeleted"] ?? false, isReadOnly: map["isReadOnly"] ?? false, firstPublishTime: DateTime.tryParse(map["firstPublishTime"] ?? ""), + rootRecordId: map["rootRecordId"], ); } @@ -140,6 +144,7 @@ class Record { required List assetManifest, String assetUri = "", List tags = const [], + String? lastModifyingMachineId, }) { final now = DateTime.now(); return Record( @@ -152,17 +157,17 @@ class Record { tags: tags, recordType: recordType, thumbnailUri: null, - isPublic: true, - isListed: true, + isPublic: false, + isListed: false, isForPatreons: false, lastModificationTime: now, creationTime: now, assetManifest: assetManifest, version: RecordVersion( - globalVersion: 1, + globalVersion: 0, localVersion: 1, lastModifyingUserId: ownerId, - lastModifyingMachineId: null, + lastModifyingMachineId: lastModifyingMachineId, ), visits: 0, rating: 0, @@ -171,6 +176,7 @@ class Record { isDeleted: false, isReadOnly: false, firstPublishTime: null, + rootRecordId: null, ); } @@ -236,6 +242,7 @@ class Record { int? rating, int? randomOrder, List? assetManifest, + String? Function()? rootRecordId, }) => Record( id: id ?? this.id, @@ -261,6 +268,7 @@ class Record { isDeleted: isDeleted ?? this.isDeleted, isReadOnly: isReadOnly ?? this.isReadOnly, firstPublishTime: firstPublishTime == null ? this.firstPublishTime : firstPublishTime(), + rootRecordId: rootRecordId == null ? this.rootRecordId : rootRecordId(), ); Map toMap() { @@ -288,6 +296,12 @@ class Record { "isDeleted": isDeleted, "isReadOnly": isReadOnly, "firstPublishTime": firstPublishTime?.toUtc().toIso8601String(), + "rootRecordId": rootRecordId, + "submissions": null, // no idea what this is yet + "migrationMetadata": null, // don't need to care about this + "IsValidOwnerId": true, + "IsValidRecordId": true, + "neosDBmanifest": null, // legacy stuff I assume }; } diff --git a/lib/widgets/messages/message_input_bar.dart b/lib/widgets/messages/message_input_bar.dart index 4f5c27a..1c5b2b0 100644 --- a/lib/widgets/messages/message_input_bar.dart +++ b/lib/widgets/messages/message_input_bar.dart @@ -6,6 +6,8 @@ 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:recon/apis/record_api.dart'; import 'package:recon/auxiliary.dart'; @@ -17,6 +19,7 @@ import 'package:recon/models/message.dart'; import 'package:recon/models/users/friend.dart'; import 'package:recon/widgets/messages/message_attachment_list.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}); @@ -101,14 +104,16 @@ class _MessageInputBarState extends State { String machineId, void Function(double progress) progressCallback, ) async { + final msgId = Message.generateId(); final record = await RecordApi.uploadVoiceClip( client, voiceClip: file, machineId: machineId, + messageId: msgId, progressCallback: progressCallback, ); final message = Message( - id: record.extractMessageId() ?? Message.generateId(), + id: msgId, recipientId: widget.recipient.contactUserId, senderId: client.userId, type: MessageType.sound, @@ -258,7 +263,14 @@ class _MessageInputBarState extends State { }); if (await _recorder.isRecording()) { - final recording = await _recorder.stop(); + String? recording; + try { + recording = await _recorder.stop(); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Failed to finalize recording: $e"))); + } + } if (recording == null) return; final file = File(recording); @@ -268,9 +280,7 @@ class _MessageInputBarState extends State { }); final apiClient = cHolder.apiClient; try { - await sendVoiceMessage( - apiClient, mClient, file, cHolder.settingsClient.currentSettings.machineId.valueOrDefault, - (progress) { + await sendVoiceMessage(apiClient, mClient, file, cHolder.settingsClient.currentSettings.machineId.valueOrDefault, (progress) { setState(() { _sendProgress = progress; }); @@ -620,40 +630,43 @@ class _MessageInputBarState extends State { onTapDown: widget.disabled ? null : (_) async { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Sorry, this feature is not yet available")), - ); - return; - // 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; - // } + unawaited(HapticFeedback.vibrate()); + bool hadToAsk; + try { + hadToAsk = await Permission.microphone.isDenied; + } catch (_) { + hadToAsk = false; + } + 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", - // const RecordConfig( - // numChannels: 1, - // sampleRate: 44100, - // encoder: AudioEncoder.wav)); - // setState(() { - // _isRecording = true; - // }); + final dir = await getTemporaryDirectory(); + await _recorder.start( + path: "${dir.path}/A-${const Uuid().v4()}.wav", + const RecordConfig( + numChannels: 1, + sampleRate: 44100, + encoder: AudioEncoder.wav, + ), + ); + setState(() { + _isRecording = true; + }); }, child: IconButton( icon: const Icon(Icons.mic_outlined), diff --git a/pubspec.lock b/pubspec.lock index d366f2a..a7d8cd4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: audio_session - sha256: "7217b229db57cc4dc577a8abb56b7429a5a212b978517a5be578704bfe5e568b" + sha256: f9e7711a0e24ca8b40f5d7ac374c3c6e55016f3157912badd18199cdbbca5c4d url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.4" background_downloader: dependency: "direct main" description: @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: camera_android_camerax - sha256: e20c1e92ce6797d9ae9b1db1e09a4c1039a04827d0b24985f5da3840b96948ac + sha256: "50c3fd81228826635af4875330febb193c74a2e12a4d19c1b95e2a67f0017bb3" url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "0.7.3" camera_avfoundation: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" url: "https://pub.dev" source: hosted - version: "0.3.5+2" + version: "0.3.5+4" crypto: dependency: "direct main" description: @@ -476,10 +476,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" + sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" image_picker_android: dependency: transitive description: @@ -540,10 +540,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" jni: dependency: transitive description: @@ -564,10 +564,10 @@ packages: dependency: "direct main" description: name: just_audio - sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908" + sha256: e60aa97b233ddea025e13dfac59195207f528fa5e9e4a4a49a8c0766701e5fe6 url: "https://pub.dev" source: hosted - version: "0.10.5" + version: "0.10.6" just_audio_media_kit: dependency: "direct main" description: @@ -724,10 +724,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852" + sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.2.0" package_info_plus_platform_interface: dependency: transitive description: @@ -772,10 +772,10 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_platform_interface: dependency: transitive description: @@ -900,28 +900,28 @@ packages: dependency: "direct main" description: name: record - sha256: d28ec249ee4af6753a68a7a5077d6a2eee71e38dc61a241724aded53263274ba + sha256: "82539d1372e23cf51375fdfcba084f39912bcbf9a953b75d56596691f8f11c0f" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.1.1" record_android: dependency: transitive description: name: record_android - sha256: "6722be803d8b2a0945112d69cfbda1391970c5cff2cb53c16afd307a6b7ef0c4" + sha256: "28f1108626a190e249b01ffa9f639070e31e5157474b64a5ae380bf36aec9559" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" record_ios: dependency: transitive description: name: record_ios - sha256: de6f660b02c2a909c963d5c3c929fdf100b03cea9e5552599d0e5ae1b7d1a89d + sha256: "21d189f49a598af4697dac4cc9e48389ac0a1fb3e916622b5504a58d9b96313e" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" record_linux: - dependency: transitive + dependency: "direct main" description: name: record_linux sha256: "305526af0560866a0cc4219aa3726ef8541f819e14bd5f65b73ffdb7dc5911be" @@ -932,10 +932,10 @@ packages: dependency: transitive description: name: record_macos - sha256: "9c29a7efdace0662db2aa7284873d49f8b48992432db3b5f0d38cbe2da8eecde" + sha256: ced7495abf3d683e8a7dbe8fc96df8ea5722837a26f922b7cb7c5de11661bb46 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" record_platform_interface: dependency: transitive description: @@ -956,18 +956,18 @@ packages: dependency: transitive description: name: record_web - sha256: "9d2d43162afff63d8608eb09c2982b9c6898dd8fbf5b05395ca7d08305b6db40" + sha256: "7d75ed681b5bf40c3a9b51b6c105fe53705f752284d859d5f030ab5a2bfd49cf" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" record_windows: dependency: transitive description: name: record_windows - sha256: a1db1c0b996acd4161a93cc412b81a2a09a4fd7161c6353f32bf97683003b52f + sha256: "9cedfacee553a02b48977a5e1d74bf08a36cc5647eba56fb83e57da20552f443" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.2" rxdart: dependency: transitive description: @@ -980,18 +980,18 @@ packages: dependency: transitive description: name: safe_local_storage - sha256: "287ea1f667c0b93cdc127dccc707158e2d81ee59fba0459c31a0c7da4d09c755" + sha256: "7483b3d5e8976f0bd263647c03b96131ee8e43f48b56fa8a8ec459e8515d74b0" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" share_plus: dependency: "direct main" description: name: share_plus - sha256: a857d8b1479250aff6b57a51b2c02d31ca05848d441817c43f1640c885c286c0 + sha256: "9eee8283462d91a7a1c8bdb67d08874abd75a2f8fae3bc0ca033035e375fb3d8" url: "https://pub.dev" source: hosted - version: "13.1.0" + version: "13.2.0" share_plus_platform_interface: dependency: transitive description: @@ -1041,10 +1041,10 @@ packages: dependency: transitive description: name: sqflite_darwin - sha256: "164a5d73ab87a134566057219988bafde837029a64264e61f1f04376ef3cfcd2" + sha256: c86ca18b8f666bbf903924687fe21cc16fc385d086005067e26619ca530bef9f url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.3+1" sqflite_platform_interface: dependency: transitive description: @@ -1113,10 +1113,10 @@ packages: dependency: transitive description: name: timezone - sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b" + sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb" url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.11.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d38f556..668d729 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,7 +55,7 @@ dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0 file_picker: ^12.0.0-beta.7 - record: ^7.1.0 + record: ^7.1.1 camera: ^0.12.0+1 path_provider: ^2.1.5 crypto: ^3.0.7 @@ -92,6 +92,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/ + - assets/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware From 068a1439cb58059ef03d4278b140fcfd1a0571ec Mon Sep 17 00:00:00 2001 From: Nutcake Date: Sat, 4 Jul 2026 15:59:11 +0200 Subject: [PATCH 4/5] Fix UploadState enum not generating valid strings --- lib/models/records/asset_upload_data.dart | 2 +- pubspec.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/records/asset_upload_data.dart b/lib/models/records/asset_upload_data.dart index 6514526..6fc2331 100644 --- a/lib/models/records/asset_upload_data.dart +++ b/lib/models/records/asset_upload_data.dart @@ -16,7 +16,7 @@ enum UploadState { } @override - String toString() => toBeginningOfSentenceCase(super.toString()); + String toString() => toBeginningOfSentenceCase(name); } class AssetUploadData { diff --git a/pubspec.lock b/pubspec.lock index a7d8cd4..6f8e720 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -921,7 +921,7 @@ packages: source: hosted version: "2.1.1" record_linux: - dependency: "direct main" + dependency: transitive description: name: record_linux sha256: "305526af0560866a0cc4219aa3726ef8541f819e14bd5f65b73ffdb7dc5911be" From 0d45d31cd3bb8a16ff8592ac1162839f81caa049 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Sun, 5 Jul 2026 14:15:34 +0200 Subject: [PATCH 5/5] Implement image upload --- lib/apis/record_api.dart | 157 +- lib/clients/api_client.dart | 106 +- lib/main.dart | 94 +- lib/models/records/json_template.dart | 3657 +++++-------------- lib/widgets/messages/message_asset.dart | 1 + lib/widgets/messages/message_input_bar.dart | 271 +- pubspec.lock | 62 +- pubspec.yaml | 7 +- test/widget_test.dart | 2 + 9 files changed, 1232 insertions(+), 3125 deletions(-) diff --git a/lib/apis/record_api.dart b/lib/apis/record_api.dart index 40a2ef9..84c9825 100644 --- a/lib/apis/record_api.dart +++ b/lib/apis/record_api.dart @@ -3,20 +3,22 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:bson/bson.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:recon/clients/api_client.dart'; import 'package:recon/models/records/asset_chunk.dart'; import 'package:recon/models/records/asset_diff.dart'; -import 'package:recon/models/records/asset_digest.dart'; import 'package:recon/models/records/asset_manifest.dart'; import 'package:recon/models/records/asset_upload_data.dart'; import 'package:recon/models/records/cloudflare_chunk_result.dart'; +import 'package:recon/models/records/json_template.dart'; import 'package:recon/models/records/preprocess_status.dart'; import 'package:recon/models/records/record.dart'; import 'package:recon/models/records/search_sort.dart'; +import 'package:image/image.dart' as img; class RecordApi { static Future getUserRecord(ApiClient client, {required String recordId, String? user}) async { @@ -63,11 +65,7 @@ class RecordApi { return body.map((e) => Record.fromMap(e)).toList(); } - static Future> getGroupRecordsAt( - ApiClient client, { - required String path, - required String groupId, - }) async { + static Future> getGroupRecordsAt(ApiClient client, {required String path, required String groupId}) async { final response = await client.get("/groups/$groupId/records?path=$path"); client.checkResponse(response); final body = jsonDecode(response.body) as List; @@ -87,13 +85,8 @@ class RecordApi { 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}", - ); + 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); @@ -107,48 +100,24 @@ class RecordApi { } static Future finalizeUpload(ApiClient client, {required AssetUploadData uploadData}) async { - final response = await client.patch( - "/users/${client.userId}/assets/${uploadData.hash}/upload/${uploadData.id}", - body: jsonEncode(uploadData.toMap()), - ); + final response = await client.patch("/users/${client.userId}/assets/${uploadData.hash}/upload/${uploadData.id}", body: jsonEncode(uploadData.toMap())); client.checkResponse(response); } static Future getUploadInfo(ApiClient client, {required AssetUploadData uploadData}) async { - final response = await client.patch( - "/users/${client.userId}/assets/${uploadData.hash}/upload/${uploadData.id}", - body: jsonEncode(uploadData.toMap()), - ); + final response = await client.patch("/users/${client.userId}/assets/${uploadData.hash}/upload/${uploadData.id}", body: jsonEncode(uploadData.toMap())); client.checkResponse(response); final body = jsonDecode(response.body); return AssetUploadData.fromMap(body); } static Future _directAssetUpload({required AssetUploadData uploadData, required Uint8List assetData}) async { - final response = await http.put( - Uri.parse(uploadData.uploadEndpoint), - headers: { - "Upload-Key": uploadData.uploadKey, - "Upload-Timestamp": uploadData.createdOn, - }, - body: assetData, - ); + final response = await http.put(Uri.parse(uploadData.uploadEndpoint), headers: {"Upload-Key": uploadData.uploadKey, "Upload-Timestamp": uploadData.createdOn}, body: assetData); ApiClient.checkResponseCode(response); } - static Future _chunkedAssetUpload({ - required AssetUploadData uploadData, - required int chunkIndex, - required Uint8List chunkData, - }) async { - final response = await http.put( - Uri.parse(uploadData.uploadEndpoint), - headers: { - "Upload-Key": uploadData.uploadKey, - "Part-Number": chunkIndex.toString(), - }, - body: chunkData, - ); + static Future _chunkedAssetUpload({required AssetUploadData uploadData, required int chunkIndex, required Uint8List chunkData}) async { + final response = await http.put(Uri.parse(uploadData.uploadEndpoint), headers: {"Upload-Key": uploadData.uploadKey, "Part-Number": chunkIndex.toString()}, body: chunkData); ApiClient.checkResponseCode(response); final body = jsonDecode(response.body); return CloudflareChunkResult.fromMap(body); @@ -167,11 +136,7 @@ class RecordApi { } else { final chunks = data.slices(uploadData.chunkSize); for (final (cIdx, chunk) in chunks.indexed) { - final chunkResult = await _chunkedAssetUpload( - uploadData: uploadData, - chunkIndex: cIdx + 1, - chunkData: Uint8List.fromList(chunk), - ); + final chunkResult = await _chunkedAssetUpload(uploadData: uploadData, chunkIndex: cIdx + 1, chunkData: Uint8List.fromList(chunk)); final chunkInfo = AssetChunk(index: cIdx, key: chunkResult.eTag); uploadData.chunks.add(chunkInfo); progressCallback?.call(cIdx / chunks.length); @@ -184,27 +149,70 @@ class RecordApi { } static Future createRecord(ApiClient client, {required Record record, bool ensureFolder = false}) async { - final response = await client.put( - "/users/${client.userId}/records/${record.id}?ensureFolder=$ensureFolder", - body: jsonEncode(record.toMap()), - ); + final response = await client.put("/users/${client.userId}/records/${record.id}?ensureFolder=$ensureFolder", body: jsonEncode(record.toMap())); client.checkResponse(response); final body = jsonDecode(response.body); return Record.fromMap(body); } - static Future uploadImage( - ApiClient client, { - required File image, - required String machineId, - void Function(double progress)? progressCallback, - }) async { + static Future uploadImage(ApiClient client, {required File image, required String machineId, String? messageId, 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 imageData = await image.readAsBytes(); + final cmd = img.Command() + ..decodeImage(imageData) + ..copyResize(width: 512) + ..encodeJpg(quality: 90); + final res = await cmd.executeThread(); + final thumbnail = res.outputBytes!; + final imageManifest = AssetManifest.fromData(imageData); + final thumbnailManifest = AssetManifest.fromData(thumbnail); + final assetUri = "resdb:///${imageManifest.hash}${extension(image.path)}"; + final dataTree = JsonTemplate.image(imageResDb: assetUri, machineId: machineId); + // Prefix FrDT header + final bson = Uint8List.fromList([70, 114, 68, 84, 0, 0, 0, 0] + BsonCodec.serialize(dataTree).byteList); + final bsonManifest = AssetManifest.fromData(bson); + final assetManifest = {imageManifest: imageData, thumbnailManifest: thumbnail, bsonManifest: bson}; + + final record = Record.local( + name: "Photo from ReCon", + recordType: RecordType.object, + ownerId: client.userId, + assetManifest: assetManifest.keys.toList(), + assetUri: assetUri, + tags: ["holder", "photo", "camera_photo", "texture_asset:$assetUri", "message_item", "message_id:$messageId"], + lastModifyingMachineId: machineId, + ).copyWith(thumbnailUri: () => "resdb:///${thumbnailManifest.hash}${extension(image.path)}"); + + var preproc = await preprocessRecord(client, record: record); + while (preproc.state != RecordPreprocessState.success) { + preproc = await getPreprocessStatus(client, preprocessStatus: preproc); + if (preproc.state == RecordPreprocessState.failed) { + throw "Failed to upload asset: ${preproc.failReason}"; + } + progressCallback?.call(preproc.progress * 0.2); + await Future.delayed(const Duration(seconds: 1)); + } + final chunkedUploads = []; + for (final diff in preproc.resultDiffs.where((element) => element.state == Diff.added && !(element.isUploaded ?? false))) { + final dataKey = assetManifest.keys.firstWhere((element) => element.hash == diff.hash); + final chunked = await _uploadAsset(client, manifest: diff, data: assetManifest[dataKey]!, progressCallback: (progress) => progressCallback?.call(0.2 + progress * 0.9)); + chunkedUploads.addAll(chunked); + } + + for (var uploadData in chunkedUploads) { + while (uploadData.uploadState != UploadState.uploaded) { + uploadData = await getUploadInfo(client, uploadData: uploadData); + if (uploadData.uploadState == UploadState.failed) { + throw "Upload failed: Unknown cloud error when combining asset chunks"; + } + await Future.delayed(const Duration(milliseconds: 1500)); + } + } + + await createRecord(client, record: record); progressCallback?.call(1); - return Record.inventoryRoot(); + return record; } static Future uploadVoiceClip( @@ -226,10 +234,7 @@ class RecordApi { ownerId: client.userId, assetManifest: assetManifests.keys.toList(), assetUri: "resdb:///${voiceManifest.hash}${extension(voiceClip.path)}", - tags: [ - "message_item", - if (messageId != null) "message_id:$messageId", - ], + tags: ["message_item", if (messageId != null) "message_id:$messageId"], lastModifyingMachineId: machineId, ); @@ -243,18 +248,9 @@ class RecordApi { await Future.delayed(const Duration(seconds: 1)); } final chunkedUploads = []; - for (final diff in preproc.resultDiffs.where( - (element) => element.state == Diff.added && !(element.isUploaded ?? false), - )) { - final dataKey = assetManifests.keys.firstWhere( - (element) => element.hash == diff.hash, - ); - final chunked = await _uploadAsset( - client, - manifest: diff, - data: assetManifests[dataKey]!, - progressCallback: (progress) => progressCallback?.call(0.2 + progress * 0.9), - ); + for (final diff in preproc.resultDiffs.where((element) => element.state == Diff.added && !(element.isUploaded ?? false))) { + final dataKey = assetManifests.keys.firstWhere((element) => element.hash == diff.hash); + final chunked = await _uploadAsset(client, manifest: diff, data: assetManifests[dataKey]!, progressCallback: (progress) => progressCallback?.call(0.2 + progress * 0.9)); chunkedUploads.addAll(chunked); } @@ -274,12 +270,7 @@ class RecordApi { return record; } - static Future uploadRawFile( - ApiClient client, { - required File file, - required String machineId, - void Function(double progress)? progressCallback, - }) async { + static Future uploadRawFile(ApiClient client, {required File file, required String machineId, void Function(double progress)? progressCallback}) async { progressCallback?.call(0); progressCallback?.call(1); return Record.inventoryRoot(); diff --git a/lib/clients/api_client.dart b/lib/clients/api_client.dart index bea8744..783a36e 100644 --- a/lib/clients/api_client.dart +++ b/lib/clients/api_client.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; +import 'package:http/io_client.dart'; import 'package:logging/logging.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/models/authentication_data.dart'; import 'package:uuid/uuid.dart'; @@ -19,13 +22,14 @@ class ApiClient { static const String passwordKey = "password"; static const String uidKey = "uid"; - ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData; + ApiClient({required this._authenticationData, required this._pkgInfo}); final AuthenticationData _authenticationData; final Logger _logger = Logger("API"); final _logoutNotifier = EventNotifier(); - final http.Client _client = http.Client(); + late final _client = IOClient(HttpClient()..userAgent = "${_pkgInfo.appName}/${_pkgInfo.version}"); + final PackageInfo _pkgInfo; AuthenticationData get authenticationData => _authenticationData; @@ -35,32 +39,15 @@ class ApiClient { void addLogoutListener(VoidCallback listener) => _logoutNotifier.addListener(listener); - static Future tryLogin({ - required String username, - required String password, - bool rememberMe = true, - bool rememberPass = true, - String? oneTimePad, - }) async { + static Future tryLogin({required String username, required String password, bool rememberMe = true, bool rememberPass = true, String? oneTimePad}) async { final body = { (username.contains("@") ? "email" : "username"): username.trim(), - "authentication": { - r"$type": "password", - "password": password, - }, + "authentication": {r"$type": "password", "password": password}, "rememberMe": rememberMe, "secretMachineId": const Uuid().v4(), }; final uid = const Uuid().v4().replaceAll("-", ""); - final response = await http.post( - buildFullUri("/userSessions"), - headers: { - "Content-Type": "application/json", - "UID": uid, - if (oneTimePad != null) totpKey: oneTimePad, - }, - body: jsonEncode(body), - ); + final response = await http.post(buildFullUri("/userSessions"), headers: {"Content-Type": "application/json", "UID": uid, totpKey: ?oneTimePad}, body: jsonEncode(body)); if (response.statusCode == 403 && response.body == totpKey) { throw totpKey; } @@ -72,9 +59,7 @@ class ApiClient { (data["entity"] as Map)["uid"] = uid; final authData = AuthenticationData.fromMap(data); if (authData.isAuthenticated) { - const storage = FlutterSecureStorage( - aOptions: AndroidOptions(encryptedSharedPreferences: true), - ); + const storage = FlutterSecureStorage(aOptions: AndroidOptions.defaultOptions); await storage.write(key: userIdKey, value: authData.userId); await storage.write(key: machineIdKey, value: authData.secretMachineIdHash); await storage.write(key: tokenKey, value: authData.token); @@ -85,9 +70,7 @@ class ApiClient { } static Future tryCachedLogin() async { - const storage = FlutterSecureStorage( - aOptions: AndroidOptions(encryptedSharedPreferences: true), - ); + const storage = FlutterSecureStorage(aOptions: AndroidOptions.defaultOptions); var userId = await storage.read(key: userIdKey); final machineId = await storage.read(key: machineIdKey); final token = await storage.read(key: tokenKey); @@ -99,21 +82,9 @@ class ApiClient { } if (token != null) { - final response = await http.patch( - buildFullUri("/userSessions"), - headers: { - "Authorization": "res $userId:$token", - "UID": uid, - }, - ); + final response = await http.patch(buildFullUri("/userSessions"), headers: {"Authorization": "res $userId:$token", "UID": uid}); if (response.statusCode < 300) { - return AuthenticationData( - userId: userId, - token: token, - secretMachineIdHash: machineId, - isAuthenticated: true, - uid: uid, - ); + return AuthenticationData(userId: userId, token: token, secretMachineIdHash: machineId, isAuthenticated: true, uid: uid); } } @@ -131,9 +102,7 @@ class ApiClient { Future logout() async { //TODO: Fix messaging/hub clients not being disposed on logout - const storage = FlutterSecureStorage( - aOptions: AndroidOptions(encryptedSharedPreferences: true), - ); + const storage = FlutterSecureStorage(aOptions: AndroidOptions.defaultOptions); await storage.delete(key: userIdKey); await storage.delete(key: machineIdKey); await storage.delete(key: tokenKey); @@ -162,20 +131,16 @@ class ApiClient { static void checkResponseCode(http.Response response) { if (response.statusCode < 300) return; - final error = "${response.request?.method ?? "Unknown Method"}|${response.request?.url ?? "Unknown URL"}: ${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.isNotEmpty ? "|${response.body}" : ""})"; - - FlutterError.reportError( - FlutterErrorDetails( - exception: error, - stack: StackTrace.current, - ), - ); + final error = + "${response.request?.method ?? "Unknown Method"}|${response.request?.url ?? "Unknown URL"}: ${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.isNotEmpty ? "|${response.body}" : ""})"; + + FlutterError.reportError(FlutterErrorDetails(exception: error, stack: StackTrace.current)); throw error; } @@ -183,10 +148,23 @@ class ApiClient { static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}$path"); + Future _requestWrapper(Future Function() requestFunc) async { + int responseCode; + var attempts = 0; + http.Response response; + do { + await Future.delayed(Duration(seconds: attempts)); + response = await requestFunc(); + attempts++; + responseCode = response.statusCode; + } while (responseCode == 429 && attempts < 5); + return response; + } + Future get(String path, {Map? headers}) async { headers ??= {}; headers.addAll(authorizationHeader); - final response = await _client.get(buildFullUri(path), headers: headers); + final response = await _requestWrapper(() async => _client.get(buildFullUri(path), headers: headers)); _logger.info("GET $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}"); return response; } @@ -195,7 +173,7 @@ class ApiClient { headers ??= {}; headers["Content-Type"] = "application/json"; headers.addAll(authorizationHeader); - final response = await _client.post(buildFullUri(path), headers: headers, body: body); + final response = await _requestWrapper(() async => _client.post(buildFullUri(path), headers: headers, body: body)); _logger.info("PST $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}"); return response; } @@ -204,7 +182,7 @@ class ApiClient { headers ??= {}; headers["Content-Type"] = "application/json"; headers.addAll(authorizationHeader); - final response = await _client.put(buildFullUri(path), headers: headers, body: body); + final response = await _requestWrapper(() async => _client.put(buildFullUri(path), headers: headers, body: body)); _logger.info("PUT $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}"); return response; } @@ -212,7 +190,7 @@ class ApiClient { Future delete(String path, {Map? headers}) async { headers ??= {}; headers.addAll(authorizationHeader); - final response = await _client.delete(buildFullUri(path), headers: headers); + final response = await _requestWrapper(() async => _client.delete(buildFullUri(path), headers: headers)); _logger.info("DEL $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}"); return response; } @@ -221,7 +199,7 @@ class ApiClient { headers ??= {}; headers["Content-Type"] = "application/json"; headers.addAll(authorizationHeader); - final response = await _client.patch(buildFullUri(path), headers: headers, body: body); + final response = await _requestWrapper(() async => _client.patch(buildFullUri(path), headers: headers, body: body)); _logger.info("PAT $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}"); return response; } diff --git a/lib/main.dart b/lib/main.dart index 3f0bbbb..c1ae433 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,11 +36,7 @@ void main() async { log("Failed to initialize JustAudioMediaKit, audio features will be unavailable. Error: $e"); } SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - systemStatusBarContrastEnforced: true, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarDividerColor: Colors.transparent, - ), + const SystemUiOverlayStyle(systemStatusBarContrastEnforced: true, systemNavigationBarColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent), ); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: [SystemUiOverlay.top]); @@ -48,15 +44,13 @@ void main() async { await Hive.initFlutter(); final dateFormat = DateFormat.Hms(); - Logger.root.onRecord.listen( - (event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time), - ); + Logger.root.onRecord.listen((event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time)); Logger.root.level = Level.WARNING; final settingsClient = SettingsClient(); await settingsClient.loadSettings(); final newSettings = settingsClient.currentSettings.copyWith(machineId: settingsClient.currentSettings.machineId.valueOrDefault); await settingsClient.changeSettings(newSettings); // Save generated machineId to disk - + final packageInfo = await PackageInfo.fromPlatform(); var cachedAuth = AuthenticationData.unauthenticated(); try { cachedAuth = await ApiClient.tryCachedLogin(); @@ -64,14 +58,15 @@ void main() async { // Ignore } - runApp(ReCon(settingsClient: settingsClient, cachedAuthentication: cachedAuth)); + runApp(ReCon(settingsClient: settingsClient, cachedAuthentication: cachedAuth, packageInfo: packageInfo)); } class ReCon extends StatefulWidget { - const ReCon({required this.settingsClient, required this.cachedAuthentication, super.key}); + const ReCon({required this.settingsClient, required this.cachedAuthentication, required this.packageInfo, super.key}); final SettingsClient settingsClient; final AuthenticationData cachedAuthentication; + final PackageInfo packageInfo; @override State createState() => _ReConState(); @@ -120,10 +115,7 @@ class _ReConState extends State { await showDialog( context: navigator.overlay!.context, builder: (context) { - return UpdateNotifier( - remoteVersion: remoteSem, - localVersion: currentSem, - ); + return UpdateNotifier(remoteVersion: remoteSem, localVersion: currentSem); }, ); } @@ -156,10 +148,7 @@ class _ReConState extends State { // Workaround for issue https://github.com/material-foundation/flutter-packages/issues/582 // Dynamic color schemes do not generate new additional surface container colours so we have to do it manually - (ColorScheme light, ColorScheme dark) _generateDynamicColourSchemes( - ColorScheme lightDynamic, - ColorScheme darkDynamic, - ) { + (ColorScheme light, ColorScheme dark) _generateDynamicColourSchemes(ColorScheme lightDynamic, ColorScheme darkDynamic) { final lightBase = ColorScheme.fromSeed(seedColor: lightDynamic.primary); final darkBase = ColorScheme.fromSeed(seedColor: darkDynamic.primary, brightness: Brightness.dark); @@ -173,41 +162,39 @@ class _ReConState extends State { } List _extractAdditionalColours(ColorScheme scheme) => [ - scheme.surface, - scheme.surfaceDim, - scheme.surfaceBright, - scheme.surfaceContainerLowest, - scheme.surfaceContainerLow, - scheme.surfaceContainer, - scheme.surfaceContainerHigh, - scheme.surfaceContainerHighest, - ]; + scheme.surface, + scheme.surfaceDim, + scheme.surfaceBright, + scheme.surfaceContainerLowest, + scheme.surfaceContainerLow, + scheme.surfaceContainer, + scheme.surfaceContainerHigh, + scheme.surfaceContainerHighest, + ]; ColorScheme _insertAdditionalColours(ColorScheme scheme, List additionalColours) => scheme.copyWith( - surface: additionalColours[0], - surfaceDim: additionalColours[1], - surfaceBright: additionalColours[2], - surfaceContainerLowest: additionalColours[3], - surfaceContainerLow: additionalColours[4], - surfaceContainer: additionalColours[5], - surfaceContainerHigh: additionalColours[6], - surfaceContainerHighest: additionalColours[7], - ); + surface: additionalColours[0], + surfaceDim: additionalColours[1], + surfaceBright: additionalColours[2], + surfaceContainerLowest: additionalColours[3], + surfaceContainerLow: additionalColours[4], + surfaceContainer: additionalColours[5], + surfaceContainerHigh: additionalColours[6], + surfaceContainerHighest: additionalColours[7], + ); @override Widget build(BuildContext context) { return Phoenix( child: Builder( builder: (context) { - final apiClient = ApiClient(authenticationData: _authData) - ..addLogoutListener( - () { - setState(() { - _authData = AuthenticationData.unauthenticated(); - }); - Phoenix.rebirth(context); - }, - ); + final apiClient = ApiClient(authenticationData: _authData, pkgInfo: widget.packageInfo) + ..addLogoutListener(() { + setState(() { + _authData = AuthenticationData.unauthenticated(); + }); + Phoenix.rebirth(context); + }); return ClientHolder( settingsClient: widget.settingsClient, apiClient: apiClient, @@ -246,21 +233,12 @@ class _ReConState extends State { ), ), ChangeNotifierProvider( - create: (context) => SessionClient( - apiClient: clientHolder.apiClient, - settingsClient: clientHolder.settingsClient, - ), - ), - ChangeNotifierProvider( - create: (context) => InventoryClient( - apiClient: clientHolder.apiClient, - ), + create: (context) => SessionClient(apiClient: clientHolder.apiClient, settingsClient: clientHolder.settingsClient), ), + ChangeNotifierProvider(create: (context) => InventoryClient(apiClient: clientHolder.apiClient)), ], child: AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Theme.of(context).colorScheme.surfaceContainerHighest, - ), + value: SystemUiOverlayStyle(statusBarColor: Theme.of(context).colorScheme.surfaceContainerHighest), child: const Home(), ), ) diff --git a/lib/models/records/json_template.dart b/lib/models/records/json_template.dart index 9237aca..41d73c1 100644 --- a/lib/models/records/json_template.dart +++ b/lib/models/records/json_template.dart @@ -2,2799 +2,1000 @@ import 'package:path/path.dart'; import 'package:uuid/uuid.dart'; class JsonTemplate { - static const String thumbUrl = "resdb:///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": [] - }, + static Map image({required String imageResDb, required String machineId}) { + final data = + { "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(), + "[FrooxEngine]FrooxEngine.InventoryItem": 1, + "[FrooxEngine]FrooxEngine.PhotoMetadata": 1, + "[FrooxEngine]FrooxEngine.Grabbable": 2, + "[FrooxEngine]FrooxEngine.QuadMesh": 1, + "[FrooxEngine]FrooxEngine.BoxCollider": 1 + }, + "Object": { "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 - ] + "ID": "00000013-0000-0000-0000-000000000000", + "Data": [-0.03113400936126709, 0.07841096818447113, 0.1895841360092163] }, "Scale": { - "ID": const Uuid().v4(), - "Data": [ - 1.00000036, - 0.99999994, - 1.00000036 - ] + "ID": "00000015-0000-0000-0000-000000000000", + "Data": [1.0000001192092896, 1.000000238418579, 1.0000001192092896] }, - "OrderOffset": { - "ID": const Uuid().v4(), - "Data": 0 - }, - "ParentReference": const Uuid().v4(), "Children": [ { - "ID": const Uuid().v4(), - "Components": { - "ID": const Uuid().v4(), - "Data": [] + "Persistent-ID": "000000f0-0000-0000-0000-000000000000", + "Position": { + "Data": [ + -0.008691459894180298, -0.016768932342529297, -0.0020900964736938477 + ], + "ID": "000000f1-0000-0000-0000-000000000000" }, - "Name": { - "ID": const Uuid().v4(), - "Data": "FileVisual" + "Scale": { + "ID": "000000f3-0000-0000-0000-000000000000", + "Data": [0.25000008940696716, 0.25, 0.2500000298023224] }, - "Tag": { - "ID": const Uuid().v4(), - "Data": "" + "ParentReference": "000000f5-0000-0000-0000-000000000000", + "Children": [], + "ID": "00000017-0000-0000-0000-000000000000", + "Name": { + "ID": "000000ed-0000-0000-0000-000000000000", + "Data": "Photo" }, + "Tag": { "ID": "000000ee-0000-0000-0000-000000000000", "Data": null }, "Active": { - "ID": const Uuid().v4(), + "ID": "000000ef-0000-0000-0000-000000000000", "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(), + "ID": "000000f2-0000-0000-0000-000000000000", "Data": [ - 1.0, - 1.0, - 1.0 + 0.055005572736263275, -0.013591023162007332, 0.0007486905087716877, + 0.9983932971954346 ] }, "OrderOffset": { - "ID": const Uuid().v4(), + "ID": "000000f4-0000-0000-0000-000000000000", "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 - } - } + "Components": { + "ID": "00000018-0000-0000-0000-000000000000", + "Data": [ + { + "Type": 2, + "Data": { + "ID": "00000019-0000-0000-0000-000000000000", + "Enabled": { + "ID": "0000001c-0000-0000-0000-000000000000", + "Data": true }, - { - "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 - ] - } - } + "PreferredFormat": { + "ID": "00000023-0000-0000-0000-000000000000", + "Data": null + }, + "IsNormalMap": { + "ID": "00000026-0000-0000-0000-000000000000", + "Data": false + }, + "WrapModeV": { + "ID": "00000028-0000-0000-0000-000000000000", + "Data": "Clamp" + }, + "MaxSize": { + "ID": "0000002c-0000-0000-0000-000000000000", + "Data": null + }, + "MipMaps": { + "ID": "0000002d-0000-0000-0000-000000000000", + "Data": true + }, + "URL": { + "ID": "0000001d-0000-0000-0000-000000000000", + "Data": "@$imageResDb" + }, + "AnisotropicLevel": { + "ID": "0000001f-0000-0000-0000-000000000000", + "Data": null + }, + "Uncompressed": { + "ID": "00000020-0000-0000-0000-000000000000", + "Data": false + }, + "PreferredProfile": { + "ID": "00000024-0000-0000-0000-000000000000", + "Data": null + }, + "WrapModeU": { + "ID": "00000027-0000-0000-0000-000000000000", + "Data": "Clamp" + }, + "MinSize": { + "ID": "0000002b-0000-0000-0000-000000000000", + "Data": null + }, + "MipMapFilter": { + "ID": "0000002f-0000-0000-0000-000000000000", + "Data": "Box" + }, + "Readable": { + "ID": "00000030-0000-0000-0000-000000000000", + "Data": false + }, + "UpdateOrder": { + "ID": "0000001b-0000-0000-0000-000000000000", + "Data": 0 + }, + "MipMapBias": { + "ID": "00000025-0000-0000-0000-000000000000", + "Data": 0 + }, + "PowerOfTwoAlignThreshold": { + "ID": "00000029-0000-0000-0000-000000000000", + "Data": 0.05000000074505806 + }, + "CrunchCompressed": { + "ID": "0000002a-0000-0000-0000-000000000000", + "Data": true + }, + "persistent-ID": "0000001a-0000-0000-0000-000000000000", + "FilterMode": { + "ID": "0000001e-0000-0000-0000-000000000000", + "Data": null + }, + "DirectLoad": { + "ID": "00000021-0000-0000-0000-000000000000", + "Data": false + }, + "ForceExactVariant": { + "ID": "00000022-0000-0000-0000-000000000000", + "Data": false + }, + "KeepOriginalMipMaps": { + "ID": "0000002e-0000-0000-0000-000000000000", + "Data": false } - ] - }, - "Name": { - "ID": const Uuid().v4(), - "Data": "File Mesh" - }, - "Tag": { - "ID": const Uuid().v4(), - "Data": "" + } }, - "Active": { - "ID": const Uuid().v4(), - "Data": true + { + "Type": 3, + "Data": { + "UpdateOrder": { + "Data": 0, + "ID": "00000033-0000-0000-0000-000000000000" + }, + "Enabled": { + "ID": "00000034-0000-0000-0000-000000000000", + "Data": true + }, + "Texture": { + "ID": "00000035-0000-0000-0000-000000000000", + "Data": "00000019-0000-0000-0000-000000000000" + }, + "ID": "00000031-0000-0000-0000-000000000000", + "persistent-ID": "00000032-0000-0000-0000-000000000000" + } }, - "Persistent-ID": const Uuid().v4(), - "Position": { - "ID": const Uuid().v4(), - "Data": [ - 5.96046448E-08, - 0.0, - 0.0 - ] + { + "Type": 4, + "Data": { + "UpdateOrder": { + "ID": "00000038-0000-0000-0000-000000000000", + "Data": 0 + }, + "Enabled": { + "ID": "00000039-0000-0000-0000-000000000000", + "Data": true + }, + "Texture": { + "ID": "0000003a-0000-0000-0000-000000000000", + "Data": "00000019-0000-0000-0000-000000000000" + }, + "Crop": { + "ID": "0000003b-0000-0000-0000-000000000000", + "Data": null + }, + "ID": "00000036-0000-0000-0000-000000000000", + "persistent-ID": "00000037-0000-0000-0000-000000000000" + } }, - "Rotation": { - "ID": const Uuid().v4(), - "Data": [ - -1.19209275E-07, - 0.0, - 0.0, - 1.0 - ] + { + "Type": 5, + "Data": { + "ID": "0000003c-0000-0000-0000-000000000000", + "persistent-ID": "0000003d-0000-0000-0000-000000000000", + "UpdateOrder": { + "ID": "0000003e-0000-0000-0000-000000000000", + "Data": 0 + }, + "Enabled": { + "Data": true, + "ID": "0000003f-0000-0000-0000-000000000000" + }, + "Normal": { + "ID": "00000040-0000-0000-0000-000000000000", + "Data": [0, 0, 1] + }, + "SnapParent": { + "Data": null, + "ID": "00000041-0000-0000-0000-000000000000" + } + } }, - "Scale": { - "ID": const Uuid().v4(), - "Data": [ - 0.04071409, - 0.0407139659, - 0.0407141037 - ] + { + "Type": 6, + "Data": { + "SpawnInstanceOnTrigger": { + "ID": "00000047-0000-0000-0000-000000000000", + "Data": false + }, + "ID": "00000042-0000-0000-0000-000000000000", + "persistent-ID": "00000043-0000-0000-0000-000000000000", + "UpdateOrder": { + "ID": "00000044-0000-0000-0000-000000000000", + "Data": 0 + }, + "Enabled": { + "ID": "00000045-0000-0000-0000-000000000000", + "Data": true + }, + "Reference": { + "ID": "00000046-0000-0000-0000-000000000000", + "Data": "00000019-0000-0000-0000-000000000000" + } + } }, - "OrderOffset": { - "ID": const Uuid().v4(), - "Data": 0 + { + "Type": 7, + "Data": { + "ID": "00000048-0000-0000-0000-000000000000", + "persistent-ID": "00000049-0000-0000-0000-000000000000", + "UpdateOrder": { + "ID": "0000004a-0000-0000-0000-000000000000", + "Data": 0 + }, + "Enabled": { + "Data": true, + "ID": "0000004b-0000-0000-0000-000000000000" + }, + "AssetReference": { + "ID": "0000004c-0000-0000-0000-000000000000", + "Data": "00000019-0000-0000-0000-000000000000" + } + } }, - "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 + { + "Type": 8, + "Data": { + "__legacyPresentUsers-ID": "0000006e-0000-0000-0000-000000000000", + "StereoLayout": { + "ID": "00000073-0000-0000-0000-000000000000", + "Data": "None" + }, + "Enabled": { + "ID": "00000050-0000-0000-0000-000000000000", + "Data": true + }, + "TakenGlobalPosition": { + "ID": "0000005e-0000-0000-0000-000000000000", + "Data": [ + 0.1488455832004547, -0.0002761205250862986, + -1.3219715356826782 + ] + }, + "UserInfos": { + "ID": "00000063-0000-0000-0000-000000000000", + "Data": [ + { + "IsPresent": { + "Data": true, + "ID": "0000006a-0000-0000-0000-000000000000" + }, + "HeadPosition": { + "ID": "0000006b-0000-0000-0000-000000000000", + "Data": [ + 0.13439173996448517, 1.663012146949768, + -1.2103259563446045 ] + }, + "HeadOrientation": { + "ID": "0000006c-0000-0000-0000-000000000000", + "Data": [ + 0.06871938705444336, -0.10541222244501114, + 0.0019961402285844088, 0.9920493960380554 + ] + }, + "SessionJoinTimestamp": { + "Data": "2026-07-05T10:08:56.132Z", + "ID": "0000006d-0000-0000-0000-000000000000" + }, + "ID": "00000064-0000-0000-0000-000000000000", + "User": { + "User": { + "ID": "00000066-0000-0000-0000-000000000000", + "Data": null + }, + "_machineId": { + "ID": "00000067-0000-0000-0000-000000000000", + "Data": machineId + }, + "_userId": { + "ID": "00000068-0000-0000-0000-000000000000", + "Data": null + }, + "ID": "00000065-0000-0000-0000-000000000000" + }, + "IsInVR": { + "Data": false, + "ID": "00000069-0000-0000-0000-000000000000" } - }, - "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 - ] } + ] + }, + "CameraManufacturer": { + "ID": "0000006f-0000-0000-0000-000000000000", + "Data": "Resonite" + }, + "CameraFOV": { + "ID": "00000071-0000-0000-0000-000000000000", + "Data": 85 + }, + "_exportedUsers-ID": "00000074-0000-0000-0000-000000000000", + "ID": "0000004d-0000-0000-0000-000000000000", + "UpdateOrder": { + "Data": 0, + "ID": "0000004f-0000-0000-0000-000000000000" + }, + "LocationURL": { + "ID": "00000052-0000-0000-0000-000000000000", + "Data": null + }, + "LocationHost": { + "_userId": { + "Data": null, + "ID": "00000056-0000-0000-0000-000000000000" + }, + "ID": "00000053-0000-0000-0000-000000000000", + "User": { + "ID": "00000054-0000-0000-0000-000000000000", + "Data": null + }, + "_machineId": { + "ID": "00000055-0000-0000-0000-000000000000", + "Data": machineId, } }, - { - "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 - ] - } + "LocationHiddenFromListing": { + "ID": "00000058-0000-0000-0000-000000000000", + "Data": false + }, + "TakenGlobalRotation": { + "ID": "0000005f-0000-0000-0000-000000000000", + "Data": [0, 0.000553934252820909, 0, 0.9999998211860657] + }, + "TakenGlobalScale": { + "Data": [1, 1, 1], + "ID": "00000060-0000-0000-0000-000000000000" + }, + "AppVersion": { + "ID": "00000061-0000-0000-0000-000000000000", + "Data": "2026.6.24.835" + }, + "LocationAccessLevel": { + "ID": "00000057-0000-0000-0000-000000000000", + "Data": "Private" + }, + "TimeTaken": { + "ID": "00000059-0000-0000-0000-000000000000", + "Data": "2026-07-05T10:10:01.042Z" + }, + "TakenBy": { + "ID": "0000005a-0000-0000-0000-000000000000", + "User": { + "Data": null, + "ID": "0000005b-0000-0000-0000-000000000000" + }, + "_machineId": { + "ID": "0000005c-0000-0000-0000-000000000000", + "Data": machineId + }, + "_userId": { + "ID": "0000005d-0000-0000-0000-000000000000", + "Data": null } + }, + "RendererName": { + "ID": "00000062-0000-0000-0000-000000000000", + "Data": "Renderite.Renderer.Unity 2026.6.23.743 (2019.4.19f1)" + }, + "CameraModel": { + "ID": "00000070-0000-0000-0000-000000000000", + "Data": "PhotoCaptureManager" + }, + "Is360": { + "ID": "00000072-0000-0000-0000-000000000000", + "Data": false + }, + "persistent-ID": "0000004e-0000-0000-0000-000000000000", + "LocationName": { + "ID": "00000051-0000-0000-0000-000000000000", + "Data": "Local" } - ] - }, - "Name": { - "ID": const Uuid().v4(), - "Data": "NameLabel" - }, - "Tag": { - "ID": const Uuid().v4(), - "Data": "" + } }, - "Active": { - "ID": const Uuid().v4(), - "Data": true + { + "Type": 9, + "Data": { + "ReparentOnRelease": { + "ID": "00000079-0000-0000-0000-000000000000", + "Data": true + }, + "GrabPriorityWhenGrabbed": { + "ID": "0000007d-0000-0000-0000-000000000000", + "Data": null + }, + "AllowSteal": { + "ID": "00000080-0000-0000-0000-000000000000", + "Data": false + }, + "UpdateOrder": { + "ID": "00000077-0000-0000-0000-000000000000", + "Data": 0 + }, + "CustomCanGrabCheck": { + "ID": "0000007e-0000-0000-0000-000000000000", + "Data": { "Target": null } + }, + "DropOnDisable": { + "ID": "00000081-0000-0000-0000-000000000000", + "Data": true + }, + "ActiveUserFilter": { + "ID": "00000082-0000-0000-0000-000000000000", + "Data": "Disabled" + }, + "OnlyUsers": { + "ID": "00000083-0000-0000-0000-000000000000", + "Data": [] + }, + "Enabled": { + "Data": true, + "ID": "00000078-0000-0000-0000-000000000000" + }, + "PreserveUserSpace": { + "ID": "0000007a-0000-0000-0000-000000000000", + "Data": true + }, + "Scalable": { + "ID": "00000084-0000-0000-0000-000000000000", + "Data": true + }, + "_grabber": { + "ID": "00000087-0000-0000-0000-000000000000", + "Data": null + }, + "__legacyActiveUserRootOnly-ID": "0000008a-0000-0000-0000-000000000000", + "DestroyOnRelease": { + "ID": "0000007b-0000-0000-0000-000000000000", + "Data": false + }, + "GrabPriority": { + "ID": "0000007c-0000-0000-0000-000000000000", + "Data": 0 + }, + "EditModeOnly": { + "ID": "0000007f-0000-0000-0000-000000000000", + "Data": false + }, + "Receivable": { + "ID": "00000085-0000-0000-0000-000000000000", + "Data": true + }, + "AllowOnlyPhysicalGrab": { + "ID": "00000086-0000-0000-0000-000000000000", + "Data": false + }, + "_lastParent": { + "ID": "00000088-0000-0000-0000-000000000000", + "Data": null + }, + "_lastParentIsUserSpace": { + "Data": false, + "ID": "00000089-0000-0000-0000-000000000000" + }, + "ID": "00000075-0000-0000-0000-000000000000", + "persistent-ID": "00000076-0000-0000-0000-000000000000" + } }, - "Persistent-ID": const Uuid().v4(), - "Position": { - "ID": const Uuid().v4(), - "Data": [ - 0.0009058714, - -0.08701205, - 0.00394916534 - ] + { + "Type": 10, + "Data": { + "Materials": { + "ID": "00000091-0000-0000-0000-000000000000", + "Data": [ + { + "ID": "00000092-0000-0000-0000-000000000000", + "Data": "00000093-0000-0000-0000-000000000000" + } + ] + }, + "MaterialPropertyBlocks": { + "ID": "00000094-0000-0000-0000-000000000000", + "Data": [] + }, + "MotionVectorMode": { + "ID": "00000096-0000-0000-0000-000000000000", + "Data": "Object" + }, + "ShadowCastMode": { + "ID": "00000095-0000-0000-0000-000000000000", + "Data": "On" + }, + "SortingOrder": { + "ID": "00000097-0000-0000-0000-000000000000", + "Data": 0 + }, + "ID": "0000008b-0000-0000-0000-000000000000", + "persistent-ID": "0000008c-0000-0000-0000-000000000000", + "UpdateOrder": { + "Data": 0, + "ID": "0000008d-0000-0000-0000-000000000000" + }, + "Enabled": { + "ID": "0000008e-0000-0000-0000-000000000000", + "Data": true + }, + "Mesh": { + "ID": "0000008f-0000-0000-0000-000000000000", + "Data": "00000090-0000-0000-0000-000000000000" + } + } }, - "Rotation": { - "ID": const Uuid().v4(), - "Data": [ - 0.0009555904, - 0.999872863, - 0.000245468284, - 0.01591436 - ] + { + "Type": 11, + "Data": { + "persistent-ID": "00000098-0000-0000-0000-000000000000", + "Enabled": { + "ID": "0000009a-0000-0000-0000-000000000000", + "Data": true + }, + "Size": { + "ID": "000000a0-0000-0000-0000-000000000000", + "Data": [0.8715755343437195, 0.4902612566947937] + }, + "LowerLeftColor": { + "ID": "000000a7-0000-0000-0000-000000000000", + "Data": [1, 1, 1, 1, "sRGB"] + }, + "UpdateOrder": { + "ID": "00000099-0000-0000-0000-000000000000", + "Data": 0 + }, + "OverrideBoundingBox": { + "ID": "0000009c-0000-0000-0000-000000000000", + "Data": false + }, + "Profile": { + "ID": "0000009e-0000-0000-0000-000000000000", + "Data": "Linear" + }, + "UVOffset": { + "ID": "000000a1-0000-0000-0000-000000000000", + "Data": [0, 0] + }, + "UVScale": { + "Data": [1, 1], + "ID": "000000a2-0000-0000-0000-000000000000" + }, + "DualSided": { + "ID": "000000a4-0000-0000-0000-000000000000", + "Data": false + }, + "UpperLeftColor": { + "Data": [1, 1, 1, 1, "sRGB"], + "ID": "000000a6-0000-0000-0000-000000000000" + }, + "LowerRightColor": { + "ID": "000000a8-0000-0000-0000-000000000000", + "Data": [1, 1, 1, 1, "sRGB"] + }, + "ID": "00000090-0000-0000-0000-000000000000", + "Rotation": { + "ID": "0000009f-0000-0000-0000-000000000000", + "Data": [0, 0, 0, 1] + }, + "ScaleUVWithSize": { + "ID": "000000a3-0000-0000-0000-000000000000", + "Data": false + }, + "UseVertexColors": { + "Data": true, + "ID": "000000a5-0000-0000-0000-000000000000" + }, + "UpperRightColor": { + "ID": "000000a9-0000-0000-0000-000000000000", + "Data": [1, 1, 1, 1, "sRGB"] + }, + "HighPriorityIntegration": { + "ID": "0000009b-0000-0000-0000-000000000000", + "Data": false + }, + "OverridenBoundingBox": { + "Data": { "Max": [0, 0, 0], "Min": [0, 0, 0] }, + "ID": "0000009d-0000-0000-0000-000000000000" + } + } }, - "Scale": { - "ID": const Uuid().v4(), - "Data": [ - 0.3075354, - 0.307534128, - 0.307536483 - ] + { + "Data": { + "UpdateOrder": { + "ID": "000000ab-0000-0000-0000-000000000000", + "Data": 0 + }, + "TintColor": { + "ID": "000000ae-0000-0000-0000-000000000000", + "Data": [1, 1, 1, 1, "sRGB"] + }, + "MaskTexture": { + "ID": "000000b2-0000-0000-0000-000000000000", + "Data": null + }, + "StereoTextureTransform": { + "Data": false, + "ID": "000000c2-0000-0000-0000-000000000000" + }, + "_unlit-ID": "000000ce-0000-0000-0000-000000000000", + "TextureOffset": { + "ID": "000000b1-0000-0000-0000-000000000000", + "Data": [0, 0] + }, + "MaskOffset": { + "Data": [0, 0], + "ID": "000000b4-0000-0000-0000-000000000000" + }, + "BlendMode": { + "ID": "000000b6-0000-0000-0000-000000000000", + "Data": "Opaque" + }, + "OffsetMagnitude": { + "ID": "000000bd-0000-0000-0000-000000000000", + "Data": [0, 0] + }, + "OffsetTextureScale": { + "ID": "000000be-0000-0000-0000-000000000000", + "Data": [1, 1] + }, + "UsePerBillboardScale": { + "Data": false, + "ID": "000000c7-0000-0000-0000-000000000000" + }, + "_unlitBillboard-ID": "000000cf-0000-0000-0000-000000000000", + "MaskMode": { + "ID": "000000b5-0000-0000-0000-000000000000", + "Data": "MultiplyAlpha" + }, + "AlphaCutoff": { + "ID": "000000b7-0000-0000-0000-000000000000", + "Data": 0.5 + }, + "OffsetTextureOffset": { + "ID": "000000bf-0000-0000-0000-000000000000", + "Data": [0, 0] + }, + "RightEyeTextureOffset": { + "ID": "000000c4-0000-0000-0000-000000000000", + "Data": [0, 0] + }, + "OffsetUnits": { + "ID": "000000cc-0000-0000-0000-000000000000", + "Data": 0 + }, + "ID": "00000093-0000-0000-0000-000000000000", + "MaskScale": { + "ID": "000000b3-0000-0000-0000-000000000000", + "Data": [1, 1] + }, + "Sidedness": { + "ID": "000000ba-0000-0000-0000-000000000000", + "Data": "Double" + }, + "DecodeAsNormalMap": { + "ID": "000000c5-0000-0000-0000-000000000000", + "Data": false + }, + "UsePerBillboardRotation": { + "ID": "000000c8-0000-0000-0000-000000000000", + "Data": false + }, + "HighPriorityIntegration": { + "ID": "000000ad-0000-0000-0000-000000000000", + "Data": false + }, + "TextureScale": { + "ID": "000000b0-0000-0000-0000-000000000000", + "Data": [1, 1] + }, + "ZWrite": { + "ID": "000000bb-0000-0000-0000-000000000000", + "Data": "Auto" + }, + "OffsetTexture": { + "Data": null, + "ID": "000000bc-0000-0000-0000-000000000000" + }, + "PolarUVmapping": { + "Data": false, + "ID": "000000c0-0000-0000-0000-000000000000" + }, + "OffsetFactor": { + "ID": "000000cb-0000-0000-0000-000000000000", + "Data": 0 + }, + "PolarPower": { + "ID": "000000c1-0000-0000-0000-000000000000", + "Data": 1 + }, + "BillboardSize": { + "ID": "000000ca-0000-0000-0000-000000000000", + "Data": [0.004999999888241291, 0.004999999888241291] + }, + "Texture": { + "Data": "00000019-0000-0000-0000-000000000000", + "ID": "000000af-0000-0000-0000-000000000000" + }, + "UseVertexColors": { + "Data": true, + "ID": "000000b8-0000-0000-0000-000000000000" + }, + "VertexColorInterpolationSpace": { + "ID": "000000b9-0000-0000-0000-000000000000", + "Data": "Linear" + }, + "UsePerBillboardUV": { + "ID": "000000c9-0000-0000-0000-000000000000", + "Data": false + }, + "RenderQueue": { + "ID": "000000cd-0000-0000-0000-000000000000", + "Data": -1 + }, + "persistent-ID": "000000aa-0000-0000-0000-000000000000", + "Enabled": { + "Data": true, + "ID": "000000ac-0000-0000-0000-000000000000" + }, + "RightEyeTextureScale": { + "ID": "000000c3-0000-0000-0000-000000000000", + "Data": [1, 1] + }, + "UseBillboardGeometry": { + "Data": false, + "ID": "000000c6-0000-0000-0000-000000000000" + } + }, + "Type": 12 }, - "OrderOffset": { - "ID": const Uuid().v4(), - "Data": 0 + { + "Type": 13, + "Data": { + "persistent-ID": "000000d1-0000-0000-0000-000000000000", + "Enabled": { + "ID": "000000d3-0000-0000-0000-000000000000", + "Data": true + }, + "Target": { + "ID": "000000d5-0000-0000-0000-000000000000", + "Data": "000000a0-0000-0000-0000-000000000000" + }, + "Premultiply": { + "ID": "000000d7-0000-0000-0000-000000000000", + "Data": [1, 1] + }, + "Ratio": { + "ID": "000000d8-0000-0000-0000-000000000000", + "Data": [1, 1] + }, + "MaxSize": { + "ID": "000000d9-0000-0000-0000-000000000000", + "Data": [3.4028234663852886e38, 3.4028234663852886e38] + }, + "ID": "000000d0-0000-0000-0000-000000000000", + "UpdateOrder": { + "ID": "000000d2-0000-0000-0000-000000000000", + "Data": 0 + }, + "Texture": { + "ID": "000000d4-0000-0000-0000-000000000000", + "Data": "00000019-0000-0000-0000-000000000000" + }, + "DriveMode": { + "ID": "000000d6-0000-0000-0000-000000000000", + "Data": "Normalized" + } + } }, - "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": 14, + "Data": { + "ID": "000000da-0000-0000-0000-000000000000", + "persistent-ID": "000000db-0000-0000-0000-000000000000", + "Offset": { + "ID": "000000de-0000-0000-0000-000000000000", + "Data": [0, 0, 0] + }, + "Type": { + "Data": "NoCollision", + "ID": "000000df-0000-0000-0000-000000000000" + }, + "Mass": { + "ID": "000000e0-0000-0000-0000-000000000000", + "Data": 1 + }, + "IgnoreRaycasts": { + "ID": "000000e2-0000-0000-0000-000000000000", + "Data": false + }, + "Size": { + "ID": "000000e3-0000-0000-0000-000000000000", + "Data": [0.8715755343437195, 0.4902612566947937, 0] + }, + "UpdateOrder": { + "ID": "000000dc-0000-0000-0000-000000000000", + "Data": 1000000 }, - { - "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 - ] - } - } + "Enabled": { + "ID": "000000dd-0000-0000-0000-000000000000", + "Data": true }, - { - "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 - ] - } - } + "CharacterCollider": { + "ID": "000000e1-0000-0000-0000-000000000000", + "Data": false } - ] - }, - "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": 15, + "Data": { + "X": { + "ID": "000000ea-0000-0000-0000-000000000000", + "Data": 0 }, - { - "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() - } + "Y": { + "Data": 1, + "ID": "000000eb-0000-0000-0000-000000000000" + }, + "UpdateOrder": { + "ID": "000000e6-0000-0000-0000-000000000000", + "Data": 0 + }, + "Target": { + "ID": "000000e9-0000-0000-0000-000000000000", + "Data": "000000e3-0000-0000-0000-000000000000" + }, + "Z": { + "ID": "000000ec-0000-0000-0000-000000000000", + "Data": -1 + }, + "ID": "000000e4-0000-0000-0000-000000000000", + "persistent-ID": "000000e5-0000-0000-0000-000000000000", + "Enabled": { + "ID": "000000e7-0000-0000-0000-000000000000", + "Data": true + }, + "Source": { + "ID": "000000e8-0000-0000-0000-000000000000", + "Data": "000000a0-0000-0000-0000-000000000000" } - ] - }, - "Name": { - "ID": const Uuid().v4(), - "Data": "OutlineMaterial" + } + } + ] + } + } + ], + "Name": { "ID": "0000000f-0000-0000-0000-000000000000", "Data": "Holder" }, + "Persistent-ID": "00000012-0000-0000-0000-000000000000", + "Rotation": { + "ID": "00000014-0000-0000-0000-000000000000", + "Data": [ + 0.17878229916095734, 0.11211946606636047, 0.6981614828109741, + 0.6841321587562561 + ] + }, + "OrderOffset": { "Data": 0, "ID": "00000016-0000-0000-0000-000000000000" }, + "ParentReference": "000000f6-0000-0000-0000-000000000000", + "ID": "00000000-0000-0000-0000-000000000000", + "Components": { + "ID": "00000001-0000-0000-0000-000000000000", + "Data": [ + { + "Data": { + "MaxDepth": { + "ID": "00000007-0000-0000-0000-000000000000", + "Data": 1 }, - "Tag": { - "ID": const Uuid().v4(), - "Data": "" + "ID": "00000002-0000-0000-0000-000000000000", + "persistent-ID": "00000003-0000-0000-0000-000000000000", + "UpdateOrder": { + "Data": 0, + "ID": "00000004-0000-0000-0000-000000000000" }, - "Active": { - "ID": const Uuid().v4(), + "Enabled": { + "ID": "00000005-0000-0000-0000-000000000000", "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(), + "DontReparent": { + "ID": "00000006-0000-0000-0000-000000000000", "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": "" + "Type": 0 + }, + { + "Type": 1, + "Data": { + "UpdateOrder": { + "ID": "0000000a-0000-0000-0000-000000000000", + "Data": 0 }, - "Active": { - "ID": const Uuid().v4(), + "Enabled": { + "ID": "0000000b-0000-0000-0000-000000000000", "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 - ] + "RelativeToUserRoot": { + "ID": "0000000c-0000-0000-0000-000000000000", + "Data": true }, - "Scale": { - "ID": const Uuid().v4(), + "SavedRotation": { + "ID": "0000000d-0000-0000-0000-000000000000", "Data": [ - 0.000407140964, - 0.000407139567, - 0.000407140964 + 7.440260851865332e-9, -0.08889392763376236, + 1.8630576192890658e-8, 0.9960411787033081 ] }, - "OrderOffset": { - "ID": const Uuid().v4(), - "Data": 0 + "SavedScale": { + "ID": "0000000e-0000-0000-0000-000000000000", + "Data": [1, 1, 1] }, - "ParentReference": const Uuid().v4(), - "Children": [] + "ID": "00000008-0000-0000-0000-000000000000", + "persistent-ID": "00000009-0000-0000-0000-000000000000" } - ] - } - ] - }, - "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": "@resdb:///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": "@resdb:///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": "@resdb:///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": "@resdb:///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": "@resdb:///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": "@resdb:///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": "@resdb:///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 - } + "Tag": { "ID": "00000010-0000-0000-0000-000000000000", "Data": null }, + "Active": { "Data": true, "ID": "00000011-0000-0000-0000-000000000000" } + }, + "VersionNumber": "2026.6.24.835", + "FeatureFlags": { + "NetCore": 0, + "TEXTURE_QUALITY": 0, + "TypeManagement": 0, + "PhotonDust": 0, + "RESONITE_LINK": 0, + "ColorManagement": 0, + "ResetGUID": 0, + "ProtoFlux": 0, + "ALIGNER_FILTERING": 0, + "Awwdio": 0 + }, + "Types": [ + "[FrooxEngine]FrooxEngine.GrabbableReparentBlock", + "[FrooxEngine]FrooxEngine.InventoryItem", + "[FrooxEngine]FrooxEngine.StaticTexture2D", + "[FrooxEngine]FrooxEngine.TextureExportable", + "[FrooxEngine]FrooxEngine.ItemTextureThumbnailSource", + "[FrooxEngine]FrooxEngine.SnapPlane", + "[FrooxEngine]FrooxEngine.ReferenceProxy", + "[FrooxEngine]FrooxEngine.AssetProxy<[FrooxEngine]FrooxEngine.Texture2D>", + "[FrooxEngine]FrooxEngine.PhotoMetadata", + "[FrooxEngine]FrooxEngine.Grabbable", + "[FrooxEngine]FrooxEngine.MeshRenderer", + "[FrooxEngine]FrooxEngine.QuadMesh", + "[FrooxEngine]FrooxEngine.UnlitMaterial", + "[FrooxEngine]FrooxEngine.TextureSizeDriver", + "[FrooxEngine]FrooxEngine.BoxCollider", + "[FrooxEngine]FrooxEngine.Float2ToFloat3SwizzleDriver" + ] }; - return JsonTemplate(data: data); + return data; } } \ No newline at end of file diff --git a/lib/widgets/messages/message_asset.dart b/lib/widgets/messages/message_asset.dart index 597a615..d18b1c8 100644 --- a/lib/widgets/messages/message_asset.dart +++ b/lib/widgets/messages/message_asset.dart @@ -22,6 +22,7 @@ class MessageAsset extends StatelessWidget { final formattedName = FormatNode.fromText(content["name"]); return Container( constraints: const BoxConstraints(maxWidth: 300), + padding: const EdgeInsets.all(8.0), child: Column( children: [ SizedBox( diff --git a/lib/widgets/messages/message_input_bar.dart b/lib/widgets/messages/message_input_bar.dart index 1c5b2b0..ab73f50 100644 --- a/lib/widgets/messages/message_input_bar.dart +++ b/lib/widgets/messages/message_input_bar.dart @@ -72,21 +72,11 @@ class _MessageInputBarState extends State { 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, - ); + Future sendImageMessage(ApiClient client, MessagingClient mClient, File file, String machineId, void Function(double progress) progressCallback) async { + final msgId = Message.generateId(); + final record = await RecordApi.uploadImage(client, image: file, machineId: machineId, messageId: msgId, progressCallback: progressCallback); final message = Message( - id: record.extractMessageId() ?? Message.generateId(), + id: msgId, recipientId: widget.recipient.contactUserId, senderId: client.userId, type: MessageType.object, @@ -97,21 +87,9 @@ class _MessageInputBarState extends State { mClient.sendMessage(message); } - Future sendVoiceMessage( - ApiClient client, - MessagingClient mClient, - File file, - String machineId, - void Function(double progress) progressCallback, - ) async { + Future sendVoiceMessage(ApiClient client, MessagingClient mClient, File file, String machineId, void Function(double progress) progressCallback) async { final msgId = Message.generateId(); - final record = await RecordApi.uploadVoiceClip( - client, - voiceClip: file, - machineId: machineId, - messageId: msgId, - progressCallback: progressCallback, - ); + final record = await RecordApi.uploadVoiceClip(client, voiceClip: file, machineId: machineId, messageId: msgId, progressCallback: progressCallback); final message = Message( id: msgId, recipientId: widget.recipient.contactUserId, @@ -124,19 +102,8 @@ class _MessageInputBarState extends State { 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, - ); + 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.contactUserId, @@ -154,7 +121,6 @@ class _MessageInputBarState extends State { _isSending = true; _sendProgress = 0; _attachmentPickerOpen = false; - _loadedFiles.clear(); }); final cHolder = ClientHolder.of(context); final mClient = Provider.of(context, listen: false); @@ -181,9 +147,7 @@ class _MessageInputBarState extends State { mClient, file.$2, settings.machineId.valueOrDefault, - (progress) => setState( - () => _sendProgress = totalProgress + progress * 1 / toSend.length, - ), + (progress) => setState(() => _sendProgress = totalProgress + progress * 1 / toSend.length), ); } } @@ -286,12 +250,7 @@ class _MessageInputBarState extends State { }); }); } catch (e, s) { - FlutterError.reportError( - FlutterErrorDetails( - exception: e, - stack: s, - ), - ); + FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Failed to send voice message: $e"))); } @@ -340,19 +299,16 @@ class _MessageInputBarState extends State { children: [ if (_isSending && _sendProgress != null) LinearProgressIndicator(value: _sendProgress), DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - ), + decoration: BoxDecoration(color: Theme.of(context).colorScheme.surfaceContainerHighest), child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeOut, - transitionBuilder: (child, animation) => SizeTransition( - sizeFactor: animation, - child: child, - ), + transitionBuilder: (child, animation) => SizeTransition(sizeFactor: animation, child: child), child: switch ((_attachmentPickerOpen, _loadedFiles)) { - (true, []) => Row( + (true, []) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( key: const ValueKey("attachment-picker"), children: [ TextButton.icon( @@ -362,9 +318,7 @@ class _MessageInputBarState extends State { final result = await FilePicker.pickFiles(type: FileType.image); if (result != null) { setState(() { - _loadedFiles.addAll( - result.files.map((e) => e.path != null ? (FileType.image, File(e.path!)) : null).nonNulls, - ); + _loadedFiles.addAll(result.files.map((e) => e.path != null ? (FileType.image, File(e.path!)) : null).nonNulls); }); } }, @@ -403,9 +357,7 @@ class _MessageInputBarState extends State { final result = await FilePicker.pickFiles(type: FileType.any); if (result != null) { setState(() { - _loadedFiles.addAll( - result.files.map((e) => e.path != null ? (FileType.any, File(e.path!)) : null).nonNulls, - ); + _loadedFiles.addAll(result.files.map((e) => e.path != null ? (FileType.any, File(e.path!)) : null).nonNulls); }); } }, @@ -414,16 +366,17 @@ class _MessageInputBarState extends State { ), ], ), + ), (false, []) => null, (_, _) => MessageAttachmentList( - disabled: _isSending, - initialFiles: _loadedFiles, - onChange: (loadedFiles) => setState(() { - _loadedFiles - ..clear() - ..addAll(loadedFiles); - }), - ), + disabled: _isSending, + initialFiles: _loadedFiles, + onChange: (loadedFiles) => setState(() { + _loadedFiles + ..clear() + ..addAll(loadedFiles); + }), + ), }, ), ), @@ -433,77 +386,63 @@ class _MessageInputBarState extends State { duration: const Duration(milliseconds: 200), transitionBuilder: (child, animation) => FadeTransition( opacity: animation, - child: RotationTransition( - turns: Tween(begin: 0.6, end: 1).animate(animation), - child: child, - ), + 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, - ), - ), + onPressed: () {}, + icon: Icon(Icons.delete, color: _recordingCancelled ? Theme.of(context).colorScheme.error : null), + ), (false, _) => IconButton( - key: const ValueKey("add-attachment-icon"), - onPressed: _isSending - ? null - : () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Sorry, this feature is not yet available")), - ); - return; - // setState(() { - // _attachmentPickerOpen = true; - // }); - }, - icon: const Icon( - Icons.attach_file, - ), - ), + 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, - ), - ), + 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( @@ -535,10 +474,7 @@ class _MessageInputBarState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), fillColor: Colors.black26, filled: true, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(24), - ), + border: OutlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(24)), ), ), AnimatedSwitcher( @@ -546,10 +482,7 @@ class _MessageInputBarState extends State { transitionBuilder: (child, animation) => FadeTransition( opacity: animation, child: SlideTransition( - position: Tween( - begin: const Offset(0, .2), - end: Offset.zero, - ).animate(animation), + position: Tween(begin: const Offset(0, .2), end: Offset.zero).animate(animation), child: child, ), ), @@ -560,16 +493,10 @@ class _MessageInputBarState extends State { ? Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - const SizedBox( - width: 8, - ), + const SizedBox(width: 8), const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Icon( - Icons.cancel, - color: Colors.red, - size: 16, - ), + child: Icon(Icons.cancel, color: Colors.red, size: 16), ), Text("Cancel Recording", style: Theme.of(context).textTheme.titleMedium), ], @@ -577,24 +504,15 @@ class _MessageInputBarState extends State { : Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - const SizedBox( - width: 8, - ), + const SizedBox(width: 8), const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Icon( - Icons.circle, - color: Colors.red, - size: 16, - ), + 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, - ); + return Text("Recording: ${snapshot.data?.format()}", style: Theme.of(context).textTheme.titleMedium); }, ), ], @@ -610,10 +528,7 @@ class _MessageInputBarState extends State { duration: const Duration(milliseconds: 200), transitionBuilder: (child, animation) => FadeTransition( opacity: animation, - child: RotationTransition( - turns: Tween(begin: 0.5, end: 1).animate(animation), - child: child, - ), + child: RotationTransition(turns: Tween(begin: 0.5, end: 1).animate(animation), child: child), ), child: _currentText.trim().isNotEmpty || _loadedFiles.isNotEmpty ? IconButton( @@ -640,13 +555,7 @@ class _MessageInputBarState extends State { final hasPermission = !await _recorder.hasPermission(); if (hasPermission) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - "No permission to record audio.", - ), - ), - ); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("No permission to record audio."))); } return; } @@ -658,11 +567,7 @@ class _MessageInputBarState extends State { final dir = await getTemporaryDirectory(); await _recorder.start( path: "${dir.path}/A-${const Uuid().v4()}.wav", - const RecordConfig( - numChannels: 1, - sampleRate: 44100, - encoder: AudioEncoder.wav, - ), + const RecordConfig(numChannels: 1, sampleRate: 44100, encoder: AudioEncoder.wav), ); setState(() { _isRecording = true; diff --git a/pubspec.lock b/pubspec.lock index 6f8e720..a77d16a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + bson: + dependency: "direct main" + description: + name: bson + sha256: "6e322cdf827db905ca48b018deed1804934840bce8ff6b3c2f5b2f81f99d8344" + url: "https://pub.dev" + source: hosted + version: "5.0.8" cached_network_image: dependency: "direct main" description: @@ -181,10 +189,18 @@ packages: dependency: transitive description: name: dbus - sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91" + url: "https://pub.dev" + source: hosted + version: "0.7.13" + decimal: + dependency: transitive + description: + name: decimal + sha256: fc706a5618b81e5b367b01dd62621def37abc096f2b46a9bd9068b64c1fa36d0 url: "https://pub.dev" source: hosted - version: "0.7.14" + version: "3.2.4" dynamic_color: dependency: "direct main" description: @@ -193,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.1" + es_compression: + dependency: "direct main" + description: + name: es_compression + sha256: c1ff7af54802631cf5c3942cb67bb99daadcc087f573ca99a9de91002d1a7ece + url: "https://pub.dev" + source: hosted + version: "2.0.15" fake_async: dependency: transitive description: @@ -465,13 +489,13 @@ packages: source: hosted version: "4.1.2" image: - dependency: transitive + dependency: "direct main" description: name: image - sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52" url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.9.1" image_picker: dependency: "direct main" description: @@ -736,6 +760,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + packages_extensions: + dependency: transitive + description: + name: packages_extensions + sha256: a2e207f3345fc5cbea60155842e71be1c3059b427a61ed1c5d7c41f5d03264e7 + url: "https://pub.dev" + source: hosted + version: "0.1.2" path: dependency: "direct main" description: @@ -880,6 +912,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + power_extensions: + dependency: transitive + description: + name: power_extensions + sha256: ad4e27d040f80bb7e566d96399b6419343d946d7c37912e1bce56c9f2b32e525 + url: "https://pub.dev" + source: hosted + version: "0.2.4" provider: dependency: "direct main" description: @@ -896,6 +936,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" record: dependency: "direct main" description: @@ -1305,10 +1353,10 @@ packages: dependency: transitive description: name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" url: "https://pub.dev" source: hosted - version: "6.6.1" + version: "7.0.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 668d729..0b72a33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,10 +16,10 @@ 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: 0.12.2-beta +version: 0.13.0-beta environment: - sdk: ">=3.0.1" + sdk: ">=3.12.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -67,6 +67,9 @@ dependencies: just_audio_media_kit: ^2.1.0 media_kit_libs_linux: ^1.2.1 media_kit_libs_windows_audio: ^1.0.9 + es_compression: ^2.0.15 + bson: ^5.0.8 + image: ^4.9.1 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart index 59c6120..d14923d 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:recon/clients/settings_client.dart'; import 'package:recon/main.dart'; import 'package:recon/models/authentication_data.dart'; @@ -17,6 +18,7 @@ void main() { await tester.pumpWidget(ReCon( settingsClient: SettingsClient(), cachedAuthentication: AuthenticationData.unauthenticated(), + packageInfo: await PackageInfo.fromPlatform(), )); // Verify that our counter starts at 0.