💄 Optimize article compose

This commit is contained in:
2025-07-31 02:32:03 +08:00
parent fd186f8391
commit ba709012d7
4 changed files with 297 additions and 284 deletions

View File

@@ -169,16 +169,27 @@ class ExploreScreen extends HookConsumerWidget {
), ),
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: InkWell(
heroTag: Key("explore-page-fab"), onLongPress: () {
onPressed: () { context.pushNamed('postCompose', queryParameters: {'type': '1'}).then(
context.pushNamed('postCompose').then((value) { (value) {
if (value != null) { if (value != null) {
activitiesNotifier.forceRefresh(); activitiesNotifier.forceRefresh();
} }
}); },
);
}, },
child: const Icon(Symbols.edit), child: FloatingActionButton(
heroTag: Key("explore-page-fab"),
onPressed: () {
context.pushNamed('postCompose').then((value) {
if (value != null) {
activitiesNotifier.forceRefresh();
}
});
},
child: const Icon(Symbols.edit),
),
), ),
floatingActionButtonLocation: TabbedFabLocation(context), floatingActionButtonLocation: TabbedFabLocation(context),
body: Builder( body: Builder(

View File

@@ -18,7 +18,8 @@ import 'package:island/widgets/post/publishers_modal.dart';
import 'package:island/screens/posts/post_detail.dart'; import 'package:island/screens/posts/post_detail.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/services/compose_storage_db.dart';
import 'package:island/widgets/post/draft_manager.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';
@@ -422,85 +423,17 @@ class PostComposeScreen extends HookConsumerWidget {
), ),
// Bottom toolbar // Bottom toolbar
Material( ComposeToolbar(
elevation: 4, ref: ref,
child: Center( context: context,
child: ConstrainedBox( colorScheme: colorScheme,
constraints: BoxConstraints(maxWidth: 560), isEmpty: state.isEmpty,
child: Row( titleController: state.titleController,
children: [ descriptionController: state.descriptionController,
IconButton( contentController: state.contentController,
onPressed: visibility: state.visibility,
() => ComposeLogic.pickPhotoMedia(ref, state), attachments: state.attachments,
tooltip: 'addPhoto'.tr(), originalPost: originalPost,
icon: const Icon(Symbols.add_a_photo),
color: colorScheme.primary,
),
IconButton(
onPressed:
() => ComposeLogic.pickVideoMedia(ref, state),
tooltip: 'addVideo'.tr(),
icon: const Icon(Symbols.videocam),
color: colorScheme.primary,
),
IconButton(
onPressed:
() => ComposeLogic.linkAttachment(
ref,
state,
context,
),
icon: const Icon(Symbols.attach_file),
tooltip: 'linkAttachment'.tr(),
color: colorScheme.primary,
),
Spacer(),
if (originalPost == null && state.isEmpty)
IconButton(
icon: const Icon(Symbols.draft),
color: colorScheme.primary,
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => DraftManagerSheet(
onDraftSelected: (draftId) {
final draft =
ref.read(
composeStorageNotifierProvider,
)[draftId];
if (draft != null) {
state.titleController.text =
draft.title ?? '';
state.descriptionController.text =
draft.description ?? '';
state.contentController.text =
draft.content ?? '';
state.visibility.value =
draft.visibility;
}
},
),
);
},
tooltip: 'drafts'.tr(),
)
else if (originalPost == null)
IconButton(
icon: const Icon(Symbols.save),
color: colorScheme.primary,
onPressed: () => ComposeLogic.saveDraft(ref, state),
tooltip: 'saveDraft'.tr(),
),
],
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
horizontal: 16,
top: 8,
),
),
),
), ),
], ],
), ),

View File

@@ -19,6 +19,7 @@ import 'package:island/widgets/content/markdown.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/services/compose_storage_db.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:island/widgets/post/draft_manager.dart'; import 'package:island/widgets/post/draft_manager.dart';
@@ -233,155 +234,112 @@ class ArticleComposeScreen extends HookConsumerWidget {
} }
Widget buildEditorPane() { Widget buildEditorPane() {
return Column( return Center(
crossAxisAlignment: CrossAxisAlignment.start, child: ConstrainedBox(
children: [ constraints: const BoxConstraints(maxWidth: 560),
// Publisher row child: Column(
Card( crossAxisAlignment: CrossAxisAlignment.start,
margin: EdgeInsets.only(top: 8), children: [
elevation: 1, Expanded(
child: Padding( child: RawKeyboardListener(
padding: const EdgeInsets.all(12), focusNode: FocusNode(),
child: Row( onKey:
children: [ (event) => _handleKeyPress(
GestureDetector( event,
child: ProfilePictureWidget( state,
fileId: state.currentPublisher.value?.picture?.id, ref,
radius: 20, context,
fallbackIcon: originalPost: originalPost,
state.currentPublisher.value == null ),
? Symbols.question_mark child: TextField(
: null, controller: state.contentController,
style: theme.textTheme.bodyMedium,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'postContent'.tr(),
contentPadding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 8,
),
), ),
onTap: () { maxLines: null,
showModalBottomSheet( expands: true,
isScrollControlled: true, textAlignVertical: TextAlignVertical.top,
context: context, onTapOutside:
builder: (context) => const PublisherModal(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
).then((value) {
if (value != null) {
state.currentPublisher.value = value;
}
});
},
), ),
const Gap(16),
if (state.currentPublisher.value == null)
Text(
'postPublisherUnselected'.tr(),
style: theme.textTheme.bodyMedium,
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(state.currentPublisher.value!.nick).bold(),
Text(
'@${state.currentPublisher.value!.name}',
).fontSize(12),
],
),
],
),
),
),
// Content field with keyboard listener
Expanded(
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey:
(event) => _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.all(8),
), ),
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
),
),
// Attachments preview // Attachments preview
ValueListenableBuilder<List<UniversalFile>>( ValueListenableBuilder<List<UniversalFile>>(
valueListenable: state.attachments, valueListenable: state.attachments,
builder: (context, attachments, _) { builder: (context, attachments, _) {
if (attachments.isEmpty) return const SizedBox.shrink(); if (attachments.isEmpty) return const SizedBox.shrink();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Gap(16), const Gap(16),
Text( Text(
'articleAttachmentHint'.tr(), 'articleAttachmentHint'.tr(),
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
).padding(bottom: 8), ).padding(bottom: 8),
ValueListenableBuilder<Map<int, double>>( ValueListenableBuilder<Map<int, double>>(
valueListenable: state.attachmentProgress, valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) { builder: (context, progressMap, _) {
return Wrap( return Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: [ children: [
for (var idx = 0; idx < attachments.length; idx++) for (var idx = 0; idx < attachments.length; idx++)
SizedBox( SizedBox(
width: 280, width: 280,
height: 280, height: 280,
child: AttachmentPreview( child: AttachmentPreview(
item: attachments[idx], item: attachments[idx],
progress: progressMap[idx], progress: progressMap[idx],
onRequestUpload: onRequestUpload:
() => ComposeLogic.uploadAttachment( () => ComposeLogic.uploadAttachment(
ref, ref,
state, state,
idx, idx,
), ),
onDelete: onDelete:
() => ComposeLogic.deleteAttachment( () => ComposeLogic.deleteAttachment(
ref, ref,
state, state,
idx, idx,
), ),
onMove: (delta) { onMove: (delta) {
state state
.attachments .attachments
.value = ComposeLogic.moveAttachment( .value = ComposeLogic.moveAttachment(
state.attachments.value, state.attachments.value,
idx, idx,
delta, delta,
); );
}, },
onInsert: onInsert:
() => ComposeLogic.insertAttachment( () => ComposeLogic.insertAttachment(
ref, ref,
state, state,
idx, idx,
), ),
), ),
), ),
], ],
); );
}, },
), ),
], ],
); );
}, },
),
],
), ),
], ),
); );
} }
@@ -406,38 +364,26 @@ class ArticleComposeScreen extends HookConsumerWidget {
actions: [ actions: [
// Info banner for article compose // Info banner for article compose
const SizedBox.shrink(), const SizedBox.shrink(),
if (originalPost == null) // Only show drafts for new articles
IconButton(
icon: const Icon(Symbols.draft),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => DraftManagerSheet(
onDraftSelected: (draftId) {
final draft =
ref.read(
composeStorageNotifierProvider,
)[draftId];
if (draft != null) {
state.titleController.text = draft.title ?? '';
state.descriptionController.text =
draft.description ?? '';
state.contentController.text =
draft.content ?? '';
state.visibility.value = draft.visibility;
}
},
),
);
},
tooltip: 'drafts'.tr(),
),
IconButton( IconButton(
icon: const Icon(Symbols.save), icon: ProfilePictureWidget(
onPressed: () => ComposeLogic.saveDraft(ref, state), fileId: state.currentPublisher.value?.picture?.id,
tooltip: 'saveDraft'.tr(), radius: 12,
fallbackIcon:
state.currentPublisher.value == null
? Symbols.question_mark
: null,
),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => const PublisherModal(),
).then((value) {
if (value != null) {
state.currentPublisher.value = value;
}
});
},
), ),
IconButton( IconButton(
icon: const Icon(Symbols.settings), icon: const Icon(Symbols.settings),
@@ -500,7 +446,12 @@ class ArticleComposeScreen extends HookConsumerWidget {
child: buildEditorPane(), child: buildEditorPane(),
), ),
if (showPreview.value) if (showPreview.value)
Expanded(child: buildPreviewPane()), Expanded(
child: buildPreviewPane().padding(
vertical: 16,
horizontal: 24,
),
),
], ],
) )
: showPreview.value : showPreview.value
@@ -510,26 +461,17 @@ class ArticleComposeScreen extends HookConsumerWidget {
), ),
// Bottom toolbar // Bottom toolbar
Material( ComposeToolbar(
elevation: 4, ref: ref,
child: Row( context: context,
children: [ colorScheme: colorScheme,
IconButton( isEmpty: state.isEmpty,
onPressed: () => ComposeLogic.pickPhotoMedia(ref, state), titleController: state.titleController,
icon: const Icon(Symbols.add_a_photo), descriptionController: state.descriptionController,
color: colorScheme.primary, contentController: state.contentController,
), visibility: state.visibility,
IconButton( attachments: state.attachments,
onPressed: () => ComposeLogic.pickVideoMedia(ref, state), originalPost: originalPost,
icon: const Icon(Symbols.videocam),
color: colorScheme.primary,
),
],
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
horizontal: 16,
top: 8,
),
), ),
], ],
), ),

View File

@@ -0,0 +1,127 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import '../../models/post.dart';
import '../../models/file.dart';
import '../../services/compose_storage_db.dart';
import '../../widgets/post/draft_manager.dart';
class ComposeToolbar extends StatelessWidget {
final WidgetRef ref;
final BuildContext context;
final ColorScheme colorScheme;
final SnPost? originalPost;
final bool isEmpty;
final TextEditingController titleController;
final TextEditingController descriptionController;
final TextEditingController contentController;
final ValueNotifier<int> visibility;
final ValueNotifier<List<UniversalFile>> attachments;
const ComposeToolbar({
super.key,
required this.ref,
required this.context,
required this.colorScheme,
required this.isEmpty,
required this.titleController,
required this.descriptionController,
required this.contentController,
required this.visibility,
required this.attachments,
this.originalPost,
});
void _pickPhotoMedia() {
// TODO: Implement photo picking logic
}
void _pickVideoMedia() {
// TODO: Implement video picking logic
}
void _linkAttachment() {
// TODO: Implement link attachment logic
}
void _saveDraft() {
// TODO: Implement draft saving logic
}
@override
Widget build(BuildContext context) {
return Material(
elevation: 4,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Row(
children: [
IconButton(
onPressed: _pickPhotoMedia,
tooltip: 'addPhoto'.tr(),
icon: const Icon(Symbols.add_a_photo),
color: colorScheme.primary,
),
IconButton(
onPressed: _pickVideoMedia,
tooltip: 'addVideo'.tr(),
icon: const Icon(Symbols.videocam),
color: colorScheme.primary,
),
IconButton(
onPressed: _linkAttachment,
icon: const Icon(Symbols.attach_file),
tooltip: 'linkAttachment'.tr(),
color: colorScheme.primary,
),
const Spacer(),
if (originalPost == null && isEmpty)
IconButton(
icon: const Icon(Symbols.draft),
color: colorScheme.primary,
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => DraftManagerSheet(
onDraftSelected: (draftId) {
final draft =
ref.read(
composeStorageNotifierProvider,
)[draftId];
if (draft != null) {
titleController.text = draft.title ?? '';
descriptionController.text =
draft.description ?? '';
contentController.text = draft.content ?? '';
visibility.value = draft.visibility;
}
},
),
);
},
tooltip: 'drafts'.tr(),
)
else if (originalPost == null)
IconButton(
icon: const Icon(Symbols.save),
color: colorScheme.primary,
onPressed: _saveDraft,
tooltip: 'saveDraft'.tr(),
),
],
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
horizontal: 16,
top: 8,
),
),
),
);
}
}