✨ Upload photos
This commit is contained in:
parent
a303f5c30c
commit
52304a7633
212
lib/screens/publish/attachment_list.dart
Normal file
212
lib/screens/publish/attachment_list.dart
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
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<Attachment> current;
|
||||||
|
final void Function(List<Attachment> data) onUpdate;
|
||||||
|
|
||||||
|
const AttachmentList(
|
||||||
|
{super.key, required this.current, required this.onUpdate});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AttachmentList> createState() => _AttachmentListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AttachmentListState extends State<AttachmentList> {
|
||||||
|
final imagePicker = ImagePicker();
|
||||||
|
|
||||||
|
bool isSubmitting = false;
|
||||||
|
|
||||||
|
List<Attachment> attachments = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> 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<void> 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<void> 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<String> 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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:solaragent/auth.dart';
|
import 'package:solaragent/auth.dart';
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:solaragent/auth.dart';
|
import 'package:solaragent/auth.dart';
|
||||||
|
import 'package:solaragent/models/feed.dart';
|
||||||
import 'package:solaragent/router.dart';
|
import 'package:solaragent/router.dart';
|
||||||
|
import 'package:solaragent/screens/publish/attachment_list.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class MomentEditorScreen extends StatefulWidget {
|
class MomentEditorScreen extends StatefulWidget {
|
||||||
@ -16,13 +16,14 @@ class MomentEditorScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
||||||
final picker = ImagePicker();
|
|
||||||
final contentController = TextEditingController();
|
final contentController = TextEditingController();
|
||||||
|
|
||||||
bool isSubmitting = false;
|
bool isSubmitting = false;
|
||||||
|
|
||||||
bool showRecommendationBanner = true;
|
bool showRecommendationBanner = true;
|
||||||
|
|
||||||
|
List<Attachment> attachments = List.empty(growable: true);
|
||||||
|
|
||||||
Future<void> postMoment() async {
|
Future<void> postMoment() async {
|
||||||
if (!await authClient.isAuthorized()) return;
|
if (!await authClient.isAuthorized()) return;
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
|||||||
},
|
},
|
||||||
body: jsonEncode(<String, dynamic>{
|
body: jsonEncode(<String, dynamic>{
|
||||||
'content': contentController.value.text,
|
'content': contentController.value.text,
|
||||||
|
'attachments': attachments,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
@ -49,6 +51,16 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
|||||||
setState(() => isSubmitting = false);
|
setState(() => isSubmitting = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void viewAttachments(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AttachmentList(
|
||||||
|
current: attachments,
|
||||||
|
onUpdate: (value) => attachments = value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -112,9 +124,9 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: null,
|
|
||||||
style: TextButton.styleFrom(shape: const CircleBorder()),
|
style: TextButton.styleFrom(shape: const CircleBorder()),
|
||||||
child: const Icon(Icons.camera_alt),
|
child: const Icon(Icons.camera_alt),
|
||||||
|
onPressed: () => viewAttachments(context),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
10
pubspec.lock
10
pubspec.lock
@ -90,7 +90,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3+8"
|
version: "0.3.3+8"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||||
@ -129,6 +129,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "7.0.0"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: d1d0ac3966b36dc3e66eeefb40280c17feb87fa2099c6e22e6a1fc959327bd03
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.0+1"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -48,6 +48,8 @@ dependencies:
|
|||||||
flutter_carousel_widget: ^2.2.0
|
flutter_carousel_widget: ^2.2.0
|
||||||
image_picker: ^1.0.7
|
image_picker: ^1.0.7
|
||||||
sentry_flutter: ^7.18.0
|
sentry_flutter: ^7.18.0
|
||||||
|
crypto: ^3.0.3
|
||||||
|
file_picker: ^8.0.0+1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user