313 lines
11 KiB
Dart
313 lines
11 KiB
Dart
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:gap/gap.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:island/models/file.dart';
|
|
import 'package:island/pods/config.dart';
|
|
import 'package:island/pods/network.dart';
|
|
import 'package:island/screens/posts/compose.dart';
|
|
import 'package:island/services/file.dart';
|
|
import 'package:island/widgets/alert.dart';
|
|
import 'package:material_symbols_icons/symbols.dart';
|
|
import 'package:styled_widget/styled_widget.dart';
|
|
|
|
class CloudFilePicker extends HookConsumerWidget {
|
|
final bool allowMultiple;
|
|
const CloudFilePicker({super.key, this.allowMultiple = false});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final files = useState<List<UniversalFile>>([]);
|
|
|
|
final uploadPosition = useState<int?>(null);
|
|
final uploadProgress = useState<double?>(null);
|
|
|
|
final uploadOverallProgress = useMemoized<double?>(() {
|
|
if (uploadPosition.value == null || uploadProgress.value == null) {
|
|
return null;
|
|
}
|
|
|
|
// Calculate completed files (100% each) + current file progress
|
|
final completedProgress = uploadPosition.value! * 100.0;
|
|
final currentProgress = uploadProgress.value!;
|
|
|
|
// Calculate overall progress as percentage
|
|
return (completedProgress + currentProgress) /
|
|
(files.value.length * 100.0);
|
|
}, [uploadPosition.value, uploadProgress.value, files.value.length]);
|
|
|
|
Future<void> startUpload() async {
|
|
if (files.value.isEmpty) return;
|
|
|
|
final baseUrl = ref.read(serverUrlProvider);
|
|
final atk = await getFreshAtk(
|
|
ref.watch(tokenPairProvider),
|
|
baseUrl,
|
|
onRefreshed: (atk, rtk) {
|
|
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
|
|
ref.invalidate(tokenPairProvider);
|
|
},
|
|
);
|
|
if (atk == null) throw Exception("Unauthorized");
|
|
|
|
List<SnCloudFile> result = List.empty(growable: true);
|
|
|
|
uploadProgress.value = 0;
|
|
uploadPosition.value = 0;
|
|
try {
|
|
for (var idx = 0; idx < files.value.length; idx++) {
|
|
uploadPosition.value = idx;
|
|
final file = files.value[idx];
|
|
final cloudFile =
|
|
await putMediaToCloud(
|
|
fileData: file.data,
|
|
atk: atk,
|
|
baseUrl: baseUrl,
|
|
filename: file.data.name ?? 'Post media',
|
|
mimetype:
|
|
file.data.mimeType ??
|
|
switch (file.type) {
|
|
UniversalFileType.image => 'image/unknown',
|
|
UniversalFileType.video => 'video/unknown',
|
|
UniversalFileType.audio => 'audio/unknown',
|
|
UniversalFileType.file => 'application/octet-stream',
|
|
},
|
|
onProgress: (progress, _) {
|
|
uploadProgress.value = progress;
|
|
},
|
|
).future;
|
|
if (cloudFile == null) {
|
|
throw ArgumentError('Failed to upload the file...');
|
|
}
|
|
result.add(cloudFile);
|
|
}
|
|
|
|
if (context.mounted) Navigator.pop(context, result);
|
|
} catch (err) {
|
|
showErrorAlert(err);
|
|
}
|
|
}
|
|
|
|
void pickFile() async {
|
|
showLoadingModal(context);
|
|
final result = await FilePickerIO().pickFiles(
|
|
allowMultiple: allowMultiple,
|
|
);
|
|
if (result == null) {
|
|
if (context.mounted) hideLoadingModal(context);
|
|
return;
|
|
}
|
|
|
|
final newFiles =
|
|
result.files
|
|
.map((e) => UniversalFile(data: e, type: UniversalFileType.file))
|
|
.toList();
|
|
|
|
if (!allowMultiple) {
|
|
files.value = newFiles;
|
|
if (context.mounted) {
|
|
hideLoadingModal(context);
|
|
startUpload();
|
|
}
|
|
return;
|
|
}
|
|
|
|
files.value = [...files.value, ...newFiles];
|
|
if (context.mounted) hideLoadingModal(context);
|
|
}
|
|
|
|
void pickImage() async {
|
|
showLoadingModal(context);
|
|
final result =
|
|
allowMultiple
|
|
? await ref.read(imagePickerProvider).pickMultiImage()
|
|
: [
|
|
await ref
|
|
.read(imagePickerProvider)
|
|
.pickImage(source: ImageSource.gallery),
|
|
];
|
|
if (result.isEmpty) {
|
|
if (context.mounted) hideLoadingModal(context);
|
|
return;
|
|
}
|
|
|
|
final newFiles =
|
|
result
|
|
.map((e) => UniversalFile(data: e, type: UniversalFileType.image))
|
|
.toList();
|
|
|
|
if (!allowMultiple) {
|
|
files.value = newFiles;
|
|
if (context.mounted) {
|
|
hideLoadingModal(context);
|
|
startUpload();
|
|
}
|
|
return;
|
|
}
|
|
|
|
files.value = [...files.value, ...newFiles];
|
|
if (context.mounted) hideLoadingModal(context);
|
|
}
|
|
|
|
void pickVideo() async {
|
|
showLoadingModal(context);
|
|
final result = await ref
|
|
.read(imagePickerProvider)
|
|
.pickVideo(source: ImageSource.gallery);
|
|
if (result == null) {
|
|
if (context.mounted) hideLoadingModal(context);
|
|
return;
|
|
}
|
|
|
|
final newFile = UniversalFile(
|
|
data: result,
|
|
type: UniversalFileType.video,
|
|
);
|
|
|
|
if (!allowMultiple) {
|
|
files.value = [newFile];
|
|
if (context.mounted) {
|
|
hideLoadingModal(context);
|
|
startUpload();
|
|
}
|
|
return;
|
|
}
|
|
|
|
files.value = [...files.value, newFile];
|
|
if (context.mounted) hideLoadingModal(context);
|
|
}
|
|
|
|
return Container(
|
|
constraints: BoxConstraints(
|
|
maxHeight: MediaQuery.of(context).size.height * 0.5,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
'pickFile'.tr(),
|
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.5,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
IconButton(
|
|
icon: const Icon(Symbols.close),
|
|
onPressed: () => Navigator.pop(context),
|
|
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 1),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
spacing: 16,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (uploadOverallProgress != null)
|
|
Column(
|
|
spacing: 6,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text('uploadingProgress')
|
|
.tr(
|
|
args: [
|
|
((uploadPosition.value ?? 0) + 1).toString(),
|
|
files.value.length.toString(),
|
|
],
|
|
)
|
|
.opacity(0.85),
|
|
LinearProgressIndicator(
|
|
value: uploadOverallProgress,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
backgroundColor:
|
|
Theme.of(context).colorScheme.surfaceVariant,
|
|
),
|
|
],
|
|
),
|
|
if (files.value.isNotEmpty)
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: ElevatedButton.icon(
|
|
onPressed: startUpload,
|
|
icon: const Icon(Symbols.play_arrow),
|
|
label: Text('uploadAll'.tr()),
|
|
),
|
|
),
|
|
if (files.value.isNotEmpty)
|
|
SizedBox(
|
|
height: 280,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: files.value.length,
|
|
itemBuilder: (context, idx) {
|
|
return AttachmentPreview(
|
|
onDelete:
|
|
uploadOverallProgress != null
|
|
? null
|
|
: () {
|
|
files.value = [
|
|
...files.value.where(
|
|
(e) => e != files.value[idx],
|
|
),
|
|
];
|
|
},
|
|
item: files.value[idx],
|
|
progress: null,
|
|
);
|
|
},
|
|
separatorBuilder: (_, __) => const Gap(8),
|
|
),
|
|
),
|
|
Card(
|
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
|
margin: EdgeInsets.zero,
|
|
child: Column(
|
|
children: [
|
|
ListTile(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
leading: const Icon(Symbols.photo),
|
|
title: Text('addPhoto'.tr()),
|
|
onTap: () => pickImage(),
|
|
),
|
|
ListTile(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
leading: const Icon(Symbols.video_call),
|
|
title: Text('addVideo'.tr()),
|
|
onTap: () => pickVideo(),
|
|
),
|
|
ListTile(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
leading: const Icon(Symbols.draft),
|
|
title: Text('addFile'.tr()),
|
|
onTap: () => pickFile(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
).padding(all: 24),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|