Compare commits

...

4 Commits

Author SHA1 Message Date
92f7e92018 🐛 Bug fixes due to post editor changes 2025-03-08 16:04:51 +08:00
5c483bd3b8 ♻️ Move the post editor mode into editor itself 2025-03-08 16:00:10 +08:00
1c510d63fe 🐛 Fix share via image errored 2025-03-06 22:46:02 +08:00
115cb4adc1 💄 Redesigned attachment zoom view 2025-03-06 22:35:06 +08:00
14 changed files with 487 additions and 418 deletions

View File

@ -153,6 +153,11 @@
"publisherRunBy": "Run by {}", "publisherRunBy": "Run by {}",
"fieldPublisherBelongToRealm": "Belongs to", "fieldPublisherBelongToRealm": "Belongs to",
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm", "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
"writePost": "Compose",
"postTypeStory": "Story",
"postTypeArticle": "Article",
"postTypeQuestion": "Question",
"postTypeVideo": "Video",
"writePostTypeStory": "Post a story", "writePostTypeStory": "Post a story",
"writePostTypeArticle": "Write an article", "writePostTypeArticle": "Write an article",
"writePostTypeQuestion": "Ask a question", "writePostTypeQuestion": "Ask a question",

View File

@ -137,6 +137,11 @@
"publisherRunBy": "由 {} 管理", "publisherRunBy": "由 {} 管理",
"fieldPublisherBelongToRealm": "所属领域", "fieldPublisherBelongToRealm": "所属领域",
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域", "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
"writePost": "撰写",
"postTypeStory": "动态",
"postTypeArticle": "文章",
"postTypeQuestion": "问题",
"postTypeVideo": "视频",
"writePostTypeStory": "发动态", "writePostTypeStory": "发动态",
"writePostTypeArticle": "写文章", "writePostTypeArticle": "写文章",
"writePostTypeQuestion": "提问题", "writePostTypeQuestion": "提问题",

View File

@ -71,7 +71,8 @@ class PostWriteMedia {
} }
} }
PostWriteMedia.fromBytes(this.raw, this.name, this.type, {this.attachment, this.file}); PostWriteMedia.fromBytes(this.raw, this.name, this.type,
{this.attachment, this.file});
bool get isEmpty => attachment == null && file == null && raw == null; bool get isEmpty => attachment == null && file == null && raw == null;
@ -105,7 +106,8 @@ class PostWriteMedia {
}) { }) {
if (attachment != null) { if (attachment != null) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid)); final ImageProvider provider =
UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
if (width != null && height != null && !kIsWeb) { if (width != null && height != null && !kIsWeb) {
return ResizeImage( return ResizeImage(
provider, provider,
@ -116,7 +118,8 @@ class PostWriteMedia {
} }
return provider; return provider;
} else if (file != null) { } else if (file != null) {
final ImageProvider provider = kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path)); final ImageProvider provider =
kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
if (width != null && height != null) { if (width != null && height != null) {
return ResizeImage( return ResizeImage(
provider, provider,
@ -159,11 +162,14 @@ class PostWriteController extends ChangeNotifier {
final TextEditingController aliasController = TextEditingController(); final TextEditingController aliasController = TextEditingController();
final TextEditingController rewardController = TextEditingController(); final TextEditingController rewardController = TextEditingController();
ContentInsertionConfiguration get contentInsertionConfiguration => ContentInsertionConfiguration( ContentInsertionConfiguration get contentInsertionConfiguration =>
ContentInsertionConfiguration(
onContentInserted: (KeyboardInsertedContent content) { onContentInserted: (KeyboardInsertedContent content) {
if (content.hasData) { if (content.hasData) {
addAttachments( addAttachments([
[PostWriteMedia.fromBytes(content.data!, 'attachmentInsertedImage'.tr(), SnMediaType.image)]); PostWriteMedia.fromBytes(content.data!,
'attachmentInsertedImage'.tr(), SnMediaType.image)
]);
} }
}, },
); );
@ -193,7 +199,8 @@ class PostWriteController extends ChangeNotifier {
String get description => descriptionController.text; String get description => descriptionController.text;
bool get isRelatedNull => ![editingPost, repostingPost, replyingPost].any((ele) => ele != null); bool get isRelatedNull =>
![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
bool isLoading = false, isBusy = false; bool isLoading = false, isBusy = false;
double? progress; double? progress;
@ -237,14 +244,18 @@ class PostWriteController extends ChangeNotifier {
publishedAt = post.publishedAt; publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil; publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true); visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true); invisibleUsers =
List.from(post.invisibleUsersList ?? [], growable: true);
visibility = post.visibility; visibility = post.visibility;
tags = List.from(post.tags.map((ele) => ele.alias), growable: true); tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
categories = List.from(post.categories.map((ele) => ele.alias), growable: true); categories =
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []); List.from(post.categories.map((ele) => ele.alias), growable: true);
attachments.addAll(
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
poll = post.preload?.poll; poll = post.preload?.poll;
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { if (post.preload?.thumbnail != null &&
(post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
thumbnail = PostWriteMedia(post.preload!.thumbnail); thumbnail = PostWriteMedia(post.preload!.thumbnail);
} }
if (post.preload?.realm != null) { if (post.preload?.realm != null) {
@ -272,7 +283,8 @@ class PostWriteController extends ChangeNotifier {
} }
} }
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media, Future<SnAttachment> _uploadAttachment(
BuildContext context, PostWriteMedia media,
{bool isCompressed = false}) async { {bool isCompressed = false}) async {
final attach = context.read<SnAttachmentProvider>(); final attach = context.read<SnAttachmentProvider>();
@ -281,7 +293,9 @@ class PostWriteController extends ChangeNotifier {
media.name, media.name,
'interactive', 'interactive',
null, null,
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null, mimetype: media.raw != null && media.type == SnMediaType.image
? 'image/png'
: null,
); );
var item = await attach.chunkedUploadParts( var item = await attach.chunkedUploadParts(
@ -297,9 +311,11 @@ class PostWriteController extends ChangeNotifier {
if (media.type == SnMediaType.video && !isCompressed && context.mounted) { if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
try { try {
final compressedAttachment = await _tryCompressVideoCopy(context, media); final compressedAttachment =
await _tryCompressVideoCopy(context, media);
if (compressedAttachment != null) { if (compressedAttachment != null) {
item = await attach.updateOne(item, compressedId: compressedAttachment.id); item = await attach.updateOne(item,
compressedId: compressedAttachment.id);
} }
} catch (err) { } catch (err) {
if (context.mounted) context.showErrorDialog(err); if (context.mounted) context.showErrorDialog(err);
@ -309,8 +325,10 @@ class PostWriteController extends ChangeNotifier {
return item; return item;
} }
Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async { Future<SnAttachment?> _tryCompressVideoCopy(
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null; BuildContext context, PostWriteMedia media) async {
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS))
return null;
if (media.type != SnMediaType.video) return null; if (media.type != SnMediaType.video) return null;
if (media.file == null) return null; if (media.file == null) return null;
if (VideoCompress.isCompressing) return null; if (VideoCompress.isCompressing) return null;
@ -334,7 +352,8 @@ class PostWriteController extends ChangeNotifier {
if (!context.mounted) return null; if (!context.mounted) return null;
final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!)); final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true); final compressedAttachment =
await _uploadAttachment(context, compressedMedia, isCompressed: true);
return compressedAttachment; return compressedAttachment;
} }
@ -370,18 +389,25 @@ class PostWriteController extends ChangeNotifier {
'content': contentController.text, 'content': contentController.text,
if (aliasController.text.isNotEmpty) 'alias': aliasController.text, if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
if (titleController.text.isNotEmpty) 'title': titleController.text, if (titleController.text.isNotEmpty) 'title': titleController.text,
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text, if (descriptionController.text.isNotEmpty)
'description': descriptionController.text,
if (rewardController.text.isNotEmpty) 'reward': rewardController.text, if (rewardController.text.isNotEmpty) 'reward': rewardController.text,
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(), if (thumbnail != null && thumbnail!.attachment != null)
'attachments': 'thumbnail': thumbnail!.attachment!.toJson(),
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true), 'attachments': attachments
.where((e) => e.attachment != null)
.map((e) => e.attachment!.toJson())
.toList(growable: true),
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true), 'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true), 'categories':
categories.map((ele) => {'alias': ele}).toList(growable: true),
'visibility': visibility, 'visibility': visibility,
'visible_users_list': visibleUsers, 'visible_users_list': visibleUsers,
'invisible_users_list': invisibleUsers, 'invisible_users_list': invisibleUsers,
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(), if (publishedAt != null)
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(), 'published_at': publishedAt!.toUtc().toIso8601String(),
if (publishedUntil != null)
'published_until': publishedAt!.toUtc().toIso8601String(),
if (replyingPost != null) 'reply_to': replyingPost!.toJson(), if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
if (repostingPost != null) 'repost_to': repostingPost!.toJson(), if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
if (poll != null) 'poll': poll!.toJson(), if (poll != null) 'poll': poll!.toJson(),
@ -391,6 +417,12 @@ class PostWriteController extends ChangeNotifier {
}); });
} }
bool get isNotEmpty =>
title.isNotEmpty ||
description.isNotEmpty ||
contentController.text.isNotEmpty ||
attachments.isNotEmpty;
bool temporaryRestored = false; bool temporaryRestored = false;
void _temporaryLoad() { void _temporaryLoad() {
@ -403,18 +435,24 @@ class PostWriteController extends ChangeNotifier {
titleController.text = data['title'] ?? ''; titleController.text = data['title'] ?? '';
descriptionController.text = data['description'] ?? ''; descriptionController.text = data['description'] ?? '';
rewardController.text = data['reward']?.toString() ?? ''; rewardController.text = data['reward']?.toString() ?? '';
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail'])); if (data['thumbnail'] != null)
attachments thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>()); attachments.addAll(data['attachments']
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
.cast<PostWriteMedia>());
tags = List.from(data['tags'].map((ele) => ele['alias'])); tags = List.from(data['tags'].map((ele) => ele['alias']));
categories = List.from(data['categories'].map((ele) => ele['alias'])); categories = List.from(data['categories'].map((ele) => ele['alias']));
visibility = data['visibility']; visibility = data['visibility'];
visibleUsers = List.from(data['visible_users_list'] ?? []); visibleUsers = List.from(data['visible_users_list'] ?? []);
invisibleUsers = List.from(data['invisible_users_list'] ?? []); invisibleUsers = List.from(data['invisible_users_list'] ?? []);
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal(); if (data['published_at'] != null)
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal(); publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null; if (data['published_until'] != null)
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null; publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
replyingPost =
data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
repostingPost =
data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null; poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null; realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null;
temporaryRestored = true; temporaryRestored = true;
@ -463,7 +501,9 @@ class PostWriteController extends ChangeNotifier {
media.name, media.name,
'interactive', 'interactive',
null, null,
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null, mimetype: media.raw != null && media.type == SnMediaType.image
? 'image/png'
: null,
); );
var item = await attach.chunkedUploadParts( var item = await attach.chunkedUploadParts(
@ -472,16 +512,20 @@ class PostWriteController extends ChangeNotifier {
place.$2, place.$2,
onProgress: (value) { onProgress: (value) {
// Calculate overall progress for attachments // Calculate overall progress for attachments
progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value); progress = math.max(
((i + value) / attachments.length) * kAttachmentProgressWeight,
value);
notifyListeners(); notifyListeners();
}, },
); );
try { try {
if (context.mounted) { if (context.mounted) {
final compressedAttachment = await _tryCompressVideoCopy(context, media); final compressedAttachment =
await _tryCompressVideoCopy(context, media);
if (compressedAttachment != null) { if (compressedAttachment != null) {
item = await attach.updateOne(item, compressedId: compressedAttachment.id); item = await attach.updateOne(item,
compressedId: compressedAttachment.id);
} }
} }
} catch (err) { } catch (err) {
@ -518,16 +562,23 @@ class PostWriteController extends ChangeNotifier {
'content': contentController.text, 'content': contentController.text,
if (aliasController.text.isNotEmpty) 'alias': aliasController.text, if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
if (titleController.text.isNotEmpty) 'title': titleController.text, if (titleController.text.isNotEmpty) 'title': titleController.text,
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text, if (descriptionController.text.isNotEmpty)
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid, 'description': descriptionController.text,
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(), if (thumbnail != null && thumbnail!.attachment != null)
'thumbnail': thumbnail!.attachment!.rid,
'attachments': attachments
.where((e) => e.attachment != null)
.map((e) => e.attachment!.rid)
.toList(),
'tags': tags.map((ele) => {'alias': ele}).toList(), 'tags': tags.map((ele) => {'alias': ele}).toList(),
'categories': categories.map((ele) => {'alias': ele}).toList(), 'categories': categories.map((ele) => {'alias': ele}).toList(),
'visibility': visibility, 'visibility': visibility,
'visible_users_list': visibleUsers, 'visible_users_list': visibleUsers,
'invisible_users_list': invisibleUsers, 'invisible_users_list': invisibleUsers,
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(), if (publishedAt != null)
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(), 'published_at': publishedAt!.toUtc().toIso8601String(),
if (publishedUntil != null)
'published_until': publishedAt!.toUtc().toIso8601String(),
if (replyingPost != null) 'reply_to': replyingPost!.id, if (replyingPost != null) 'reply_to': replyingPost!.id,
if (repostingPost != null) 'repost_to': repostingPost!.id, if (repostingPost != null) 'repost_to': repostingPost!.id,
if (reward != null) 'reward': reward, if (reward != null) 'reward': reward,
@ -536,11 +587,14 @@ class PostWriteController extends ChangeNotifier {
if (realm != null) 'realm': realm!.id, if (realm != null) 'realm': realm!.id,
}, },
onSendProgress: (count, total) { onSendProgress: (count, total) {
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2); progress =
baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
notifyListeners(); notifyListeners();
}, },
onReceiveProgress: (count, total) { onReceiveProgress: (count, total) {
progress = baseProgressVal + (kPostingProgressWeight / 2) + (count / total) * (kPostingProgressWeight / 2); progress = baseProgressVal +
(kPostingProgressWeight / 2) +
(count / total) * (kPostingProgressWeight / 2);
notifyListeners(); notifyListeners();
}, },
options: Options( options: Options(
@ -683,7 +737,8 @@ class PostWriteController extends ChangeNotifier {
repostingPost = null; repostingPost = null;
mode = kTitleMap.keys.first; mode = kTitleMap.keys.first;
temporaryRestored = false; temporaryRestored = false;
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey)); SharedPreferences.getInstance()
.then((prefs) => prefs.remove(kTemporaryStorageKey));
notifyListeners(); notifyListeners();
} }

View File

@ -66,10 +66,10 @@ final _appRoutes = [
builder: (context, state) => const ExploreScreen(), builder: (context, state) => const ExploreScreen(),
routes: [ routes: [
GoRoute( GoRoute(
path: '/write/:mode', path: '/write',
name: 'postEditor', name: 'postEditor',
builder: (context, state) => PostEditorScreen( builder: (context, state) => PostEditorScreen(
mode: state.pathParameters['mode']!, mode: state.uri.queryParameters['mode'],
postEditId: int.tryParse( postEditId: int.tryParse(
state.uri.queryParameters['editing'] ?? '', state.uri.queryParameters['editing'] ?? '',
), ),

View File

@ -204,7 +204,6 @@ class _ChatScreenState extends State<ChatScreen> {
Theme.of(context).floatingActionButtonTheme.foregroundColor, Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor: backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor, Theme.of(context).floatingActionButtonTheme.backgroundColor,
shape: const CircleBorder(),
), ),
closeButtonBuilder: DefaultFloatingActionButtonBuilder( closeButtonBuilder: DefaultFloatingActionButtonBuilder(
child: const Icon(Symbols.close, size: 28), child: const Icon(Symbols.close, size: 28),
@ -213,7 +212,6 @@ class _ChatScreenState extends State<ChatScreen> {
Theme.of(context).floatingActionButtonTheme.foregroundColor, Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor: backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor, Theme.of(context).floatingActionButtonTheme.backgroundColor,
shape: const CircleBorder(),
), ),
children: [ children: [
Row( Row(

View File

@ -111,7 +111,6 @@ class _ExploreScreenState extends State<ExploreScreen>
Theme.of(context).floatingActionButtonTheme.foregroundColor, Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor: backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor, Theme.of(context).floatingActionButtonTheme.backgroundColor,
shape: const CircleBorder(),
), ),
closeButtonBuilder: DefaultFloatingActionButtonBuilder( closeButtonBuilder: DefaultFloatingActionButtonBuilder(
child: const Icon(Symbols.close, size: 28), child: const Icon(Symbols.close, size: 28),
@ -120,90 +119,24 @@ class _ExploreScreenState extends State<ExploreScreen>
Theme.of(context).floatingActionButtonTheme.foregroundColor, Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor: backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor, Theme.of(context).floatingActionButtonTheme.backgroundColor,
shape: const CircleBorder(),
), ),
children: [ children: [
Row( Row(
children: [ children: [
Text('writePostTypeStory').tr(), Text('writePost').tr(),
const Gap(20), const Gap(20),
FloatingActionButton( FloatingActionButton(
heroTag: null, heroTag: null,
tooltip: 'writePostTypeStory'.tr(), tooltip: 'writePost'.tr(),
onPressed: () { onPressed: () {
GoRouter.of(context).pushNamed('postEditor', pathParameters: { GoRouter.of(context).pushNamed('postEditor').then((value) {
'mode': 'stories',
}).then((value) {
if (value == true) { if (value == true) {
refreshPosts(); refreshPosts();
} }
}); });
_fabKey.currentState!.toggle(); _fabKey.currentState!.toggle();
}, },
child: const Icon(Symbols.post_rounded), child: const Icon(Symbols.edit),
),
],
),
Row(
children: [
Text('writePostTypeArticle').tr(),
const Gap(20),
FloatingActionButton(
heroTag: null,
tooltip: 'writePostTypeArticle'.tr(),
onPressed: () {
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
'mode': 'articles',
}).then((value) {
if (value == true) {
refreshPosts();
}
});
_fabKey.currentState!.toggle();
},
child: const Icon(Symbols.news),
),
],
),
Row(
children: [
Text('writePostTypeQuestion').tr(),
const Gap(20),
FloatingActionButton(
heroTag: null,
tooltip: 'writePostTypeQuestion'.tr(),
onPressed: () {
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
'mode': 'questions',
}).then((value) {
if (value == true) {
refreshPosts();
}
});
_fabKey.currentState!.toggle();
},
child: const Icon(Symbols.question_answer),
),
],
),
Row(
children: [
Text('writePostTypeVideo').tr(),
const Gap(20),
FloatingActionButton(
heroTag: null,
tooltip: 'writePostTypeVideo'.tr(),
onPressed: () {
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
'mode': 'videos',
}).then((value) {
if (value == true) {
refreshPosts();
}
});
_fabKey.currentState!.toggle();
},
child: const Icon(Symbols.video_call),
), ),
], ],
), ),

View File

@ -18,6 +18,7 @@ import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.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';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
@ -36,7 +37,8 @@ import 'package:provider/provider.dart';
import 'package:surface/widgets/post/post_poll_editor.dart'; import 'package:surface/widgets/post/post_poll_editor.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../../providers/sn_realm.dart'; const kPostTypes = ['Story', 'Article', 'Question', 'Video'];
const kPostTypeAliases = ['stories', 'articles', 'questions', 'videos'];
class PostEditorExtra { class PostEditorExtra {
final String? text; final String? text;
@ -53,7 +55,7 @@ class PostEditorExtra {
} }
class PostEditorScreen extends StatefulWidget { class PostEditorScreen extends StatefulWidget {
final String mode; final String? mode;
final int? postEditId; final int? postEditId;
final int? postReplyId; final int? postReplyId;
final int? postRepostId; final int? postRepostId;
@ -72,7 +74,10 @@ class PostEditorScreen extends StatefulWidget {
State<PostEditorScreen> createState() => _PostEditorScreenState(); State<PostEditorScreen> createState() => _PostEditorScreenState();
} }
class _PostEditorScreenState extends State<PostEditorScreen> { class _PostEditorScreenState extends State<PostEditorScreen>
with SingleTickerProviderStateMixin {
late final TabController _tabController =
TabController(length: 4, vsync: this);
late final PostWriteController _writeController = PostWriteController( late final PostWriteController _writeController = PostWriteController(
doLoadFromTemporary: widget.postEditId == null, doLoadFromTemporary: widget.postEditId == null,
); );
@ -209,6 +214,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
@override @override
void dispose() { void dispose() {
_tabController.dispose();
_writeController.dispose(); _writeController.dispose();
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
hotKeyManager.unregister(_pasteHotKey); hotKeyManager.unregister(_pasteHotKey);
@ -220,14 +226,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_registerHotKey(); _registerHotKey();
if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
context.showErrorDialog('Unknown post type');
Navigator.pop(context);
} else {
_writeController.setMode(widget.mode);
}
_fetchRealms(); _fetchRealms();
_fetchPublishers(); _fetchPublishers();
if (widget.mode != null) {
_writeController.setMode(widget.mode!);
}
_tabController.addListener(() {
if (_tabController.indexIsChanging) {
_writeController.setMode(kPostTypeAliases[_tabController.index]);
}
});
_writeController.fetchRelatedPost( _writeController.fetchRelatedPost(
context, context,
editing: widget.postEditId, editing: widget.postEditId,
@ -255,26 +263,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
title: RichText( title: Text(
textAlign: TextAlign.center, _writeController.title.isNotEmpty
text: TextSpan(children: [
TextSpan(
text: _writeController.title.isNotEmpty
? _writeController.title ? _writeController.title
: 'untitled'.tr(), : 'untitled'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: PostWriteController.kTitleMap[widget.mode]!.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
]),
maxLines: 2,
), ),
actions: [ actions: [
IconButton( IconButton(
@ -283,6 +275,24 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
const Gap(8), const Gap(8),
], ],
bottom: _writeController.isNotEmpty || widget.mode != null
? null
: TabBar(
controller: _tabController,
tabs: [
for (final type in kPostTypes)
Tab(
child: Text(
'postType$type'.tr(),
style: TextStyle(
color: Theme.of(context)
.appBarTheme
.foregroundColor!,
),
),
),
],
),
), ),
body: Column( body: Column(
children: [ children: [
@ -374,7 +384,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [ children: [
SingleChildScrollView( SingleChildScrollView(
padding: EdgeInsets.only(bottom: 160), padding: EdgeInsets.only(bottom: 160),
child: StyledWidget(switch (_writeController.mode) { child: switch (_writeController.mode) {
'stories' => _PostStoryEditor( 'stories' => _PostStoryEditor(
controller: _writeController, controller: _writeController,
onTapPublisher: _showPublisherPopup, onTapPublisher: _showPublisherPopup,
@ -396,8 +406,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
onTapRealm: _showRealmPopup, onTapRealm: _showRealmPopup,
), ),
_ => const Placeholder(), _ => const Placeholder(),
}) },
.padding(top: 8),
), ),
if (_writeController.attachments.isNotEmpty || if (_writeController.attachments.isNotEmpty ||
_writeController.thumbnail != null) _writeController.thumbnail != null)
@ -720,7 +729,7 @@ class _PostStoryEditor extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
constraints: const BoxConstraints(maxWidth: 640), constraints: const BoxConstraints(maxWidth: 640),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -969,7 +978,7 @@ class _PostQuestionEditor extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
constraints: const BoxConstraints(maxWidth: 640), constraints: const BoxConstraints(maxWidth: 640),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -1053,7 +1062,7 @@ class _PostQuestionEditor extends StatelessWidget {
), ),
), ),
], ],
).padding(top: 8), ),
); );
} }
} }
@ -1154,7 +1163,7 @@ class _PostVideoEditor extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
constraints: const BoxConstraints(maxWidth: 640), constraints: const BoxConstraints(maxWidth: 640),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -61,7 +61,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postEditor', 'postEditor',
pathParameters: { queryParameters: {
'mode': 'stories', 'mode': 'stories',
}, },
extra: PostEditorExtra( extra: PostEditorExtra(

View File

@ -22,12 +22,14 @@ class AttachmentItem extends StatelessWidget {
final SnAttachment? data; final SnAttachment? data;
final String? heroTag; final String? heroTag;
final BoxFit fit; final BoxFit fit;
final FilterQuality? filterQuality;
const AttachmentItem({ const AttachmentItem({
super.key, super.key,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
required this.data, required this.data,
required this.heroTag, required this.heroTag,
this.filterQuality,
}); });
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
@ -47,6 +49,7 @@ class AttachmentItem extends StatelessWidget {
sn.getAttachmentUrl(data!.rid), sn.getAttachmentUrl(data!.rid),
key: Key('attachment-${data!.rid}-$tag'), key: Key('attachment-${data!.rid}-$tag'),
fit: fit, fit: fit,
filterQuality: filterQuality,
), ),
); );
case 'video': case 'video':
@ -83,13 +86,16 @@ class _AttachmentItemSensitiveBlur extends StatefulWidget {
final Widget child; final Widget child;
final bool isCompact; final bool isCompact;
const _AttachmentItemSensitiveBlur({required this.child, this.isCompact = false}); const _AttachmentItemSensitiveBlur(
{required this.child, this.isCompact = false});
@override @override
State<_AttachmentItemSensitiveBlur> createState() => _AttachmentItemSensitiveBlurState(); State<_AttachmentItemSensitiveBlur> createState() =>
_AttachmentItemSensitiveBlurState();
} }
class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBlur> { class _AttachmentItemSensitiveBlurState
extends State<_AttachmentItemSensitiveBlur> {
bool _doesShow = false; bool _doesShow = false;
@override @override
@ -124,10 +130,15 @@ class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBl
Text( Text(
'sensitiveContentDescription', 'sensitiveContentDescription',
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr().fontSize(14).textColor(Colors.white.withOpacity(0.8)), )
.tr()
.fontSize(14)
.textColor(Colors.white.withOpacity(0.8)),
if (!widget.isCompact) const Gap(16), if (!widget.isCompact) const Gap(16),
InkWell( InkWell(
child: Text('sensitiveContentReveal').tr().textColor(Colors.white), child: Text('sensitiveContentReveal')
.tr()
.textColor(Colors.white),
onTap: () { onTap: () {
setState(() => _doesShow = !_doesShow); setState(() => _doesShow = !_doesShow);
}, },
@ -137,7 +148,9 @@ class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBl
).center(), ).center(),
), ),
), ),
).opacity(_doesShow ? 0 : 1, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut), )
.opacity(_doesShow ? 0 : 1, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
if (_doesShow) if (_doesShow)
Positioned( Positioned(
top: 0, top: 0,
@ -174,10 +187,12 @@ class _AttachmentItemContentVideo extends StatefulWidget {
}); });
@override @override
State<_AttachmentItemContentVideo> createState() => _AttachmentItemContentVideoState(); State<_AttachmentItemContentVideo> createState() =>
_AttachmentItemContentVideoState();
} }
class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> { class _AttachmentItemContentVideoState
extends State<_AttachmentItemContentVideo> {
bool _showContent = false; bool _showContent = false;
bool _showOriginal = false; bool _showOriginal = false;
@ -188,7 +203,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
setState(() => _showContent = true); setState(() => _showContent = true);
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final url = _showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid); final url = _showOriginal
? sn.getAttachmentUrl(widget.data.rid)
: sn.getAttachmentUrl(widget.data.compressed!.rid);
_videoPlayer = Player(); _videoPlayer = Player();
_videoController = VideoController(_videoPlayer!); _videoController = VideoController(_videoPlayer!);
_videoPlayer!.open(Media(url), play: !widget.isAutoload); _videoPlayer!.open(Media(url), play: !widget.isAutoload);
@ -201,7 +218,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
_videoPlayer?.open( _videoPlayer?.open(
Media( Media(
_showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid), _showOriginal
? sn.getAttachmentUrl(widget.data.rid)
: sn.getAttachmentUrl(widget.data.compressed!.rid),
), ),
play: true, play: true,
); );
@ -283,7 +302,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
), ),
Text( Text(
Duration( Duration(
milliseconds: (widget.data.data['duration'] ?? 0).toInt() * 1000, milliseconds:
(widget.data.data['duration'] ?? 0).toInt() *
1000,
).toString(), ).toString(),
style: GoogleFonts.robotoMono( style: GoogleFonts.robotoMono(
fontSize: 12, fontSize: 12,
@ -346,7 +367,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
MaterialDesktopCustomButton( MaterialDesktopCustomButton(
iconSize: 24, iconSize: 24,
onPressed: _toggleOriginal, onPressed: _toggleOriginal,
icon: _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24), icon: _showOriginal
? const Icon(Symbols.high_quality, size: 24)
: const Icon(Symbols.sd, size: 24),
), ),
], ],
), ),
@ -354,8 +377,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
child: Video( child: Video(
controller: _videoController!, controller: _videoController!,
aspectRatio: ratio, aspectRatio: ratio,
controls: controls: !kIsWeb && (Platform.isAndroid || Platform.isIOS)
!kIsWeb && (Platform.isAndroid || Platform.isIOS) ? MaterialVideoControls : MaterialDesktopVideoControls, ? MaterialVideoControls
: MaterialDesktopVideoControls,
), ),
), ),
); );
@ -378,10 +402,12 @@ class _AttachmentItemContentAudio extends StatefulWidget {
}); });
@override @override
State<_AttachmentItemContentAudio> createState() => _AttachmentItemContentAudioState(); State<_AttachmentItemContentAudio> createState() =>
_AttachmentItemContentAudioState();
} }
class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio> { class _AttachmentItemContentAudioState
extends State<_AttachmentItemContentAudio> {
bool _showContent = false; bool _showContent = false;
double? _draggingValue; double? _draggingValue;
@ -552,8 +578,12 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
overlayShape: SliderComponentShape.noOverlay, overlayShape: SliderComponentShape.noOverlay,
), ),
child: Slider( child: Slider(
secondaryTrackValue: _bufferedPosition.inMilliseconds.abs().toDouble(), secondaryTrackValue: _bufferedPosition
value: _draggingValue?.abs() ?? _position.inMilliseconds.toDouble().abs(), .inMilliseconds
.abs()
.toDouble(),
value: _draggingValue?.abs() ??
_position.inMilliseconds.toDouble().abs(),
min: 0, min: 0,
max: math max: math
.max( .max(
@ -593,7 +623,9 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
), ),
const Gap(16), const Gap(16),
IconButton.filled( IconButton.filled(
icon: _isPlaying ? const Icon(Symbols.pause) : const Icon(Symbols.play_arrow), icon: _isPlaying
? const Icon(Symbols.pause)
: const Icon(Symbols.play_arrow),
onPressed: () { onPressed: () {
_audioPlayer!.playOrPause(); _audioPlayer!.playOrPause();
}, },

View File

@ -21,6 +21,7 @@ class AttachmentList extends StatefulWidget {
final double? minWidth; final double? minWidth;
final double? maxWidth; final double? maxWidth;
final EdgeInsets? padding; final EdgeInsets? padding;
final FilterQuality? filterQuality;
const AttachmentList({ const AttachmentList({
super.key, super.key,
@ -33,23 +34,27 @@ class AttachmentList extends StatefulWidget {
this.minWidth, this.minWidth,
this.maxWidth, this.maxWidth,
this.padding, this.padding,
this.filterQuality,
}); });
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8)); static const BorderRadius kDefaultRadius =
BorderRadius.all(Radius.circular(8));
@override @override
State<AttachmentList> createState() => _AttachmentListState(); State<AttachmentList> createState() => _AttachmentListState();
} }
class _AttachmentListState extends State<AttachmentList> { class _AttachmentListState extends State<AttachmentList> {
late final List<String> heroTags = List.generate(widget.data.length, (_) => const Uuid().v4()); late final List<String> heroTags =
List.generate(widget.data.length, (_) => const Uuid().v4());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, layoutConstraints) { builder: (context, layoutConstraints) {
final borderSide = final borderSide = widget.bordered
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none; ? BorderSide(width: 1, color: Theme.of(context).dividerColor)
: BorderSide.none;
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer; final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
final constraints = BoxConstraints( final constraints = BoxConstraints(
minWidth: widget.minWidth ?? 80, minWidth: widget.minWidth ?? 80,
@ -58,13 +63,13 @@ class _AttachmentListState extends State<AttachmentList> {
if (widget.data.isEmpty) return const SizedBox.shrink(); if (widget.data.isEmpty) return const SizedBox.shrink();
if (widget.data.length == 1) { if (widget.data.length == 1) {
final singleAspectRatio = final singleAspectRatio = widget.data[0]?.data['ratio']?.toDouble() ??
widget.data[0]?.data['ratio']?.toDouble() ??
switch (widget.data[0]?.mimetype.split('/').firstOrNull) { switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
'audio' => 16 / 9, 'audio' => 16 / 9,
'video' => 16 / 9, 'video' => 16 / 9,
_ => 1, _ => 1,
}.toDouble(); }
.toDouble();
return Container( return Container(
padding: widget.padding ?? EdgeInsets.zero, padding: widget.padding ?? EdgeInsets.zero,
@ -80,12 +85,18 @@ class _AttachmentListState extends State<AttachmentList> {
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius, borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(data: widget.data[0], heroTag: heroTags[0], fit: widget.fit), child: AttachmentItem(
data: widget.data[0],
heroTag: heroTags[0],
fit: widget.fit,
filterQuality: widget.filterQuality,
),
), ),
), ),
), ),
onTap: () { onTap: () {
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return; if (widget.data.firstOrNull?.mediaType != SnMediaType.image)
return;
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentZoomView( AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(), data: widget.data.where((ele) => ele != null).cast(),
@ -100,8 +111,10 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
final fullOfImage = final fullOfImage = widget.data
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length; .where((ele) => ele?.mediaType == SnMediaType.image)
.length ==
widget.data.length;
if (widget.gridded && fullOfImage) { if (widget.gridded && fullOfImage) {
return Container( return Container(
@ -117,19 +130,26 @@ class _AttachmentListState extends State<AttachmentList> {
crossAxisCount: math.min(widget.data.length, 2), crossAxisCount: math.min(widget.data.length, 2),
crossAxisSpacing: 4, crossAxisSpacing: 4,
mainAxisSpacing: 4, mainAxisSpacing: 4,
children: children: widget.data
widget.data
.mapIndexed( .mapIndexed(
(idx, ele) => GestureDetector( (idx, ele) => GestureDetector(
child: Container( child: Container(
constraints: constraints, constraints: constraints,
child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover), child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
filterQuality: widget.filterQuality,
),
), ),
onTap: () { onTap: () {
if (widget.data[idx]!.mediaType != SnMediaType.image) return; if (widget.data[idx]!.mediaType != SnMediaType.image)
return;
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentZoomView( AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(), data: widget.data
.where((ele) => ele != null)
.cast(),
initialIndex: idx, initialIndex: idx,
heroTags: heroTags, heroTags: heroTags,
), ),
@ -156,15 +176,19 @@ class _AttachmentListState extends State<AttachmentList> {
child: ClipRRect( child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius, borderRadius: AttachmentList.kDefaultRadius,
child: Column( child: Column(
children: children: widget.data
widget.data
.mapIndexed( .mapIndexed(
(idx, ele) => GestureDetector( (idx, ele) => GestureDetector(
child: AspectRatio( child: AspectRatio(
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1, aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
child: Container( child: Container(
constraints: constraints, constraints: constraints,
child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover), child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
filterQuality: widget.filterQuality,
),
), ),
), ),
), ),
@ -189,16 +213,22 @@ class _AttachmentListState extends State<AttachmentList> {
itemCount: widget.data.length, itemCount: widget.data.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return Container( return Container(
constraints: constraints.copyWith(maxWidth: widget.maxWidth), constraints:
constraints.copyWith(maxWidth: widget.maxWidth),
child: AspectRatio( child: AspectRatio(
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), aspectRatio:
(widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (widget.data[idx]?.mediaType != SnMediaType.image) return; if (widget.data[idx]?.mediaType != SnMediaType.image)
return;
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentZoomView( AttachmentZoomView(
data: data: widget.data
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), .where((ele) =>
ele != null &&
ele.mediaType == SnMediaType.image)
.cast(),
initialIndex: idx, initialIndex: idx,
heroTags: heroTags, heroTags: heroTags,
), ),
@ -212,18 +242,25 @@ class _AttachmentListState extends State<AttachmentList> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor, color: backgroundColor,
border: Border(top: borderSide, bottom: borderSide), border:
Border(top: borderSide, bottom: borderSide),
borderRadius: AttachmentList.kDefaultRadius, borderRadius: AttachmentList.kDefaultRadius,
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius, borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(data: widget.data[idx], heroTag: heroTags[idx]), child: AttachmentItem(
data: widget.data[idx],
heroTag: heroTags[idx],
filterQuality: widget.filterQuality,
),
), ),
), ),
Positioned( Positioned(
right: 8, right: 8,
bottom: 8, bottom: 8,
child: Chip(label: Text('${idx + 1}/${widget.data.length}')), child: Chip(
label:
Text('${idx + 1}/${widget.data.length}')),
), ),
], ],
), ),
@ -245,5 +282,6 @@ class _AttachmentListState extends State<AttachmentList> {
class _AttachmentListScrollBehavior extends MaterialScrollBehavior { class _AttachmentListScrollBehavior extends MaterialScrollBehavior {
@override @override
Set<PointerDeviceKind> get dragDevices => {PointerDeviceKind.touch, PointerDeviceKind.mouse}; Set<PointerDeviceKind> get dragDevices =>
{PointerDeviceKind.touch, PointerDeviceKind.mouse};
} }

View File

@ -1,5 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' show max;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
@ -48,11 +47,14 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
bool _showOverlay = true; bool _showOverlay = true;
bool _dismissable = true; bool _dismissable = true;
int _page = 0;
void _updatePage() { void _updatePage() {
setState(() { setState(() {
if (_isCompletedDownload) { if (_isCompletedDownload) {
setState(() => _isCompletedDownload = false); setState(() => _isCompletedDownload = false);
} }
_page = _pageController.page?.round() ?? 0;
}); });
} }
@ -222,31 +224,11 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
BoxDecoration(color: Colors.transparent), BoxDecoration(color: Colors.transparent),
); );
}), }),
Positioned(
top: max(MediaQuery.of(context).padding.top, 8),
left: 14,
child: IgnorePointer(
ignoring: !_showOverlay,
child: IconButton(
constraints: const BoxConstraints(),
icon: const Icon(Icons.close),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.surface.withOpacity(0.5),
),
),
onPressed: () {
Navigator.of(context).pop();
},
).opacity(_showOverlay ? 1 : 0, animate: true).animate(
const Duration(milliseconds: 300), Curves.easeInOut),
),
),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: IgnorePointer( child: IgnorePointer(
child: Container( child: Container(
height: 300, height: 200,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.bottomCenter, begin: Alignment.bottomCenter,
@ -269,153 +251,130 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Builder(builder: (context) { child: Builder(builder: (context) {
final ud = context.read<UserDirectoryProvider>(); return Row(
final item = widget.data.elementAt(
widget.data.length > 1
? _pageController.page?.round() ?? 0
: 0,
);
final account = ud.getFromCache(item.accountId);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (item.accountId > 0) IconButton(
Row( iconSize: 18,
children: [ constraints: const BoxConstraints(),
IgnorePointer( icon: const Icon(Icons.close),
child: AccountImage( style: ButtonStyle(
content: account?.avatar, backgroundColor: MaterialStateProperty.all(
radius: 19, Theme.of(context)
.colorScheme
.surface
.withOpacity(0.5),
), ),
), ),
const Gap(8), onPressed: () {
Navigator.of(context).pop();
},
),
IconButton(
iconSize: 20,
constraints: const BoxConstraints(),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
icon: const Icon(Symbols.hide).padding(all: 6),
onPressed: () {
setState(() => _showOverlay = false);
}),
Expanded( Expanded(
child: IgnorePointer( child: IgnorePointer(
child: Column( child: Builder(builder: (context) {
crossAxisAlignment: final item = widget.data.elementAt(_page);
CrossAxisAlignment.start, final doShowCameraInfo =
item.metadata['exif']?['Model'] != null;
final exif = item.metadata['exif'];
return Column(
children: [ children: [
if (widget.data.length > 1)
Text( Text(
'attachmentUploadBy'.tr(), '${_page + 1}/${widget.data.length}',
style: Theme.of(context) style:
.textTheme GoogleFonts.robotoMono(fontSize: 13),
.bodySmall, ).padding(right: 8),
if (doShowCameraInfo)
Text(
'attachmentShotOn'
.tr(args: [exif?['Model']]),
style: metaTextStyle,
textAlign: TextAlign.center,
), ),
if (doShowCameraInfo)
Row(
spacing: 4,
mainAxisSize: MainAxisSize.min,
children: [
if (exif?['Megapixels'] != null)
Text( Text(
account?.nick ?? 'unknown'.tr(), '${exif?['Megapixels']}MP',
style: Theme.of(context) style: metaTextStyle,
.textTheme ),
.bodyMedium, if (exif?['ISO'] != null)
Text(
'ISO${exif['ISO']}',
style: metaTextStyle,
),
if (exif?['FNumber'] != null)
Text(
'f/${exif['FNumber']}',
style: metaTextStyle,
), ),
], ],
)
],
);
}),
), ),
), ),
), IconButton(
if (widget.data.length > 1) constraints: const BoxConstraints(),
IgnorePointer( padding: EdgeInsets.zero,
child: Text( visualDensity: VisualDensity.compact,
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}', icon: Container(
style: GoogleFonts.robotoMono(fontSize: 13),
).padding(right: 8),
),
InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () => _saveToAlbum(widget.data.length > 1
? _pageController.page?.round() ?? 0
: 0),
child: Container(
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
child: !_isDownloading child: !_isDownloading
? !_isCompletedDownload ? !_isCompletedDownload
? const Icon(Symbols.save_alt) ? const Icon(Symbols.save_alt)
: const Icon(Symbols.download_done) : const Icon(Symbols.download_done)
: SizedBox( : SizedBox(
width: 24, width: 20,
height: 24, height: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
value: _progressOfDownload, value: _progressOfDownload,
strokeWidth: 3, strokeWidth: 3,
), ),
), ),
), ),
onPressed:
_isDownloading ? null : () => _saveToAlbum(_page),
), ),
], IconButton(
), iconSize: 18,
const Gap(4), constraints: const BoxConstraints(),
IgnorePointer( icon: const Icon(Icons.info_outline),
child: Text( style: ButtonStyle(
item.alt, backgroundColor: MaterialStateProperty.all(
maxLines: 2, Theme.of(context)
overflow: TextOverflow.ellipsis, .colorScheme
style: const TextStyle( .surface
fontSize: 15, .withOpacity(0.5),
fontWeight: FontWeight.w500,
), ),
), ),
), onPressed: () {
const Gap(2),
IgnorePointer(
child: Wrap(
spacing: 6,
children: [
if (item.metadata['exif'] == null)
Text(
'#${item.rid}',
style: metaTextStyle,
),
if (item.metadata['exif']?['Model'] != null)
Text(
'attachmentShotOn'.tr(args: [
item.metadata['exif']?['Model'],
]),
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] !=
null &&
item.metadata['exif']?['Model'] != null)
Text(
'${item.metadata['exif']?['Megapixels']}MP',
style: metaTextStyle,
)
else
Text(
item.size.formatBytes(),
style: metaTextStyle,
),
if (item.metadata['width'] != null &&
item.metadata['height'] != null)
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
),
],
),
),
const Gap(4),
InkWell(
onTap: () {
_showDetail = true; _showDetail = true;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => _AttachmentZoomDetailPopup( builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data.elementAt( data: widget.data.elementAt(_page),
widget.data.length > 1
? _pageController.page?.round() ?? 0
: 0),
), ),
).then((_) { ).then((_) {
_showDetail = false; _showDetail = false;
}); });
}, },
child: Text(
'viewDetailedAttachment'.tr(),
style: metaTextStyle.copyWith(
decoration: TextDecoration.underline),
),
), ),
], ],
); );
@ -427,18 +386,20 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
), ),
), ),
onTap: () { onTap: () {
if (_showOverlay) {
Navigator.pop(context);
return;
}
setState(() => _showOverlay = !_showOverlay); setState(() => _showOverlay = !_showOverlay);
}, },
onVerticalDragUpdate: (details) { onVerticalDragUpdate: (details) {
if (_showDetail) return; if (_showDetail || !_dismissable) return;
if (details.delta.dy <= -20) { if (details.delta.dy <= -20) {
_showDetail = true; _showDetail = true;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => _AttachmentZoomDetailPopup( builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data.elementAt(widget.data.length > 1 data: widget.data.elementAt(_page),
? _pageController.page?.round() ?? 0
: 0),
), ),
).then((_) { ).then((_) {
_showDetail = false; _showDetail = false;

View File

@ -149,7 +149,6 @@ class PostItem extends StatelessWidget {
void _doShareViaPicture(BuildContext context) async { void _doShareViaPicture(BuildContext context) async {
final box = context.findRenderObject() as RenderBox?; final box = context.findRenderObject() as RenderBox?;
context.showSnackbar('postSharingViaPicture'.tr());
final controller = ScreenshotController(); final controller = ScreenshotController();
final capturedImage = await controller.captureFromLongWidget( final capturedImage = await controller.captureFromLongWidget(
@ -160,9 +159,9 @@ class PostItem extends StatelessWidget {
child: Material( child: Material(
child: MultiProvider( child: MultiProvider(
providers: [ providers: [
// Create a copy of environments
Provider<SnNetworkProvider>(create: (_) => context.read()), Provider<SnNetworkProvider>(create: (_) => context.read()),
ChangeNotifierProvider<ConfigProvider>( Provider<UserDirectoryProvider>(create: (_) => context.read()),
create: (_) => context.read()),
], ],
child: ResponsiveBreakpoints.builder( child: ResponsiveBreakpoints.builder(
breakpoints: ResponsiveBreakpoints.of(context).breakpoints, breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
@ -507,6 +506,8 @@ class PostShareImageWidget extends StatelessWidget {
StyledWidget(AttachmentList( StyledWidget(AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
columned: true, columned: true,
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
)).padding(horizontal: 16, bottom: 8), )).padding(horizontal: 16, bottom: 8),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -1037,8 +1038,10 @@ class _PostContentHeader extends StatelessWidget {
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postEditor', 'postEditor',
pathParameters: {'mode': data.typePlural}, queryParameters: {
queryParameters: {'editing': data.id.toString()}, 'editing': data.id.toString(),
'mode': data.typePlural,
},
); );
}, },
), ),
@ -1065,8 +1068,10 @@ class _PostContentHeader extends StatelessWidget {
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postEditor', 'postEditor',
pathParameters: {'mode': 'stories'}, queryParameters: {
queryParameters: {'replying': data.id.toString()}, 'replying': data.id.toString(),
'mode': data.typePlural,
},
); );
}, },
), ),
@ -1081,8 +1086,10 @@ class _PostContentHeader extends StatelessWidget {
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postEditor', 'postEditor',
pathParameters: {'mode': 'stories'}, queryParameters: {
queryParameters: {'reposting': data.id.toString()}, 'reposting': data.id.toString(),
'mode': 'stories',
},
); );
}, },
), ),

View File

@ -25,7 +25,8 @@ class PostMiniEditor extends StatefulWidget {
} }
class _PostMiniEditorState extends State<PostMiniEditor> { class _PostMiniEditorState extends State<PostMiniEditor> {
final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false); final PostWriteController _writeController =
PostWriteController(doLoadFromTemporary: false);
bool _isFetching = false; bool _isFetching = false;
@ -44,8 +45,9 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
); );
final beforeId = config.prefs.getInt('int_last_publisher_id'); final beforeId = config.prefs.getInt('int_last_publisher_id');
_writeController _writeController.setPublisher(
.setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull); _publishers?.where((ele) => ele.id == beforeId).firstOrNull ??
_publishers?.firstOrNull);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -99,11 +101,17 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Text(item.nick).textStyle(Theme.of(context).textTheme.bodyMedium!), Text(item.nick).textStyle(
Theme.of(context)
.textTheme
.bodyMedium!),
Text('@${item.name}') Text('@${item.name}')
.textStyle(Theme.of(context).textTheme.bodySmall!) .textStyle(Theme.of(context)
.textTheme
.bodySmall!)
.fontSize(12), .fontSize(12),
], ],
), ),
@ -120,7 +128,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
CircleAvatar( CircleAvatar(
radius: 16, radius: 16,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
foregroundColor: Theme.of(context).colorScheme.onSurface, foregroundColor:
Theme.of(context).colorScheme.onSurface,
child: const Icon(Symbols.add), child: const Icon(Symbols.add),
), ),
const Gap(8), const Gap(8),
@ -129,7 +138,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('publishersNew').tr().textStyle(Theme.of(context).textTheme.bodyMedium!), Text('publishersNew').tr().textStyle(
Theme.of(context).textTheme.bodyMedium!),
], ],
), ),
), ),
@ -140,7 +150,9 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
value: _writeController.publisher, value: _writeController.publisher,
onChanged: (SnPublisher? value) { onChanged: (SnPublisher? value) {
if (value == null) { if (value == null) {
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) { GoRouter.of(context)
.pushNamed('accountPublisherNew')
.then((value) {
if (value == true) { if (value == true) {
_publishers = null; _publishers = null;
_fetchPublishers(); _fetchPublishers();
@ -176,7 +188,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
), ),
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(8), const Gap(8),
@ -185,7 +198,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _writeController.progress), tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2), builder: (context, value, _) =>
LinearProgressIndicator(value: value, minHeight: 2),
) )
else if (_writeController.isBusy) else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2), const LinearProgressIndicator(value: null, minHeight: 2),
@ -200,15 +214,17 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
onPressed: () { onPressed: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postEditor', 'postEditor',
pathParameters: {'mode': 'stories'},
queryParameters: { queryParameters: {
if (widget.postReplyId != null) 'replying': widget.postReplyId.toString(), if (widget.postReplyId != null)
'replying': widget.postReplyId.toString(),
'mode': 'stories',
}, },
); );
}, },
), ),
TextButton.icon( TextButton.icon(
onPressed: (_writeController.isBusy || _writeController.publisher == null) onPressed: (_writeController.isBusy ||
_writeController.publisher == null)
? null ? null
: () { : () {
_writeController.sendPost(context).then((_) { _writeController.sendPost(context).then((_) {

View File

@ -34,11 +34,14 @@ class UniversalImage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null; final double? resizeHeight =
final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null; cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
final double? resizeWidth =
cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
return Image( return Image(
filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality, filterQuality:
filterQuality ?? context.read<ConfigProvider>().imageQuality,
image: kIsWeb image: kIsWeb
? UniversalImage.provider(url) ? UniversalImage.provider(url)
: ResizeImage( : ResizeImage(
@ -52,7 +55,8 @@ class UniversalImage extends StatelessWidget {
fit: fit, fit: fit,
loadingBuilder: noProgressIndicator loadingBuilder: noProgressIndicator
? null ? null
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { : (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return Container( return Container(
constraints: BoxConstraints(maxHeight: 80), constraints: BoxConstraints(maxHeight: 80),
@ -61,12 +65,15 @@ class UniversalImage extends StatelessWidget {
tween: Tween( tween: Tween(
begin: 0, begin: 0,
end: loadingProgress.expectedTotalBytes != null end: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! ? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: 0, : 0,
), ),
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator( builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null, value: loadingProgress.expectedTotalBytes != null
? value.toDouble()
: null,
), ),
), ),
), ),
@ -114,6 +121,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
final BoxFit? fit; final BoxFit? fit;
final bool noProgressIndicator; final bool noProgressIndicator;
final bool noErrorWidget; final bool noErrorWidget;
final FilterQuality? filterQuality;
const AutoResizeUniversalImage( const AutoResizeUniversalImage(
this.url, { this.url, {
@ -123,6 +131,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
this.fit, this.fit,
this.noProgressIndicator = false, this.noProgressIndicator = false,
this.noErrorWidget = false, this.noErrorWidget = false,
this.filterQuality,
}); });
@override @override
@ -137,6 +146,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
noErrorWidget: noErrorWidget, noErrorWidget: noErrorWidget,
cacheHeight: constraints.maxHeight, cacheHeight: constraints.maxHeight,
cacheWidth: constraints.maxWidth, cacheWidth: constraints.maxWidth,
filterQuality: filterQuality,
); );
}); });
} }