♻️ Dialog based editor for normal post

This commit is contained in:
2025-09-29 01:16:32 +08:00
parent 56fb5451cd
commit de9e235d0c
5 changed files with 290 additions and 124 deletions

View File

@@ -20,6 +20,7 @@ import 'package:island/widgets/check_in.dart';
import 'package:island/widgets/post/post_featured.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/compose_card.dart';
import 'package:island/widgets/post/compose_dialog.dart';
import 'package:island/screens/tabs.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -43,6 +44,7 @@ Widget notificationIndicatorWidget(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
minTileHeight: 48,
leading: const Icon(Symbols.notifications),
title: Row(
children: [
@@ -109,7 +111,7 @@ class ExploreScreen extends HookConsumerWidget {
final isWide = isWideScreen(context);
final filterBar = Card(
margin: EdgeInsets.zero,
margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: Row(
children: [
Expanded(
@@ -122,28 +124,19 @@ class ExploreScreen extends HookConsumerWidget {
Tab(
icon: Tooltip(
message: 'explore'.tr(),
child: Icon(
Symbols.explore,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
child: Icon(Symbols.explore),
),
),
Tab(
icon: Tooltip(
message: 'exploreFilterSubscriptions'.tr(),
child: Icon(
Symbols.subscriptions,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
child: Icon(Symbols.subscriptions),
),
),
Tab(
icon: Tooltip(
message: 'exploreFilterFriends'.tr(),
child: Icon(
Symbols.people,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
child: Icon(Symbols.people),
),
),
],
@@ -153,10 +146,7 @@ class ExploreScreen extends HookConsumerWidget {
onPressed: () {
context.pushNamed('articles');
},
icon: Icon(
Symbols.auto_stories,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
icon: Icon(Symbols.auto_stories),
tooltip: 'webArticlesStand'.tr(),
),
PopupMenuButton(
@@ -211,10 +201,7 @@ class ExploreScreen extends HookConsumerWidget {
},
),
],
icon: Icon(
Symbols.action_key,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
icon: Icon(Symbols.action_key),
tooltip: 'search'.tr(),
),
],
@@ -227,23 +214,19 @@ class ExploreScreen extends HookConsumerWidget {
isWide
? null
: InkWell(
onLongPress: () {
context
.pushNamed('postCompose', queryParameters: {'type': '1'})
.then((value) {
if (value != null) {
activitiesNotifier.forceRefresh();
}
});
onLongPress: () async {
final result = await PostComposeDialog.show(context);
if (result != null) {
activitiesNotifier.forceRefresh();
}
},
child: FloatingActionButton(
heroTag: Key("explore-page-fab"),
onPressed: () {
context.pushNamed('postCompose').then((value) {
if (value != null) {
activitiesNotifier.forceRefresh();
}
});
onPressed: () async {
final result = await PostComposeDialog.show(context);
if (result != null) {
activitiesNotifier.forceRefresh();
}
},
child: const Icon(Symbols.edit),
),

View File

@@ -155,6 +155,7 @@ class PostComposeCard extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => ComposeSettingsSheet(state: state),
);
}
@@ -278,6 +279,7 @@ class PostComposeCard extends HookConsumerWidget {
final config = await showModalBottomSheet<AttachmentUploadConfig>(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder:
(context) => AttachmentUploaderSheet(
ref: ref,
@@ -325,6 +327,7 @@ class PostComposeCard extends HookConsumerWidget {
await showModalBottomSheet<AttachmentUploadConfig>(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder:
(context) => AttachmentUploaderSheet(
ref: ref,
@@ -370,6 +373,7 @@ class PostComposeCard extends HookConsumerWidget {
children: [
// Header with actions
Container(
height: 65,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(
@@ -393,7 +397,7 @@ class PostComposeCard extends HookConsumerWidget {
tooltip: 'postSettings'.tr(),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
vertical: -2,
),
),
IconButton(
@@ -418,7 +422,7 @@ class PostComposeCard extends HookConsumerWidget {
: 'postPublish'.tr(),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
vertical: -2,
),
),
if (onCancel != null)
@@ -428,7 +432,7 @@ class PostComposeCard extends HookConsumerWidget {
tooltip: 'cancel'.tr(),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
vertical: -2,
),
),
],
@@ -473,6 +477,7 @@ class PostComposeCard extends HookConsumerWidget {
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder: (context) => const PublisherModal(),
).then((value) {
@@ -570,12 +575,19 @@ class PostComposeCard extends HookConsumerWidget {
),
// Bottom toolbar
ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
SizedBox(
height: 65,
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
child: ComposeToolbar(
state: state,
originalPost: originalPost,
isCompact: true,
),
),
child: ComposeToolbar(state: state, originalPost: originalPost),
),
],
),
@@ -721,6 +733,7 @@ class PostComposeCard extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
backgroundColor: Colors.transparent,
builder:
(context) => DraggableScrollableSheet(

View File

@@ -12,8 +12,14 @@ import 'package:styled_widget/styled_widget.dart';
class ComposeToolbar extends HookConsumerWidget {
final ComposeState state;
final SnPost? originalPost;
final bool isCompact;
const ComposeToolbar({super.key, required this.state, this.originalPost});
const ComposeToolbar({
super.key,
required this.state,
this.originalPost,
this.isCompact = false,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -74,6 +80,160 @@ class ComposeToolbar extends HookConsumerWidget {
final colorScheme = Theme.of(context).colorScheme;
if (isCompact) {
return Container(
color: Theme.of(context).colorScheme.surfaceContainerLow,
padding: EdgeInsets.symmetric(horizontal: 8),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
IconButton(
onPressed: pickPhotoMedia,
tooltip: 'addPhoto'.tr(),
icon: const Icon(Symbols.add_a_photo),
color: colorScheme.primary,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -2,
),
),
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(),
icon: const Icon(Symbols.file_upload),
color: colorScheme.primary,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -2,
),
),
IconButton(
onPressed: linkAttachment,
icon: const Icon(Symbols.attach_file),
tooltip: 'linkAttachment'.tr(),
color: colorScheme.primary,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -2,
),
),
// Poll button with visual state when a poll is linked
ListenableBuilder(
listenable: state.pollId,
builder: (context, _) {
return IconButton(
onPressed: pickPoll,
icon: const Icon(Symbols.how_to_vote),
tooltip: 'poll'.tr(),
color: colorScheme.primary,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -2,
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
state.pollId.value != null
? colorScheme.primary.withOpacity(0.15)
: null,
),
),
);
},
),
// Embed button with visual state when embed is present
ListenableBuilder(
listenable: state.embedView,
builder: (context, _) {
return IconButton(
onPressed: showEmbedSheet,
icon: const Icon(Symbols.iframe),
tooltip: 'embedView'.tr(),
color: colorScheme.primary,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -2,
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
state.embedView.value != null
? colorScheme.primary.withOpacity(0.15)
: null,
),
),
);
},
),
],
),
),
),
if (originalPost == null && state.isEmpty)
IconButton(
icon: const Icon(Symbols.draft, size: 20),
color: colorScheme.primary,
onPressed: showDraftManager,
tooltip: 'drafts'.tr(),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
)
else if (originalPost == null)
IconButton(
icon: const Icon(Symbols.save, size: 20),
color: colorScheme.primary,
onPressed: saveDraft,
onLongPress: showDraftManager,
tooltip: 'saveDraft'.tr(),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
],
).padding(horizontal: 8, vertical: 4),
),
),
);
}
return Material(
elevation: 4,
color: Theme.of(context).colorScheme.surfaceContainerLow,

View File

@@ -102,72 +102,75 @@ class PostFeaturedList extends HookConsumerWidget {
margin: EdgeInsets.zero,
child: Column(
children: [
Row(
spacing: 8,
children: [
const Icon(Symbols.highlight),
const Text('highlightPost').tr(),
Spacer(),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
onPressed: () {
pageViewController.animateToPage(
pageViewCurrent.value - 1,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
},
icon: const Icon(Symbols.arrow_left),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
onPressed: () {
pageViewController.animateToPage(
pageViewCurrent.value + 1,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
},
icon: const Icon(Symbols.arrow_right),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
onPressed: () {
isCollapsed.value = !isCollapsed.value;
debugPrint(
'PostFeaturedList: Manual toggle. isCollapsed set to ${isCollapsed.value}',
);
if (isCollapsed.value &&
featuredPostsAsync.hasValue &&
featuredPostsAsync.value!.isNotEmpty) {
prefs.setString(
kFeaturedPostsCollapsedId,
featuredPostsAsync.value!.first.id,
SizedBox(
height: 48,
child: Row(
spacing: 8,
children: [
const Icon(Symbols.highlight),
const Text('highlightPost').tr(),
Spacer(),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
onPressed: () {
pageViewController.animateToPage(
pageViewCurrent.value - 1,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
debugPrint(
'PostFeaturedList: Stored collapsed ID: ${featuredPostsAsync.value!.first.id}',
);
} else {
prefs.remove(kFeaturedPostsCollapsedId);
debugPrint(
'PostFeaturedList: Removed stored collapsed ID.',
);
}
},
icon: Icon(
isCollapsed.value
? Symbols.expand_more
: Symbols.expand_less,
},
icon: const Icon(Symbols.arrow_left),
),
),
],
).padding(horizontal: 16, vertical: 8),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
onPressed: () {
pageViewController.animateToPage(
pageViewCurrent.value + 1,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
},
icon: const Icon(Symbols.arrow_right),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
onPressed: () {
isCollapsed.value = !isCollapsed.value;
debugPrint(
'PostFeaturedList: Manual toggle. isCollapsed set to ${isCollapsed.value}',
);
if (isCollapsed.value &&
featuredPostsAsync.hasValue &&
featuredPostsAsync.value!.isNotEmpty) {
prefs.setString(
kFeaturedPostsCollapsedId,
featuredPostsAsync.value!.first.id,
);
debugPrint(
'PostFeaturedList: Stored collapsed ID: ${featuredPostsAsync.value!.first.id}',
);
} else {
prefs.remove(kFeaturedPostsCollapsedId);
debugPrint(
'PostFeaturedList: Removed stored collapsed ID.',
);
}
},
icon: Icon(
isCollapsed.value
? Symbols.expand_more
: Symbols.expand_less,
),
),
],
).padding(horizontal: 16, vertical: 8),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,

View File

@@ -24,6 +24,7 @@ import 'package:island/widgets/post/post_shared.dart';
import 'package:island/widgets/post/embed_view_renderer.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:island/widgets/share/share_sheet.dart';
import 'package:island/widgets/post/compose_dialog.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:path_provider/path_provider.dart' show getTemporaryDirectory;
import 'package:screenshot/screenshot.dart';
@@ -174,14 +175,14 @@ class PostActionableItem extends HookConsumerWidget {
MenuAction(
title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit),
callback: () {
context
.pushNamed('postEdit', pathParameters: {'id': item.id})
.then((value) {
if (value != null) {
onRefresh?.call();
}
});
callback: () async {
final result = await PostComposeDialog.show(
context,
originalPost: item,
);
if (result != null) {
onRefresh?.call();
}
},
),
if (isAuthor)
@@ -221,21 +222,27 @@ class PostActionableItem extends HookConsumerWidget {
MenuAction(
title: 'reply'.tr(),
image: MenuImage.icon(Symbols.reply),
callback: () {
context.pushNamed(
'postCompose',
extra: PostComposeInitialState(replyingTo: item),
callback: () async {
final result = await PostComposeDialog.show(
context,
initialState: PostComposeInitialState(replyingTo: item),
);
if (result != null) {
onRefresh?.call();
}
},
),
MenuAction(
title: 'forward'.tr(),
image: MenuImage.icon(Symbols.forward),
callback: () {
context.pushNamed(
'postCompose',
extra: PostComposeInitialState(forwardingTo: item),
callback: () async {
final result = await PostComposeDialog.show(
context,
initialState: PostComposeInitialState(forwardingTo: item),
);
if (result != null) {
onRefresh?.call();
}
},
),
if (isAuthor && item.pinMode == null)