312 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			312 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/network.dart';
 | 
						|
import 'package:island/services/file_uploader.dart';
 | 
						|
import 'package:island/widgets/alert.dart';
 | 
						|
import 'package:island/widgets/content/attachment_preview.dart';
 | 
						|
import 'package:material_symbols_icons/symbols.dart';
 | 
						|
import 'package:styled_widget/styled_widget.dart';
 | 
						|
 | 
						|
class CloudFilePicker extends HookConsumerWidget {
 | 
						|
  final bool allowMultiple;
 | 
						|
  final Set<UniversalFileType> allowedTypes;
 | 
						|
  const CloudFilePicker({
 | 
						|
    super.key,
 | 
						|
    this.allowMultiple = false,
 | 
						|
    this.allowedTypes = const {
 | 
						|
      UniversalFileType.image,
 | 
						|
      UniversalFileType.video,
 | 
						|
      UniversalFileType.file,
 | 
						|
    },
 | 
						|
  });
 | 
						|
 | 
						|
  @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;
 | 
						|
 | 
						|
      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 FileUploader.createCloudFile(
 | 
						|
                fileData: file,
 | 
						|
                client: ref.read(apiClientProvider),
 | 
						|
                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 FilePicker.platform.pickFiles(
 | 
						|
        allowMultiple: allowMultiple,
 | 
						|
      );
 | 
						|
      if (result == null) {
 | 
						|
        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.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 = await FilePicker.platform.pickFiles(
 | 
						|
        allowMultiple: allowMultiple,
 | 
						|
        type: FileType.image,
 | 
						|
      );
 | 
						|
      if (result == null || result.files.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();
 | 
						|
 | 
						|
      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 FilePicker.platform.pickFiles(
 | 
						|
        allowMultiple: allowMultiple,
 | 
						|
        type: FileType.video,
 | 
						|
      );
 | 
						|
      if (result == null || result.files.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.video);
 | 
						|
          }).toList();
 | 
						|
 | 
						|
      if (!allowMultiple) {
 | 
						|
        files.value = newFiles;
 | 
						|
        if (context.mounted) {
 | 
						|
          hideLoadingModal(context);
 | 
						|
          startUpload();
 | 
						|
        }
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      files.value = [...files.value, ...newFiles];
 | 
						|
      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: [
 | 
						|
                        if (allowedTypes.contains(UniversalFileType.image))
 | 
						|
                          ListTile(
 | 
						|
                            shape: RoundedRectangleBorder(
 | 
						|
                              borderRadius: BorderRadius.circular(8),
 | 
						|
                            ),
 | 
						|
                            leading: const Icon(Symbols.photo),
 | 
						|
                            title: Text('addPhoto'.tr()),
 | 
						|
                            onTap: () => pickImage(),
 | 
						|
                          ),
 | 
						|
                        if (allowedTypes.contains(UniversalFileType.video))
 | 
						|
                          ListTile(
 | 
						|
                            shape: RoundedRectangleBorder(
 | 
						|
                              borderRadius: BorderRadius.circular(8),
 | 
						|
                            ),
 | 
						|
                            leading: const Icon(Symbols.video_call),
 | 
						|
                            title: Text('addVideo'.tr()),
 | 
						|
                            onTap: () => pickVideo(),
 | 
						|
                          ),
 | 
						|
                        if (allowedTypes.contains(UniversalFileType.file))
 | 
						|
                          ListTile(
 | 
						|
                            shape: RoundedRectangleBorder(
 | 
						|
                              borderRadius: BorderRadius.circular(8),
 | 
						|
                            ),
 | 
						|
                            leading: const Icon(Symbols.draft),
 | 
						|
                            title: Text('addFile'.tr()),
 | 
						|
                            onTap: () => pickFile(),
 | 
						|
                          ),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              ).padding(all: 24),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |