♻️ Refactored web feed, poll

This commit is contained in:
2025-10-12 12:19:50 +08:00
parent bbb07d574a
commit 27157e7cc1
9 changed files with 415 additions and 476 deletions

View File

@@ -50,7 +50,6 @@ import 'package:island/screens/discovery/feeds/feed_detail.dart';
import 'package:island/screens/creators/poll/poll_list.dart';
import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
import 'package:island/screens/poll/poll_editor.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/screens/posts/post_detail.dart';
@@ -162,27 +161,6 @@ final routerProvider = Provider<GoRouter>((ref) {
final name = state.pathParameters['name']!;
return WebFeedListScreen(pubName: name);
},
routes: [
GoRoute(
name: 'creatorFeedNew',
path: 'new',
builder: (context, state) {
return WebFeedNewScreen(
pubName: state.pathParameters['name']!,
);
},
),
GoRoute(
name: 'creatorFeedEdit',
path: ':feedId',
builder: (context, state) {
return WebFeedEditScreen(
pubName: state.pathParameters['name']!,
feedId: state.pathParameters['feedId'],
);
},
),
],
),
GoRoute(
name: 'creatorPosts',

View File

@@ -175,7 +175,7 @@ class AccountScreen extends HookConsumerWidget {
],
).padding(horizontal: 16, vertical: 12),
onTap: () {
context.pushNamed('creatorHub');
context.goNamed('creatorHub');
},
),
).height(140),

View File

@@ -379,6 +379,21 @@ class CreatorHubScreen extends HookConsumerWidget {
);
},
),
ListTile(
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
minTileHeight: 48,
title: const Text('webFeeds').tr(),
trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.rss_feed),
onTap: () {
context.push('/creators/${currentPublisher.value!.name}/feeds');
},
),
];
final rightItems = [
ListTile(
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
@@ -398,21 +413,6 @@ class CreatorHubScreen extends HookConsumerWidget {
);
},
),
];
final rightItems = [
ListTile(
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
minTileHeight: 48,
title: const Text('webFeeds').tr(),
trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.rss_feed),
onTap: () {
context.push('/creators/${currentPublisher.value!.name}/feeds');
},
),
ExpansionTile(
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),

View File

@@ -106,10 +106,13 @@ class CreatorPollListScreen extends HookConsumerWidget {
return endItemView;
}
final pollWithStats = data.items[index];
return _CreatorPollItem(
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640),
child: _CreatorPollItem(
pollWithStats: pollWithStats,
pubName: pubName,
);
),
).center();
},
),
),

View File

@@ -1,12 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart';
class CreatorPostListScreen extends HookConsumerWidget {
final String pubName;
@@ -16,62 +13,19 @@ class CreatorPostListScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final refreshKey = useState(0);
void showCreatePostSheet() {
showModalBottomSheet(
context: context,
builder:
(context) => SheetScaffold(
titleText: 'create'.tr(),
child: Column(
children: [
ListTile(
leading: const Icon(Symbols.edit),
title: Text('Post'),
subtitle: Text('Create a regular post'),
onTap: () async {
Navigator.pop(context);
final result = await context.pushNamed(
'postCompose',
queryParameters: {'type': '0'},
);
if (result == true) {
refreshKey.value++;
}
},
),
ListTile(
leading: const Icon(Symbols.article),
title: Text('Article'),
subtitle: Text('Create a detailed article'),
onTap: () async {
Navigator.pop(context);
final result = await context.pushNamed(
'postCompose',
queryParameters: {'type': '1'},
);
if (result == true) {
refreshKey.value++;
}
},
),
],
),
),
);
}
return AppScaffold(
isNoBackground: false,
appBar: AppBar(title: Text('posts').tr()),
body: CustomScrollView(
key: ValueKey(refreshKey.value),
slivers: [
SliverPostList(pubName: pubName, itemType: PostItemType.creator),
],
SliverPostList(
pubName: pubName,
itemType: PostItemType.creator,
maxWidth: 640,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
),
floatingActionButton: FloatingActionButton(
onPressed: showCreatePostSheet,
child: const Icon(Symbols.add),
],
),
);
}

View File

@@ -6,25 +6,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/webfeed.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class WebFeedNewScreen extends StatelessWidget {
final String pubName;
const WebFeedNewScreen({super.key, required this.pubName});
@override
Widget build(BuildContext context) {
return WebFeedEditScreen(pubName: pubName, feedId: null);
}
}
class WebFeedEditScreen extends HookConsumerWidget {
class WebfeedForm extends HookConsumerWidget {
final String pubName;
final String? feedId;
const WebFeedEditScreen({super.key, required this.pubName, this.feedId});
const WebfeedForm({super.key, required this.pubName, this.feedId});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -113,16 +103,14 @@ class WebFeedEditScreen extends HookConsumerWidget {
);
return feedAsync.when(
loading:
() => const AppScaffold(
isNoBackground: false,
body: Center(child: CircularProgressIndicator()),
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => AppScaffold(
isNoBackground: false,
appBar: AppBar(title: const Text('Error')),
body: Center(child: Text('Error: $error')),
(error, _) => ResponseErrorWidget(
error: error,
onRetry:
() => ref.invalidate(
webFeedNotifierProvider((pubName: pubName, feedId: feedId)),
),
),
data: (feed) {
// Initialize form fields if they're empty and we have a feed
@@ -133,40 +121,8 @@ class WebFeedEditScreen extends HookConsumerWidget {
isScrapEnabled.value = feed.config.scrapPage;
}
return _buildForm(
context,
formKey: formKey,
titleController: titleController,
urlController: urlController,
descriptionController: descriptionController,
isScrapEnabled: isScrapEnabled.value,
onScrapEnabledChanged: (value) => isScrapEnabled.value = value,
onSave: saveFeed,
onDelete: deleteFeed,
isLoading: isLoading.value,
ref: ref,
hasFeedId: feedId != null,
);
},
);
}
Widget _buildForm(
BuildContext context, {
required WidgetRef ref,
required GlobalKey<FormState> formKey,
required TextEditingController titleController,
required TextEditingController urlController,
required TextEditingController descriptionController,
required bool isScrapEnabled,
required ValueChanged<bool> onScrapEnabledChanged,
required VoidCallback onSave,
required VoidCallback onDelete,
required bool isLoading,
required bool hasFeedId,
}) {
final scrapNow = useCallback(() async {
showLoadingModal(context);
isLoading.value = true;
try {
await ref
.read(
@@ -183,31 +139,20 @@ class WebFeedEditScreen extends HookConsumerWidget {
} catch (e) {
showErrorAlert(e);
} finally {
if (context.mounted) hideLoadingModal(context);
isLoading.value = false;
}
}, [pubName, feedId, ref, context]);
}, [pubName, feedId, ref, context, isLoading]);
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'),
actions: [
if (hasFeedId)
IconButton(
icon: const Icon(Symbols.delete_forever),
onPressed: isLoading ? null : onDelete,
),
const SizedBox(width: 8),
],
),
body: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
final formFields = Column(
children: [
TextFormField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Title'),
decoration: const InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a title';
@@ -223,6 +168,9 @@ class WebFeedEditScreen extends HookConsumerWidget {
decoration: const InputDecoration(
labelText: 'URL',
hintText: 'https://example.com/feed',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
keyboardType: TextInputType.url,
validator: (value) {
@@ -244,6 +192,9 @@ class WebFeedEditScreen extends HookConsumerWidget {
decoration: const InputDecoration(
labelText: 'Description',
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
@@ -263,30 +214,49 @@ class WebFeedEditScreen extends HookConsumerWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
value: isScrapEnabled,
onChanged: onScrapEnabledChanged,
value: isScrapEnabled.value,
onChanged: (value) => isScrapEnabled.value = value,
),
],
),
),
const SizedBox(height: 20),
if (hasFeedId) ...[
FilledButton.tonalIcon(
onPressed: isLoading ? null : scrapNow,
if (feedId != null) ...[
TextButton.icon(
onPressed: isLoading.value ? null : scrapNow,
icon: const Icon(Symbols.refresh),
label: const Text('Scrape Now'),
).alignment(Alignment.centerRight),
const SizedBox(height: 16),
],
FilledButton.icon(
onPressed: isLoading ? null : onSave,
],
).padding(all: 20);
final formWidget = Form(
key: formKey,
child: SingleChildScrollView(child: formFields),
);
final buttonsRow = Row(
children: [
if (feedId != null)
TextButton.icon(
onPressed: isLoading.value ? null : deleteFeed,
icon: const Icon(Symbols.delete_forever),
label: const Text('Delete Web Feed'),
style: TextButton.styleFrom(foregroundColor: Colors.red),
),
const Spacer(),
TextButton.icon(
onPressed: isLoading.value ? null : saveFeed,
icon: const Icon(Symbols.save),
label: Text('saveChanges').tr(),
).alignment(Alignment.centerRight),
),
],
).padding(all: 20),
),
),
).padding(horizontal: 20, vertical: 12);
return Column(children: [Expanded(child: formWidget), buttonsRow]);
},
);
}
}

View File

@@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:island/pods/webfeed.dart';
import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/empty_state.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class WebFeedListScreen extends ConsumerWidget {
final String pubName;
@@ -22,9 +24,14 @@ class WebFeedListScreen extends ConsumerWidget {
floatingActionButton: FloatingActionButton(
child: const Icon(Symbols.add),
onPressed: () {
context.pushNamed(
'creatorFeedNew',
pathParameters: {'name': pubName},
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'New Web Feed',
child: WebfeedForm(pubName: pubName, feedId: null),
),
);
},
),
@@ -44,7 +51,9 @@ class WebFeedListScreen extends ConsumerWidget {
itemCount: feeds.length,
itemBuilder: (context, index) {
final feed = feeds[index];
return Card(
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640),
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
@@ -67,13 +76,22 @@ class WebFeedListScreen extends ConsumerWidget {
),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.pushNamed(
'creatorFeedEdit',
pathParameters: {'name': pubName, 'feedId': feed.id},
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'Edit Web Feed',
child: WebfeedForm(
pubName: pubName,
feedId: feed.id,
),
),
);
},
),
);
),
).center();
},
),
);

View File

@@ -434,7 +434,10 @@ class PollEditorScreen extends ConsumerWidget {
body: Column(
children: [
Expanded(
child: Form(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640),
child:
Form(
key: ValueKey(model.id),
child: ListView(
padding: const EdgeInsets.all(16),
@@ -442,16 +445,19 @@ class PollEditorScreen extends ConsumerWidget {
TextFormField(
initialValue: model.title ?? '',
decoration: InputDecoration(
labelText: 'title'.tr(),
labelText: 'postTitle'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
borderRadius: BorderRadius.all(
Radius.circular(16),
),
),
),
textInputAction: TextInputAction.next,
maxLength: 256,
onChanged: notifier.setTitle,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'pollTitleRequired'.tr();
@@ -466,14 +472,17 @@ class PollEditorScreen extends ConsumerWidget {
labelText: 'description'.tr(),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
borderRadius: BorderRadius.all(
Radius.circular(16),
),
),
),
maxLines: 3,
maxLength: 4096,
onChanged: notifier.setDescription,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
_EndDatePicker(
@@ -505,7 +514,8 @@ class PollEditorScreen extends ConsumerWidget {
.map(
(t) => MenuItemButton(
leadingIcon: Icon(_iconForType(t)),
onPressed: () => notifier.addQuestion(t),
onPressed:
() => notifier.addQuestion(t),
child: Text(_labelForType(t)),
),
)
@@ -553,13 +563,17 @@ class PollEditorScreen extends ConsumerWidget {
question: q,
onMoveUp:
index > 0
? () => notifier.moveQuestionUp(index)
? () =>
notifier.moveQuestionUp(index)
: null,
onMoveDown:
index < model.questions.length - 1
? () => notifier.moveQuestionDown(index)
? () => notifier.moveQuestionDown(
index,
)
: null,
onDelete: () => notifier.removeQuestion(index),
onDelete:
() => notifier.removeQuestion(index),
),
const Divider(height: 1),
Padding(
@@ -577,11 +591,15 @@ class PollEditorScreen extends ConsumerWidget {
const Gap(96),
],
),
).center(),
),
),
Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainer,
child:
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640),
child: Row(
children: [
OutlinedButton.icon(
@@ -597,7 +615,9 @@ class PollEditorScreen extends ConsumerWidget {
_submitPoll(context, ref);
},
icon: const Icon(Icons.cloud_upload_outlined),
label: Text(model.id == null ? 'create'.tr() : 'update'.tr()),
label: Text(
model.id == null ? 'create'.tr() : 'update'.tr(),
),
),
],
).padding(
@@ -605,6 +625,7 @@ class PollEditorScreen extends ConsumerWidget {
top: 16,
bottom: MediaQuery.of(context).padding.bottom + 16,
),
).center(),
),
],
),

View File

@@ -149,18 +149,13 @@ class SliverPostList extends HookConsumerWidget {
Widget _buildPostItem(SnPost post) {
switch (itemType) {
case PostItemType.creator:
return Column(
children: [
PostItemCreator(
return PostItemCreator(
item: post,
backgroundColor: backgroundColor,
padding: padding,
isOpenable: isOpenable,
onRefresh: onRefresh,
onUpdate: onUpdate,
),
const Divider(),
],
);
case PostItemType.regular:
return Card(