♻️ Refactored publisher subscription
This commit is contained in:
@@ -12,7 +12,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/screens/account/profile.dart';
|
||||
import 'package:island/screens/creators/publishers_form.dart';
|
||||
import 'package:island/screens/posts/publisher_profile.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/cloud_file_lightbox.dart';
|
||||
@@ -64,14 +64,16 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
final matches = stickerPattern.allMatches(content);
|
||||
|
||||
// Content should only contain one sticker and nothing else (except whitespace)
|
||||
final contentWithoutStickers =
|
||||
content.replaceAll(stickerPattern, '').trim();
|
||||
final contentWithoutStickers = content
|
||||
.replaceAll(stickerPattern, '')
|
||||
.trim();
|
||||
return matches.length == 1 && contentWithoutStickers.isEmpty;
|
||||
}, [content]);
|
||||
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final config =
|
||||
isDark ? MarkdownConfig.darkConfig : MarkdownConfig.defaultConfig;
|
||||
final config = isDark
|
||||
? MarkdownConfig.darkConfig
|
||||
: MarkdownConfig.defaultConfig;
|
||||
|
||||
final onMentionTap = useCallback((String type, String id) {
|
||||
final fullPath = '/$type/$id';
|
||||
@@ -128,11 +130,10 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
TableConfig(
|
||||
wrapper:
|
||||
(child) => SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: child,
|
||||
),
|
||||
wrapper: (child) => SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
LinkConfig(
|
||||
style:
|
||||
@@ -203,8 +204,9 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
@@ -288,11 +290,10 @@ class _MetionInlineSyntax extends markdown.InlineSyntax {
|
||||
"c" => 'chat',
|
||||
_ => '',
|
||||
};
|
||||
final element =
|
||||
markdown.Element('mention-chip', [markdown.Text(alias)])
|
||||
..attributes['alias'] = alias
|
||||
..attributes['type'] = type
|
||||
..attributes['id'] = parts.last;
|
||||
final element = markdown.Element('mention-chip', [markdown.Text(alias)])
|
||||
..attributes['alias'] = alias
|
||||
..attributes['type'] = type
|
||||
..attributes['id'] = parts.last;
|
||||
parser.addNode(element);
|
||||
|
||||
return true;
|
||||
@@ -373,18 +374,19 @@ class MentionChipGenerator extends SpanNodeGeneratorWithTag {
|
||||
required void Function(String type, String id) onTap,
|
||||
}) : super(
|
||||
tag: 'mention-chip',
|
||||
generator: (
|
||||
markdown.Element element,
|
||||
MarkdownConfig config,
|
||||
WidgetVisitor visitor,
|
||||
) {
|
||||
return MentionChipSpanNode(
|
||||
attributes: element.attributes,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
onTap: onTap,
|
||||
);
|
||||
},
|
||||
generator:
|
||||
(
|
||||
markdown.Element element,
|
||||
MarkdownConfig config,
|
||||
WidgetVisitor visitor,
|
||||
) {
|
||||
return MentionChipSpanNode(
|
||||
attributes: element.attributes,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
onTap: onTap,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -440,19 +442,17 @@ class MentionChipSpanNode extends SpanNode {
|
||||
builder: (context, ref, _) {
|
||||
final userData = ref.watch(accountProvider(parts.last));
|
||||
return userData.when(
|
||||
data:
|
||||
(data) => ProfilePictureWidget(
|
||||
file: data.profile.picture,
|
||||
fallbackIcon: Symbols.person_rounded,
|
||||
radius: 9,
|
||||
),
|
||||
data: (data) => ProfilePictureWidget(
|
||||
file: data.profile.picture,
|
||||
fallbackIcon: Symbols.person_rounded,
|
||||
radius: 9,
|
||||
),
|
||||
error: (_, _) => const Icon(Symbols.close),
|
||||
loading:
|
||||
() => const SizedBox(
|
||||
width: 9,
|
||||
height: 9,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
loading: () => const SizedBox(
|
||||
width: 9,
|
||||
height: 9,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -460,19 +460,17 @@ class MentionChipSpanNode extends SpanNode {
|
||||
builder: (context, ref, _) {
|
||||
final pubData = ref.watch(publisherProvider(parts.last));
|
||||
return pubData.when(
|
||||
data:
|
||||
(data) => ProfilePictureWidget(
|
||||
file: data?.picture,
|
||||
fallbackIcon: Symbols.design_services_rounded,
|
||||
radius: 9,
|
||||
),
|
||||
data: (data) => ProfilePictureWidget(
|
||||
file: data.picture,
|
||||
fallbackIcon: Symbols.design_services_rounded,
|
||||
radius: 9,
|
||||
),
|
||||
error: (_, _) => const Icon(Symbols.close),
|
||||
loading:
|
||||
() => const SizedBox(
|
||||
width: 9,
|
||||
height: 9,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
loading: () => const SizedBox(
|
||||
width: 9,
|
||||
height: 9,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -508,16 +506,17 @@ class HighlightGenerator extends SpanNodeGeneratorWithTag {
|
||||
HighlightGenerator({required Color highlightColor})
|
||||
: super(
|
||||
tag: 'highlight',
|
||||
generator: (
|
||||
markdown.Element element,
|
||||
MarkdownConfig config,
|
||||
WidgetVisitor visitor,
|
||||
) {
|
||||
return HighlightSpanNode(
|
||||
text: element.textContent,
|
||||
highlightColor: highlightColor,
|
||||
);
|
||||
},
|
||||
generator:
|
||||
(
|
||||
markdown.Element element,
|
||||
MarkdownConfig config,
|
||||
WidgetVisitor visitor,
|
||||
) {
|
||||
return HighlightSpanNode(
|
||||
text: element.textContent,
|
||||
highlightColor: highlightColor,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -545,20 +544,21 @@ class SpoilerGenerator extends SpanNodeGeneratorWithTag {
|
||||
required VoidCallback onToggle,
|
||||
}) : super(
|
||||
tag: 'spoiler',
|
||||
generator: (
|
||||
markdown.Element element,
|
||||
MarkdownConfig config,
|
||||
WidgetVisitor visitor,
|
||||
) {
|
||||
return SpoilerSpanNode(
|
||||
text: element.textContent,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
outlineColor: outlineColor,
|
||||
revealed: revealed,
|
||||
onToggle: onToggle,
|
||||
);
|
||||
},
|
||||
generator:
|
||||
(
|
||||
markdown.Element element,
|
||||
MarkdownConfig config,
|
||||
WidgetVisitor visitor,
|
||||
) {
|
||||
return SpoilerSpanNode(
|
||||
text: element.textContent,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
outlineColor: outlineColor,
|
||||
revealed: revealed,
|
||||
onToggle: onToggle,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -591,35 +591,33 @@ class SpoilerSpanNode extends SpanNode {
|
||||
border: revealed ? Border.all(color: outlineColor, width: 1) : null,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child:
|
||||
revealed
|
||||
? Row(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Symbols.visibility, size: 18).padding(top: 1),
|
||||
Flexible(child: Text(text)),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.visibility_off,
|
||||
color: foregroundColor,
|
||||
size: 18,
|
||||
),
|
||||
Flexible(
|
||||
child:
|
||||
Text(
|
||||
'spoiler',
|
||||
style: TextStyle(color: foregroundColor),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: revealed
|
||||
? Row(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Symbols.visibility, size: 18).padding(top: 1),
|
||||
Flexible(child: Text(text)),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.visibility_off,
|
||||
color: foregroundColor,
|
||||
size: 18,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'spoiler',
|
||||
style: TextStyle(color: foregroundColor),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -634,19 +632,20 @@ class StickerGenerator extends SpanNodeGeneratorWithTag {
|
||||
required String baseUrl,
|
||||
}) : super(
|
||||
tag: 'sticker',
|
||||
generator: (
|
||||
markdown.Element element,
|
||||
MarkdownConfig config,
|
||||
WidgetVisitor visitor,
|
||||
) {
|
||||
return StickerSpanNode(
|
||||
placeholder: element.textContent,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
isEnlarged: isEnlarged,
|
||||
baseUrl: baseUrl,
|
||||
);
|
||||
},
|
||||
generator:
|
||||
(
|
||||
markdown.Element element,
|
||||
MarkdownConfig config,
|
||||
WidgetVisitor visitor,
|
||||
) {
|
||||
return StickerSpanNode(
|
||||
placeholder: element.textContent,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
isEnlarged: isEnlarged,
|
||||
baseUrl: baseUrl,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/post/post_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class PostFilterWidget extends StatefulWidget {
|
||||
class PostFilterWidget extends HookConsumerWidget {
|
||||
final TabController categoryTabController;
|
||||
final PostListQuery initialQuery;
|
||||
final ValueChanged<PostListQuery> onQueryChanged;
|
||||
@@ -19,79 +21,55 @@ class PostFilterWidget extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<PostFilterWidget> createState() => _PostFilterWidgetState();
|
||||
}
|
||||
|
||||
class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
late bool? _includeReplies;
|
||||
late bool _mediaOnly;
|
||||
late String? _queryTerm;
|
||||
late String? _order;
|
||||
late bool _orderDesc;
|
||||
late int? _periodStart;
|
||||
late int? _periodEnd;
|
||||
late int? _type;
|
||||
late bool _showAdvancedFilters;
|
||||
late TextEditingController _searchController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_includeReplies = widget.initialQuery.includeReplies;
|
||||
_mediaOnly = widget.initialQuery.mediaOnly ?? false;
|
||||
_queryTerm = widget.initialQuery.queryTerm;
|
||||
_order = widget.initialQuery.order;
|
||||
_orderDesc = widget.initialQuery.orderDesc;
|
||||
_periodStart = widget.initialQuery.periodStart;
|
||||
_periodEnd = widget.initialQuery.periodEnd;
|
||||
_type = widget.initialQuery.type;
|
||||
_showAdvancedFilters = false;
|
||||
_searchController = TextEditingController(text: _queryTerm);
|
||||
|
||||
widget.categoryTabController.addListener(_onTabChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.categoryTabController.removeListener(_onTabChanged);
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTabChanged() {
|
||||
final tabIndex = widget.categoryTabController.index;
|
||||
setState(() {
|
||||
_type = switch (tabIndex) {
|
||||
1 => 0,
|
||||
2 => 1,
|
||||
_ => null,
|
||||
};
|
||||
});
|
||||
_updateQuery();
|
||||
}
|
||||
|
||||
void _updateQuery() {
|
||||
final newQuery = widget.initialQuery.copyWith(
|
||||
includeReplies: _includeReplies,
|
||||
mediaOnly: _mediaOnly,
|
||||
queryTerm: _queryTerm,
|
||||
order: _order,
|
||||
periodStart: _periodStart,
|
||||
periodEnd: _periodEnd,
|
||||
orderDesc: _orderDesc,
|
||||
type: _type,
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final includeReplies = useState<bool?>(initialQuery.includeReplies);
|
||||
final mediaOnly = useState<bool>(initialQuery.mediaOnly ?? false);
|
||||
final queryTerm = useState<String?>(initialQuery.queryTerm);
|
||||
final order = useState<String?>(initialQuery.order);
|
||||
final orderDesc = useState<bool>(initialQuery.orderDesc);
|
||||
final periodStart = useState<int?>(initialQuery.periodStart);
|
||||
final periodEnd = useState<int?>(initialQuery.periodEnd);
|
||||
final type = useState<int?>(initialQuery.type);
|
||||
final showAdvancedFilters = useState<bool>(false);
|
||||
final searchController = useTextEditingController(
|
||||
text: initialQuery.queryTerm,
|
||||
);
|
||||
widget.onQueryChanged(newQuery);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void updateQuery() {
|
||||
final newQuery = initialQuery.copyWith(
|
||||
includeReplies: includeReplies.value,
|
||||
mediaOnly: mediaOnly.value,
|
||||
queryTerm: queryTerm.value,
|
||||
order: order.value,
|
||||
periodStart: periodStart.value,
|
||||
periodEnd: periodEnd.value,
|
||||
orderDesc: orderDesc.value,
|
||||
type: type.value,
|
||||
);
|
||||
onQueryChanged(newQuery);
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
void onTabChanged() {
|
||||
final tabIndex = categoryTabController.index;
|
||||
type.value = switch (tabIndex) {
|
||||
1 => 0,
|
||||
2 => 1,
|
||||
_ => null,
|
||||
};
|
||||
updateQuery();
|
||||
}
|
||||
|
||||
categoryTabController.addListener(onTabChanged);
|
||||
return () => categoryTabController.removeListener(onTabChanged);
|
||||
}, [categoryTabController]);
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: widget.categoryTabController,
|
||||
controller: categoryTabController,
|
||||
dividerColor: Colors.transparent,
|
||||
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
tabs: [
|
||||
@@ -108,20 +86,18 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
title: Text('reply'.tr()),
|
||||
value: _includeReplies,
|
||||
value: includeReplies.value,
|
||||
tristate: true,
|
||||
onChanged: (value) {
|
||||
// Cycle through: null -> false -> true -> null
|
||||
setState(() {
|
||||
if (_includeReplies == null) {
|
||||
_includeReplies = false;
|
||||
} else if (_includeReplies == false) {
|
||||
_includeReplies = true;
|
||||
} else {
|
||||
_includeReplies = null;
|
||||
}
|
||||
});
|
||||
_updateQuery();
|
||||
final current = includeReplies.value;
|
||||
if (current == null) {
|
||||
includeReplies.value = false;
|
||||
} else if (current == false) {
|
||||
includeReplies.value = true;
|
||||
} else {
|
||||
includeReplies.value = null;
|
||||
}
|
||||
updateQuery();
|
||||
},
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
@@ -131,14 +107,12 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
title: Text('attachments'.tr()),
|
||||
value: _mediaOnly,
|
||||
value: mediaOnly.value,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value != null) {
|
||||
_mediaOnly = value;
|
||||
}
|
||||
});
|
||||
_updateQuery();
|
||||
if (value != null) {
|
||||
mediaOnly.value = value;
|
||||
}
|
||||
updateQuery();
|
||||
},
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
@@ -149,14 +123,12 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text('descendingOrder'.tr()),
|
||||
value: _orderDesc,
|
||||
value: orderDesc.value,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value != null) {
|
||||
_orderDesc = value;
|
||||
}
|
||||
});
|
||||
_updateQuery();
|
||||
if (value != null) {
|
||||
orderDesc.value = value;
|
||||
}
|
||||
updateQuery();
|
||||
},
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
@@ -173,24 +145,24 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
borderRadius: BorderRadius.all(const Radius.circular(8)),
|
||||
),
|
||||
trailing: Icon(
|
||||
_showAdvancedFilters ? Symbols.expand_less : Symbols.expand_more,
|
||||
showAdvancedFilters.value
|
||||
? Symbols.expand_less
|
||||
: Symbols.expand_more,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_showAdvancedFilters = !_showAdvancedFilters;
|
||||
});
|
||||
showAdvancedFilters.value = !showAdvancedFilters.value;
|
||||
},
|
||||
),
|
||||
if (_showAdvancedFilters) ...[
|
||||
if (showAdvancedFilters.value) ...[
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!widget.hideSearch)
|
||||
if (!hideSearch)
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'search'.tr(),
|
||||
hintText: 'searchPosts'.tr(),
|
||||
@@ -204,13 +176,11 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_queryTerm = value.isEmpty ? null : value;
|
||||
});
|
||||
_updateQuery();
|
||||
queryTerm.value = value.isEmpty ? null : value;
|
||||
updateQuery();
|
||||
},
|
||||
),
|
||||
if (!widget.hideSearch) const Gap(12),
|
||||
if (!hideSearch) const Gap(12),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'sortBy'.tr(),
|
||||
@@ -222,7 +192,7 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
value: _order,
|
||||
value: order.value,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'date', child: Text('date'.tr())),
|
||||
DropdownMenuItem(
|
||||
@@ -231,10 +201,8 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_order = value;
|
||||
});
|
||||
_updateQuery();
|
||||
order.value = value;
|
||||
updateQuery();
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
@@ -245,9 +213,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
onTap: () async {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _periodStart != null
|
||||
initialDate: periodStart.value != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
_periodStart! * 1000,
|
||||
periodStart.value! * 1000,
|
||||
)
|
||||
: DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
@@ -256,11 +224,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
),
|
||||
);
|
||||
if (pickedDate != null) {
|
||||
setState(() {
|
||||
_periodStart =
|
||||
pickedDate.millisecondsSinceEpoch ~/ 1000;
|
||||
});
|
||||
_updateQuery();
|
||||
periodStart.value =
|
||||
pickedDate.millisecondsSinceEpoch ~/ 1000;
|
||||
updateQuery();
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
@@ -278,9 +244,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
suffixIcon: const Icon(Symbols.calendar_today),
|
||||
),
|
||||
child: Text(
|
||||
_periodStart != null
|
||||
periodStart.value != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
_periodStart! * 1000,
|
||||
periodStart.value! * 1000,
|
||||
).toString().split(' ')[0]
|
||||
: 'selectDate'.tr(),
|
||||
),
|
||||
@@ -293,9 +259,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
onTap: () async {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _periodEnd != null
|
||||
initialDate: periodEnd.value != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
_periodEnd! * 1000,
|
||||
periodEnd.value! * 1000,
|
||||
)
|
||||
: DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
@@ -304,11 +270,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
),
|
||||
);
|
||||
if (pickedDate != null) {
|
||||
setState(() {
|
||||
_periodEnd =
|
||||
pickedDate.millisecondsSinceEpoch ~/ 1000;
|
||||
});
|
||||
_updateQuery();
|
||||
periodEnd.value =
|
||||
pickedDate.millisecondsSinceEpoch ~/ 1000;
|
||||
updateQuery();
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
@@ -326,9 +290,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
suffixIcon: const Icon(Symbols.calendar_today),
|
||||
),
|
||||
child: Text(
|
||||
_periodEnd != null
|
||||
periodEnd.value != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
_periodEnd! * 1000,
|
||||
periodEnd.value! * 1000,
|
||||
).toString().split(' ')[0]
|
||||
: 'selectDate'.tr(),
|
||||
),
|
||||
|
||||
157
lib/widgets/posts/post_subscription_filter.dart
Normal file
157
lib/widgets/posts/post_subscription_filter.dart
Normal file
@@ -0,0 +1,157 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/post/post_subscriptions.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class PostSubscriptionFilterWidget extends HookConsumerWidget {
|
||||
final List<String> initialSelectedPublisherIds;
|
||||
final ValueChanged<List<String>> onSelectedPublishersChanged;
|
||||
final bool hideSearch;
|
||||
|
||||
const PostSubscriptionFilterWidget({
|
||||
super.key,
|
||||
required this.initialSelectedPublisherIds,
|
||||
required this.onSelectedPublishersChanged,
|
||||
this.hideSearch = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedPublisherIds = useState<List<String>>(
|
||||
initialSelectedPublisherIds,
|
||||
);
|
||||
final showSubscriptions = useState<bool>(false);
|
||||
|
||||
final subscriptionsAsync = ref.watch(subscriptionsProvider);
|
||||
|
||||
void updateSelection() {
|
||||
onSelectedPublishersChanged(selectedPublisherIds.value);
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('filterBySubscriptions'.tr()),
|
||||
leading: const Icon(Symbols.subscriptions),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(const Radius.circular(8)),
|
||||
),
|
||||
trailing: Icon(
|
||||
showSubscriptions.value
|
||||
? Symbols.expand_less
|
||||
: Symbols.expand_more,
|
||||
),
|
||||
onTap: () {
|
||||
showSubscriptions.value = !showSubscriptions.value;
|
||||
},
|
||||
),
|
||||
if (showSubscriptions.value) ...[
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
subscriptionsAsync.when(
|
||||
data: (subscriptions) {
|
||||
if (subscriptions.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text('noSubscriptions'.tr()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: subscriptions.map((subscription) {
|
||||
final isSelected = selectedPublisherIds.value
|
||||
.contains(subscription.publisherId);
|
||||
final publisher = subscription.publisher;
|
||||
|
||||
return CheckboxListTile(
|
||||
title: Text(publisher.name),
|
||||
subtitle:
|
||||
publisher.nick.isNotEmpty &&
|
||||
publisher.nick != publisher.name
|
||||
? Text(publisher.nick)
|
||||
: null,
|
||||
value: isSelected,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
selectedPublisherIds.value = [
|
||||
...selectedPublisherIds.value,
|
||||
subscription.publisherId,
|
||||
];
|
||||
} else {
|
||||
selectedPublisherIds.value =
|
||||
selectedPublisherIds.value
|
||||
.where(
|
||||
(id) =>
|
||||
id != subscription.publisherId,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
updateSelection();
|
||||
},
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
secondary: const Icon(Symbols.person),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
error: (error, stack) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text('errorLoadingSubscriptions'.tr()),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (subscriptionsAsync.hasValue &&
|
||||
subscriptionsAsync.value!.isNotEmpty) ...[
|
||||
const Gap(12),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
selectedPublisherIds.value = subscriptionsAsync
|
||||
.value!
|
||||
.map((s) => s.publisherId)
|
||||
.toList();
|
||||
updateSelection();
|
||||
},
|
||||
child: Text('selectAll'.tr()),
|
||||
),
|
||||
const Gap(8),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
selectedPublisherIds.value = [];
|
||||
updateSelection();
|
||||
},
|
||||
child: Text('selectNone'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user