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

View File

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

View File

@@ -12,8 +12,14 @@ import 'package:styled_widget/styled_widget.dart';
class ComposeToolbar extends HookConsumerWidget { class ComposeToolbar extends HookConsumerWidget {
final ComposeState state; final ComposeState state;
final SnPost? originalPost; 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -74,6 +80,160 @@ class ComposeToolbar extends HookConsumerWidget {
final colorScheme = Theme.of(context).colorScheme; 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( return Material(
elevation: 4, elevation: 4,
color: Theme.of(context).colorScheme.surfaceContainerLow, color: Theme.of(context).colorScheme.surfaceContainerLow,

View File

@@ -102,72 +102,75 @@ class PostFeaturedList extends HookConsumerWidget {
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Column( child: Column(
children: [ children: [
Row( SizedBox(
spacing: 8, height: 48,
children: [ child: Row(
const Icon(Symbols.highlight), spacing: 8,
const Text('highlightPost').tr(), children: [
Spacer(), const Icon(Symbols.highlight),
IconButton( const Text('highlightPost').tr(),
padding: EdgeInsets.zero, Spacer(),
visualDensity: VisualDensity.compact, IconButton(
constraints: const BoxConstraints(), padding: EdgeInsets.zero,
onPressed: () { visualDensity: VisualDensity.compact,
pageViewController.animateToPage( constraints: const BoxConstraints(),
pageViewCurrent.value - 1, onPressed: () {
duration: const Duration(milliseconds: 250), pageViewController.animateToPage(
curve: Curves.easeInOut, 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,
); );
debugPrint( },
'PostFeaturedList: Stored collapsed ID: ${featuredPostsAsync.value!.first.id}', icon: const Icon(Symbols.arrow_left),
);
} else {
prefs.remove(kFeaturedPostsCollapsedId);
debugPrint(
'PostFeaturedList: Removed stored collapsed ID.',
);
}
},
icon: Icon(
isCollapsed.value
? Symbols.expand_more
: Symbols.expand_less,
), ),
), IconButton(
], padding: EdgeInsets.zero,
).padding(horizontal: 16, vertical: 8), 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( AnimatedSize(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, 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/post/embed_view_renderer.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart'; import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:island/widgets/share/share_sheet.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:material_symbols_icons/symbols.dart';
import 'package:path_provider/path_provider.dart' show getTemporaryDirectory; import 'package:path_provider/path_provider.dart' show getTemporaryDirectory;
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
@@ -174,14 +175,14 @@ class PostActionableItem extends HookConsumerWidget {
MenuAction( MenuAction(
title: 'edit'.tr(), title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit), image: MenuImage.icon(Symbols.edit),
callback: () { callback: () async {
context final result = await PostComposeDialog.show(
.pushNamed('postEdit', pathParameters: {'id': item.id}) context,
.then((value) { originalPost: item,
if (value != null) { );
onRefresh?.call(); if (result != null) {
} onRefresh?.call();
}); }
}, },
), ),
if (isAuthor) if (isAuthor)
@@ -221,21 +222,27 @@ class PostActionableItem extends HookConsumerWidget {
MenuAction( MenuAction(
title: 'reply'.tr(), title: 'reply'.tr(),
image: MenuImage.icon(Symbols.reply), image: MenuImage.icon(Symbols.reply),
callback: () { callback: () async {
context.pushNamed( final result = await PostComposeDialog.show(
'postCompose', context,
extra: PostComposeInitialState(replyingTo: item), initialState: PostComposeInitialState(replyingTo: item),
); );
if (result != null) {
onRefresh?.call();
}
}, },
), ),
MenuAction( MenuAction(
title: 'forward'.tr(), title: 'forward'.tr(),
image: MenuImage.icon(Symbols.forward), image: MenuImage.icon(Symbols.forward),
callback: () { callback: () async {
context.pushNamed( final result = await PostComposeDialog.show(
'postCompose', context,
extra: PostComposeInitialState(forwardingTo: item), initialState: PostComposeInitialState(forwardingTo: item),
); );
if (result != null) {
onRefresh?.call();
}
}, },
), ),
if (isAuthor && item.pinMode == null) if (isAuthor && item.pinMode == null)