Compare commits

...

3 Commits

Author SHA1 Message Date
e4019dadc8 💄 Optimize file upload prograss indicates 2025-11-09 01:59:24 +08:00
5e7d77e1a1 🐛 Fix share sheet error 2025-11-08 20:05:18 +08:00
bfcbed035c ♻️ Refactored file uploading 2025-11-08 20:04:54 +08:00
15 changed files with 208 additions and 121 deletions

View File

@@ -1,7 +1,5 @@
PODS:
- Alamofire (5.10.2)
- app_links (6.4.1):
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- croppy (0.0.1):
@@ -52,18 +50,18 @@ PODS:
- Firebase/Messaging (12.4.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0)
- firebase_analytics (12.0.3):
- firebase_analytics (12.0.4):
- firebase_core
- FirebaseAnalytics (= 12.4.0)
- Flutter
- firebase_core (4.2.0):
- firebase_core (4.2.1):
- Firebase/CoreOnly (= 12.4.0)
- Flutter
- firebase_crashlytics (5.0.3):
- firebase_crashlytics (5.0.4):
- Firebase/Crashlytics (= 12.4.0)
- firebase_core
- Flutter
- firebase_messaging (16.0.3):
- firebase_messaging (16.0.4):
- Firebase/Messaging (= 12.4.0)
- firebase_core
- Flutter
@@ -265,6 +263,8 @@ PODS:
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- protocol_handler_ios (0.0.1):
- Flutter
- receive_sharing_intent (1.8.1):
- Flutter
- record_ios (1.1.0):
@@ -323,7 +323,6 @@ PODS:
DEPENDENCIES:
- Alamofire
- app_links (from `.symlinks/plugins/app_links/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@@ -358,6 +357,7 @@ DEPENDENCIES:
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- record_ios (from `.symlinks/plugins/record_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
@@ -404,8 +404,6 @@ SPEC REPOS:
- WebRTC-SDK
EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
croppy:
@@ -470,6 +468,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
pointer_interceptor_ios:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
protocol_handler_ios:
:path: ".symlinks/plugins/protocol_handler_ios/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
record_ios:
@@ -497,7 +497,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
@@ -506,10 +505,10 @@ SPEC CHECKSUMS:
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_analytics: 1d024068b1d4707d5ba7a42a12976ddf3316d835
firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464
firebase_crashlytics: f3a9a4338ab99b67042f64e9e22e1bf349cb44ed
firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
firebase_crashlytics: 83c7467d7534975a4d779af43bd226d0a4616464
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
@@ -553,6 +552,7 @@ SPEC CHECKSUMS:
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c

View File

@@ -28,7 +28,7 @@ class MessagesNotifier extends _$MessagesNotifier {
late final SnChatMember _identity;
final Map<String, LocalChatMessage> _pendingMessages = {};
final Map<String, Map<int, double>> _fileUploadProgress = {};
final Map<String, Map<int, double?>> _fileUploadProgress = {};
int? _totalCount;
String? _searchQuery;
bool? _withLinks;
@@ -438,7 +438,7 @@ class MessagesNotifier extends _$MessagesNotifier {
SnChatMessage? editingTo,
SnChatMessage? forwardingTo,
SnChatMessage? replyingTo,
Function(String, Map<int, double>)? onProgress,
Function(String, Map<int, double?>)? onProgress,
}) async {
final nonce = const Uuid().v4();
talker.log('Sending message with nonce $nonce');
@@ -474,7 +474,7 @@ class MessagesNotifier extends _$MessagesNotifier {
fileData: attachments[idx],
client: ref.read(apiClientProvider),
onProgress: (progress, _) {
_fileUploadProgress[localMessage.id]?[idx] = progress;
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
onProgress?.call(
localMessage.id,
_fileUploadProgress[localMessage.id] ?? {},

View File

@@ -2,6 +2,7 @@ import "dart:async";
import "dart:math" as math;
import "package:easy_localization/easy_localization.dart";
import "package:file_picker/file_picker.dart";
import "package:image_picker/image_picker.dart";
import "package:flutter/material.dart";
import "package:go_router/go_router.dart";
import "package:flutter_hooks/flutter_hooks.dart";
@@ -148,7 +149,7 @@ class ChatRoomScreen extends HookConsumerWidget {
final messageForwardingTo = useState<SnChatMessage?>(null);
final messageEditingTo = useState<SnChatMessage?>(null);
final attachments = useState<List<UniversalFile>>([]);
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
final attachmentProgress = useState<Map<String, Map<int, double?>>>({});
// Selection mode state
final isSelectionMode = useState<bool>(false);
@@ -181,16 +182,13 @@ class ChatRoomScreen extends HookConsumerWidget {
}, [scrollController]);
Future<void> pickPhotoMedia() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
allowCompression: false,
);
if (result == null || result.count == 0) return;
final ImagePicker picker = ImagePicker();
final List<XFile> results = await picker.pickMultiImage();
if (results.isEmpty) return;
attachments.value = [
...attachments.value,
...result.files.map(
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
...results.map(
(xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
),
];
}
@@ -573,7 +571,7 @@ class ChatRoomScreen extends HookConsumerWidget {
onProgress: (progress, _) {
attachmentProgress.value = {
...attachmentProgress.value,
'chat-upload': {index: progress},
'chat-upload': {index: progress ?? 0.0},
};
},
).future;

View File

@@ -306,7 +306,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
],
),
children: [
ValueListenableBuilder<Map<int, double>>(
ValueListenableBuilder<Map<int, double?>>(
valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) {
return Wrap(

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:convert/convert.dart';
import 'package:cross_file/cross_file.dart';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
@@ -21,9 +22,51 @@ class FileUploader {
return digest.toString();
}
/// Calculates the MD5 hash from a stream.
Future<String> _calculateFileHashFromStream(Stream<List<int>> stream) async {
final accumulator = AccumulatorSink<Digest>();
final converter = md5.startChunkedConversion(accumulator);
await for (final chunk in stream) {
converter.add(chunk);
}
converter.close();
final digest = accumulator.events.single;
return digest.toString();
}
/// Reads the next chunk from a stream subscription.
Future<Uint8List> _readNextChunk(
StreamSubscription<List<int>> subscription,
int size,
) async {
final completer = Completer<Uint8List>();
final buffer = <int>[];
int remaining = size;
void onData(List<int> data) {
buffer.addAll(data);
remaining -= data.length;
if (remaining <= 0) {
subscription.pause();
completer.complete(Uint8List.fromList(buffer.sublist(0, size)));
}
}
void onDone() {
if (!completer.isCompleted) {
completer.complete(Uint8List.fromList(buffer));
}
}
subscription.onData(onData);
subscription.onDone(onDone);
return completer.future;
}
/// Creates an upload task for the given file.
Future<Map<String, dynamic>> createUploadTask({
required Uint8List bytes,
required dynamic fileData,
required String fileName,
required String contentType,
String? poolId,
@@ -32,8 +75,17 @@ class FileUploader {
String? expiredAt,
int? chunkSize,
}) async {
final hash = _calculateFileHash(bytes);
final fileSize = bytes.length;
String hash;
int fileSize;
if (fileData is XFile) {
fileSize = await fileData.length();
hash = await _calculateFileHashFromStream(fileData.openRead());
} else if (fileData is Uint8List) {
hash = _calculateFileHash(fileData);
fileSize = fileData.length;
} else {
throw ArgumentError('Invalid fileData type');
}
final response = await _client.post(
'/drive/files/upload/create',
@@ -58,6 +110,7 @@ class FileUploader {
required String taskId,
required int chunkIndex,
required Uint8List chunkData,
ProgressCallback? onSendProgress,
}) async {
final formData = FormData.fromMap({
'chunk': MultipartFile.fromBytes(
@@ -69,6 +122,7 @@ class FileUploader {
await _client.post(
'/drive/files/upload/chunk/$taskId/$chunkIndex',
data: formData,
onSendProgress: onSendProgress,
);
}
@@ -81,7 +135,7 @@ class FileUploader {
/// Uploads a file in chunks using the multi-part API.
Future<SnCloudFile> uploadFile({
required Uint8List bytes,
required dynamic fileData,
required String fileName,
required String contentType,
String? poolId,
@@ -89,10 +143,12 @@ class FileUploader {
String? encryptPassword,
String? expiredAt,
int? customChunkSize,
Function(double? progress, Duration estimate)? onProgress,
}) async {
// Step 1: Create upload task
onProgress?.call(null, Duration.zero);
final createResponse = await createUploadTask(
bytes: bytes,
fileData: fileData,
fileName: fileName,
contentType: contentType,
poolId: poolId,
@@ -110,27 +166,64 @@ class FileUploader {
final taskId = createResponse['task_id'] as String;
final chunkSize = createResponse['chunk_size'] as int;
final chunksCount = createResponse['chunks_count'] as int;
int totalSize;
if (fileData is XFile) {
totalSize = await fileData.length();
} else if (fileData is Uint8List) {
totalSize = fileData.length;
} else {
throw ArgumentError('Invalid fileData type');
}
// Step 2: Upload chunks
final chunks = <Uint8List>[];
for (int i = 0; i < bytes.length; i += chunkSize) {
final end = i + chunkSize > bytes.length ? bytes.length : i + chunkSize;
chunks.add(Uint8List.fromList(bytes.sublist(i, end)));
}
int bytesUploaded = 0;
if (fileData is XFile) {
// Use stream for XFile
final subscription = fileData.openRead().listen(null);
subscription.pause();
for (int i = 0; i < chunksCount; i++) {
subscription.resume();
final chunkData = await _readNextChunk(subscription, chunkSize);
await uploadChunk(
taskId: taskId,
chunkIndex: i,
chunkData: chunkData,
onSendProgress: (sent, total) {
final overallProgress = (bytesUploaded + sent) / totalSize;
onProgress?.call(overallProgress, Duration.zero);
},
);
bytesUploaded += chunkData.length;
}
subscription.cancel();
} else if (fileData is Uint8List) {
// Use old way for Uint8List
final chunks = <Uint8List>[];
for (int i = 0; i < fileData.length; i += chunkSize) {
final end =
i + chunkSize > fileData.length ? fileData.length : i + chunkSize;
chunks.add(Uint8List.fromList(fileData.sublist(i, end)));
}
// Ensure we have the correct number of chunks
if (chunks.length != chunksCount) {
throw Exception(
'Chunk count mismatch: expected $chunksCount, got ${chunks.length}',
);
}
// Upload each chunk
for (int i = 0; i < chunks.length; i++) {
await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]);
// Upload each chunk
for (int i = 0; i < chunks.length; i++) {
await uploadChunk(
taskId: taskId,
chunkIndex: i,
chunkData: chunks[i],
onSendProgress: (sent, total) {
final overallProgress = (bytesUploaded + sent) / totalSize;
onProgress?.call(overallProgress, Duration.zero);
},
);
bytesUploaded += chunks[i].length;
}
} else {
throw ArgumentError('Invalid fileData type');
}
// Step 3: Complete upload
onProgress?.call(null, Duration.zero);
return await completeUpload(taskId);
}
@@ -139,7 +232,7 @@ class FileUploader {
required Dio client,
String? poolId,
FileUploadMode? mode,
Function(double progress, Duration estimate)? onProgress,
Function(double? progress, Duration estimate)? onProgress,
}) {
final completer = Completer<SnCloudFile?>();
@@ -205,7 +298,7 @@ class FileUploader {
UniversalFile fileData,
Dio client,
String? poolId,
Function(double progress, Duration estimate)? onProgress,
Function(double? progress, Duration estimate)? onProgress,
Completer<SnCloudFile?> completer,
) {
String actualMimetype = getMimeType(fileData);
@@ -216,23 +309,15 @@ class FileUploader {
final data = fileData.data;
if (data is XFile) {
// Read bytes from XFile
data
.readAsBytes()
.then((readBytes) {
_performUpload(
bytes: readBytes,
fileName: fileData.displayName ?? data.name,
contentType: actualMimetype,
client: client,
poolId: poolId,
onProgress: onProgress,
completer: completer,
);
})
.catchError((e) {
completer.completeError(e);
});
_performUpload(
fileData: data,
fileName: fileData.displayName ?? data.name,
contentType: actualMimetype,
client: client,
poolId: poolId,
onProgress: onProgress,
completer: completer,
);
return completer;
} else if (data is List<int> || data is Uint8List) {
bytes = data is List<int> ? Uint8List.fromList(data) : data;
@@ -252,7 +337,7 @@ class FileUploader {
if (bytes != null) {
_performUpload(
bytes: bytes,
fileData: bytes,
fileName: actualFilename,
contentType: actualMimetype,
client: client,
@@ -267,28 +352,29 @@ class FileUploader {
// Helper method to perform the actual upload
static void _performUpload({
required Uint8List bytes,
required dynamic fileData,
required String fileName,
required String contentType,
required Dio client,
String? poolId,
Function(double progress, Duration estimate)? onProgress,
Function(double? progress, Duration estimate)? onProgress,
required Completer<SnCloudFile?> completer,
}) {
final uploader = FileUploader(client);
// Call progress start
onProgress?.call(0.0, Duration.zero);
onProgress?.call(null, Duration.zero);
uploader
.uploadFile(
bytes: bytes,
fileData: fileData,
fileName: fileName,
contentType: contentType,
poolId: poolId,
onProgress: onProgress,
)
.then((result) {
// Call progress end
onProgress?.call(1.0, Duration.zero);
onProgress?.call(null, Duration.zero);
completer.complete(result);
})
.catchError((e) {

View File

@@ -44,7 +44,7 @@ class ChatInput extends HookConsumerWidget {
final Function(int) onDeleteAttachment;
final Function(int, int) onMoveAttachment;
final Function(List<UniversalFile>) onAttachmentsChanged;
final Map<String, Map<int, double>> attachmentProgress;
final Map<String, Map<int, double?>> attachmentProgress;
const ChatInput({
super.key,

View File

@@ -40,7 +40,7 @@ class MessageItem extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Function(String action)? onAction;
final Map<int, double>? progress;
final Map<int, double?>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final bool isSelectionMode;
@@ -689,7 +689,7 @@ class MessageHoverActionMenu extends StatelessWidget {
class MessageItemDisplayBubble extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Map<int, double>? progress;
final Map<int, double?>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final String? translatedText;
@@ -821,7 +821,7 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
class MessageItemDisplayIRC extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Map<int, double>? progress;
final Map<int, double?>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final String? translatedText;
@@ -949,7 +949,7 @@ class MessageItemDisplayIRC extends HookConsumerWidget {
class MessageItemDisplayDiscord extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Map<int, double>? progress;
final Map<int, double?>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final String? translatedText;
@@ -1238,7 +1238,7 @@ class MessageQuoteWidget extends HookConsumerWidget {
}
class FileUploadProgressWidget extends StatelessWidget {
final Map<int, double>? progress;
final Map<int, double?>? progress;
final Color textColor;
final bool hasContent;
@@ -1266,7 +1266,9 @@ class FileUploadProgressWidget extends StatelessWidget {
'fileUploadingProgress'.tr(
args: [
(entry.key + 1).toString(),
(entry.value * 100).toStringAsFixed(1),
entry.value != null
? (entry.value! * 100).toStringAsFixed(1)
: '0.0',
],
),
style: TextStyle(

View File

@@ -411,10 +411,7 @@ class AttachmentPreview extends HookConsumerWidget {
),
Gap(6),
Center(
child: LinearProgressIndicator(
value:
progress != null ? progress! / 100.0 : null,
),
child: LinearProgressIndicator(value: progress),
),
],
),

View File

@@ -112,23 +112,28 @@ class CloudFilePicker extends HookConsumerWidget {
void pickImage() async {
showLoadingModal(context);
final result = await FilePicker.platform.pickFiles(
allowMultiple: allowMultiple,
type: FileType.image,
);
if (result == null || result.files.isEmpty) {
final ImagePicker picker = ImagePicker();
List<XFile> results;
if (allowMultiple) {
results = await picker.pickMultiImage();
} else {
final XFile? result = await picker.pickImage(
source: ImageSource.gallery,
);
results = result != null ? [result] : [];
}
if (results.isEmpty) {
if (context.mounted) hideLoadingModal(context);
return;
}
final newFiles =
result.files.map((e) {
final xfile =
e.bytes != null
? XFile.fromData(e.bytes!, name: e.name)
: XFile(e.path!);
return UniversalFile(data: xfile, type: UniversalFileType.image);
}).toList();
results
.map(
(xfile) =>
UniversalFile(data: xfile, type: UniversalFileType.image),
)
.toList();
if (!allowMultiple) {
files.value = newFiles;

View File

@@ -131,7 +131,7 @@ class ArticleComposeAttachments extends ConsumerWidget {
],
),
children: [
ValueListenableBuilder<Map<int, double>>(
ValueListenableBuilder<Map<int, double?>>(
valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) {
return Wrap(

View File

@@ -33,7 +33,7 @@ class ComposeState {
final TextEditingController slugController;
final ValueNotifier<int> visibility;
final ValueNotifier<List<UniversalFile>> attachments;
final ValueNotifier<Map<int, double>> attachmentProgress;
final ValueNotifier<Map<int, double?>> attachmentProgress;
final ValueNotifier<SnPublisher?> currentPublisher;
final ValueNotifier<bool> submitting;
final ValueNotifier<List<SnPostCategory>> categories;
@@ -402,16 +402,13 @@ class ComposeLogic {
}
static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
allowCompression: false,
);
if (result == null || result.count == 0) return;
final ImagePicker picker = ImagePicker();
final List<XFile> results = await picker.pickMultiImage();
if (results.isEmpty) return;
state.attachments.value = [
...state.attachments.value,
...result.files.map(
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
...results.map(
(xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
),
];
}
@@ -523,7 +520,7 @@ class ComposeLogic {
onProgress: (progress, _) {
state.attachmentProgress.value = {
...state.attachmentProgress.value,
index: progress,
index: progress ?? 0.0,
};
},
).future;

View File

@@ -246,7 +246,8 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
onProgress: (progress, _) {
if (mounted) {
setState(() {
_fileUploadProgress[messageId]?[idx] = progress;
_fileUploadProgress[messageId]?[idx] =
progress ?? 0.0;
});
}
},
@@ -306,7 +307,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
// Navigate to chat if requested
if (shouldNavigate == true && mounted) {
context.push('/sphere/chat/${chatRoom.id}');
context.push('/chat/${chatRoom.id}');
}
}
} catch (e) {

View File

@@ -19,19 +19,19 @@ PODS:
- Firebase/Messaging (12.4.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0)
- firebase_analytics (12.0.3):
- firebase_analytics (12.0.4):
- firebase_core
- FirebaseAnalytics (= 12.4.0)
- FlutterMacOS
- firebase_core (4.2.0):
- firebase_core (4.2.1):
- Firebase/CoreOnly (~> 12.4.0)
- FlutterMacOS
- firebase_crashlytics (5.0.3):
- firebase_crashlytics (5.0.4):
- Firebase/CoreOnly (~> 12.4.0)
- Firebase/Crashlytics (~> 12.4.0)
- firebase_core
- FlutterMacOS
- firebase_messaging (16.0.3):
- firebase_messaging (16.0.4):
- Firebase/CoreOnly (~> 12.4.0)
- Firebase/Messaging (~> 12.4.0)
- firebase_core
@@ -416,10 +416,10 @@ SPEC CHECKSUMS:
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_analytics: d876586269c1d8d2b3dcac085bc2d97c62abc9df
firebase_core: d81d1a44df95699ce074ae63d8cb43e9df21e142
firebase_crashlytics: 723622cc39a9fa7320585424f5864c5699893ce1
firebase_messaging: 31f412ae5a54e02d1c46d467969f7ad92c4b81ec
firebase_analytics: 09241c4796c1c42a02349ef8bf30025f5b640f0e
firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc
firebase_crashlytics: c2438b5f5bdcacf59d0eaee5852c6b0ab09dab77
firebase_messaging: 373ac3a56e5aa37bb9aff4127f700aa5973c1168
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018

View File

@@ -282,7 +282,7 @@ packages:
source: hosted
version: "4.1.0"
convert:
dependency: transitive
dependency: "direct main"
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
@@ -1185,10 +1185,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c
sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104
url: "https://pub.dev"
source: hosted
version: "16.3.0"
version: "17.0.0"
google_fonts:
dependency: "direct main"
description:

View File

@@ -38,7 +38,7 @@ dependencies:
cupertino_icons: ^1.0.8
flutter_hooks: ^0.21.3+1
hooks_riverpod: ^2.6.1
go_router: ^16.3.0
go_router: ^17.0.0
styled_widget: ^0.4.1
shared_preferences: ^2.5.3
flutter_riverpod: ^2.6.1
@@ -165,6 +165,7 @@ dependencies:
dio_smart_retry: ^7.0.1
flutter_expandable_fab: ^2.5.2
event_bus: ^2.0.1
convert: ^3.1.2
dev_dependencies:
flutter_test: