✨ Post categories selection
This commit is contained in:
@@ -769,5 +769,18 @@
|
|||||||
"addPack": "Add Pack",
|
"addPack": "Add Pack",
|
||||||
"removePack": "Remove Pack",
|
"removePack": "Remove Pack",
|
||||||
"browseAndAddStickers": "Browse and add sticker packs",
|
"browseAndAddStickers": "Browse and add sticker packs",
|
||||||
"stickerPack": "Sticker Pack"
|
"stickerPack": "Sticker Pack",
|
||||||
|
"postCategoryTechnology": "Technology",
|
||||||
|
"postCategoryTravel": "Travel",
|
||||||
|
"postCategoryFood": "Food",
|
||||||
|
"postCategoryHealth": "Health",
|
||||||
|
"postCategoryScience": "Science",
|
||||||
|
"postCategorySports": "Sports",
|
||||||
|
"postCategoryFinance": "Finance",
|
||||||
|
"postCategoryLife": "Life",
|
||||||
|
"postCategoryArt": "Art",
|
||||||
|
"postCategoryStudy": "Study",
|
||||||
|
"postCategoryGaming": "Gaming",
|
||||||
|
"postCategoryProgramming": "Programming",
|
||||||
|
"postCategoryMusic": "Music"
|
||||||
}
|
}
|
||||||
|
@@ -205,15 +205,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder:
|
builder: (context) => ComposeSettingsSheet(state: state),
|
||||||
(context) => ComposeSettingsSheet(
|
|
||||||
visibility: state.visibility,
|
|
||||||
tagsController: state.tagsController,
|
|
||||||
categoriesController: state.categoriesController,
|
|
||||||
onVisibilityChanged: () {
|
|
||||||
// Trigger rebuild if needed
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -138,15 +138,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder:
|
builder: (context) => ComposeSettingsSheet(state: state),
|
||||||
(context) => ComposeSettingsSheet(
|
|
||||||
visibility: state.visibility,
|
|
||||||
tagsController: state.tagsController,
|
|
||||||
categoriesController: state.categoriesController,
|
|
||||||
onVisibilityChanged: () {
|
|
||||||
// Trigger rebuild if needed
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,11 +1,30 @@
|
|||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/post_category.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/services/text.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:textfield_tags/textfield_tags.dart';
|
import 'package:textfield_tags/textfield_tags.dart';
|
||||||
|
|
||||||
|
part 'compose_settings_sheet.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<PostCategory>> postCategories(Ref ref) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/sphere/posts/categories');
|
||||||
|
return resp.data
|
||||||
|
.map((e) => PostCategory.fromJson(e))
|
||||||
|
.cast<PostCategory>()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
/// A reusable widget for tag input fields with chip display
|
/// A reusable widget for tag input fields with chip display
|
||||||
class ChipTagInputField extends StatelessWidget {
|
class ChipTagInputField extends StatelessWidget {
|
||||||
final InputFieldValues inputFieldValues;
|
final InputFieldValues inputFieldValues;
|
||||||
@@ -98,27 +117,20 @@ class ChipTagInputField extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComposeSettingsSheet extends HookWidget {
|
class ComposeSettingsSheet extends HookConsumerWidget {
|
||||||
final ValueNotifier<int> visibility;
|
final ComposeState state;
|
||||||
final VoidCallback? onVisibilityChanged;
|
|
||||||
final StringTagController tagsController;
|
|
||||||
final StringTagController categoriesController;
|
|
||||||
|
|
||||||
const ComposeSettingsSheet({
|
const ComposeSettingsSheet({super.key, required this.state});
|
||||||
super.key,
|
|
||||||
required this.visibility,
|
|
||||||
this.onVisibilityChanged,
|
|
||||||
required this.tagsController,
|
|
||||||
required this.categoriesController,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colorScheme = theme.colorScheme;
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
// Listen to visibility changes to trigger rebuilds
|
// Listen to visibility changes to trigger rebuilds
|
||||||
final currentVisibility = useValueListenable(visibility);
|
final currentVisibility = useValueListenable(state.visibility);
|
||||||
|
final currentCategories = useValueListenable(state.categories);
|
||||||
|
final postCategories = ref.watch(postCategoriesProvider);
|
||||||
|
|
||||||
IconData getVisibilityIcon(int visibilityValue) {
|
IconData getVisibilityIcon(int visibilityValue) {
|
||||||
switch (visibilityValue) {
|
switch (visibilityValue) {
|
||||||
@@ -156,11 +168,10 @@ class ComposeSettingsSheet extends HookWidget {
|
|||||||
leading: Icon(icon),
|
leading: Icon(icon),
|
||||||
title: Text(textKey.tr()),
|
title: Text(textKey.tr()),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
visibility.value = value;
|
state.visibility.value = value;
|
||||||
onVisibilityChanged?.call();
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
selected: visibility.value == value,
|
selected: state.visibility.value == value,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -204,6 +215,14 @@ class ComposeSettingsSheet extends HookWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getCategoryDisplayTitle(PostCategory category) {
|
||||||
|
final capitalizedSlug = category.slug.capitalizeEachWord();
|
||||||
|
if ('postCategory$capitalizedSlug'.trExists()) {
|
||||||
|
return 'postCategory$capitalizedSlug'.tr();
|
||||||
|
}
|
||||||
|
return category.name ?? category.slug;
|
||||||
|
}
|
||||||
|
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
titleText: 'postSettings'.tr(),
|
titleText: 'postSettings'.tr(),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -214,7 +233,7 @@ class ComposeSettingsSheet extends HookWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Tags field
|
// Tags field
|
||||||
TextFieldTags(
|
TextFieldTags(
|
||||||
textfieldTagsController: tagsController,
|
textfieldTagsController: state.tagsController,
|
||||||
textSeparators: const [' ', ','],
|
textSeparators: const [' ', ','],
|
||||||
letterCase: LetterCase.normal,
|
letterCase: LetterCase.normal,
|
||||||
validator: (String tag) {
|
validator: (String tag) {
|
||||||
@@ -233,23 +252,106 @@ class ComposeSettingsSheet extends HookWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Categories field
|
// Categories field
|
||||||
TextFieldTags(
|
// FIXME: Sometimes the entire dropdown crashes: 'package:flutter/src/rendering/stack.dart': Failed assertion: line 799 pos 12: 'firstChild == null || child != null': is not true.
|
||||||
textfieldTagsController: categoriesController,
|
DropdownButtonFormField2<PostCategory>(
|
||||||
textSeparators: const [' ', ','],
|
isExpanded: true,
|
||||||
letterCase: LetterCase.small,
|
decoration: InputDecoration(
|
||||||
validator: (String tag) {
|
contentPadding: const EdgeInsets.symmetric(vertical: 9),
|
||||||
if (tag.isEmpty) return 'No, cannot be empty';
|
border: OutlineInputBorder(
|
||||||
if (tag.contains(' ')) return 'Tags should be URL-safe';
|
borderRadius: BorderRadius.circular(12),
|
||||||
return null;
|
),
|
||||||
|
),
|
||||||
|
hint: Text('categories'.tr(), style: TextStyle(fontSize: 15)),
|
||||||
|
items:
|
||||||
|
(postCategories.value ?? <PostCategory>[]).map((item) {
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: item,
|
||||||
|
enabled: false,
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (context, menuSetState) {
|
||||||
|
final isSelected = state.categories.value.contains(
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
isSelected
|
||||||
|
? state.categories.value =
|
||||||
|
state.categories.value
|
||||||
|
.where((e) => e != item)
|
||||||
|
.toList()
|
||||||
|
: state.categories.value = [
|
||||||
|
...state.categories.value,
|
||||||
|
item,
|
||||||
|
];
|
||||||
|
menuSetState(() {});
|
||||||
},
|
},
|
||||||
inputFieldBuilder: (context, inputFieldValues) {
|
child: Container(
|
||||||
return ChipTagInputField(
|
height: double.infinity,
|
||||||
inputFieldValues: inputFieldValues,
|
padding: const EdgeInsets.symmetric(
|
||||||
labelText: 'categories',
|
horizontal: 16.0,
|
||||||
hintText: 'categoriesHint',
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (isSelected)
|
||||||
|
const Icon(Icons.check_box_outlined)
|
||||||
|
else
|
||||||
|
const Icon(Icons.check_box_outline_blank),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
getCategoryDisplayTitle(item),
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
value: currentCategories.isEmpty ? null : currentCategories.last,
|
||||||
|
onChanged: (_) {},
|
||||||
|
selectedItemBuilder: (context) {
|
||||||
|
return currentCategories.map((item) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
for (final category in currentCategories)
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(right: 4),
|
||||||
|
child: Text(
|
||||||
|
getCategoryDisplayTitle(category),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.only(left: 16, right: 8),
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 40,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Visibility setting
|
// Visibility setting
|
||||||
Container(
|
Container(
|
||||||
|
29
lib/widgets/post/compose_settings_sheet.g.dart
Normal file
29
lib/widgets/post/compose_settings_sheet.g.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'compose_settings_sheet.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$postCategoriesHash() => r'503ce6f0fdd728a8cb991665006c3b4bffbb94d4';
|
||||||
|
|
||||||
|
/// See also [postCategories].
|
||||||
|
@ProviderFor(postCategories)
|
||||||
|
final postCategoriesProvider =
|
||||||
|
AutoDisposeFutureProvider<List<PostCategory>>.internal(
|
||||||
|
postCategories,
|
||||||
|
name: r'postCategoriesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$postCategoriesHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef PostCategoriesRef = AutoDisposeFutureProviderRef<List<PostCategory>>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
@@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.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/models/post_category.dart';
|
||||||
import 'package:island/models/publisher.dart';
|
import 'package:island/models/publisher.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
@@ -30,8 +31,8 @@ class ComposeState {
|
|||||||
final ValueNotifier<Map<int, double>> attachmentProgress;
|
final ValueNotifier<Map<int, double>> attachmentProgress;
|
||||||
final ValueNotifier<SnPublisher?> currentPublisher;
|
final ValueNotifier<SnPublisher?> currentPublisher;
|
||||||
final ValueNotifier<bool> submitting;
|
final ValueNotifier<bool> submitting;
|
||||||
|
final ValueNotifier<List<PostCategory>> categories;
|
||||||
StringTagController tagsController;
|
StringTagController tagsController;
|
||||||
StringTagController categoriesController;
|
|
||||||
final String draftId;
|
final String draftId;
|
||||||
int postType;
|
int postType;
|
||||||
// Linked poll id for this compose session (nullable)
|
// Linked poll id for this compose session (nullable)
|
||||||
@@ -48,7 +49,7 @@ class ComposeState {
|
|||||||
required this.currentPublisher,
|
required this.currentPublisher,
|
||||||
required this.submitting,
|
required this.submitting,
|
||||||
required this.tagsController,
|
required this.tagsController,
|
||||||
required this.categoriesController,
|
required this.categories,
|
||||||
required this.draftId,
|
required this.draftId,
|
||||||
this.postType = 0,
|
this.postType = 0,
|
||||||
String? pollId,
|
String? pollId,
|
||||||
@@ -80,11 +81,7 @@ class ComposeLogic {
|
|||||||
}) {
|
}) {
|
||||||
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
|
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
final tagsController = StringTagController();
|
final tagsController = StringTagController();
|
||||||
final categoriesController = StringTagController();
|
|
||||||
originalPost?.tags.forEach((x) => tagsController.addTag(x.slug));
|
originalPost?.tags.forEach((x) => tagsController.addTag(x.slug));
|
||||||
originalPost?.categories.forEach(
|
|
||||||
(x) => categoriesController.addTag(x.slug),
|
|
||||||
);
|
|
||||||
return ComposeState(
|
return ComposeState(
|
||||||
attachments: ValueNotifier<List<UniversalFile>>(
|
attachments: ValueNotifier<List<UniversalFile>>(
|
||||||
originalPost?.attachments
|
originalPost?.attachments
|
||||||
@@ -112,7 +109,9 @@ class ComposeLogic {
|
|||||||
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
||||||
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
|
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
|
||||||
tagsController: tagsController,
|
tagsController: tagsController,
|
||||||
categoriesController: categoriesController,
|
categories: ValueNotifier<List<PostCategory>>(
|
||||||
|
originalPost?.categories ?? [],
|
||||||
|
),
|
||||||
draftId: id,
|
draftId: id,
|
||||||
postType: postType,
|
postType: postType,
|
||||||
// initialize without poll by default
|
// initialize without poll by default
|
||||||
@@ -141,7 +140,7 @@ class ComposeLogic {
|
|||||||
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
||||||
currentPublisher: ValueNotifier<SnPublisher?>(null),
|
currentPublisher: ValueNotifier<SnPublisher?>(null),
|
||||||
tagsController: tagsController,
|
tagsController: tagsController,
|
||||||
categoriesController: categoriesController,
|
categories: ValueNotifier<List<PostCategory>>([]),
|
||||||
draftId: draft.id,
|
draftId: draft.id,
|
||||||
postType: postType,
|
postType: postType,
|
||||||
pollId: null,
|
pollId: null,
|
||||||
@@ -640,7 +639,7 @@ class ComposeLogic {
|
|||||||
if (repliedPost != null) 'replied_post_id': repliedPost.id,
|
if (repliedPost != null) 'replied_post_id': repliedPost.id,
|
||||||
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
|
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
|
||||||
'tags': state.tagsController.getTags,
|
'tags': state.tagsController.getTags,
|
||||||
'categories': state.categoriesController.getTags,
|
'categories': state.categories.value.map((e) => e.slug).toList(),
|
||||||
if (state.pollId.value != null) 'poll_id': state.pollId.value,
|
if (state.pollId.value != null) 'poll_id': state.pollId.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -733,7 +732,7 @@ class ComposeLogic {
|
|||||||
state.attachmentProgress.dispose();
|
state.attachmentProgress.dispose();
|
||||||
state.currentPublisher.dispose();
|
state.currentPublisher.dispose();
|
||||||
state.tagsController.dispose();
|
state.tagsController.dispose();
|
||||||
state.categoriesController.dispose();
|
state.categories.dispose();
|
||||||
state.pollId.dispose();
|
state.pollId.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user