Add initial filter card/hiding work for timeline entries

codemagic-setup
Hank Grabowski 2023-05-08 07:18:09 -04:00
rodzic 6644fee523
commit 1c81f05f8d
7 zmienionych plików z 177 dodań i 75 usunięć

Wyświetl plik

@ -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 =>

Wyświetl plik

@ -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: '#',

Wyświetl plik

@ -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,

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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));
});
});