kopia lustrzana https://gitlab.com/mysocialportal/relatica
Add initial filter card/hiding work for timeline entries
rodzic
6644fee523
commit
1c81f05f8d
|
@ -1,13 +1,18 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
import '../../models/filters/timeline_entry_filter.dart';
|
||||
import '../../models/flattened_tree_item.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../services/timeline_entry_filter_service.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/active_profile_selector.dart';
|
||||
import '../../utils/clipboard_utils.dart';
|
||||
import '../../utils/filter_runner.dart';
|
||||
import '../../utils/html_to_edit_text_helper.dart';
|
||||
import '../../utils/responsive_sizes_calculator.dart';
|
||||
import '../../utils/url_opening_utils.dart';
|
||||
|
@ -37,8 +42,9 @@ class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
|||
static final _logger = Logger('$FlattenedTreeEntryControl');
|
||||
|
||||
var showContent = true;
|
||||
|
||||
var showFilteredPost = false;
|
||||
var showComments = false;
|
||||
var isProcessing = false;
|
||||
|
||||
FlattenedTreeItem get item => widget.originalItem;
|
||||
|
||||
|
@ -48,10 +54,11 @@ class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
|||
|
||||
bool get hasComments => entry.engagementSummary.repliesCount > 0;
|
||||
|
||||
bool isProcessing = false;
|
||||
var filteringInfo = FilterResult.show;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
showContent = entry.spoilerText.isEmpty;
|
||||
showComments = isPost ? false : true;
|
||||
}
|
||||
|
@ -59,12 +66,37 @@ class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building ${entry.toShortString()}');
|
||||
final filterService = context
|
||||
.watch<ActiveProfileSelector<TimelineEntryFilterService>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
|
||||
filteringInfo = filterService.checkTimelineEntry(entry);
|
||||
|
||||
const otherPadding = 8.0;
|
||||
final leftPadding = otherPadding + (widget.originalItem.level * 15.0);
|
||||
final color = widget.originalItem.level.isOdd
|
||||
? Theme.of(context).secondaryHeaderColor
|
||||
: Theme.of(context).dialogBackgroundColor;
|
||||
final body = Container(
|
||||
|
||||
if (filteringInfo.isFiltered &&
|
||||
filteringInfo.action == TimelineEntryFilterAction.hide) {
|
||||
return kReleaseMode
|
||||
? const SizedBox()
|
||||
: Container(
|
||||
height: 10,
|
||||
color: Colors.red,
|
||||
);
|
||||
}
|
||||
|
||||
late final Widget body;
|
||||
if (filteringInfo.isFiltered && !showFilteredPost) {
|
||||
body = buildHiddenBody(context, filteringInfo);
|
||||
} else {
|
||||
body = buildMainWidgetBody(context);
|
||||
}
|
||||
|
||||
final bodyCard = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
border: Border.all(width: 0.5),
|
||||
|
@ -73,63 +105,13 @@ class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
|||
BoxShadow(
|
||||
color: Theme.of(context).dividerColor,
|
||||
blurRadius: 2,
|
||||
offset: Offset(4, 4),
|
||||
offset: const Offset(4, 4),
|
||||
spreadRadius: 0.1,
|
||||
blurStyle: BlurStyle.normal,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: StatusHeaderControl(
|
||||
entry: entry,
|
||||
),
|
||||
),
|
||||
buildMenuControl(context),
|
||||
],
|
||||
),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
if (entry.spoilerText.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showContent = !showContent;
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Content Summary: ${entry.spoilerText} (Click to ${showContent ? "Hide" : "Show"}}')),
|
||||
if (showContent) ...[
|
||||
buildBody(context),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
if (entry.linkPreviewData != null)
|
||||
LinkPreviewControl(preview: entry.linkPreviewData!),
|
||||
buildMediaBar(context),
|
||||
],
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
InteractionsBarControl(
|
||||
entry: entry,
|
||||
isMine: item.isMine,
|
||||
showOpenControl: widget.showStatusOpenButton,
|
||||
),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: body,
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
|
@ -138,11 +120,94 @@ class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
|||
top: otherPadding,
|
||||
bottom: otherPadding,
|
||||
),
|
||||
child: body,
|
||||
child: bodyCard,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
Widget buildMainWidgetBody(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: StatusHeaderControl(
|
||||
entry: entry,
|
||||
),
|
||||
),
|
||||
if (filteringInfo.isFiltered)
|
||||
IconButton(
|
||||
onPressed: () => setState(() {
|
||||
showFilteredPost = false;
|
||||
}),
|
||||
icon: const Icon(Icons.hide_source)),
|
||||
buildMenuControl(context),
|
||||
],
|
||||
),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
if (entry.spoilerText.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showContent = !showContent;
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Content Summary: ${entry.spoilerText} (Click to ${showContent ? "Hide" : "Show"}}')),
|
||||
if (showContent) ...[
|
||||
buildContentField(context),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
if (entry.linkPreviewData != null)
|
||||
LinkPreviewControl(preview: entry.linkPreviewData!),
|
||||
buildMediaBar(context),
|
||||
],
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
InteractionsBarControl(
|
||||
entry: entry,
|
||||
isMine: item.isMine,
|
||||
showOpenControl: widget.showStatusOpenButton,
|
||||
),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildHiddenBody(BuildContext context, FilterResult result) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (result.isFiltered &&
|
||||
result.action == TimelineEntryFilterAction.warn)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showFilteredPost = true;
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'${result.trippingFilterName} filtered post. Click to show'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildContentField(BuildContext context) {
|
||||
return HtmlTextViewerControl(
|
||||
content: entry.body,
|
||||
onTapUrl: (url) async =>
|
||||
|
|
|
@ -104,6 +104,7 @@ class _FilterEditorScreenState extends State<FilterEditorScreen> {
|
|||
const Text('Action:'),
|
||||
DropdownMenu<TimelineEntryFilterAction>(
|
||||
initialSelection: action,
|
||||
onSelected: (value) => action = value!,
|
||||
dropdownMenuEntries: TimelineEntryFilterAction.values
|
||||
.map((a) =>
|
||||
DropdownMenuEntry(value: a, label: a.toLabel()))
|
||||
|
@ -396,7 +397,7 @@ class _FilterEditorScreenState extends State<FilterEditorScreen> {
|
|||
content: MultiTriggerAutocomplete(
|
||||
textEditingController: controller,
|
||||
focusNode: focusNode,
|
||||
optionsAlignment: OptionsAlignment.bottomEnd,
|
||||
optionsAlignment: OptionsAlignment.top,
|
||||
autocompleteTriggers: [
|
||||
AutocompleteTrigger(
|
||||
trigger: '@',
|
||||
|
@ -469,7 +470,7 @@ class _FilterEditorScreenState extends State<FilterEditorScreen> {
|
|||
content: MultiTriggerAutocomplete(
|
||||
textEditingController: controller,
|
||||
focusNode: focusNode,
|
||||
optionsAlignment: OptionsAlignment.bottomEnd,
|
||||
optionsAlignment: OptionsAlignment.top,
|
||||
autocompleteTriggers: [
|
||||
AutocompleteTrigger(
|
||||
trigger: '#',
|
||||
|
|
|
@ -90,11 +90,13 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
|||
reshareOriginalPostId = '';
|
||||
}
|
||||
|
||||
final List<dynamic>? tags = json['tags'];
|
||||
if (tags?.isNotEmpty ?? false) {
|
||||
final List<dynamic>? tagsJson = json['tags'];
|
||||
final tags = <String>[];
|
||||
if (tagsJson?.isNotEmpty ?? false) {
|
||||
final tagManager = getIt<HashtagService>();
|
||||
for (final tagJson in tags!) {
|
||||
for (final tagJson in tagsJson!) {
|
||||
final tag = HashtagMastodonExtensions.fromJson(tagJson);
|
||||
tags.add(tag.tag);
|
||||
tagManager.add(tag);
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +123,7 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
|||
parentAuthor: parentAuthor,
|
||||
title: title,
|
||||
links: linkData,
|
||||
tags: tags,
|
||||
mediaAttachments: mediaAttachments,
|
||||
engagementSummary: engagementSummary,
|
||||
linkPreviewData: linkPreviewData,
|
||||
|
|
|
@ -74,7 +74,7 @@ class TimelineEntryFilterService extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
FilterResult checkEntry(TimelineEntry entry) {
|
||||
FilterResult checkTimelineEntry(TimelineEntry entry) {
|
||||
if (entry == _entryCache[entry.id]?.entry) {
|
||||
return _entryCache[entry.id]!.result;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
import 'package:relatica/utils/html_to_edit_text_helper.dart';
|
||||
|
||||
import '../models/filters/string_filter.dart';
|
||||
import '../models/filters/timeline_entry_filter.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
|
||||
class FilterResult {
|
||||
static const show = FilterResult(
|
||||
false,
|
||||
TimelineEntryFilterAction.warn,
|
||||
'',
|
||||
);
|
||||
final TimelineEntryFilterAction action;
|
||||
final bool isFiltered;
|
||||
final String trippingFilterName;
|
||||
|
||||
const FilterResult(this.isFiltered, this.action);
|
||||
const FilterResult(
|
||||
this.isFiltered,
|
||||
this.action,
|
||||
this.trippingFilterName,
|
||||
);
|
||||
|
||||
String toActionString() {
|
||||
return isFiltered ? action.name : 'show';
|
||||
|
@ -18,10 +30,12 @@ class FilterResult {
|
|||
other is FilterResult &&
|
||||
runtimeType == other.runtimeType &&
|
||||
action == other.action &&
|
||||
isFiltered == other.isFiltered;
|
||||
isFiltered == other.isFiltered &&
|
||||
trippingFilterName == other.trippingFilterName;
|
||||
|
||||
@override
|
||||
int get hashCode => action.hashCode ^ isFiltered.hashCode;
|
||||
int get hashCode =>
|
||||
action.hashCode ^ isFiltered.hashCode ^ trippingFilterName.hashCode;
|
||||
}
|
||||
|
||||
FilterResult runFilters(
|
||||
|
@ -30,17 +44,23 @@ FilterResult runFilters(
|
|||
) {
|
||||
var isFiltered = false;
|
||||
var action = TimelineEntryFilterAction.warn;
|
||||
var trippingFilterName = '';
|
||||
for (final filter in filters) {
|
||||
if (filter.isFiltered(entry)) {
|
||||
isFiltered = true;
|
||||
if (trippingFilterName.isEmpty) {
|
||||
trippingFilterName = filter.name;
|
||||
}
|
||||
|
||||
if (filter.action == TimelineEntryFilterAction.hide) {
|
||||
action = TimelineEntryFilterAction.hide;
|
||||
trippingFilterName = filter.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FilterResult(isFiltered, action);
|
||||
return FilterResult(isFiltered, action, trippingFilterName);
|
||||
}
|
||||
|
||||
extension StringFilterOps on StringFilter {
|
||||
|
@ -49,7 +69,10 @@ extension StringFilterOps on StringFilter {
|
|||
case ComparisonType.contains:
|
||||
return value.contains(filterString);
|
||||
case ComparisonType.containsIgnoreCase:
|
||||
return value.toLowerCase().contains(filterString.toLowerCase());
|
||||
final lv = value.toLowerCase();
|
||||
final lf = filterString.toLowerCase();
|
||||
final c = lv.contains(lf);
|
||||
return c;
|
||||
case ComparisonType.equals:
|
||||
return value == filterString;
|
||||
case ComparisonType.equalsIgnoreCase:
|
||||
|
@ -71,7 +94,9 @@ extension TimelineEntryFilterOps on TimelineEntryFilter {
|
|||
|
||||
var authorFiltered = authorFilters.isEmpty ? true : false;
|
||||
for (final filter in authorFilters) {
|
||||
if (filter.isFiltered(entry.authorId)) {
|
||||
if (filter.isFiltered(entry.authorId) ||
|
||||
filter.isFiltered(entry.reshareAuthorId) ||
|
||||
filter.isFiltered(entry.parentAuthorId)) {
|
||||
authorFiltered = true;
|
||||
break;
|
||||
}
|
||||
|
@ -98,8 +123,11 @@ extension TimelineEntryFilterOps on TimelineEntryFilter {
|
|||
}
|
||||
|
||||
var contentFiltered = keywordFilters.isEmpty ? true : false;
|
||||
final simplifiedBody = keywordFilters.isNotEmpty
|
||||
? htmlToSimpleText(entry.body).toLowerCase()
|
||||
: '';
|
||||
for (final filter in keywordFilters) {
|
||||
if (filter.isFiltered(entry.body)) {
|
||||
if (filter.isFiltered(simplifiedBody)) {
|
||||
contentFiltered = true;
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -2,11 +2,15 @@ import 'package:html/dom.dart';
|
|||
import 'package:html/parser.dart';
|
||||
|
||||
String htmlToSimpleText(String htmlContentFragment) {
|
||||
final dom = parseFragment(htmlContentFragment);
|
||||
final segments = dom.nodes
|
||||
.map((n) => n is Element ? n.elementToEditText() : n.nodeToEditText())
|
||||
.toList();
|
||||
return segments.join('');
|
||||
try {
|
||||
final dom = parseFragment(htmlContentFragment);
|
||||
final segments = dom.nodes
|
||||
.map((n) => n is Element ? n.elementToEditText() : n.nodeToEditText())
|
||||
.toList();
|
||||
return segments.join('');
|
||||
} catch (e) {
|
||||
return htmlContentFragment;
|
||||
}
|
||||
}
|
||||
|
||||
extension NodeTextConverter on Node {
|
||||
|
|
|
@ -99,6 +99,7 @@ void main() {
|
|||
expect(filter.isFiltered('hello world'), equals(true));
|
||||
expect(filter.isFiltered('Hello World'), equals(true));
|
||||
expect(filter.isFiltered('hello world'), equals(true));
|
||||
expect(filter.isFiltered('Standard greeting #HelloWorld'), equals(true));
|
||||
expect(filter.isFiltered('help'), equals(false));
|
||||
});
|
||||
});
|
||||
|
|
Ładowanie…
Reference in New Issue