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"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/> <background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/> <foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon> </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') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View File

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

View File

@ -156,4 +156,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937 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 SearchStatus status;
final SearchParams params; final SearchParams params;
bool get hasDateFilter =>
params.filters.whereType<DateTimeRangeFilter>().isNotEmpty;
DateTimeRangeFilter? get dateFilter =>
params.filters.whereType<DateTimeRangeFilter>().singleOrNull;
SearchState copyWith({ SearchState copyWith({
List<Item>? results, List<Item>? results,
SearchStatus? status, SearchStatus? status,

View File

@ -30,14 +30,27 @@ class DateTimeRangeFilter implements NumericFilter {
@override @override
String get query { String get query {
if (startTime == null || endTime == null) return '';
final int? startTimestamp = startTime == null final int? startTimestamp = startTime == null
? null ? null
: startTime!.toUtc().millisecondsSinceEpoch ~/ 1000; : startTime!.toUtc().millisecondsSinceEpoch ~/ 1000;
final int? endTimestamp = endTime == null int? endTimestamp = endTime == null
? null ? null
: endTime!.toUtc().millisecondsSinceEpoch ~/ 1000; : 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 = 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(',')) { if (query.endsWith(',')) {
return query.replaceFirst(',', ''); return query.replaceFirst(',', '');

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart'; import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
@ -28,7 +29,39 @@ class SearchScreen extends StatefulWidget {
class _SearchScreenState extends State<SearchScreen> { class _SearchScreenState extends State<SearchScreen> {
final RefreshController refreshController = RefreshController(); final RefreshController refreshController = RefreshController();
final ScrollController scrollController = ScrollController();
final Debouncer debouncer = Debouncer(delay: const Duration(seconds: 1)); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -72,6 +105,85 @@ class _SearchScreenState extends State<SearchScreen> {
const SizedBox( const SizedBox(
height: Dimens.pt6, height: Dimens.pt6,
), ),
AnimatedCrossFade(
duration: chipsAnimationDuration,
crossFadeState: showChips
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
firstChild: SizedBox.fromSize(),
secondChild: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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!,
),
],
),
),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
@ -80,7 +192,9 @@ class _SearchScreenState extends State<SearchScreen> {
width: Dimens.pt8, width: Dimens.pt8,
), ),
DateTimeRangeFilterChip( DateTimeRangeFilterChip(
filter: state.params.get<DateTimeRangeFilter>(), filter: state.dateFilter,
initialStartDate: state.dateFilter?.startTime,
initialEndDate: state.dateFilter?.endTime,
onDateTimeRangeUpdated: context onDateTimeRangeUpdated: context
.read<SearchCubit>() .read<SearchCubit>()
.onDateTimeRangeUpdated, .onDateTimeRangeUpdated,
@ -93,8 +207,9 @@ class _SearchScreenState extends State<SearchScreen> {
), ),
PostedByFilterChip( PostedByFilterChip(
filter: state.params.get<PostedByFilter>(), filter: state.params.get<PostedByFilter>(),
onChanged: onChanged: context
context.read<SearchCubit>().onPostedByChanged, .read<SearchCubit>()
.onPostedByChanged,
), ),
const SizedBox( const SizedBox(
width: Dimens.pt8, width: Dimens.pt8,
@ -133,8 +248,9 @@ class _SearchScreenState extends State<SearchScreen> {
width: Dimens.pt8, width: Dimens.pt8,
), ),
CustomChip( CustomChip(
onSelected: (_) => onSelected: (_) => context
context.read<SearchCubit>().onToggled(filter), .read<SearchCubit>()
.onToggled(filter),
selected: context selected: context
.read<SearchCubit>() .read<SearchCubit>()
.state .state
@ -147,6 +263,9 @@ class _SearchScreenState extends State<SearchScreen> {
], ],
), ),
), ),
],
),
),
if (state.status == SearchStatus.loading && if (state.status == SearchStatus.loading &&
state.results.isEmpty) ...<Widget>[ state.results.isEmpty) ...<Widget>[
const SizedBox( const SizedBox(
@ -200,11 +319,15 @@ class _SearchScreenState extends State<SearchScreen> {
}, },
), ),
controller: refreshController, controller: refreshController,
scrollController: scrollController,
onRefresh: () {}, onRefresh: () {},
onLoading: () { onLoading: () {
context.read<SearchCubit>().loadMore(); context.read<SearchCubit>().loadMore();
}, },
child: ListView( child: ListView(
physics: state.results.isEmpty
? const NeverScrollableScrollPhysics()
: null,
children: <Widget>[ children: <Widget>[
...state.results ...state.results
.map( .map(

View File

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

View File

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

View File

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