🐛 Fix bugs
This commit is contained in:
@@ -1306,5 +1306,6 @@
|
|||||||
"activities": "Activities",
|
"activities": "Activities",
|
||||||
"presenceTypeGaming": "Playing",
|
"presenceTypeGaming": "Playing",
|
||||||
"presenceTypeMusic": "Listening to Music",
|
"presenceTypeMusic": "Listening to Music",
|
||||||
"presenceTypeWorkout": "Working out"
|
"presenceTypeWorkout": "Working out",
|
||||||
|
"articleCompose": "Compose Article"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import 'package:island/screens/creators/publishers_form.dart';
|
|||||||
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
||||||
import 'package:island/screens/poll/poll_editor.dart';
|
import 'package:island/screens/poll/poll_editor.dart';
|
||||||
import 'package:island/screens/posts/compose.dart';
|
import 'package:island/screens/posts/compose.dart';
|
||||||
|
import 'package:island/screens/posts/compose_article.dart';
|
||||||
import 'package:island/screens/posts/post_detail.dart';
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
import 'package:island/screens/posts/publisher_profile.dart';
|
import 'package:island/screens/posts/publisher_profile.dart';
|
||||||
import 'package:island/screens/auth/login.dart';
|
import 'package:island/screens/auth/login.dart';
|
||||||
@@ -106,11 +107,19 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
routes: [
|
routes: [
|
||||||
// Standalone routes without bottom navigation
|
// Standalone routes without bottom navigation
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'postEdit',
|
name: 'articleCompose',
|
||||||
path: '/posts/:id/edit',
|
path: '/articles/compose',
|
||||||
|
builder:
|
||||||
|
(context, state) => ArticleComposeScreen(
|
||||||
|
initialState: state.extra as PostComposeInitialState?,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'articleEdit',
|
||||||
|
path: '/articles/:id/edit',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final id = state.pathParameters['id']!;
|
final id = state.pathParameters['id']!;
|
||||||
return PostEditScreen(id: id);
|
return ArticleEditScreen(id: id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|||||||
@@ -177,8 +177,14 @@ class PublisherSelector extends StatelessWidget {
|
|||||||
iconStyleData: IconStyleData(
|
iconStyleData: IconStyleData(
|
||||||
icon: Icon(Icons.arrow_drop_down),
|
icon: Icon(Icons.arrow_drop_down),
|
||||||
iconSize: 19,
|
iconSize: 19,
|
||||||
iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor!,
|
iconEnabledColor:
|
||||||
iconDisabledColor: Theme.of(context).appBarTheme.foregroundColor!,
|
isWideScreen(context)
|
||||||
|
? null
|
||||||
|
: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
iconDisabledColor:
|
||||||
|
isWideScreen(context)
|
||||||
|
? null
|
||||||
|
: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -561,6 +567,7 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
? Column(
|
? Column(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox.shrink(),
|
||||||
PublisherSelector(
|
PublisherSelector(
|
||||||
currentPublisher: currentPublisher.value,
|
currentPublisher: currentPublisher.value,
|
||||||
publishersMenu: publishersMenu,
|
publishersMenu: publishersMenu,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:island/services/responsive.dart';
|
|||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
import 'package:island/widgets/check_in.dart';
|
import 'package:island/widgets/check_in.dart';
|
||||||
|
import 'package:island/widgets/navigation/fab_menu.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';
|
||||||
@@ -72,6 +73,22 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
final tabController = useTabController(initialLength: 3);
|
final tabController = useTabController(initialLength: 3);
|
||||||
final currentFilter = useState<String?>(null);
|
final currentFilter = useState<String?>(null);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
// Set FAB type to chat
|
||||||
|
final fabMenuNotifier = ref.read(fabMenuTypeProvider.notifier);
|
||||||
|
Future(() {
|
||||||
|
fabMenuNotifier.state = FabMenuType.compose;
|
||||||
|
});
|
||||||
|
return () {
|
||||||
|
// Clean up: reset FAB type to main
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (fabMenuNotifier.state == FabMenuType.compose) {
|
||||||
|
fabMenuNotifier.state = FabMenuType.main;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
void listener() {
|
void listener() {
|
||||||
switch (tabController.index) {
|
switch (tabController.index) {
|
||||||
@@ -703,7 +720,9 @@ class ActivityListNotifier extends _$ActivityListNotifier
|
|||||||
fetch(cursor: null);
|
fetch(cursor: null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<SnTimelineEvent>> fetch({required String? cursor}) async {
|
Future<CursorPagingData<SnTimelineEvent>> fetch({
|
||||||
|
required String? cursor,
|
||||||
|
}) async {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
final take = 20;
|
final take = 20;
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,6 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:gap/gap.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/posts/compose_article.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/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_toolbar.dart';
|
|
||||||
import 'package:island/widgets/post/post_item.dart';
|
|
||||||
import 'package:island/widgets/post/publishers_modal.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
|
|
||||||
part 'compose.freezed.dart';
|
part 'compose.freezed.dart';
|
||||||
part 'compose.g.dart';
|
part 'compose.g.dart';
|
||||||
@@ -41,358 +20,3 @@ sealed class PostComposeInitialState with _$PostComposeInitialState {
|
|||||||
factory PostComposeInitialState.fromJson(Map<String, dynamic> json) =>
|
factory PostComposeInitialState.fromJson(Map<String, dynamic> json) =>
|
||||||
_$PostComposeInitialStateFromJson(json);
|
_$PostComposeInitialStateFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
class PostEditScreen extends HookConsumerWidget {
|
|
||||||
final String id;
|
|
||||||
const PostEditScreen({super.key, required this.id});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final post = ref.watch(postProvider(id));
|
|
||||||
return post.when(
|
|
||||||
data: (post) => PostComposeScreen(originalPost: post),
|
|
||||||
loading:
|
|
||||||
() => AppScaffold(
|
|
||||||
isNoBackground: false,
|
|
||||||
appBar: AppBar(leading: const PageBackButton()),
|
|
||||||
body: const Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
|
||||||
error:
|
|
||||||
(e, _) => AppScaffold(
|
|
||||||
isNoBackground: false,
|
|
||||||
appBar: AppBar(leading: const PageBackButton()),
|
|
||||||
body: Text('Error: $e', textAlign: TextAlign.center),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PostComposeScreen extends HookConsumerWidget {
|
|
||||||
final SnPost? originalPost;
|
|
||||||
final int? type;
|
|
||||||
final PostComposeInitialState? initialState;
|
|
||||||
const PostComposeScreen({
|
|
||||||
super.key,
|
|
||||||
this.type,
|
|
||||||
this.initialState,
|
|
||||||
this.originalPost,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
// Determine the compose type: auto-detect from edited post or use query parameter
|
|
||||||
final composeType = originalPost?.type ?? type ?? 0;
|
|
||||||
final repliedPost = initialState?.replyingTo ?? originalPost?.repliedPost;
|
|
||||||
final forwardedPost =
|
|
||||||
initialState?.forwardingTo ?? originalPost?.forwardedPost;
|
|
||||||
|
|
||||||
// If type is 1 (article), return ArticleComposeScreen
|
|
||||||
if (composeType == 1) {
|
|
||||||
return ArticleComposeScreen(originalPost: originalPost);
|
|
||||||
}
|
|
||||||
|
|
||||||
// When editing, preserve the original replied/forwarded post references
|
|
||||||
final effectiveRepliedPost = repliedPost ?? originalPost?.repliedPost;
|
|
||||||
final effectiveForwardedPost = forwardedPost ?? originalPost?.forwardedPost;
|
|
||||||
|
|
||||||
final publishers = ref.watch(publishersManagedProvider);
|
|
||||||
final state = useMemoized(
|
|
||||||
() => ComposeLogic.createState(
|
|
||||||
originalPost: originalPost,
|
|
||||||
forwardedPost: effectiveForwardedPost,
|
|
||||||
repliedPost: effectiveRepliedPost,
|
|
||||||
postType: 0, // Regular post type
|
|
||||||
),
|
|
||||||
[originalPost, effectiveForwardedPost, effectiveRepliedPost],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add a listener to the entire state to trigger rebuilds
|
|
||||||
final stateNotifier = useMemoized(
|
|
||||||
() => Listenable.merge([
|
|
||||||
state.titleController,
|
|
||||||
state.descriptionController,
|
|
||||||
state.contentController,
|
|
||||||
state.visibility,
|
|
||||||
state.attachments,
|
|
||||||
state.attachmentProgress,
|
|
||||||
state.currentPublisher,
|
|
||||||
state.submitting,
|
|
||||||
]),
|
|
||||||
[state],
|
|
||||||
);
|
|
||||||
useListenable(stateNotifier);
|
|
||||||
|
|
||||||
// Start auto-save when component mounts
|
|
||||||
useEffect(() {
|
|
||||||
if (originalPost == null) {
|
|
||||||
// Only auto-save for new posts, not edits
|
|
||||||
state.startAutoSave(ref);
|
|
||||||
}
|
|
||||||
return () => state.stopAutoSave();
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
// Initialize publisher once when data is available
|
|
||||||
useEffect(() {
|
|
||||||
if (publishers.value?.isNotEmpty ?? false) {
|
|
||||||
if (state.currentPublisher.value == null) {
|
|
||||||
// If no publisher is set, use the first available one
|
|
||||||
state.currentPublisher.value = publishers.value!.first;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [publishers]);
|
|
||||||
|
|
||||||
// Load initial state if provided (for sharing functionality)
|
|
||||||
useEffect(() {
|
|
||||||
if (initialState != null) {
|
|
||||||
state.titleController.text = initialState!.title ?? '';
|
|
||||||
state.descriptionController.text = initialState!.description ?? '';
|
|
||||||
state.contentController.text = initialState!.content ?? '';
|
|
||||||
if (initialState!.visibility != null) {
|
|
||||||
state.visibility.value = initialState!.visibility!;
|
|
||||||
}
|
|
||||||
if (initialState!.attachments.isNotEmpty) {
|
|
||||||
state.attachments.value = List.from(initialState!.attachments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [initialState]);
|
|
||||||
|
|
||||||
// Load draft if available (only for new posts without initial state)
|
|
||||||
useEffect(() {
|
|
||||||
if (originalPost == null &&
|
|
||||||
effectiveForwardedPost == null &&
|
|
||||||
effectiveRepliedPost == null &&
|
|
||||||
initialState == null) {
|
|
||||||
// Try to load the most recent draft
|
|
||||||
final drafts = ref.read(composeStorageNotifierProvider);
|
|
||||||
if (drafts.isNotEmpty) {
|
|
||||||
final mostRecentDraft = drafts.values.reduce(
|
|
||||||
(a, b) =>
|
|
||||||
(a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0))
|
|
||||||
? a
|
|
||||||
: b,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only load if the draft has meaningful content
|
|
||||||
if (mostRecentDraft.content?.isNotEmpty == true ||
|
|
||||||
mostRecentDraft.title?.isNotEmpty == true) {
|
|
||||||
state.titleController.text = mostRecentDraft.title ?? '';
|
|
||||||
state.descriptionController.text =
|
|
||||||
mostRecentDraft.description ?? '';
|
|
||||||
state.contentController.text = mostRecentDraft.content ?? '';
|
|
||||||
state.visibility.value = mostRecentDraft.visibility;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Dispose state when widget is disposed
|
|
||||||
useEffect(() {
|
|
||||||
return () {
|
|
||||||
state.stopAutoSave();
|
|
||||||
ComposeLogic.dispose(state);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
|
|
||||||
void showSettingsSheet() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder: (context) => ComposeSettingsSheet(state: state),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PopScope(
|
|
||||||
onPopInvoked: (_) {
|
|
||||||
if (originalPost == null) {
|
|
||||||
ComposeLogic.saveDraft(ref, state);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: AppScaffold(
|
|
||||||
isNoBackground: false,
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: const PageBackButton(),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.settings),
|
|
||||||
onPressed: showSettingsSheet,
|
|
||||||
tooltip: 'postSettings'.tr(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed:
|
|
||||||
state.submitting.value
|
|
||||||
? null
|
|
||||||
: () => ComposeLogic.performAction(
|
|
||||||
ref,
|
|
||||||
state,
|
|
||||||
context,
|
|
||||||
originalPost: originalPost,
|
|
||||||
repliedPost: repliedPost,
|
|
||||||
forwardedPost: forwardedPost,
|
|
||||||
),
|
|
||||||
icon:
|
|
||||||
state.submitting.value
|
|
||||||
? SizedBox(
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
child: const CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 2.5,
|
|
||||||
),
|
|
||||||
).center()
|
|
||||||
: Icon(
|
|
||||||
originalPost != null ? Symbols.edit : Symbols.upload,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Reply/Forward info section
|
|
||||||
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
|
|
||||||
Expanded(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
|
||||||
child: Row(
|
|
||||||
spacing: 12,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Publisher profile picture
|
|
||||||
GestureDetector(
|
|
||||||
child: ProfilePictureWidget(
|
|
||||||
fileId: state.currentPublisher.value?.picture?.id,
|
|
||||||
radius: 20,
|
|
||||||
fallbackIcon:
|
|
||||||
state.currentPublisher.value == null
|
|
||||||
? Symbols.question_mark
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
isScrollControlled: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => const PublisherModal(),
|
|
||||||
).then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
state.currentPublisher.value = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
).padding(top: 16),
|
|
||||||
|
|
||||||
// Post content form
|
|
||||||
Expanded(
|
|
||||||
child: KeyboardListener(
|
|
||||||
focusNode: FocusNode(),
|
|
||||||
onKeyEvent:
|
|
||||||
(event) => ComposeLogic.handleKeyPress(
|
|
||||||
event,
|
|
||||||
state,
|
|
||||||
ref,
|
|
||||||
context,
|
|
||||||
originalPost: originalPost,
|
|
||||||
repliedPost: repliedPost,
|
|
||||||
forwardedPost: forwardedPost,
|
|
||||||
),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
ComposeFormFields(
|
|
||||||
state: state,
|
|
||||||
showPublisherAvatar: false,
|
|
||||||
onPublisherTap: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
isScrollControlled: true,
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => const PublisherModal(),
|
|
||||||
).then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
state.currentPublisher.value = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
ComposeAttachments(
|
|
||||||
state: state,
|
|
||||||
isCompact: false,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 16),
|
|
||||||
).alignment(Alignment.topCenter),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Bottom toolbar
|
|
||||||
ComposeToolbar(state: state, originalPost: originalPost),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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/compose.dart';
|
||||||
import 'package:island/screens/posts/post_detail.dart';
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
import 'package:island/services/compose_storage_db.dart';
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
@@ -49,8 +50,9 @@ class ArticleEditScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
class ArticleComposeScreen extends HookConsumerWidget {
|
class ArticleComposeScreen extends HookConsumerWidget {
|
||||||
final SnPost? originalPost;
|
final SnPost? originalPost;
|
||||||
|
final PostComposeInitialState? initialState;
|
||||||
|
|
||||||
const ArticleComposeScreen({super.key, this.originalPost});
|
const ArticleComposeScreen({super.key, this.originalPost, this.initialState});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -100,9 +102,25 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [publishers]);
|
}, [publishers]);
|
||||||
|
|
||||||
|
// Load initial state if provided (for sharing functionality)
|
||||||
|
useEffect(() {
|
||||||
|
if (initialState != null) {
|
||||||
|
state.titleController.text = initialState!.title ?? '';
|
||||||
|
state.descriptionController.text = initialState!.description ?? '';
|
||||||
|
state.contentController.text = initialState!.content ?? '';
|
||||||
|
if (initialState!.visibility != null) {
|
||||||
|
state.visibility.value = initialState!.visibility!;
|
||||||
|
}
|
||||||
|
if (initialState!.attachments.isNotEmpty) {
|
||||||
|
state.attachments.value = List.from(initialState!.attachments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [initialState]);
|
||||||
|
|
||||||
// Load draft if available (only for new articles)
|
// Load draft if available (only for new articles)
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (originalPost == null) {
|
if (originalPost == null && initialState == null) {
|
||||||
// Try to load the most recent article draft
|
// Try to load the most recent article draft
|
||||||
final drafts = ref.read(composeStorageNotifierProvider);
|
final drafts = ref.read(composeStorageNotifierProvider);
|
||||||
if (drafts.isNotEmpty) {
|
if (drafts.isNotEmpty) {
|
||||||
@@ -199,6 +217,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
border: Border.all(color: colorScheme.outline.withOpacity(0.3)),
|
border: Border.all(color: colorScheme.outline.withOpacity(0.3)),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -219,7 +238,12 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(child: widgetItem),
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: widgetItem,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -246,7 +270,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
).padding(top: 16),
|
||||||
|
|
||||||
// Attachments preview
|
// Attachments preview
|
||||||
ValueListenableBuilder<List<UniversalFile>>(
|
ValueListenableBuilder<List<UniversalFile>>(
|
||||||
|
|||||||
@@ -108,13 +108,21 @@ class PostActionButtons extends HookConsumerWidget {
|
|||||||
final editButtons = <Widget>[
|
final editButtons = <Widget>[
|
||||||
FilledButton.tonal(
|
FilledButton.tonal(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushNamed('postEdit', pathParameters: {'id': post.id}).then(
|
if (post.type == 1) {
|
||||||
(value) {
|
context
|
||||||
if (value != null) {
|
.pushNamed('articleEdit', pathParameters: {'id': post.id})
|
||||||
|
.then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
onRefresh?.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
PostComposeSheet.show(context, originalPost: post).then((value) {
|
||||||
|
if (value == true) {
|
||||||
onRefresh?.call();
|
onRefresh?.call();
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
},
|
},
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
|
|||||||
@@ -51,33 +51,33 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
final destinations = [
|
final destinations = [
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'explore'.tr(),
|
label: 'explore'.tr(),
|
||||||
icon: const Icon(Symbols.explore),
|
icon: const Icon(Symbols.explore_rounded),
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'chat'.tr(),
|
label: 'chat'.tr(),
|
||||||
icon: const Icon(Symbols.chat_rounded),
|
icon: const Icon(Symbols.forum_rounded),
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'realms'.tr(),
|
label: 'realms'.tr(),
|
||||||
icon: const Icon(Symbols.group),
|
icon: const Icon(Symbols.group_rounded),
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'account'.tr(),
|
label: 'account'.tr(),
|
||||||
icon: Badge.count(
|
icon: Badge.count(
|
||||||
count: notificationUnreadCount.value ?? 0,
|
count: notificationUnreadCount.value ?? 0,
|
||||||
isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
|
isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
|
||||||
child: const Icon(Symbols.account_circle),
|
child: const Icon(Symbols.person_rounded),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (wideScreen)
|
if (wideScreen)
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'creatorHub'.tr(),
|
label: 'creatorHub'.tr(),
|
||||||
icon: const Icon(Symbols.ink_pen),
|
icon: const Icon(Symbols.design_services_rounded),
|
||||||
),
|
),
|
||||||
if (wideScreen)
|
if (wideScreen)
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'developerHub'.tr(),
|
label: 'developerHub'.tr(),
|
||||||
icon: const Icon(Symbols.data_object),
|
icon: const Icon(Symbols.data_object_rounded),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import 'package:island/widgets/alert.dart';
|
|||||||
import 'package:island/widgets/post/compose_sheet.dart';
|
import 'package:island/widgets/post/compose_sheet.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
enum FabMenuType { main, chat, realm }
|
enum FabMenuType { main, compose, chat, realm }
|
||||||
|
|
||||||
/// Global state provider for FAB menu type
|
/// Global state provider for FAB menu type
|
||||||
final fabMenuTypeProvider = StateProvider<FabMenuType>(
|
final fabMenuTypeProvider = StateProvider<FabMenuType>(
|
||||||
@@ -69,7 +69,7 @@ class FabMenu extends HookConsumerWidget {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Symbols.notifications),
|
leading: const Icon(Symbols.notifications),
|
||||||
trailing: Badge(
|
trailing: Badge(
|
||||||
label: Text(notificationCount.toString()),
|
label: Text(notificationCount.value.toString()),
|
||||||
isLabelVisible: notificationCount.value! > 0,
|
isLabelVisible: notificationCount.value! > 0,
|
||||||
),
|
),
|
||||||
title: Text('notifications').tr(),
|
title: Text('notifications').tr(),
|
||||||
@@ -88,6 +88,38 @@ class FabMenu extends HookConsumerWidget {
|
|||||||
];
|
];
|
||||||
|
|
||||||
switch (fabType) {
|
switch (fabType) {
|
||||||
|
case FabMenuType.compose:
|
||||||
|
icon = Symbols.create;
|
||||||
|
useRootNavigator = false;
|
||||||
|
menuContent = Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Gap(24),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.post_add_rounded),
|
||||||
|
title: Text('postCompose').tr(),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
await PostComposeSheet.show(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.article),
|
||||||
|
title: Text('articleCompose').tr(),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
GoRouter.of(context).pushNamed('articleCompose');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
...commonEntires,
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
case FabMenuType.chat:
|
case FabMenuType.chat:
|
||||||
icon = Symbols.chat_add_on;
|
icon = Symbols.chat_add_on;
|
||||||
useRootNavigator = true;
|
useRootNavigator = true;
|
||||||
@@ -160,16 +192,6 @@ class FabMenu extends HookConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Gap(24),
|
const Gap(24),
|
||||||
ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.post_add_rounded),
|
|
||||||
title: Text('postCompose').tr(),
|
|
||||||
onTap: () async {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
await PostComposeSheet.show(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
...commonEntires,
|
...commonEntires,
|
||||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:island/services/time.dart';
|
|||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/post/post_item.dart';
|
import 'package:island/widgets/post/post_item.dart';
|
||||||
import 'package:island/widgets/post/post_shared.dart';
|
import 'package:island/widgets/post/post_shared.dart';
|
||||||
|
import 'package:island/widgets/post/compose_sheet.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';
|
||||||
import 'package:super_context_menu/super_context_menu.dart';
|
import 'package:super_context_menu/super_context_menu.dart';
|
||||||
@@ -45,13 +46,23 @@ class PostItemCreator extends HookConsumerWidget {
|
|||||||
title: 'edit'.tr(),
|
title: 'edit'.tr(),
|
||||||
image: MenuImage.icon(Symbols.edit),
|
image: MenuImage.icon(Symbols.edit),
|
||||||
callback: () {
|
callback: () {
|
||||||
context
|
if (item.type == 1) {
|
||||||
.pushNamed('postEdit', pathParameters: {'id': item.id})
|
context
|
||||||
.then((value) {
|
.pushNamed('articleEdit', pathParameters: {'id': item.id})
|
||||||
if (value != null) {
|
.then((value) {
|
||||||
onRefresh?.call();
|
if (value != null) {
|
||||||
}
|
onRefresh?.call();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
PostComposeSheet.show(context, originalPost: item).then((
|
||||||
|
value,
|
||||||
|
) {
|
||||||
|
if (value == true) {
|
||||||
|
onRefresh?.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuAction(
|
MenuAction(
|
||||||
|
|||||||
Reference in New Issue
Block a user