Compare commits

..

2 Commits

19 changed files with 330 additions and 79 deletions

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -24,6 +24,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -12,7 +12,8 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart';
abstract class InAppReviewPlatform extends PlatformInterface {
InAppReviewPlatform() : super(token: _token);
static InAppReviewPlatform _instance = MethodChannelInAppReview();
static InAppReviewPlatform _instance =
MethodChannelInAppReview() as InAppReviewPlatform;
static final Object _token = Object();

View File

@ -156,4 +156,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
COCOAPODS: 1.11.3
COCOAPODS: 1.12.0

View File

@ -23,6 +23,12 @@ class SearchState extends Equatable {
final SearchStatus status;
final SearchParams params;
bool get hasDateFilter =>
params.filters.whereType<DateTimeRangeFilter>().isNotEmpty;
DateTimeRangeFilter? get dateFilter =>
params.filters.whereType<DateTimeRangeFilter>().singleOrNull;
SearchState copyWith({
List<Item>? results,
SearchStatus? status,

View File

@ -30,14 +30,27 @@ class DateTimeRangeFilter implements NumericFilter {
@override
String get query {
if (startTime == null || endTime == null) return '';
final int? startTimestamp = startTime == null
? null
: startTime!.toUtc().millisecondsSinceEpoch ~/ 1000;
final int? endTimestamp = endTime == null
int? endTimestamp = endTime == null
? null
: endTime!.toUtc().millisecondsSinceEpoch ~/ 1000;
if (startTimestamp == endTimestamp) {
endTimestamp = startTime!
.add(const Duration(hours: 24))
.toUtc()
.millisecondsSinceEpoch ~/
1000;
}
if (startTimestamp == null || endTimestamp == null) return '';
final String query =
'''${startTimestamp == null ? '' : 'created_at_i>$startTimestamp'},${endTimestamp == null ? '' : 'created_at_i<$endTimestamp'}''';
'''created_at_i>=$startTimestamp, created_at_i<=$endTimestamp''';
if (query.endsWith(',')) {
return query.replaceFirst(',', '');

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/cubits/cubits.dart';
@ -28,7 +29,39 @@ class SearchScreen extends StatefulWidget {
class _SearchScreenState extends State<SearchScreen> {
final RefreshController refreshController = RefreshController();
final ScrollController scrollController = ScrollController();
final Debouncer debouncer = Debouncer(delay: const Duration(seconds: 1));
bool showChips = true;
bool shouldOffStageChips = false;
static const Duration chipsAnimationDuration = Duration(milliseconds: 300);
@override
void initState() {
super.initState();
scrollController.addListener(() {
if (scrollController.position.userScrollDirection ==
ScrollDirection.reverse &&
showChips) {
setState(() {
showChips = false;
});
} else if (scrollController.position.userScrollDirection ==
ScrollDirection.forward &&
!showChips) {
setState(() {
showChips = true;
});
}
});
}
@override
void dispose() {
refreshController.dispose();
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
@ -72,78 +105,164 @@ class _SearchScreenState extends State<SearchScreen> {
const SizedBox(
height: Dimens.pt6,
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
AnimatedCrossFade(
duration: chipsAnimationDuration,
crossFadeState: showChips
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
firstChild: SizedBox.fromSize(),
secondChild: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const SizedBox(
width: Dimens.pt8,
),
DateTimeRangeFilterChip(
filter: state.params.get<DateTimeRangeFilter>(),
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
onDateTimeRangeRemoved: context
.read<SearchCubit>()
.removeFilter<DateTimeRangeFilter>,
),
const SizedBox(
width: Dimens.pt8,
),
PostedByFilterChip(
filter: state.params.get<PostedByFilter>(),
onChanged:
context.read<SearchCubit>().onPostedByChanged,
),
const SizedBox(
width: Dimens.pt8,
),
CustomChip(
onSelected: (_) =>
context.read<SearchCubit>().onSortToggled(),
selected: state.params.sorted,
label: '''newest first''',
),
const SizedBox(
width: Dimens.pt8,
),
for (final CustomDateTimeRange range
in CustomDateTimeRange.values) ...<Widget>[
CustomRangeFilterChip(
range: range,
onTap: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
if (state.hasDateFilter &&
state.dateFilter?.startTime != null &&
state.dateFilter?.endTime != null)
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.dayBefore(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.dayAfter(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.weekBefore(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.weekAfter(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.monthBefore(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.monthAfter(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
],
),
),
const SizedBox(
width: Dimens.pt8,
),
],
],
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[
for (final TypeTagFilter filter
in TypeTagFilter.all) ...<Widget>[
const SizedBox(
width: Dimens.pt8,
),
CustomChip(
onSelected: (_) =>
context.read<SearchCubit>().onToggled(filter),
selected: context
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[
const SizedBox(
width: Dimens.pt8,
),
DateTimeRangeFilterChip(
filter: state.dateFilter,
initialStartDate: state.dateFilter?.startTime,
initialEndDate: state.dateFilter?.endTime,
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.state
.params
.get<TypeTagFilter>() ==
filter,
label: filter.query,
.onDateTimeRangeUpdated,
onDateTimeRangeRemoved: context
.read<SearchCubit>()
.removeFilter<DateTimeRangeFilter>,
),
const SizedBox(
width: Dimens.pt8,
),
PostedByFilterChip(
filter: state.params.get<PostedByFilter>(),
onChanged: context
.read<SearchCubit>()
.onPostedByChanged,
),
const SizedBox(
width: Dimens.pt8,
),
CustomChip(
onSelected: (_) =>
context.read<SearchCubit>().onSortToggled(),
selected: state.params.sorted,
label: '''newest first''',
),
const SizedBox(
width: Dimens.pt8,
),
for (final CustomDateTimeRange range
in CustomDateTimeRange.values) ...<Widget>[
CustomRangeFilterChip(
range: range,
onTap: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
),
const SizedBox(
width: Dimens.pt8,
),
],
],
),
],
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[
for (final TypeTagFilter filter
in TypeTagFilter.all) ...<Widget>[
const SizedBox(
width: Dimens.pt8,
),
CustomChip(
onSelected: (_) => context
.read<SearchCubit>()
.onToggled(filter),
selected: context
.read<SearchCubit>()
.state
.params
.get<TypeTagFilter>() ==
filter,
label: filter.query,
),
],
],
),
),
],
),
),
@ -200,11 +319,15 @@ class _SearchScreenState extends State<SearchScreen> {
},
),
controller: refreshController,
scrollController: scrollController,
onRefresh: () {},
onLoading: () {
context.read<SearchCubit>().loadMore();
},
child: ListView(
physics: state.results.isEmpty
? const NeverScrollableScrollPhysics()
: null,
children: <Widget>[
...state.results
.map(

View File

@ -6,12 +6,16 @@ import 'package:intl/intl.dart';
class DateTimeRangeFilterChip extends StatelessWidget {
const DateTimeRangeFilterChip({
required this.filter,
required this.initialStartDate,
required this.initialEndDate,
required this.onDateTimeRangeUpdated,
required this.onDateTimeRangeRemoved,
super.key,
});
final DateTimeRangeFilter? filter;
final DateTime? initialStartDate;
final DateTime? initialEndDate;
final void Function(DateTime, DateTime) onDateTimeRangeUpdated;
final VoidCallback onDateTimeRangeRemoved;
@ -25,6 +29,9 @@ class DateTimeRangeFilterChip extends StatelessWidget {
context: context,
firstDate: DateTime.now().subtract(const Duration(days: 20 * 365)),
lastDate: DateTime.now(),
initialDateRange: initialStartDate != null && initialEndDate != null
? DateTimeRange(start: initialStartDate!, end: initialEndDate!)
: null,
).then((DateTimeRange? range) {
if (range != null) {
onDateTimeRangeUpdated(range.start, range.end);
@ -34,11 +41,22 @@ class DateTimeRangeFilterChip extends StatelessWidget {
});
},
selected: filter != null,
label:
'''from ${_formatDateTime(filter?.startTime) ?? 'X'} to ${_formatDateTime(filter?.endTime) ?? 'Y'}''',
label: _label,
);
}
String get _label {
final DateTime? start = filter?.startTime;
final DateTime? end = filter?.endTime;
if (start == null && end == null) {
return '''from X to Y''';
} else if (start == end) {
return '''from ${_formatDateTime(start)}''';
} else {
return '''from ${_formatDateTime(start)} to ${_formatDateTime(end)}''';
}
}
static String? _formatDateTime(DateTime? dateTime) {
if (dateTime == null) return null;

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:hacki/screens/search/widgets/date_time_range_filter_chip.dart';
import 'package:hacki/screens/widgets/widgets.dart' show CustomChip;
typedef Calculator = DateTime Function(DateTime);
/// A set of chips that perform addition or subtraction on the date selected
/// by [DateTimeRangeFilterChip]
class DateTimeShortcutChip extends StatelessWidget {
const DateTimeShortcutChip({
required this.onDateTimeRangeUpdated,
required this.startDate,
required this.endDate,
required this.label,
required Calculator calculator,
super.key,
}) : _calculator = calculator;
DateTimeShortcutChip.dayBefore({
required this.onDateTimeRangeUpdated,
required this.startDate,
required this.endDate,
super.key,
}) : label = '- day',
_calculator =
((DateTime date) => date.subtract(const Duration(hours: 24)));
DateTimeShortcutChip.dayAfter({
required this.onDateTimeRangeUpdated,
required this.startDate,
required this.endDate,
super.key,
}) : label = '+ day',
_calculator = ((DateTime date) => date.add(const Duration(hours: 24)));
DateTimeShortcutChip.weekBefore({
required this.onDateTimeRangeUpdated,
required this.startDate,
required this.endDate,
super.key,
}) : label = '- week',
_calculator =
((DateTime date) => date.subtract(const Duration(days: 7)));
DateTimeShortcutChip.weekAfter({
required this.onDateTimeRangeUpdated,
required this.startDate,
required this.endDate,
super.key,
}) : label = '+ week',
_calculator = ((DateTime date) => date.add(const Duration(days: 7)));
DateTimeShortcutChip.monthBefore({
required this.onDateTimeRangeUpdated,
required this.startDate,
required this.endDate,
super.key,
}) : label = '- 30 days',
_calculator =
((DateTime date) => date.subtract(const Duration(days: 30)));
DateTimeShortcutChip.monthAfter({
required this.onDateTimeRangeUpdated,
required this.startDate,
required this.endDate,
super.key,
}) : label = '+ 30 days',
_calculator = ((DateTime date) => date.add(const Duration(days: 30)));
final void Function(DateTime, DateTime) onDateTimeRangeUpdated;
final DateTime startDate;
final DateTime endDate;
final String label;
final Calculator _calculator;
@override
Widget build(BuildContext context) {
return CustomChip(
onSelected: (bool value) {
final DateTime updatedStartDate = _calculator(startDate);
final DateTime updatedEndDate = _calculator(endDate);
onDateTimeRangeUpdated(updatedStartDate, updatedEndDate);
},
selected: false,
label: label,
);
}
}

View File

@ -1,3 +1,4 @@
export 'custom_range_filter_chip.dart';
export 'date_time_range_filter_chip.dart';
export 'date_time_shortcut_chip.dart';
export 'posted_by_filter_chip.dart';

View File

@ -1390,4 +1390,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.0.0 <4.0.0"
flutter: ">=3.10.1"
flutter: ">=3.10.2"

View File

@ -1,11 +1,11 @@
name: hacki
description: A Hacker News reader.
version: 1.6.0+110
version: 1.7.0+111
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: "3.10.1"
flutter: "3.10.2"
dependencies:
adaptive_theme: ^3.2.0