✨ File uploader
This commit is contained in:
@@ -908,6 +908,15 @@
|
|||||||
"attachmentOnDevice": "On-device",
|
"attachmentOnDevice": "On-device",
|
||||||
"attachmentOnCloud": "On-cloud",
|
"attachmentOnCloud": "On-cloud",
|
||||||
"attachments": "Attachments",
|
"attachments": "Attachments",
|
||||||
|
"uploadAttachment": "Upload Attachment",
|
||||||
|
"attachmentPreview": "Attachment Preview",
|
||||||
|
"selectPool": "Select Pool",
|
||||||
|
"choosePool": "Choose a pool",
|
||||||
|
"errorLoadingPools": "Error loading pools",
|
||||||
|
"quotaCostInfo": "This upload will cost {} quota points",
|
||||||
|
"uploadConstraints": "Upload Constraints",
|
||||||
|
"fileSizeExceeded": "File size exceeds the maximum limit of {}",
|
||||||
|
"fileTypeNotAccepted": "File type is not accepted by this pool",
|
||||||
"publisherCollabInvitation": "Collabration invitations",
|
"publisherCollabInvitation": "Collabration invitations",
|
||||||
"publisherCollabInvitationCount": {
|
"publisherCollabInvitationCount": {
|
||||||
"zero": "No invitation",
|
"zero": "No invitation",
|
||||||
@@ -1052,5 +1061,6 @@
|
|||||||
"confirmDeleteRecycledFiles": "Are you sure you want to delete all recycled files?",
|
"confirmDeleteRecycledFiles": "Are you sure you want to delete all recycled files?",
|
||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "Delete Recycled Files",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files"
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
|
"upload": "Upload"
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@ import "package:island/widgets/alert.dart";
|
|||||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||||
import "package:uuid/uuid.dart";
|
import "package:uuid/uuid.dart";
|
||||||
import "package:island/screens/chat/chat.dart";
|
import "package:island/screens/chat/chat.dart";
|
||||||
import "package:island/pods/room_providers.dart";
|
import "package:island/pods/chat_rooms.dart";
|
||||||
|
|
||||||
part 'messages_notifier.g.dart';
|
part 'messages_notifier.g.dart';
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/pool_provider.dart';
|
import 'package:island/pods/file_pool.dart';
|
||||||
import 'package:island/utils/format.dart';
|
import 'package:island/utils/format.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
@@ -10,6 +10,7 @@ import 'package:island/screens/creators/publishers.dart';
|
|||||||
import 'package:island/screens/posts/compose_article.dart';
|
import 'package:island/screens/posts/compose_article.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:island/widgets/attachment_uploader.dart';
|
||||||
import 'package:island/widgets/content/attachment_preview.dart';
|
import 'package:island/widgets/content/attachment_preview.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.dart';
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
@@ -225,8 +226,26 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
return AttachmentPreview(
|
return AttachmentPreview(
|
||||||
item: state.attachments.value[idx],
|
item: state.attachments.value[idx],
|
||||||
progress: progressMap[idx],
|
progress: progressMap[idx],
|
||||||
onRequestUpload:
|
onRequestUpload: () async {
|
||||||
() => ComposeLogic.uploadAttachment(ref, state, idx),
|
final config = await showModalBottomSheet<AttachmentUploadConfig>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => AttachmentUploaderSheet(
|
||||||
|
ref: ref,
|
||||||
|
state: state,
|
||||||
|
index: idx,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (config != null) {
|
||||||
|
await ComposeLogic.uploadAttachment(
|
||||||
|
ref,
|
||||||
|
state,
|
||||||
|
idx,
|
||||||
|
poolId: config.poolId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx),
|
onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx),
|
||||||
onUpdate:
|
onUpdate:
|
||||||
(value) => ComposeLogic.updateAttachment(state, value, idx),
|
(value) => ComposeLogic.updateAttachment(state, value, idx),
|
||||||
@@ -253,8 +272,27 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
return AttachmentPreview(
|
return AttachmentPreview(
|
||||||
item: state.attachments.value[idx],
|
item: state.attachments.value[idx],
|
||||||
progress: progressMap[idx],
|
progress: progressMap[idx],
|
||||||
onRequestUpload:
|
onRequestUpload: () async {
|
||||||
() => ComposeLogic.uploadAttachment(ref, state, idx),
|
final config =
|
||||||
|
await showModalBottomSheet<AttachmentUploadConfig>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => AttachmentUploaderSheet(
|
||||||
|
ref: ref,
|
||||||
|
state: state,
|
||||||
|
index: idx,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (config != null) {
|
||||||
|
await ComposeLogic.uploadAttachment(
|
||||||
|
ref,
|
||||||
|
state,
|
||||||
|
idx,
|
||||||
|
poolId: config.poolId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
onDelete:
|
onDelete:
|
||||||
() => ComposeLogic.deleteAttachment(ref, state, idx),
|
() => ComposeLogic.deleteAttachment(ref, state, idx),
|
||||||
onUpdate:
|
onUpdate:
|
||||||
|
@@ -11,6 +11,7 @@ import 'package:island/models/post.dart';
|
|||||||
import 'package:island/screens/creators/publishers.dart';
|
import 'package:island/screens/creators/publishers.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:island/widgets/attachment_uploader.dart';
|
||||||
import 'package:island/screens/posts/post_detail.dart';
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
import 'package:island/widgets/content/attachment_preview.dart';
|
import 'package:island/widgets/content/attachment_preview.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
@@ -345,12 +346,30 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
isCompact: true,
|
isCompact: true,
|
||||||
item: attachments[idx],
|
item: attachments[idx],
|
||||||
progress: progressMap[idx],
|
progress: progressMap[idx],
|
||||||
onRequestUpload:
|
onRequestUpload: () async {
|
||||||
() => ComposeLogic.uploadAttachment(
|
final config =
|
||||||
|
await showModalBottomSheet<
|
||||||
|
AttachmentUploadConfig
|
||||||
|
>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) =>
|
||||||
|
AttachmentUploaderSheet(
|
||||||
|
ref: ref,
|
||||||
|
state: state,
|
||||||
|
index: idx,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (config != null) {
|
||||||
|
await ComposeLogic.uploadAttachment(
|
||||||
ref,
|
ref,
|
||||||
state,
|
state,
|
||||||
idx,
|
idx,
|
||||||
),
|
poolId: config.poolId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
onUpdate:
|
onUpdate:
|
||||||
(value) =>
|
(value) =>
|
||||||
ComposeLogic.updateAttachment(
|
ComposeLogic.updateAttachment(
|
||||||
|
@@ -21,7 +21,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/pool_provider.dart';
|
import 'package:island/pods/file_pool.dart';
|
||||||
import 'package:island/models/file_pool.dart';
|
import 'package:island/models/file_pool.dart';
|
||||||
|
|
||||||
class SettingsScreen extends HookConsumerWidget {
|
class SettingsScreen extends HookConsumerWidget {
|
||||||
|
365
lib/widgets/attachment_uploader.dart
Normal file
365
lib/widgets/attachment_uploader.dart
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/file_pool.dart';
|
||||||
|
import 'package:island/pods/file_pool.dart';
|
||||||
|
import 'package:island/widgets/content/attachment_preview.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class AttachmentUploadConfig {
|
||||||
|
final String poolId;
|
||||||
|
final bool hasConstraints;
|
||||||
|
|
||||||
|
const AttachmentUploadConfig({
|
||||||
|
required this.poolId,
|
||||||
|
required this.hasConstraints,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentUploaderSheet extends StatefulWidget {
|
||||||
|
final WidgetRef ref;
|
||||||
|
final ComposeState state;
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
const AttachmentUploaderSheet({
|
||||||
|
super.key,
|
||||||
|
required this.ref,
|
||||||
|
required this.state,
|
||||||
|
required this.index,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AttachmentUploaderSheet> createState() =>
|
||||||
|
_AttachmentUploaderSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
||||||
|
String? selectedPoolId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final attachment = widget.state.attachments.value[widget.index];
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: 'uploadAttachment'.tr(),
|
||||||
|
child: FutureBuilder<List<SnFilePool>>(
|
||||||
|
future: widget.ref.read(poolsProvider.future),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Center(child: Text('errorLoadingPools'.tr()));
|
||||||
|
}
|
||||||
|
final pools = snapshot.data!.filterValid();
|
||||||
|
selectedPoolId ??= resolveDefaultPoolId(widget.ref, pools);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: selectedPoolId,
|
||||||
|
items:
|
||||||
|
pools.map((pool) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: pool.id,
|
||||||
|
child: Text(pool.name),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
selectedPoolId = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'selectPool'.tr(),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'choosePool'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
FutureBuilder<int?>(
|
||||||
|
future: _getFileSize(attachment),
|
||||||
|
builder: (context, sizeSnapshot) {
|
||||||
|
if (!sizeSnapshot.hasData) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final fileSize = sizeSnapshot.data!;
|
||||||
|
final selectedPool = pools.firstWhere(
|
||||||
|
(p) => p.id == selectedPoolId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check file size limit
|
||||||
|
final maxFileSize =
|
||||||
|
selectedPool.policyConfig?['max_file_size']
|
||||||
|
as int?;
|
||||||
|
final fileSizeExceeded =
|
||||||
|
maxFileSize != null && fileSize > maxFileSize;
|
||||||
|
|
||||||
|
// Check accepted types
|
||||||
|
final acceptTypes =
|
||||||
|
selectedPool.policyConfig?['accept_types']
|
||||||
|
as List?;
|
||||||
|
final mimeType =
|
||||||
|
attachment.data.mimeType ??
|
||||||
|
ComposeLogic.getMimeTypeFromFileType(
|
||||||
|
attachment.type,
|
||||||
|
);
|
||||||
|
final typeAccepted =
|
||||||
|
acceptTypes == null ||
|
||||||
|
acceptTypes.isEmpty ||
|
||||||
|
acceptTypes.any(
|
||||||
|
(type) => mimeType.startsWith(type),
|
||||||
|
);
|
||||||
|
|
||||||
|
final hasIssues = fileSizeExceeded || !typeAccepted;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (hasIssues) ...[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.errorContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.warning,
|
||||||
|
size: 18,
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.error,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'uploadConstraints'.tr(),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.error,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (fileSizeExceeded) ...[
|
||||||
|
const Gap(4),
|
||||||
|
Text(
|
||||||
|
'fileSizeExceeded'.tr(
|
||||||
|
args: [
|
||||||
|
_formatFileSize(maxFileSize),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (!typeAccepted) ...[
|
||||||
|
const Gap(4),
|
||||||
|
Text(
|
||||||
|
'fileTypeNotAccepted'.tr(),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
],
|
||||||
|
Row(
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Symbols.account_balance_wallet,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'quotaCostInfo'.tr(
|
||||||
|
args: [
|
||||||
|
_formatQuotaCost(
|
||||||
|
fileSize,
|
||||||
|
selectedPool,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium,
|
||||||
|
).fontSize(13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Row(
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.info, size: 18),
|
||||||
|
Text(
|
||||||
|
'attachmentPreview'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
).fontSize(13),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 4),
|
||||||
|
const Gap(8),
|
||||||
|
AttachmentPreview(item: attachment, isCompact: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
icon: const Icon(Symbols.close),
|
||||||
|
label: Text('cancel').tr(),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => _confirmUpload(),
|
||||||
|
icon: const Icon(Symbols.upload),
|
||||||
|
label: Text('upload').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AttachmentUploadConfig?> _getUploadConfig() async {
|
||||||
|
final attachment = widget.state.attachments.value[widget.index];
|
||||||
|
final fileSize = await _getFileSize(attachment);
|
||||||
|
|
||||||
|
if (fileSize == null) return null;
|
||||||
|
|
||||||
|
// Get the selected pool to check constraints
|
||||||
|
final pools = await widget.ref.read(poolsProvider.future);
|
||||||
|
final selectedPool = pools.filterValid().firstWhere(
|
||||||
|
(p) => p.id == selectedPoolId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check constraints
|
||||||
|
final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?;
|
||||||
|
final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize;
|
||||||
|
|
||||||
|
final acceptTypes = selectedPool.policyConfig?['accept_types'] as List?;
|
||||||
|
final mimeType =
|
||||||
|
attachment.data.mimeType ??
|
||||||
|
ComposeLogic.getMimeTypeFromFileType(attachment.type);
|
||||||
|
final typeAccepted =
|
||||||
|
acceptTypes == null ||
|
||||||
|
acceptTypes.isEmpty ||
|
||||||
|
acceptTypes.any((type) => mimeType.startsWith(type));
|
||||||
|
|
||||||
|
final hasConstraints = fileSizeExceeded || !typeAccepted;
|
||||||
|
|
||||||
|
return AttachmentUploadConfig(
|
||||||
|
poolId: selectedPoolId!,
|
||||||
|
hasConstraints: hasConstraints,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmUpload() async {
|
||||||
|
final config = await _getUploadConfig();
|
||||||
|
if (config != null && mounted) {
|
||||||
|
Navigator.pop(context, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int?> _getFileSize(UniversalFile attachment) async {
|
||||||
|
if (attachment.data is XFile) {
|
||||||
|
try {
|
||||||
|
return await (attachment.data as XFile).length();
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (attachment.data is SnCloudFile) {
|
||||||
|
return (attachment.data as SnCloudFile).size;
|
||||||
|
} else if (attachment.data is List<int>) {
|
||||||
|
return (attachment.data as List<int>).length;
|
||||||
|
} else if (attachment.data is Uint8List) {
|
||||||
|
return (attachment.data as Uint8List).length;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatNumber(int number) {
|
||||||
|
if (number >= 1000000) {
|
||||||
|
return '${(number / 1000000).toStringAsFixed(1)}M';
|
||||||
|
} else if (number >= 1000) {
|
||||||
|
return '${(number / 1000).toStringAsFixed(1)}K';
|
||||||
|
} else {
|
||||||
|
return number.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatFileSize(int bytes) {
|
||||||
|
if (bytes >= 1073741824) {
|
||||||
|
return '${(bytes / 1073741824).toStringAsFixed(1)} GB';
|
||||||
|
} else if (bytes >= 1048576) {
|
||||||
|
return '${(bytes / 1048576).toStringAsFixed(1)} MB';
|
||||||
|
} else if (bytes >= 1024) {
|
||||||
|
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||||
|
} else {
|
||||||
|
return '$bytes bytes';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatQuotaCost(int fileSize, SnFilePool pool) {
|
||||||
|
final costMultiplier = pool.billingConfig?['cost_multiplier'] ?? 1.0;
|
||||||
|
final quotaCost = ((fileSize / 1024 / 1024) * costMultiplier).round();
|
||||||
|
return _formatNumber(quotaCost);
|
||||||
|
}
|
||||||
|
}
|
@@ -470,7 +470,8 @@ class AttachmentPreview extends HookConsumerWidget {
|
|||||||
if (onRequestUpload != null)
|
if (onRequestUpload != null)
|
||||||
InkWell(
|
InkWell(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
onTap: () => onRequestUpload?.call(),
|
onTap:
|
||||||
|
item.isOnCloud ? null : () => onRequestUpload?.call(),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
@@ -20,7 +20,7 @@ import 'package:island/widgets/alert.dart';
|
|||||||
import 'package:island/widgets/post/compose_link_attachments.dart';
|
import 'package:island/widgets/post/compose_link_attachments.dart';
|
||||||
import 'package:island/widgets/post/compose_poll.dart';
|
import 'package:island/widgets/post/compose_poll.dart';
|
||||||
import 'package:island/widgets/post/compose_recorder.dart';
|
import 'package:island/widgets/post/compose_recorder.dart';
|
||||||
import 'package:island/pods/pool_provider.dart';
|
import 'package:island/pods/file_pool.dart';
|
||||||
import 'package:pasteboard/pasteboard.dart';
|
import 'package:pasteboard/pasteboard.dart';
|
||||||
import 'package:textfield_tags/textfield_tags.dart';
|
import 'package:textfield_tags/textfield_tags.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@@ -672,7 +672,7 @@ class ComposeLogic {
|
|||||||
try {
|
try {
|
||||||
state.submitting.value = true;
|
state.submitting.value = true;
|
||||||
|
|
||||||
// Upload any local attachments first
|
// pload any local attachments first
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
state.attachments.value
|
state.attachments.value
|
||||||
.asMap()
|
.asMap()
|
||||||
|
Reference in New Issue
Block a user