Compare commits

..

5 Commits

Author SHA1 Message Date
cc1e0599aa 🐛 Fix link expand match markdown link 2024-08-21 10:06:05 +08:00
221b97901f 💄 Optimize uploader 2024-08-21 10:01:09 +08:00
498bb0e5fb Run upload chunks at the same time (max 3) 2024-08-21 09:33:34 +08:00
aa94dfcfe0 Multipart upload 2024-08-21 01:53:16 +08:00
65d9253876 🐛 Fix svg site icon cause invalid image data 2024-08-21 00:48:51 +08:00
10 changed files with 355 additions and 164 deletions

View File

@ -1,5 +1,30 @@
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
class AttachmentPlaceholder {
int chunkCount;
int chunkSize;
Attachment meta;
AttachmentPlaceholder({
required this.chunkCount,
required this.chunkSize,
required this.meta,
});
factory AttachmentPlaceholder.fromJson(Map<String, dynamic> json) =>
AttachmentPlaceholder(
chunkCount: json['chunk_count'],
chunkSize: json['chunk_size'],
meta: Attachment.fromJson(json['meta']),
);
Map<String, dynamic> toJson() => {
'chunk_count': chunkCount,
'chunk_size': chunkSize,
'meta': meta.toJson(),
};
}
class Attachment { class Attachment {
int id; int id;
DateTime createdAt; DateTime createdAt;
@ -14,7 +39,9 @@ class Attachment {
String hash; String hash;
int destination; int destination;
bool isAnalyzed; bool isAnalyzed;
bool isUploaded;
Map<String, dynamic>? metadata; Map<String, dynamic>? metadata;
Map<String, dynamic>? fileChunks;
bool isMature; bool isMature;
Account? account; Account? account;
int? accountId; int? accountId;
@ -33,7 +60,9 @@ class Attachment {
required this.hash, required this.hash,
required this.destination, required this.destination,
required this.isAnalyzed, required this.isAnalyzed,
required this.isUploaded,
required this.metadata, required this.metadata,
required this.fileChunks,
required this.isMature, required this.isMature,
required this.account, required this.account,
required this.accountId, required this.accountId,
@ -55,7 +84,9 @@ class Attachment {
hash: json['hash'], hash: json['hash'],
destination: json['destination'], destination: json['destination'],
isAnalyzed: json['is_analyzed'], isAnalyzed: json['is_analyzed'],
isUploaded: json['is_uploaded'],
metadata: json['metadata'], metadata: json['metadata'],
fileChunks: json['file_chunks'],
isMature: json['is_mature'], isMature: json['is_mature'],
account: account:
json['account'] != null ? Account.fromJson(json['account']) : null, json['account'] != null ? Account.fromJson(json['account']) : null,
@ -76,7 +107,9 @@ class Attachment {
'hash': hash, 'hash': hash,
'destination': destination, 'destination': destination,
'is_analyzed': isAnalyzed, 'is_analyzed': isAnalyzed,
'is_uploaded': isUploaded,
'metadata': metadata, 'metadata': metadata,
'file_chunks': fileChunks,
'is_mature': isMature, 'is_mature': isMature,
'account': account?.toJson(), 'account': account?.toJson(),
'account_id': accountId, 'account_id': accountId,

View File

@ -1,24 +1,27 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:collection';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:cross_file/cross_file.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path/path.dart' show basename;
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
class AttachmentUploadTask { class AttachmentUploadTask {
File file; XFile file;
String usage; String pool;
Map<String, dynamic>? metadata; Map<String, dynamic>? metadata;
Map<String, int>? chunkFiles;
double progress = 0; double? progress;
bool isUploading = false; bool isUploading = false;
bool isCompleted = false; bool isCompleted = false;
dynamic error; dynamic error;
AttachmentUploadTask({ AttachmentUploadTask({
required this.file, required this.file,
required this.usage, required this.pool,
this.metadata, this.metadata,
}); });
} }
@ -73,32 +76,36 @@ class AttachmentUploaderController extends GetxController {
_startProgressSyncTimer(); _startProgressSyncTimer();
queueOfUpload[queueIndex].isUploading = true; queueOfUpload[queueIndex].isUploading = true;
queueOfUpload[queueIndex].progress = 0;
final task = queueOfUpload[queueIndex]; final task = queueOfUpload[queueIndex];
final result = await _rawUploadAttachment( try {
await task.file.readAsBytes(), final result = await _chunkedUploadAttachment(
task.file.path, task.file,
task.usage, task.pool,
null, null,
onProgress: (value) { onData: (_) {},
queueOfUpload[queueIndex].progress = value; onProgress: (progress) {
_progressOfUpload = value; queueOfUpload[queueIndex].progress = progress;
}, _progressOfUpload = progress;
onError: (err) { },
queueOfUpload[queueIndex].error = err; );
queueOfUpload[queueIndex].isUploading = false; return result;
}, } catch (err) {
); queueOfUpload[queueIndex].error = err;
queueOfUpload[queueIndex].isUploading = false;
} finally {
_progressOfUpload = 1;
if (queueOfUpload[queueIndex].error == null) {
queueOfUpload.removeAt(queueIndex);
}
_stopProgressSyncTimer();
_syncProgress();
if (queueOfUpload[queueIndex].error == null) { isUploading.value = false;
queueOfUpload.removeAt(queueIndex);
} }
_stopProgressSyncTimer();
_syncProgress();
isUploading.value = false; return null;
return result;
} }
Future<void> performUploadQueue({ Future<void> performUploadQueue({
@ -115,24 +122,26 @@ class AttachmentUploaderController extends GetxController {
} }
queueOfUpload[idx].isUploading = true; queueOfUpload[idx].isUploading = true;
queueOfUpload[idx].progress = 0;
final task = queueOfUpload[idx]; final task = queueOfUpload[idx];
final result = await _rawUploadAttachment( try {
await task.file.readAsBytes(), final result = await _chunkedUploadAttachment(
task.file.path, task.file,
task.usage, task.pool,
null, null,
onProgress: (value) { onData: (_) {},
queueOfUpload[idx].progress = value; onProgress: (progress) {
_progressOfUpload = (idx + value) / queueOfUpload.length; queueOfUpload[idx].progress = progress;
}, },
onError: (err) { );
queueOfUpload[idx].error = err; if (result != null) onData(result);
queueOfUpload[idx].isUploading = false; } catch (err) {
}, queueOfUpload[idx].error = err;
); queueOfUpload[idx].isUploading = false;
_progressOfUpload = (idx + 1) / queueOfUpload.length; } finally {
if (result != null) onData(result); _progressOfUpload = (idx + 1) / queueOfUpload.length;
}
queueOfUpload[idx].isUploading = false; queueOfUpload[idx].isUploading = false;
queueOfUpload[idx].isCompleted = true; queueOfUpload[idx].isCompleted = true;
@ -145,69 +154,94 @@ class AttachmentUploaderController extends GetxController {
isUploading.value = false; isUploading.value = false;
} }
Future<void> uploadAttachmentWithCallback( Future<Attachment?> uploadAttachmentFromData(
Uint8List data,
String path,
String pool,
Map<String, dynamic>? metadata,
Function(Attachment?) callback,
) async {
if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
pool,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
callback(result);
}
Future<Attachment?> uploadAttachment(
Uint8List data, Uint8List data,
String path, String path,
String pool, String pool,
Map<String, dynamic>? metadata, Map<String, dynamic>? metadata,
) async { ) async {
if (isUploading.value) throw Exception('uploading blocked'); if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true; isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
pool,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
return result;
}
Future<Attachment?> _rawUploadAttachment( final AttachmentProvider attach = Get.find();
Uint8List data, String path, String pool, Map<String, dynamic>? metadata,
{Function(double)? onProgress, Function(dynamic err)? onError}) async {
final AttachmentProvider provider = Get.find();
try { try {
final result = await provider.createAttachment( final result = await attach.createAttachmentDirectly(
data, data,
path, path,
pool, pool,
metadata, metadata,
onProgress: onProgress,
); );
return result; return result;
} catch (err) { } catch (_) {
if (onError != null) {
onError(err);
}
return null; return null;
} finally {
isUploading.value = false;
} }
} }
Future<Attachment?> _chunkedUploadAttachment(
XFile file,
String pool,
Map<String, dynamic>? metadata, {
required Function(AttachmentPlaceholder) onData,
required Function(double) onProgress,
}) async {
final AttachmentProvider attach = Get.find();
final holder = await attach.createAttachmentMultipartPlaceholder(
await file.length(),
file.path,
pool,
metadata,
);
onData(holder);
onProgress(0);
final filename = basename(file.path);
final chunks = holder.meta.fileChunks ?? {};
var currentTask = 0;
final queue = Queue<Future<void>>();
final activeTasks = <Future<void>>[];
for (final entry in chunks.entries) {
queue.add(() async {
final beginCursor = entry.value * holder.chunkSize;
final endCursor = (entry.value + 1) * holder.chunkSize;
final data = Uint8List.fromList(await file
.openRead(beginCursor, endCursor)
.expand((chunk) => chunk)
.toList());
final out = await attach.uploadAttachmentMultipartChunk(
data,
filename,
holder.meta.rid,
entry.key,
);
holder.meta = out;
currentTask++;
onProgress(currentTask / chunks.length);
onData(holder);
}());
}
while (queue.isNotEmpty || activeTasks.isNotEmpty) {
while (activeTasks.length < 3 && queue.isNotEmpty) {
final task = queue.removeFirst();
activeTasks.add(task);
task.then((_) => activeTasks.remove(task));
}
if (activeTasks.isNotEmpty) {
await Future.any(activeTasks);
}
}
return holder.meta;
}
} }

View File

@ -7,7 +7,6 @@ import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:dio/dio.dart' as dio;
class AttachmentProvider extends GetConnect { class AttachmentProvider extends GetConnect {
static Map<String, String> mimetypeOverrides = { static Map<String, String> mimetypeOverrides = {
@ -83,16 +82,21 @@ class AttachmentProvider extends GetConnect {
return null; return null;
} }
Future<Attachment> createAttachment( Future<Attachment> createAttachmentDirectly(
Uint8List data, String path, String pool, Map<String, dynamic>? metadata, Uint8List data,
{Function(double)? onProgress}) async { String path,
String pool,
Map<String, dynamic>? metadata,
) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized'); if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
await auth.ensureCredentials(); final client = auth.configureClient(
'uc',
timeout: const Duration(minutes: 3),
);
final filePayload = final filePayload = MultipartFile(data, filename: basename(path));
dio.MultipartFile.fromBytes(data, filename: basename(path));
final fileAlt = basename(path).contains('.') final fileAlt = basename(path).contains('.')
? basename(path).substring(0, basename(path).lastIndexOf('.')) ? basename(path).substring(0, basename(path).lastIndexOf('.'))
: basename(path); : basename(path);
@ -105,30 +109,82 @@ class AttachmentProvider extends GetConnect {
if (mimetypeOverrides.keys.contains(fileExt)) { if (mimetypeOverrides.keys.contains(fileExt)) {
mimetypeOverride = mimetypeOverrides[fileExt]; mimetypeOverride = mimetypeOverrides[fileExt];
} }
final payload = dio.FormData.fromMap({ final payload = FormData({
'alt': fileAlt, 'alt': fileAlt,
'file': filePayload, 'file': filePayload,
'pool': pool, 'pool': pool,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride, if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': jsonEncode(metadata), 'metadata': jsonEncode(metadata),
}); });
final resp = await dio.Dio( final resp = await client.post('/attachments', payload);
dio.BaseOptions(
baseUrl: ServiceFinder.buildUrl('files', null),
headers: {'Authorization': 'Bearer ${auth.credentials!.accessToken}'},
),
).post(
'/attachments',
data: payload,
onSendProgress: (count, total) {
if (onProgress != null) onProgress(count / total);
},
);
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.data); throw Exception(resp.bodyString);
} }
return Attachment.fromJson(resp.data); return Attachment.fromJson(resp.body);
}
Future<AttachmentPlaceholder> createAttachmentMultipartPlaceholder(
int size,
String path,
String pool,
Map<String, dynamic>? metadata,
) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('uc');
final fileAlt = basename(path).contains('.')
? basename(path).substring(0, basename(path).lastIndexOf('.'))
: basename(path);
final fileExt = basename(path)
.substring(basename(path).lastIndexOf('.') + 1)
.toLowerCase();
// Override for some files cannot be detected mimetype by server-side
String? mimetypeOverride;
if (mimetypeOverrides.keys.contains(fileExt)) {
mimetypeOverride = mimetypeOverrides[fileExt];
}
final resp = await client.post('/attachments/multipart', {
'alt': fileAlt,
'name': basename(path),
'size': size,
'pool': pool,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': metadata,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return AttachmentPlaceholder.fromJson(resp.body);
}
Future<Attachment> uploadAttachmentMultipartChunk(
Uint8List data,
String name,
String rid,
String cid,
) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient(
'uc',
timeout: const Duration(minutes: 3),
);
final payload = FormData({
'file': MultipartFile(data, filename: name),
});
final resp = await client.post('/attachments/multipart/$rid/$cid', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return Attachment.fromJson(resp.body);
} }
Future<Response> updateAttachment( Future<Response> updateAttachment(

View File

@ -113,7 +113,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
Attachment? attachResult; Attachment? attachResult;
try { try {
attachResult = await provider.createAttachment( attachResult = await provider.createAttachmentDirectly(
await file.readAsBytes(), await file.readAsBytes(),
file.path, file.path,
'avatar', 'avatar',

View File

@ -174,7 +174,7 @@ const i18nEnglish = {
'attachmentAttached': 'Exists Files', 'attachmentAttached': 'Exists Files',
'attachmentUploadBlocked': 'attachmentUploadBlocked':
'Upload blocked, there is currently a task in progress...', 'Upload blocked, there is currently a task in progress...',
'attachmentAdd': 'Attach attachments', 'attachmentAdd': 'Attach file',
'attachmentAddGalleryPhoto': 'Gallery photo', 'attachmentAddGalleryPhoto': 'Gallery photo',
'attachmentAddGalleryVideo': 'Gallery video', 'attachmentAddGalleryVideo': 'Gallery video',
'attachmentAddCameraPhoto': 'Capture photo', 'attachmentAddCameraPhoto': 'Capture photo',

View File

@ -64,7 +64,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
if (widget.singleMode) { if (!widget.singleMode) {
final medias = await _imagePicker.pickMultiImage( final medias = await _imagePicker.pickMultiImage(
maxWidth: widget.imageMaxWidth, maxWidth: widget.imageMaxWidth,
maxHeight: widget.imageMaxHeight, maxHeight: widget.imageMaxHeight,
@ -72,8 +72,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (medias.isEmpty) return; if (medias.isEmpty) return;
_enqueueTaskBatch(medias.map((x) { _enqueueTaskBatch(medias.map((x) {
final file = File(x.path); final file = XFile(x.path);
return AttachmentUploadTask(file: file, usage: widget.pool); return AttachmentUploadTask(file: file, pool: widget.pool);
})); }));
} else { } else {
final media = await _imagePicker.pickMedia( final media = await _imagePicker.pickMedia(
@ -83,7 +83,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (media == null) return; if (media == null) return;
_enqueueTask( _enqueueTask(
AttachmentUploadTask(file: File(media.path), usage: widget.pool), AttachmentUploadTask(file: XFile(media.path), pool: widget.pool),
); );
} }
} }
@ -95,9 +95,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final media = await _imagePicker.pickVideo(source: ImageSource.gallery); final media = await _imagePicker.pickVideo(source: ImageSource.gallery);
if (media == null) return; if (media == null) return;
final file = File(media.path);
_enqueueTask( _enqueueTask(
AttachmentUploadTask(file: file, usage: widget.pool), AttachmentUploadTask(file: XFile(media.path), pool: widget.pool),
); );
} }
@ -113,7 +112,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
List<File> files = result.paths.map((path) => File(path!)).toList(); List<File> files = result.paths.map((path) => File(path!)).toList();
_enqueueTaskBatch(files.map((x) { _enqueueTaskBatch(files.map((x) {
return AttachmentUploadTask(file: x, usage: widget.pool); return AttachmentUploadTask(file: XFile(x.path), pool: widget.pool);
})); }));
} }
@ -129,9 +128,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
} }
if (media == null) return; if (media == null) return;
final file = File(media.path);
_enqueueTask( _enqueueTask(
AttachmentUploadTask(file: file, usage: widget.pool), AttachmentUploadTask(file: XFile(media.path), pool: widget.pool),
); );
} }
@ -197,20 +195,16 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (_uploadController.isUploading.value) return; if (_uploadController.isUploading.value) return;
_uploadController.uploadAttachmentWithCallback( _uploadController
data, .uploadAttachmentFromData(data, 'Pasted Image', widget.pool, null)
'Pasted Image', .then((item) {
widget.pool, if (item == null) return;
null, widget.onAdd(item.rid);
(item) { if (mounted) {
if (item == null) return; setState(() => _attachments.add(item));
widget.onAdd(item.rid); if (widget.singleMode) Navigator.pop(context);
if (mounted) { }
setState(() => _attachments.add(item)); });
if (widget.singleMode) Navigator.pop(context);
}
},
);
} }
String _formatBytes(int bytes, {int decimals = 2}) { String _formatBytes(int bytes, {int decimals = 2}) {
@ -304,7 +298,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
], ],
); );
if (croppedFile == null) return; if (croppedFile == null) return;
_uploadController.queueOfUpload[queueIndex].file = File(croppedFile.path); _uploadController.queueOfUpload[queueIndex].file = XFile(croppedFile.path);
_uploadController.queueOfUpload.refresh(); _uploadController.queueOfUpload.refresh();
} }
@ -347,9 +341,25 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
fontFamily: 'monospace', fontFamily: 'monospace',
), ),
), ),
Text( Row(
'In queue #${index + 1}', children: [
style: const TextStyle(fontSize: 12), FutureBuilder(
future: element.file.length(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox();
return Text(
_formatBytes(snapshot.data!),
style: Theme.of(context).textTheme.bodySmall,
);
},
),
const SizedBox(width: 6),
if (element.progress != null)
Text(
'${(element.progress! * 100).toStringAsFixed(2)}%',
style: Theme.of(context).textTheme.bodySmall,
),
],
), ),
], ],
), ),
@ -581,8 +591,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
onDragDone: (detail) async { onDragDone: (detail) async {
if (_uploadController.isUploading.value) return; if (_uploadController.isUploading.value) return;
_enqueueTaskBatch(detail.files.map((x) { _enqueueTaskBatch(detail.files.map((x) {
final file = File(x.path); final file = XFile(x.path);
return AttachmentUploadTask(file: file, usage: widget.pool); return AttachmentUploadTask(file: file, pool: widget.pool);
})); }));
}, },
child: Column( child: Column(
@ -596,15 +606,13 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
children: [ children: [
Expanded( Expanded(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Text(
child: Text( 'attachmentAdd'.tr,
'attachmentAdd'.tr, style: Theme.of(context).textTheme.headlineSmall,
style: maxLines: 1,
Theme.of(context).textTheme.headlineSmall, overflow: TextOverflow.ellipsis,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Obx(() { Obx(() {

View File

@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/link_expander.dart'; import 'package:solian/providers/link_expander.dart';
@ -12,6 +13,9 @@ class LinkExpansion extends StatelessWidget {
const LinkExpansion({super.key, required this.content}); const LinkExpansion({super.key, required this.content});
Widget _buildImage(String url, {double? width, double? height}) { Widget _buildImage(String url, {double? width, double? height}) {
if (url.endsWith('svg')) {
return SvgPicture.network(url, width: width, height: height);
}
return PlatformInfo.canCacheImage return PlatformInfo.canCacheImage
? CachedNetworkImage(imageUrl: url, width: width, height: height) ? CachedNetworkImage(imageUrl: url, width: width, height: height)
: Image.network(url, width: width, height: height); : Image.network(url, width: width, height: height);
@ -20,10 +24,7 @@ class LinkExpansion extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final linkRegex = RegExp( final linkRegex = RegExp(
r'(?:(?:https?|ftp):\/\/|www\.)' r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
r'(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)'
r'[^\s<]*'
r'[^\s<?!.,:*_~]',
); );
final matches = linkRegex.allMatches(content); final matches = linkRegex.allMatches(content);
if (matches.isEmpty) { if (matches.isEmpty) {
@ -46,7 +47,7 @@ class LinkExpansion extends StatelessWidget {
} }
final isRichDescription = [ final isRichDescription = [
"solsynth.dev", 'solsynth.dev',
].contains(Uri.parse(snapshot.data!.url).host); ].contains(Uri.parse(snapshot.data!.url).host);
return GestureDetector( return GestureDetector(

View File

@ -163,6 +163,12 @@ PODS:
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- macos_window_utils (1.0.0): - macos_window_utils (1.0.0):
- FlutterMacOS - FlutterMacOS
- media_kit_libs_macos_video (1.0.4):
- FlutterMacOS
- media_kit_native_event_loop (1.0.0):
- FlutterMacOS
- media_kit_video (0.0.1):
- FlutterMacOS
- nanopb (2.30910.0): - nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0) - nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0) - nanopb/encode (= 2.30910.0)
@ -180,6 +186,8 @@ PODS:
- PromisesObjC (= 2.4.0) - PromisesObjC (= 2.4.0)
- protocol_handler_macos (0.0.1): - protocol_handler_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- screen_brightness_macos (0.1.0):
- FlutterMacOS
- share_plus (0.0.1): - share_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@ -190,9 +198,6 @@ PODS:
- FlutterMacOS - FlutterMacOS
- url_launcher_macos (0.0.1): - url_launcher_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (125.6422.04) - WebRTC-SDK (125.6422.04)
@ -212,15 +217,18 @@ DEPENDENCIES:
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`) - protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`)
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
SPEC REPOS: SPEC REPOS:
@ -272,6 +280,12 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
macos_window_utils: macos_window_utils:
:path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos :path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos
media_kit_libs_macos_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
media_kit_native_event_loop:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos
media_kit_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
package_info_plus: package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
pasteboard: pasteboard:
@ -280,6 +294,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
protocol_handler_macos: protocol_handler_macos:
:path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos
screen_brightness_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos
share_plus: share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation: shared_preferences_foundation:
@ -288,8 +304,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
url_launcher_macos: url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_player_avfoundation:
:path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin
wakelock_plus: wakelock_plus:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
@ -321,6 +335,9 @@ SPEC CHECKSUMS:
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
livekit_client: 95f3b71e6545845aa658a6df0a3a62dcc3471d7c livekit_client: 95f3b71e6545845aa658a6df0a3a62dcc3471d7c
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
nanopb: 438bc412db1928dac798aa6fd75726007be04262 nanopb: 438bc412db1928dac798aa6fd75726007be04262
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
@ -328,11 +345,11 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3 WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3

View File

@ -263,7 +263,7 @@ packages:
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
cross_file: cross_file:
dependency: transitive dependency: "direct main"
description: description:
name: cross_file name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
@ -787,6 +787,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.0" version: "0.7.0"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
url: "https://pub.dev"
source: hosted
version: "2.0.10+1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -1293,6 +1301,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
url: "https://pub.dev"
source: hosted
version: "1.0.1"
path_provider: path_provider:
dependency: transitive dependency: transitive
description: description:
@ -1978,6 +1994,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.4.2" version: "4.4.2"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.1+19 version: 1.2.1+20
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@ -71,6 +71,8 @@ dependencies:
media_kit: ^1.1.10+1 media_kit: ^1.1.10+1
media_kit_video: ^1.2.4 media_kit_video: ^1.2.4
media_kit_libs_video: ^1.0.4 media_kit_libs_video: ^1.0.4
flutter_svg: ^2.0.10+1
cross_file: ^0.3.4+2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: