Skip to content

Commit

Permalink
Merge pull request #586 from verdigado/212-news-filter
Browse files Browse the repository at this point in the history
212: Add news search and filter
  • Loading branch information
steffenkleinle authored Feb 7, 2025
2 parents 4569441 + 6aec645 commit 705428c
Show file tree
Hide file tree
Showing 31 changed files with 853 additions and 104 deletions.
3 changes: 2 additions & 1 deletion lib/app/constants/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class Routes {
child: MainLayout(child: NewsDetailScreen(newsId: state.pathParameters['newsId']!)),
),
);
static GoRoute news = buildRoute('/news', t.news.news, NewsScreen(), routes: [newsDetail]);
static GoRoute news =
buildRoute('/news', t.news.news, NewsScreenContainer(), routes: [newsDetail], withMainLayout: false);
static GoRoute campaigns = buildRoute('/campaigns', t.campaigns.campaigns, CampaignsScreen());
static GoRoute profiles = buildRoute('/profiles', t.profiles.profiles, OwnProfileScreen());
static GoRoute mfaTokenScan = buildRoute('token-scan', t.mfa.tokenScan.title, TokenScanScreen());
Expand Down
18 changes: 0 additions & 18 deletions lib/app/models/division_model.dart

This file was deleted.

9 changes: 9 additions & 0 deletions lib/app/screens/future_loading_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ class _FutureLoadingScreenState<T> extends State<FutureLoadingScreen<T>> {
_data = widget.load();
}

@override
void didUpdateWidget(covariant FutureLoadingScreen<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.load != oldWidget.load) {
setState(() {});
_data = widget.load();
}
}

@override
Widget build(BuildContext context) {
final layoutBuilder = widget.layoutBuilder ?? (Widget child) => child;
Expand Down
5 changes: 4 additions & 1 deletion lib/app/services/access_token_authenticator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ class AccessTokenAuthenticator implements Authenticator {

@override
FutureOr<Request?> authenticate(Request request, Response<dynamic> response, [Request? originalRequest]) async {
logger.d('${request.method} ${request.baseUri} ${request.uri}, ${request.parameters}, body: ${request.body}');
logger.d('${request.method} ${request.url}');
if (request.body != null) {
logger.d('Body: ${request.body}');
}
logger.d('Response: ${response.statusCode}');

if (response.statusCode == HttpStatus.unauthorized) {
Expand Down
1 change: 1 addition & 0 deletions lib/app/theme/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,5 @@ final ThemeData appTheme = ThemeData.light().copyWith(
space: 0.5,
thickness: 0.5,
),
datePickerTheme: DatePickerThemeData(rangeSelectionBackgroundColor: ThemeColors.textDisabled),
);
5 changes: 5 additions & 0 deletions lib/app/utils/date.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'dart:io';

import 'package:intl/intl.dart';

final dateFormatter = DateFormat.yMd(Platform.localeName);
16 changes: 16 additions & 0 deletions lib/app/utils/debouncer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'dart:async';
import 'package:flutter/material.dart';

class Debouncer {
final int milliseconds;
Timer? _timer;

Debouncer({this.milliseconds = 250});

void run(VoidCallback action) {
if (_timer?.isActive ?? false) {
_timer?.cancel();
}
_timer = Timer(Duration(milliseconds: milliseconds), action);
}
}
15 changes: 15 additions & 0 deletions lib/app/utils/divisions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart';

extension DivisionExtension on Division {
String shortDisplayName() => level == DivisionLevel.bv ? name2 : shortName;
}

extension DivisionFilter on Iterable<Division> {
List<Division> filterByLevel(DivisionLevel level) {
final filtered = where((division) => division.level == level).toList();
filtered.sort((a, b) => a.name2.compareTo(b.name2));
return filtered;
}

Division bundesverband() => firstWhere((it) => it.divisionKey == '10000000');
}
19 changes: 19 additions & 0 deletions lib/app/utils/utils.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';

extension IterableX<T> on Iterable<T> {
T? firstWhereOrNull(bool Function(T) test) {
for (var item in this) {
Expand All @@ -8,3 +10,20 @@ extension IterableX<T> on Iterable<T> {
return null;
}
}

extension IsBetween on DateTime {
bool isBetween(DateTimeRange dateRange) {
final safeEndDate = dateRange.end.copyWith(day: dateRange.end.day + 1);
return !dateRange.start.isAfter(this) && safeEndDate.isAfter(this);
}
}

extension ContainsAny<T> on List<T> {
bool containsAny(List<T> other) {
return any((element) => other.contains(element));
}
}

extension WithDividers on Iterable<Widget> {
List<Widget> withDividers([Widget? divider]) => expand((item) => [item, Divider()]).toList()..removeLast();
}
62 changes: 62 additions & 0 deletions lib/app/widgets/date_range_picker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:gruene_app/app/utils/date.dart';
import 'package:gruene_app/i18n/translations.g.dart';

class DateRangePicker extends StatelessWidget {
final void Function(DateTimeRange?) setDateRange;
final DateTimeRange? dateRange;

const DateRangePicker({
super.key,
required this.setDateRange,
required this.dateRange,
});

Future<void> openDateRangePicker(BuildContext context) async {
final theme = Theme.of(context);
final newDateRange = await showDateRangePicker(
context: context,
firstDate: DateTime(1980),
lastDate: DateTime.now(),
initialDateRange: dateRange,
barrierColor: theme.colorScheme.secondary,
);
setDateRange(newDateRange);
}

String formatDate(DateTime? date) => date == null ? '–' : dateFormatter.format(date.toLocal());

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
children: [
SizedBox(width: 24),
Text(t.common.dateFrom),
SizedBox(width: 16),
TextButton(
onPressed: () => openDateRangePicker(context),
style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.surfaceDim,
side: BorderSide(color: theme.colorScheme.surfaceDim),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
child: SizedBox(width: 84, child: Center(child: Text(formatDate(dateRange?.start)))),
),
SizedBox(width: 16),
Text(t.common.dateUntil),
SizedBox(width: 16),
TextButton(
onPressed: () => openDateRangePicker(context),
style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.surfaceDim,
side: BorderSide(color: theme.colorScheme.surfaceDim),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
child: SizedBox(width: 84, child: Center(child: Text(formatDate(dateRange?.end)))),
),
SizedBox(width: 24),
],
);
}
}
26 changes: 26 additions & 0 deletions lib/app/widgets/expansion_list_tile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:gruene_app/app/utils/utils.dart';

class ExpansionListTile extends StatelessWidget {
final List<Widget> children;
final String title;

const ExpansionListTile({super.key, required this.children, required this.title});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24),
color: theme.colorScheme.surface,
child: ExpansionTile(
shape: const Border(),
title: Text(title),
tilePadding: const EdgeInsets.all(0),
backgroundColor: theme.colorScheme.surface,
collapsedBackgroundColor: theme.colorScheme.surface,
children: children.withDividers(),
),
);
}
}
27 changes: 27 additions & 0 deletions lib/app/widgets/full_screen_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';

class FullScreenDialog extends StatelessWidget {
final Widget? child;
final List<Widget>? appBarActions;

const FullScreenDialog({super.key, this.child, this.appBarActions});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.surfaceDim,
appBar: AppBar(
backgroundColor: theme.colorScheme.surfaceDim,
surfaceTintColor: theme.colorScheme.surfaceDim,
leading: IconButton(icon: Icon(Icons.close), onPressed: Navigator.of(context).pop),
actions: appBarActions,
),
body: child,
);
}
}

void showFullScreenDialog(BuildContext context, WidgetBuilder builder) {
Navigator.of(context).push(MaterialPageRoute<void>(fullscreenDialog: true, builder: builder));
}
47 changes: 47 additions & 0 deletions lib/app/widgets/rounded_icon_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';

class RoundedIconButton extends StatelessWidget {
final void Function() onPressed;
final IconData icon;
final Color iconColor;
final Color backgroundColor;
final bool selected;
final double width;
final double height;

const RoundedIconButton({
super.key,
required this.onPressed,
required this.icon,
required this.iconColor,
required this.backgroundColor,
this.selected = false,
this.width = 48,
this.height = 48,
});

@override
Widget build(BuildContext context) {
return Container(
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: selected ? backgroundColor : iconColor, width: 1),
),
),
child: Container(
width: width,
height: height,
decoration: ShapeDecoration(
color: selected ? iconColor : backgroundColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: IconButton(
icon: Icon(icon, color: selected ? backgroundColor : iconColor),
onPressed: onPressed,
style: IconButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
),
),
);
}
}
52 changes: 52 additions & 0 deletions lib/app/widgets/search_bar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:gruene_app/app/theme/theme.dart';
import 'package:gruene_app/app/utils/debouncer.dart';
import 'package:gruene_app/i18n/translations.g.dart';

class CustomSearchBar extends StatefulWidget {
final void Function(String selected) setQuery;
final String query;

const CustomSearchBar({super.key, required this.setQuery, required this.query});

@override
State<CustomSearchBar> createState() => _CustomSearchBarState();
}

class _CustomSearchBarState extends State<CustomSearchBar> {
final Debouncer _debouncer = Debouncer();
final _controller = TextEditingController();

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SearchBar(
controller: _controller,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onChanged: (query) => _debouncer.run(() => widget.setQuery(query)),
leading: Icon(Icons.search_outlined, color: ThemeColors.textDisabled),
hintText: t.common.search,
hintStyle: WidgetStatePropertyAll(const TextStyle(color: ThemeColors.textDisabled)),
trailing: widget.query.isNotEmpty
? [
IconButton(
onPressed: () {
_controller.clear();
widget.setQuery('');
},
icon: Icon(Icons.clear, color: ThemeColors.textDisabled),
),
]
: [],
backgroundColor: WidgetStatePropertyAll(theme.colorScheme.surface),
padding: WidgetStatePropertyAll(EdgeInsets.only(left: 8)),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: ThemeColors.textDisabled),
),
),
elevation: WidgetStatePropertyAll(0),
);
}
}
37 changes: 37 additions & 0 deletions lib/app/widgets/selection_list_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:gruene_app/app/theme/theme.dart';
import 'package:gruene_app/app/widgets/text_list_item.dart';

class SelectionListItem extends StatelessWidget {
final void Function() toggleSelection;
final String title;
final bool selected;

const SelectionListItem({super.key, required this.title, required this.toggleSelection, required this.selected});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return TextListItem(
onPress: toggleSelection,
title: title,
trailing: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: selected ? theme.colorScheme.secondary : theme.colorScheme.surface,
border: Border.all(width: 2, color: selected ? theme.colorScheme.secondary : ThemeColors.textDisabled),
),
child: Checkbox(
value: selected,
checkColor: theme.colorScheme.surface,
activeColor: theme.colorScheme.secondary,
onChanged: (_) => toggleSelection(),
shape: CircleBorder(),
side: BorderSide.none,
),
),
);
}
}
Loading

0 comments on commit 705428c

Please sign in to comment.