✨ Stickers & packs
This commit is contained in:
312
lib/widgets/content/cloud_file_picker.dart
Normal file
312
lib/widgets/content/cloud_file_picker.dart
Normal file
@ -0,0 +1,312 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user