✨ Croppable post image
This commit is contained in:
parent
f23ffe61f5
commit
b166a6e85c
@ -34,6 +34,9 @@
|
|||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"unlink": "Unlink",
|
||||||
|
"crop": "Crop",
|
||||||
|
"compress": "Compress",
|
||||||
"report": "Report",
|
"report": "Report",
|
||||||
"repost": "Repost",
|
"repost": "Repost",
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
|
@ -34,6 +34,9 @@
|
|||||||
"create": "创建",
|
"create": "创建",
|
||||||
"preview": "预览",
|
"preview": "预览",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
|
"unlink": "解除链接",
|
||||||
|
"crop": "裁剪",
|
||||||
|
"compress": "压缩",
|
||||||
"report": "检举",
|
"report": "检举",
|
||||||
"repost": "转帖",
|
"repost": "转帖",
|
||||||
"reply": "回贴",
|
"reply": "回贴",
|
||||||
|
@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
@ -47,7 +48,10 @@ class PostWriteMedia {
|
|||||||
PostWriteMedia.fromFile(this.file, {this.attachment, this.raw}) {
|
PostWriteMedia.fromFile(this.file, {this.attachment, this.raw}) {
|
||||||
name = file!.name;
|
name = file!.name;
|
||||||
|
|
||||||
switch (file?.mimeType?.split('/').firstOrNull) {
|
String? mimetype = file!.mimeType;
|
||||||
|
mimetype ??= lookupMimeType(file!.path);
|
||||||
|
|
||||||
|
switch (mimetype?.split('/').firstOrNull) {
|
||||||
case 'image':
|
case 'image':
|
||||||
type = PostWriteMediaType.image;
|
type = PostWriteMediaType.image;
|
||||||
break;
|
break;
|
||||||
@ -94,7 +98,17 @@ class PostWriteMedia {
|
|||||||
}) {
|
}) {
|
||||||
if (attachment != null) {
|
if (attachment != null) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
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) {
|
} else if (file != null) {
|
||||||
final ImageProvider provider =
|
final ImageProvider provider =
|
||||||
kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
|
kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
|
||||||
@ -324,6 +338,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAttachmentAt(int idx, PostWriteMedia item) {
|
||||||
|
attachments[idx] = item;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void removeAttachmentAt(int idx) {
|
void removeAttachmentAt(int idx) {
|
||||||
attachments.removeAt(idx);
|
attachments.removeAt(idx);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@ -165,7 +166,10 @@ class SnAttachmentProvider {
|
|||||||
for (final entry in chunks.entries) {
|
for (final entry in chunks.entries) {
|
||||||
queue.add(() async {
|
queue.add(() async {
|
||||||
final beginCursor = entry.value * chunkSize;
|
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
|
final data = Uint8List.fromList(await file
|
||||||
.openRead(beginCursor, endCursor)
|
.openRead(beginCursor, endCursor)
|
||||||
.expand((chunk) => chunk)
|
.expand((chunk) => chunk)
|
||||||
|
@ -343,10 +343,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
),
|
),
|
||||||
if (_writeController.attachments.isNotEmpty)
|
if (_writeController.attachments.isNotEmpty)
|
||||||
PostMediaPendingList(
|
PostMediaPendingList(
|
||||||
data: _writeController.attachments,
|
controller: _writeController,
|
||||||
onRemove: (idx) {
|
|
||||||
_writeController.removeAttachmentAt(idx);
|
|
||||||
},
|
|
||||||
).padding(bottom: 8),
|
).padding(bottom: 8),
|
||||||
Material(
|
Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
|
@ -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:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
import 'package:surface/widgets/attachment/attachment_detail.dart';
|
||||||
|
|
||||||
class PostMediaPendingList extends StatelessWidget {
|
class PostMediaPendingList extends StatelessWidget {
|
||||||
final List<PostWriteMedia> data;
|
final PostWriteController controller;
|
||||||
final Function(int idx)? onRemove;
|
const PostMediaPendingList({super.key, required this.controller});
|
||||||
const PostMediaPendingList({
|
|
||||||
super.key,
|
void _cropImage(BuildContext context, int idx) async {
|
||||||
required this.data,
|
final media = controller.attachments[idx];
|
||||||
this.onRemove,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: controller,
|
||||||
|
builder: (context, _) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: const BoxConstraints(maxHeight: 120),
|
constraints: const BoxConstraints(maxHeight: 120),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
itemCount: data.length,
|
itemCount: controller.attachments.length,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
final file = data[idx];
|
final media = controller.attachments[idx];
|
||||||
return ContextMenuRegion(
|
return ContextMenuRegion(
|
||||||
contextMenu: ContextMenu(
|
contextMenu: ContextMenu(
|
||||||
entries: [
|
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(
|
MenuItem(
|
||||||
label: 'delete'.tr(),
|
label: 'delete'.tr(),
|
||||||
icon: Symbols.delete,
|
icon: Symbols.delete,
|
||||||
onSelected: () {
|
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)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: switch (file.type) {
|
child: switch (media.type) {
|
||||||
PostWriteMediaType.image =>
|
PostWriteMediaType.image =>
|
||||||
LayoutBuilder(builder: (context, constraints) {
|
LayoutBuilder(builder: (context, constraints) {
|
||||||
return Image(
|
return Image(
|
||||||
image: file.getImageProvider(
|
image: media.getImageProvider(
|
||||||
context,
|
context,
|
||||||
width: (constraints.maxWidth * devicePixelRatio)
|
width: (constraints.maxWidth * devicePixelRatio)
|
||||||
.round(),
|
.round(),
|
||||||
height: (constraints.maxHeight * devicePixelRatio)
|
height:
|
||||||
|
(constraints.maxHeight * devicePixelRatio)
|
||||||
.round(),
|
.round(),
|
||||||
)!,
|
)!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@ -79,5 +141,7 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
pubspec.lock
46
pubspec.lock
@ -5,23 +5,23 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "72.0.0"
|
version: "76.0.0"
|
||||||
_macros:
|
_macros:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: dart
|
description: dart
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.3.2"
|
version: "0.3.3"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.7.0"
|
version: "6.11.0"
|
||||||
animations:
|
animations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -202,10 +202,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.19.0"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -790,18 +790,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.5"
|
version: "10.0.8"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.5"
|
version: "3.0.9"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -838,10 +838,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: macros
|
name: macros
|
||||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2-main.4"
|
version: "0.1.3-main.0"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -883,7 +883,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.15.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: mime
|
name: mime
|
||||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
@ -1150,7 +1150,7 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.0"
|
||||||
source_gen:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1227,10 +1227,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.1"
|
version: "1.12.0"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1251,10 +1251,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
styled_widget:
|
styled_widget:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1291,10 +1291,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.2"
|
version: "0.7.3"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1411,10 +1411,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.5"
|
version: "14.3.0"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -72,6 +72,7 @@ dependencies:
|
|||||||
shared_preferences: ^2.3.3
|
shared_preferences: ^2.3.3
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
|
mime: ^2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user