💄 Optimize compose
This commit is contained in:
@@ -8,19 +8,18 @@ import 'package:island/models/file.dart';
|
|||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
import 'package:island/screens/creators/publishers_form.dart';
|
import 'package:island/screens/creators/publishers_form.dart';
|
||||||
import 'package:island/screens/posts/compose_article.dart';
|
import 'package:island/screens/posts/compose_article.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/attachment_uploader.dart';
|
|
||||||
import 'package:island/widgets/content/attachment_preview.dart';
|
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/post/compose_attachments.dart';
|
||||||
|
import 'package:island/widgets/post/compose_form_fields.dart';
|
||||||
|
import 'package:island/widgets/post/compose_info_banner.dart';
|
||||||
|
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.dart';
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
|
import 'package:island/widgets/post/compose_toolbar.dart';
|
||||||
import 'package:island/widgets/post/post_item.dart';
|
import 'package:island/widgets/post/post_item.dart';
|
||||||
import 'package:island/widgets/post/publishers_modal.dart';
|
import 'package:island/widgets/post/publishers_modal.dart';
|
||||||
import 'package:island/screens/posts/post_detail.dart';
|
|
||||||
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
|
||||||
import 'package:island/services/compose_storage_db.dart';
|
|
||||||
// DraftManagerSheet is now imported through compose_toolbar.dart
|
|
||||||
import 'package:island/widgets/post/compose_toolbar.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';
|
||||||
|
|
||||||
@@ -92,9 +91,6 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
return ArticleComposeScreen(originalPost: originalPost);
|
return ArticleComposeScreen(originalPost: originalPost);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, continue with regular post compose
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
// When editing, preserve the original replied/forwarded post references
|
// When editing, preserve the original replied/forwarded post references
|
||||||
final effectiveRepliedPost = repliedPost ?? originalPost?.repliedPost;
|
final effectiveRepliedPost = repliedPost ?? originalPost?.repliedPost;
|
||||||
final effectiveForwardedPost = forwardedPost ?? originalPost?.forwardedPost;
|
final effectiveForwardedPost = forwardedPost ?? originalPost?.forwardedPost;
|
||||||
@@ -210,109 +206,6 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildWideAttachmentGrid() {
|
|
||||||
return GridView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
crossAxisSpacing: 8,
|
|
||||||
mainAxisSpacing: 8,
|
|
||||||
),
|
|
||||||
itemCount: state.attachments.value.length,
|
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
final progressMap = state.attachmentProgress.value;
|
|
||||||
return AttachmentPreview(
|
|
||||||
item: state.attachments.value[idx],
|
|
||||||
progress: progressMap[idx],
|
|
||||||
onRequestUpload: () async {
|
|
||||||
final config = await showModalBottomSheet<AttachmentUploadConfig>(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder:
|
|
||||||
(context) => AttachmentUploaderSheet(
|
|
||||||
ref: ref,
|
|
||||||
state: state,
|
|
||||||
index: idx,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (config != null) {
|
|
||||||
await ComposeLogic.uploadAttachment(
|
|
||||||
ref,
|
|
||||||
state,
|
|
||||||
idx,
|
|
||||||
poolId: config.poolId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx),
|
|
||||||
onUpdate:
|
|
||||||
(value) => ComposeLogic.updateAttachment(state, value, idx),
|
|
||||||
onMove: (delta) {
|
|
||||||
state.attachments.value = ComposeLogic.moveAttachment(
|
|
||||||
state.attachments.value,
|
|
||||||
idx,
|
|
||||||
delta,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildNarrowAttachmentList() {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
for (var idx = 0; idx < state.attachments.value.length; idx++)
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
|
||||||
child: () {
|
|
||||||
final progressMap = state.attachmentProgress.value;
|
|
||||||
return AttachmentPreview(
|
|
||||||
item: state.attachments.value[idx],
|
|
||||||
progress: progressMap[idx],
|
|
||||||
onRequestUpload: () async {
|
|
||||||
final config =
|
|
||||||
await showModalBottomSheet<AttachmentUploadConfig>(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder:
|
|
||||||
(context) => AttachmentUploaderSheet(
|
|
||||||
ref: ref,
|
|
||||||
state: state,
|
|
||||||
index: idx,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (config != null) {
|
|
||||||
await ComposeLogic.uploadAttachment(
|
|
||||||
ref,
|
|
||||||
state,
|
|
||||||
idx,
|
|
||||||
poolId: config.poolId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDelete:
|
|
||||||
() => ComposeLogic.deleteAttachment(ref, state, idx),
|
|
||||||
onUpdate:
|
|
||||||
(value) =>
|
|
||||||
ComposeLogic.updateAttachment(state, value, idx),
|
|
||||||
onMove: (delta) {
|
|
||||||
state.attachments.value = ComposeLogic.moveAttachment(
|
|
||||||
state.attachments.value,
|
|
||||||
idx,
|
|
||||||
delta,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build UI
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
onPopInvoked: (_) {
|
onPopInvoked: (_) {
|
||||||
if (originalPost == null) {
|
if (originalPost == null) {
|
||||||
@@ -362,7 +255,57 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Reply/Forward info section
|
// Reply/Forward info section
|
||||||
_buildInfoBanner(context),
|
ComposeInfoBanner(
|
||||||
|
originalPost: originalPost,
|
||||||
|
replyingTo: repliedPost,
|
||||||
|
forwardingTo: forwardedPost,
|
||||||
|
onReferencePostTap: (context, post) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder:
|
||||||
|
(context) => DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.7,
|
||||||
|
maxChildSize: 0.9,
|
||||||
|
minChildSize: 0.5,
|
||||||
|
builder:
|
||||||
|
(context, scrollController) => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.outline,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: PostItem(item: post),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// Main content area
|
// Main content area
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -414,78 +357,27 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
ComposeFormFields(
|
||||||
controller: state.titleController,
|
state: state,
|
||||||
decoration: InputDecoration(
|
showPublisherAvatar: false,
|
||||||
hintText: 'postTitle'.tr(),
|
onPublisherTap: () {
|
||||||
border: InputBorder.none,
|
showModalBottomSheet(
|
||||||
isCollapsed: true,
|
isScrollControlled: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
context: context,
|
||||||
vertical: 8,
|
builder:
|
||||||
horizontal: 8,
|
(context) => const PublisherModal(),
|
||||||
),
|
).then((value) {
|
||||||
),
|
if (value != null) {
|
||||||
style: theme.textTheme.titleMedium,
|
state.currentPublisher.value = value;
|
||||||
onTapOutside:
|
}
|
||||||
(_) =>
|
});
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: state.descriptionController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'postDescription'.tr(),
|
|
||||||
border: InputBorder.none,
|
|
||||||
isCollapsed: true,
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(
|
|
||||||
8,
|
|
||||||
4,
|
|
||||||
8,
|
|
||||||
12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: 3,
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
// Content field with borderless design
|
|
||||||
TextField(
|
|
||||||
controller: state.contentController,
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintText: 'postContent'.tr(),
|
|
||||||
isCollapsed: true,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 8,
|
|
||||||
horizontal: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
maxLines: null,
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
|
|
||||||
const Gap(8),
|
|
||||||
|
|
||||||
// Attachments preview
|
|
||||||
if (state.attachments.value.isNotEmpty)
|
|
||||||
LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final isWide = isWideScreen(context);
|
|
||||||
return isWide
|
|
||||||
? buildWideAttachmentGrid()
|
|
||||||
: buildNarrowAttachmentList();
|
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
else
|
const Gap(8),
|
||||||
const SizedBox.shrink(),
|
ComposeAttachments(
|
||||||
|
state: state,
|
||||||
|
isCompact: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -503,262 +395,4 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInfoBanner(BuildContext context) {
|
|
||||||
// When editing, preserve the original replied/forwarded post references
|
|
||||||
final effectiveRepliedPost =
|
|
||||||
initialState?.replyingTo ?? originalPost?.repliedPost;
|
|
||||||
final effectiveForwardedPost =
|
|
||||||
initialState?.forwardingTo ?? originalPost?.forwardedPost;
|
|
||||||
|
|
||||||
// Show editing banner when editing a post
|
|
||||||
if (originalPost != null) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Symbols.edit,
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Text(
|
|
||||||
'postEditing'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 16, vertical: 8),
|
|
||||||
),
|
|
||||||
// Show reply/forward banners below editing banner if they exist
|
|
||||||
if (effectiveRepliedPost != null)
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Symbols.reply, size: 16),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
'postReplyingTo'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
_buildCompactReferencePost(context, effectiveRepliedPost),
|
|
||||||
],
|
|
||||||
).padding(all: 16),
|
|
||||||
),
|
|
||||||
if (effectiveForwardedPost != null)
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Symbols.forward, size: 16),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
'postForwardingTo'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
_buildCompactReferencePost(context, effectiveForwardedPost),
|
|
||||||
],
|
|
||||||
).padding(all: 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show banner for replies (including when editing a reply)
|
|
||||||
if (effectiveRepliedPost != null) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Symbols.reply, size: 16),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
'postReplyingTo'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
_buildCompactReferencePost(context, effectiveRepliedPost),
|
|
||||||
],
|
|
||||||
).padding(all: 16),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show banner for forwards (including when editing a forward)
|
|
||||||
if (effectiveForwardedPost != null) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Symbols.forward, size: 16),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
'postForwardingTo'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
_buildCompactReferencePost(context, effectiveForwardedPost),
|
|
||||||
],
|
|
||||||
).padding(all: 16),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCompactReferencePost(BuildContext context, SnPost post) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder:
|
|
||||||
(context) => DraggableScrollableSheet(
|
|
||||||
initialChildSize: 0.7,
|
|
||||||
maxChildSize: 0.9,
|
|
||||||
minChildSize: 0.5,
|
|
||||||
builder:
|
|
||||||
(context, scrollController) => Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
borderRadius: const BorderRadius.vertical(
|
|
||||||
top: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.outline,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: PostItem(item: post),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
ProfilePictureWidget(
|
|
||||||
fileId: post.publisher.picture?.id,
|
|
||||||
radius: 16,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
post.publisher.nick,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (post.title?.isNotEmpty ?? false)
|
|
||||||
Text(
|
|
||||||
post.title!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 13,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
if (post.content?.isNotEmpty ?? false)
|
|
||||||
Text(
|
|
||||||
post.content!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
if (post.attachments.isNotEmpty)
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Symbols.attach_file,
|
|
||||||
size: 12,
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
'postHasAttachments'.plural(post.attachments.length),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
fontSize: 11,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Symbols.open_in_full,
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context).colorScheme.outline,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -7,21 +7,20 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
|
|
||||||
import 'package:island/screens/creators/publishers_form.dart';
|
import 'package:island/screens/creators/publishers_form.dart';
|
||||||
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/attachment_uploader.dart';
|
import 'package:island/widgets/attachment_uploader.dart';
|
||||||
import 'package:island/screens/posts/post_detail.dart';
|
|
||||||
import 'package:island/widgets/content/attachment_preview.dart';
|
import 'package:island/widgets/content/attachment_preview.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/markdown.dart';
|
import 'package:island/widgets/content/markdown.dart';
|
||||||
|
import 'package:island/widgets/post/compose_form_fields.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.dart';
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||||
import 'package:island/services/compose_storage_db.dart';
|
|
||||||
import 'package:island/widgets/post/compose_toolbar.dart';
|
import 'package:island/widgets/post/compose_toolbar.dart';
|
||||||
import 'package:island/widgets/post/publishers_modal.dart';
|
import 'package:island/widgets/post/publishers_modal.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';
|
||||||
|
|
||||||
@@ -233,64 +232,20 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
ComposeFormFields(
|
||||||
controller: state.titleController,
|
state: state,
|
||||||
decoration: InputDecoration(
|
showPublisherAvatar: false,
|
||||||
hintText: 'postTitle'.tr(),
|
onPublisherTap: () {
|
||||||
border: InputBorder.none,
|
showModalBottomSheet(
|
||||||
isCollapsed: true,
|
isScrollControlled: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
context: context,
|
||||||
vertical: 8,
|
builder: (context) => const PublisherModal(),
|
||||||
horizontal: 8,
|
).then((value) {
|
||||||
),
|
if (value != null) {
|
||||||
),
|
state.currentPublisher.value = value;
|
||||||
style: theme.textTheme.titleMedium,
|
}
|
||||||
onTapOutside:
|
});
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
},
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: state.descriptionController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'postDescription'.tr(),
|
|
||||||
border: InputBorder.none,
|
|
||||||
isCollapsed: true,
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
|
|
||||||
),
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: 3,
|
|
||||||
onTapOutside:
|
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: KeyboardListener(
|
|
||||||
focusNode: FocusNode(),
|
|
||||||
onKeyEvent:
|
|
||||||
(event) => ComposeLogic.handleKeyPress(
|
|
||||||
event,
|
|
||||||
state,
|
|
||||||
ref,
|
|
||||||
context,
|
|
||||||
originalPost: originalPost,
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
controller: state.contentController,
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintText: 'postContent'.tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 16,
|
|
||||||
horizontal: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
maxLines: null,
|
|
||||||
expands: true,
|
|
||||||
textAlignVertical: TextAlignVertical.top,
|
|
||||||
onTapOutside:
|
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Attachments preview
|
// Attachments preview
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.dart';
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
@@ -29,6 +30,7 @@ class ComposeFormFields extends StatelessWidget {
|
|||||||
// Publisher profile picture
|
// Publisher profile picture
|
||||||
if (showPublisherAvatar)
|
if (showPublisherAvatar)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
|
onTap: onPublisherTap,
|
||||||
child: ProfilePictureWidget(
|
child: ProfilePictureWidget(
|
||||||
fileId: state.currentPublisher.value?.picture?.id,
|
fileId: state.currentPublisher.value?.picture?.id,
|
||||||
radius: 20,
|
radius: 20,
|
||||||
@@ -37,7 +39,6 @@ class ComposeFormFields extends StatelessWidget {
|
|||||||
? Icons.question_mark
|
? Icons.question_mark
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
onTap: onPublisherTap,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Post content form
|
// Post content form
|
||||||
@@ -79,7 +80,7 @@ class ComposeFormFields extends StatelessWidget {
|
|||||||
controller: state.titleController,
|
controller: state.titleController,
|
||||||
enabled: enabled && state.currentPublisher.value != null,
|
enabled: enabled && state.currentPublisher.value != null,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'postTitle',
|
hintText: 'postTitle'.tr(),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
@@ -97,7 +98,7 @@ class ComposeFormFields extends StatelessWidget {
|
|||||||
controller: state.descriptionController,
|
controller: state.descriptionController,
|
||||||
enabled: enabled && state.currentPublisher.value != null,
|
enabled: enabled && state.currentPublisher.value != null,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'postDescription',
|
hintText: 'postDescription'.tr(),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
|
contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
|
||||||
@@ -116,7 +117,7 @@ class ComposeFormFields extends StatelessWidget {
|
|||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
hintText: 'postContent',
|
hintText: 'postContent'.tr(),
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
|
@@ -96,38 +96,16 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
MenuAnchor(
|
||||||
onPressed: pickPhotoMedia,
|
builder:
|
||||||
tooltip: 'addPhoto'.tr(),
|
(context, controller, child) => IconButton(
|
||||||
icon: const Icon(Symbols.add_a_photo),
|
onPressed: () {
|
||||||
color: colorScheme.primary,
|
if (controller.isOpen) {
|
||||||
visualDensity: const VisualDensity(
|
controller.close();
|
||||||
horizontal: -4,
|
} else {
|
||||||
vertical: -2,
|
controller.open();
|
||||||
),
|
}
|
||||||
),
|
},
|
||||||
IconButton(
|
|
||||||
onPressed: pickVideoMedia,
|
|
||||||
tooltip: 'addVideo'.tr(),
|
|
||||||
icon: const Icon(Symbols.videocam),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: addAudio,
|
|
||||||
tooltip: 'addAudio'.tr(),
|
|
||||||
icon: const Icon(Symbols.mic),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: pickGeneralFile,
|
|
||||||
tooltip: 'uploadFile'.tr(),
|
tooltip: 'uploadFile'.tr(),
|
||||||
icon: const Icon(Symbols.file_upload),
|
icon: const Icon(Symbols.file_upload),
|
||||||
color: colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
@@ -136,6 +114,69 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
vertical: -2,
|
vertical: -2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
pickPhotoMedia();
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStatePropertyAll(
|
||||||
|
EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leadingIcon: const Icon(Symbols.add_a_photo),
|
||||||
|
child: Text('addPhoto'.tr()),
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
pickVideoMedia();
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Symbols.videocam),
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStatePropertyAll(
|
||||||
|
EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text('addVideo'.tr()),
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
addAudio();
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Symbols.mic),
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStatePropertyAll(
|
||||||
|
EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text('addAudio'.tr()),
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
pickGeneralFile();
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Symbols.file_upload),
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStatePropertyAll(
|
||||||
|
EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text('uploadFile'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: linkAttachment,
|
onPressed: linkAttachment,
|
||||||
icon: const Icon(Symbols.attach_file),
|
icon: const Icon(Symbols.attach_file),
|
||||||
@@ -249,30 +290,63 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
MenuAnchor(
|
||||||
onPressed: pickPhotoMedia,
|
builder:
|
||||||
tooltip: 'addPhoto'.tr(),
|
(context, controller, child) => IconButton(
|
||||||
icon: const Icon(Symbols.add_a_photo),
|
onPressed: () {
|
||||||
color: colorScheme.primary,
|
if (controller.isOpen) {
|
||||||
),
|
controller.close();
|
||||||
IconButton(
|
} else {
|
||||||
onPressed: pickVideoMedia,
|
controller.open();
|
||||||
tooltip: 'addVideo'.tr(),
|
}
|
||||||
icon: const Icon(Symbols.videocam),
|
},
|
||||||
color: colorScheme.primary,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: addAudio,
|
|
||||||
tooltip: 'addAudio'.tr(),
|
|
||||||
icon: const Icon(Symbols.mic),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: pickGeneralFile,
|
|
||||||
tooltip: 'uploadFile'.tr(),
|
tooltip: 'uploadFile'.tr(),
|
||||||
icon: const Icon(Symbols.file_upload),
|
icon: const Icon(Symbols.file_upload),
|
||||||
color: colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
pickPhotoMedia();
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Symbols.add_a_photo),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text('addPhoto'.tr()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
pickVideoMedia();
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Symbols.videocam),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text('addVideo'.tr()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
addAudio();
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Symbols.mic),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text('addAudio'.tr()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
pickGeneralFile();
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Symbols.file_upload),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text('uploadFile'.tr()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: linkAttachment,
|
onPressed: linkAttachment,
|
||||||
icon: const Icon(Symbols.attach_file),
|
icon: const Icon(Symbols.attach_file),
|
||||||
|
Reference in New Issue
Block a user