import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; import 'package:image_picker/image_picker.dart'; import 'package:solaragent/auth.dart'; import 'package:solaragent/models/feed.dart'; class AttachmentList extends StatefulWidget { final List current; final void Function(List data) onUpdate; const AttachmentList( {super.key, required this.current, required this.onUpdate}); @override State createState() => _AttachmentListState(); } class _AttachmentListState extends State { final imagePicker = ImagePicker(); bool isSubmitting = false; List attachments = List.empty(growable: true); Future pickImageToUpload(BuildContext context) async { if (!await authClient.isAuthorized()) return; final image = await imagePicker.pickImage(source: ImageSource.gallery); if (image == null) return; final file = File(image.path); final hashcode = await calculateSha256(file); try { await uploadAttachment(file, hashcode); } catch (err) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Something went wrong... $err")), ); } } Future uploadAttachment(File file, String hashcode) async { var req = MultipartRequest( 'POST', Uri.parse('https://co.solsynth.dev/api/attachments'), ); req.files.add(await MultipartFile.fromPath('attachment', file.path)); req.fields['hashcode'] = hashcode; setState(() => isSubmitting = true); var res = await authClient.client!.send(req); if (res.statusCode == 200) { var result = Attachment.fromJson( jsonDecode(utf8.decode(await res.stream.toBytes()))["info"], ); setState(() => attachments.add(result)); widget.onUpdate(attachments); } else { throw Exception(utf8.decode(await res.stream.toBytes())); } setState(() => isSubmitting = false); } Future disposeAttachment( BuildContext context, Attachment item, int index) async { var req = MultipartRequest( 'DELETE', Uri.parse('https://co.solsynth.dev/api/attachments/${item.id}'), ); setState(() => isSubmitting = true); var res = await authClient.client!.send(req); if (res.statusCode == 200) { setState(() => attachments.removeAt(index)); widget.onUpdate(attachments); } else { final err = utf8.decode(await res.stream.toBytes()); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Something went wrong... $err")), ); } setState(() => isSubmitting = false); } Future calculateSha256(File file) async { final bytes = await file.readAsBytes(); final digest = sha256.convert(bytes); return digest.toString(); } String getFileName(Attachment item) { return item.filename.replaceAll(RegExp(r'\.[^/.]+$'), ''); } String getFileType(Attachment item) { switch (item.type) { case 1: return 'Photo'; case 2: return 'Video'; case 3: return 'Audio'; default: return 'Others'; } } String formatBytes(int bytes, {int decimals = 2}) { if (bytes == 0) return '0 Bytes'; const k = 1024; final dm = decimals < 0 ? 0 : decimals; final sizes = [ 'Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB' ]; final i = (math.log(bytes) / math.log(k)).floor().toInt(); return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; } @override void initState() { attachments = widget.current; super.initState(); } @override Widget build(BuildContext context) { return Column( children: [ Container( padding: const EdgeInsets.only(left: 10, right: 10, top: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 12.0, ), child: Text( 'Attachments', style: Theme.of(context).textTheme.headlineSmall, ), ), FutureBuilder( future: authClient.isAuthorized(), builder: (context, snapshot) { if (snapshot.hasData && snapshot.data == true) { return Tooltip( message: "Add a photo", child: TextButton( onPressed: isSubmitting ? null : () => pickImageToUpload(context), style: TextButton.styleFrom( shape: const CircleBorder(), ), child: const Icon(Icons.add_photo_alternate), ), ); } else { return Container(); } }, ), ], ), ), isSubmitting ? const LinearProgressIndicator() : Container(), Expanded( child: ListView.separated( itemCount: attachments.length, itemBuilder: (context, index) { var element = attachments[index]; return Container( padding: const EdgeInsets.only(left: 8.0), child: ListTile( title: Text(getFileName(element)), subtitle: Text( "${getFileType(element)} ยท ${formatBytes(element.filesize)}", ), trailing: TextButton( style: TextButton.styleFrom( shape: const CircleBorder(), foregroundColor: Colors.red, ), child: const Icon(Icons.delete), onPressed: () => disposeAttachment(context, element, index), ), ), ); }, separatorBuilder: (context, index) => const Divider(), ), ), ], ); } }