Croppable post image

This commit is contained in:
LittleSheep 2024-11-11 21:48:50 +08:00
parent f23ffe61f5
commit b166a6e85c
8 changed files with 185 additions and 94 deletions

View File

@ -34,6 +34,9 @@
"preview": "Preview",
"loading": "Loading...",
"delete": "Delete",
"unlink": "Unlink",
"crop": "Crop",
"compress": "Compress",
"report": "Report",
"repost": "Repost",
"reply": "Reply",

View File

@ -34,6 +34,9 @@
"create": "创建",
"preview": "预览",
"delete": "删除",
"unlink": "解除链接",
"crop": "裁剪",
"compress": "压缩",
"report": "检举",
"repost": "转帖",
"reply": "回贴",

View File

@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
@ -47,7 +48,10 @@ class PostWriteMedia {
PostWriteMedia.fromFile(this.file, {this.attachment, this.raw}) {
name = file!.name;
switch (file?.mimeType?.split('/').firstOrNull) {
String? mimetype = file!.mimeType;
mimetype ??= lookupMimeType(file!.path);
switch (mimetype?.split('/').firstOrNull) {
case 'image':
type = PostWriteMediaType.image;
break;
@ -94,7 +98,17 @@ class PostWriteMedia {
}) {
if (attachment != null) {
final sn = context.read<SnNetworkProvider>();
return UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
final ImageProvider provider =
UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
if (width != null && height != null) {
return ResizeImage(
provider,
width: width,
height: height,
policy: ResizeImagePolicy.fit,
);
}
return provider;
} else if (file != null) {
final ImageProvider provider =
kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
@ -324,6 +338,11 @@ class PostWriteController extends ChangeNotifier {
notifyListeners();
}
void setAttachmentAt(int idx, PostWriteMedia item) {
attachments[idx] = item;
notifyListeners();
}
void removeAttachmentAt(int idx) {
attachments.removeAt(idx);
notifyListeners();

View File

@ -1,4 +1,5 @@
import 'dart:collection';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:dio/dio.dart';
@ -165,7 +166,10 @@ class SnAttachmentProvider {
for (final entry in chunks.entries) {
queue.add(() async {
final beginCursor = entry.value * chunkSize;
final endCursor = (entry.value + 1) * chunkSize;
final endCursor = math.min<int>(
(entry.value + 1) * chunkSize,
await file.length(),
);
final data = Uint8List.fromList(await file
.openRead(beginCursor, endCursor)
.expand((chunk) => chunk)

View File

@ -343,10 +343,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
),
if (_writeController.attachments.isNotEmpty)
PostMediaPendingList(
data: _writeController.attachments,
onRemove: (idx) {
_writeController.removeAttachmentAt(idx);
},
controller: _writeController,
).padding(bottom: 8),
Material(
elevation: 2,

View File

@ -1,42 +1,103 @@
import 'dart:io';
import 'dart:ui';
import 'package:croppy/croppy.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/widgets/attachment/attachment_detail.dart';
class PostMediaPendingList extends StatelessWidget {
final List<PostWriteMedia> data;
final Function(int idx)? onRemove;
const PostMediaPendingList({
super.key,
required this.data,
this.onRemove,
});
final PostWriteController controller;
const PostMediaPendingList({super.key, required this.controller});
void _cropImage(BuildContext context, int idx) async {
final media = controller.attachments[idx];
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously
context,
// ignore: use_build_context_synchronously
imageProvider: media.getImageProvider(context)!,
)
: await showMaterialImageCropper(
// ignore: use_build_context_synchronously
context,
// ignore: use_build_context_synchronously
imageProvider: media.getImageProvider(context)!,
);
if (result == null) return;
if (!context.mounted) return;
final rawBytes =
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
controller.setAttachmentAt(
idx,
PostWriteMedia.fromBytes(rawBytes, media.name, media.type),
);
}
@override
Widget build(BuildContext context) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return ListenableBuilder(
listenable: controller,
builder: (context, _) {
return Container(
constraints: const BoxConstraints(maxHeight: 120),
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
separatorBuilder: (context, index) => const Gap(8),
itemCount: data.length,
itemCount: controller.attachments.length,
itemBuilder: (context, idx) {
final file = data[idx];
final media = controller.attachments[idx];
return ContextMenuRegion(
contextMenu: ContextMenu(
entries: [
if (onRemove != null)
if (media.type == PostWriteMediaType.image &&
media.attachment != null)
MenuItem(
label: 'preview'.tr(),
icon: Symbols.preview,
onSelected: () {
context.pushTransparentRoute(
AttachmentDetailPopup(data: media.attachment!),
rootNavigator: true,
);
},
),
if (media.type == PostWriteMediaType.image &&
media.attachment == null)
MenuItem(
label: 'crop'.tr(),
icon: Symbols.crop,
onSelected: () => _cropImage(context, idx),
),
if (media.attachment == null)
MenuItem(
label: 'delete'.tr(),
icon: Symbols.delete,
onSelected: () {
onRemove!(idx);
controller.removeAttachmentAt(idx);
},
)
else
MenuItem(
label: 'unlink'.tr(),
icon: Symbols.link_off,
onSelected: () {
controller.removeAttachmentAt(idx);
},
),
],
@ -53,15 +114,16 @@ class PostMediaPendingList extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 1,
child: switch (file.type) {
child: switch (media.type) {
PostWriteMediaType.image =>
LayoutBuilder(builder: (context, constraints) {
return Image(
image: file.getImageProvider(
image: media.getImageProvider(
context,
width: (constraints.maxWidth * devicePixelRatio)
.round(),
height: (constraints.maxHeight * devicePixelRatio)
height:
(constraints.maxHeight * devicePixelRatio)
.round(),
)!,
fit: BoxFit.cover,
@ -79,5 +141,7 @@ class PostMediaPendingList extends StatelessWidget {
},
),
);
},
);
}
}

View File

@ -5,23 +5,23 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
version: "72.0.0"
version: "76.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
version: "0.3.3"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
version: "6.7.0"
version: "6.11.0"
animations:
dependency: "direct main"
description:
@ -202,10 +202,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.0"
connectivity_plus:
dependency: transitive
description:
@ -790,18 +790,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.5"
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
@ -838,10 +838,10 @@ packages:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
version: "0.1.3-main.0"
markdown:
dependency: "direct main"
description:
@ -883,7 +883,7 @@ packages:
source: hosted
version: "1.15.0"
mime:
dependency: transitive
dependency: "direct main"
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
@ -1150,7 +1150,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
version: "0.0.0"
source_gen:
dependency: transitive
description:
@ -1227,10 +1227,10 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "1.12.0"
stream_channel:
dependency: transitive
description:
@ -1251,10 +1251,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
styled_widget:
dependency: "direct main"
description:
@ -1291,10 +1291,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
version: "0.7.3"
timing:
dependency: transitive
description:
@ -1411,10 +1411,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
version: "14.2.5"
version: "14.3.0"
watcher:
dependency: transitive
description:

View File

@ -72,6 +72,7 @@ dependencies:
shared_preferences: ^2.3.3
path_provider: ^2.1.5
collection: ^1.18.0
mime: ^2.0.0
dev_dependencies:
flutter_test: