Compare commits

...

4 Commits

37 changed files with 483 additions and 158 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

@ -78,3 +78,15 @@ abstract class RegExpConstants {
static const String linkSuffix = r'(\)|]|,|\*)(.)*$'; static const String linkSuffix = r'(\)|]|,|\*)(.)*$';
static const String number = '[0-9]+'; static const String number = '[0-9]+';
} }
abstract class Durations {
static const Duration ms100 = Duration(milliseconds: 100);
static const Duration ms200 = Duration(milliseconds: 200);
static const Duration ms300 = Duration(milliseconds: 300);
static const Duration ms400 = Duration(milliseconds: 400);
static const Duration ms500 = Duration(milliseconds: 500);
static const Duration ms600 = Duration(milliseconds: 600);
static const Duration oneSecond = Duration(seconds: 1);
static const Duration twoSeconds = Duration(seconds: 2);
static const Duration tenSeconds = Duration(seconds: 10);
}

View File

@ -5,6 +5,7 @@ import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/main.dart'; import 'package:hacki/main.dart';
@ -348,6 +349,7 @@ class CommentsCubit extends Cubit<CommentsState> {
init(useCommentCache: true); init(useCommentCache: true);
} }
/// Jump to next root level comment.
void jump( void jump(
ItemScrollController itemScrollController, ItemScrollController itemScrollController,
ItemPositionsListener itemPositionsListener, ItemPositionsListener itemPositionsListener,
@ -378,13 +380,14 @@ class CommentsCubit extends Cubit<CommentsState> {
itemScrollController.scrollTo( itemScrollController.scrollTo(
index: i + 1, index: i + 1,
alignment: 0.15, alignment: 0.15,
duration: const Duration(milliseconds: 400), duration: Durations.ms400,
); );
return; return;
} }
} }
} }
/// Jump to previous root level comment.
void jumpUp( void jumpUp(
ItemScrollController itemScrollController, ItemScrollController itemScrollController,
ItemPositionsListener itemPositionsListener, ItemPositionsListener itemPositionsListener,
@ -416,7 +419,7 @@ class CommentsCubit extends Cubit<CommentsState> {
itemScrollController.scrollTo( itemScrollController.scrollTo(
index: i + 1, index: i + 1,
alignment: 0.15, alignment: 0.15,
duration: const Duration(milliseconds: 400), duration: Durations.ms400,
); );
return; return;
} }

View File

@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -11,7 +12,7 @@ part 'edit_state.dart';
class EditCubit extends HydratedCubit<EditState> { class EditCubit extends HydratedCubit<EditState> {
EditCubit({DraftCache? draftCache}) EditCubit({DraftCache? draftCache})
: _draftCache = draftCache ?? locator.get<DraftCache>(), : _draftCache = draftCache ?? locator.get<DraftCache>(),
_debouncer = Debouncer(delay: const Duration(seconds: 1)), _debouncer = Debouncer(delay: Durations.oneSecond),
super(const EditState.init()); super(const EditState.init());
final DraftCache _draftCache; final DraftCache _draftCache;

View File

@ -4,6 +4,7 @@ import 'dart:math';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -31,7 +32,7 @@ class NotificationCubit extends Cubit<NotificationState> {
if (authState.isLoggedIn && authState.username != _username) { if (authState.isLoggedIn && authState.username != _username) {
// Get the user setting. // Get the user setting.
if (_preferenceCubit.state.notificationEnabled) { if (_preferenceCubit.state.notificationEnabled) {
Future<void>.delayed(const Duration(seconds: 2), init); Future<void>.delayed(Durations.twoSeconds, init);
} }
// Listen for setting changes in the future. // Listen for setting changes in the future.

View File

@ -20,9 +20,6 @@ class PostCubit extends Cubit<PostState> {
text: text, text: text,
); );
// final successful =
// await Future<bool>.delayed(const Duration(seconds: 2), () => true);
if (successful) { if (successful) {
emit(state.copyWith(status: PostStatus.successful)); emit(state.copyWith(status: PostStatus.successful));
} else { } else {

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

@ -7,6 +7,7 @@ import 'package:equatable/equatable.dart';
import 'package:feature_discovery/feature_discovery.dart'; import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
@ -268,8 +269,8 @@ class HackiApp extends StatelessWidget {
AsyncSnapshot<AdaptiveThemeMode?> snapshot, AsyncSnapshot<AdaptiveThemeMode?> snapshot,
) { ) {
final AdaptiveThemeMode? mode = snapshot.data; final AdaptiveThemeMode? mode = snapshot.data;
ThemeUtil.updateAndroidStatusBarSetting( ThemeUtil.updateStatusBarSetting(
Theme.of(context).brightness, SchedulerBinding.instance.platformDispatcher.platformBrightness,
mode, mode,
); );
return BlocBuilder<PreferenceCubit, PreferenceState>( return BlocBuilder<PreferenceCubit, PreferenceState>(

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

@ -57,7 +57,7 @@ class _HomeScreenState extends State<HomeScreen>
DeviceScreenType.mobile) { DeviceScreenType.mobile) {
locator.get<Logger>().i('resetting comments in CommentCache'); locator.get<Logger>().i('resetting comments in CommentCache');
Future<void>.delayed( Future<void>.delayed(
const Duration(milliseconds: 500), Durations.ms500,
locator.get<CommentCache>().resetComments, locator.get<CommentCache>().resetComments,
); );
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart' hide Badge; import 'package:flutter/material.dart' hide Badge;
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
@ -36,7 +37,7 @@ class TabletHomeScreen extends StatelessWidget {
top: Dimens.zero, top: Dimens.zero,
bottom: Dimens.zero, bottom: Dimens.zero,
width: homeScreenWidth, width: homeScreenWidth,
duration: const Duration(milliseconds: 300), duration: Durations.ms300,
curve: Curves.elasticOut, curve: Curves.elasticOut,
child: homeScreen, child: homeScreen,
), ),
@ -52,7 +53,7 @@ class TabletHomeScreen extends StatelessWidget {
top: Dimens.zero, top: Dimens.zero,
bottom: Dimens.zero, bottom: Dimens.zero,
left: state.expanded ? Dimens.zero : homeScreenWidth, left: state.expanded ? Dimens.zero : homeScreenWidth,
duration: const Duration(milliseconds: 300), duration: Durations.ms300,
curve: Curves.elasticOut, curve: Curves.elasticOut,
child: const _TabletStoryView(), child: const _TabletStoryView(),
), ),

View File

@ -153,9 +153,9 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
); );
final GlobalKey fontSizeIconButtonKey = GlobalKey(); final GlobalKey fontSizeIconButtonKey = GlobalKey();
static const Duration _storyLinkTapThrottleDelay = Duration(seconds: 2); static const Duration _storyLinkTapThrottleDelay = Durations.twoSeconds;
static const Duration _featureDiscoveryDismissThrottleDelay = static const Duration _featureDiscoveryDismissThrottleDelay =
Duration(seconds: 1); Durations.oneSecond;
@override @override
void didPop() { void didPop() {

View File

@ -139,6 +139,7 @@ class MainView extends StatelessWidget {
}, },
onMoreTapped: onMoreTapped, onMoreTapped: onMoreTapped,
onRightMoreTapped: onRightMoreTapped, onRightMoreTapped: onRightMoreTapped,
itemScrollController: itemScrollController,
), ),
); );
}, },

View File

@ -2,6 +2,7 @@ import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/item/item.dart'; import 'package:hacki/models/item/item.dart';
@ -60,7 +61,7 @@ class _ReplyBoxState extends State<ReplyBox> {
), ),
child: AnimatedContainer( child: AnimatedContainer(
height: expanded ? expandedHeight : _collapsedHeight, height: expanded ? expandedHeight : _collapsedHeight,
duration: const Duration(milliseconds: 200), duration: Durations.ms200,
decoration: BoxDecoration( decoration: BoxDecoration(
boxShadow: <BoxShadow>[ boxShadow: <BoxShadow>[
if (!context.read<SplitViewCubit>().state.enabled) if (!context.read<SplitViewCubit>().state.enabled)
@ -79,7 +80,7 @@ class _ReplyBoxState extends State<ReplyBox> {
), ),
AnimatedContainer( AnimatedContainer(
height: expanded ? Dimens.pt36 : Dimens.zero, height: expanded ? Dimens.pt36 : Dimens.zero,
duration: const Duration(milliseconds: 200), duration: Durations.ms200,
), ),
Row( Row(
children: <Widget>[ children: <Widget>[
@ -107,7 +108,7 @@ class _ReplyBoxState extends State<ReplyBox> {
AnimatedOpacity( AnimatedOpacity(
opacity: opacity:
expanded ? NumSwitch.on : NumSwitch.off, expanded ? NumSwitch.on : NumSwitch.off,
duration: const Duration(milliseconds: 300), duration: Durations.ms300,
child: IconButton( child: IconButton(
key: const Key('quote'), key: const Key('quote'),
icon: const Icon( icon: const Icon(

View File

@ -29,7 +29,7 @@ class _ProfileScreenState extends State<ProfileScreen>
final RefreshController refreshControllerFav = RefreshController(); final RefreshController refreshControllerFav = RefreshController();
final RefreshController refreshControllerNotification = RefreshController(); final RefreshController refreshControllerNotification = RefreshController();
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final Throttle throttle = Throttle(delay: const Duration(seconds: 2)); final Throttle throttle = Throttle(delay: Durations.twoSeconds);
PageType pageType = PageType.notification; PageType pageType = PageType.notification;

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart';
@ -404,8 +405,9 @@ class _SettingsState extends State<Settings> {
AdaptiveTheme.of(context).setSystem(); AdaptiveTheme.of(context).setSystem();
} }
final Brightness brightness = Theme.of(context).brightness; final Brightness brightness =
ThemeUtil.updateAndroidStatusBarSetting(brightness, val); SchedulerBinding.instance.platformDispatcher.platformBrightness;
ThemeUtil.updateStatusBarSetting(brightness, val);
} }
void showClearCacheDialog() { void showClearCacheDialog() {

View File

@ -1,6 +1,8 @@
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/config/constants.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -28,7 +30,39 @@ class SearchScreen extends StatefulWidget {
class _SearchScreenState extends State<SearchScreen> { class _SearchScreenState extends State<SearchScreen> {
final RefreshController refreshController = RefreshController(); final RefreshController refreshController = RefreshController();
final Debouncer debouncer = Debouncer(delay: const Duration(seconds: 1)); final ScrollController scrollController = ScrollController();
final Debouncer debouncer = Debouncer(delay: Durations.oneSecond);
bool showChips = true;
bool shouldOffStageChips = false;
static const Duration chipsAnimationDuration = Durations.ms300;
@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,78 +106,164 @@ class _SearchScreenState extends State<SearchScreen> {
const SizedBox( const SizedBox(
height: Dimens.pt6, height: Dimens.pt6,
), ),
SingleChildScrollView( AnimatedCrossFade(
scrollDirection: Axis.horizontal, duration: chipsAnimationDuration,
child: Row( crossFadeState: showChips
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
firstChild: SizedBox.fromSize(),
secondChild: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
const SizedBox( if (state.hasDateFilter &&
width: Dimens.pt8, state.dateFilter?.startTime != null &&
), state.dateFilter?.endTime != null)
DateTimeRangeFilterChip( SingleChildScrollView(
filter: state.params.get<DateTimeRangeFilter>(), scrollDirection: Axis.horizontal,
onDateTimeRangeUpdated: context child: Row(
.read<SearchCubit>() children: <Widget>[
.onDateTimeRangeUpdated, const SizedBox(
onDateTimeRangeRemoved: context width: Dimens.pt8,
.read<SearchCubit>() ),
.removeFilter<DateTimeRangeFilter>, DateTimeShortcutChip.dayBefore(
), onDateTimeRangeUpdated: context
const SizedBox( .read<SearchCubit>()
width: Dimens.pt8, .onDateTimeRangeUpdated,
), startDate: state.dateFilter!.startTime!,
PostedByFilterChip( endDate: state.dateFilter!.endTime!,
filter: state.params.get<PostedByFilter>(), ),
onChanged: const SizedBox(
context.read<SearchCubit>().onPostedByChanged, width: Dimens.pt8,
), ),
const SizedBox( DateTimeShortcutChip.dayAfter(
width: Dimens.pt8, onDateTimeRangeUpdated: context
), .read<SearchCubit>()
CustomChip( .onDateTimeRangeUpdated,
onSelected: (_) => startDate: state.dateFilter!.startTime!,
context.read<SearchCubit>().onSortToggled(), endDate: state.dateFilter!.endTime!,
selected: state.params.sorted, ),
label: '''newest first''', const SizedBox(
), width: Dimens.pt8,
const SizedBox( ),
width: Dimens.pt8, DateTimeShortcutChip.weekBefore(
), onDateTimeRangeUpdated: context
for (final CustomDateTimeRange range .read<SearchCubit>()
in CustomDateTimeRange.values) ...<Widget>[ .onDateTimeRangeUpdated,
CustomRangeFilterChip( startDate: state.dateFilter!.startTime!,
range: range, endDate: state.dateFilter!.endTime!,
onTap: context ),
.read<SearchCubit>() const SizedBox(
.onDateTimeRangeUpdated, 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( SingleChildScrollView(
width: Dimens.pt8, scrollDirection: Axis.horizontal,
), child: Row(
], children: <Widget>[
], const SizedBox(
), width: Dimens.pt8,
), ),
SingleChildScrollView( DateTimeRangeFilterChip(
scrollDirection: Axis.horizontal, filter: state.dateFilter,
child: Row( initialStartDate: state.dateFilter?.startTime,
children: <Widget>[ initialEndDate: state.dateFilter?.endTime,
for (final TypeTagFilter filter onDateTimeRangeUpdated: context
in TypeTagFilter.all) ...<Widget>[
const SizedBox(
width: Dimens.pt8,
),
CustomChip(
onSelected: (_) =>
context.read<SearchCubit>().onToggled(filter),
selected: context
.read<SearchCubit>() .read<SearchCubit>()
.state .onDateTimeRangeUpdated,
.params onDateTimeRangeRemoved: context
.get<TypeTagFilter>() == .read<SearchCubit>()
filter, .removeFilter<DateTimeRangeFilter>,
label: filter.query, ),
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 +320,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

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -9,6 +10,7 @@ import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class CommentTile extends StatelessWidget { class CommentTile extends StatelessWidget {
const CommentTile({ const CommentTile({
@ -23,6 +25,7 @@ class CommentTile extends StatelessWidget {
this.actionable = true, this.actionable = true,
this.level = 0, this.level = 0,
this.onTap, this.onTap,
this.itemScrollController,
}); });
final String? opUsername; final String? opUsername;
@ -30,6 +33,7 @@ class CommentTile extends StatelessWidget {
final int level; final int level;
final bool actionable; final bool actionable;
final FetchMode fetchMode; final FetchMode fetchMode;
final ItemScrollController? itemScrollController;
final void Function(Comment)? onReplyTapped; final void Function(Comment)? onReplyTapped;
final void Function(Comment, Rect?)? onMoreTapped; final void Function(Comment, Rect?)? onMoreTapped;
@ -116,8 +120,7 @@ class CommentTile extends StatelessWidget {
child: InkWell( child: InkWell(
onTap: () { onTap: () {
if (actionable) { if (actionable) {
HapticFeedbackUtil.selection(); _collapse(context);
context.read<CollapseCubit>().collapse();
} else { } else {
onTap?.call(); onTap?.call();
} }
@ -159,7 +162,7 @@ class CommentTile extends StatelessWidget {
), ),
), ),
AnimatedSize( AnimatedSize(
duration: const Duration(milliseconds: 200), duration: Durations.ms200,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@ -339,8 +342,26 @@ class CommentTile extends StatelessWidget {
void _onTextTapped(BuildContext context) { void _onTextTapped(BuildContext context) {
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) { if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
HapticFeedbackUtil.selection(); _collapse(context);
context.read<CollapseCubit>().collapse(); }
}
void _collapse(BuildContext context) {
HapticFeedbackUtil.selection();
context.read<CollapseCubit>().collapse();
if (context.read<CollapseCubit>().state.collapsed) {
Future<void>.delayed(
Durations.ms300,
() {
itemScrollController?.scrollTo(
index:
context.read<CommentsCubit>().state.comments.indexOf(comment) +
1,
alignment: 0.1,
duration: Durations.ms300,
);
},
);
} }
} }
} }

View File

@ -66,7 +66,7 @@ class _CustomTabBarState extends State<CustomTabBar> {
currentIndex == i ? TextDimens.pt14 : TextDimens.pt10, currentIndex == i ? TextDimens.pt14 : TextDimens.pt10,
color: currentIndex == i ? Palette.orange : Palette.grey, color: currentIndex == i ? Palette.orange : Palette.grey,
), ),
duration: const Duration(milliseconds: 200), duration: Durations.ms200,
child: Text( child: Text(
state.tabs.elementAt(i).label, state.tabs.elementAt(i).label,
key: ValueKey<String>( key: ValueKey<String>(

View File

@ -233,7 +233,7 @@ class _LinkPreviewState extends State<LinkPreview> {
secondChild: loadedWidget, secondChild: loadedWidget,
crossFadeState: crossFadeState:
_loading ? CrossFadeState.showFirst : CrossFadeState.showSecond, _loading ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: const Duration(milliseconds: 500), duration: Durations.ms500,
); );
} }
} }

View File

@ -15,7 +15,7 @@ class _OnboardingViewState extends State<OnboardingView> {
final PageController pageController = PageController(); final PageController pageController = PageController();
final Throttle throttle = Throttle(delay: _throttleDelay); final Throttle throttle = Throttle(delay: _throttleDelay);
static const Duration _throttleDelay = Duration(milliseconds: 100); static const Duration _throttleDelay = Durations.ms100;
static const double _screenshotHeight = 550; static const double _screenshotHeight = 550;
@override @override
@ -80,7 +80,7 @@ class _OnboardingViewState extends State<OnboardingView> {
} else { } else {
throttle.run(() { throttle.run(() {
pageController.nextPage( pageController.nextPage(
duration: const Duration(milliseconds: 600), duration: Durations.ms600,
curve: SpringCurve.underDamped, curve: SpringCurve.underDamped,
); );
}); });

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart';
class TapDownWrapper extends StatefulWidget { class TapDownWrapper extends StatefulWidget {
const TapDownWrapper({ const TapDownWrapper({
@ -22,7 +23,7 @@ class _TapDownWrapperState extends State<TapDownWrapper>
@override @override
void initState() { void initState() {
controller = AnimationController( controller = AnimationController(
duration: const Duration(milliseconds: 100), duration: Durations.ms100,
vsync: this, vsync: this,
); );

View File

@ -5,6 +5,7 @@ import 'dart:io';
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:fast_gbk/fast_gbk.dart'; import 'package:fast_gbk/fast_gbk.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -321,7 +322,7 @@ class WebAnalyzer {
final Uri uri = Uri.parse(url); final Uri uri = Uri.parse(url);
final HttpClient ioClient = HttpClient() final HttpClient ioClient = HttpClient()
..badCertificateCallback = _certificateCheck ..badCertificateCallback = _certificateCheck
..connectionTimeout = const Duration(seconds: 2); ..connectionTimeout = Durations.twoSeconds;
final IOClient client = IOClient(ioClient); final IOClient client = IOClient(ioClient);
final BaseRequest request = Request('GET', uri) final BaseRequest request = Request('GET', uri)
..followRedirects = true ..followRedirects = true
@ -337,7 +338,7 @@ class WebAnalyzer {
try { try {
final IOStreamedResponse stream = final IOStreamedResponse stream =
await client.send(request).timeout(const Duration(seconds: 10)); await client.send(request).timeout(Durations.tenSeconds);
if (stream.statusCode == HttpStatus.movedTemporarily || if (stream.statusCode == HttpStatus.movedTemporarily ||
stream.statusCode == HttpStatus.movedPermanently) { stream.statusCode == HttpStatus.movedPermanently) {

View File

@ -1,61 +1,92 @@
import 'dart:io'; import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
abstract class ThemeUtil { abstract class ThemeUtil {
/// Temp fix for the issue: static Future<void> updateStatusBarSetting(
/// https://github.com/flutter/flutter/issues/119465
static Future<void> updateAndroidStatusBarSetting(
Brightness brightness, Brightness brightness,
AdaptiveThemeMode? mode, AdaptiveThemeMode? mode,
) async { ) async {
if (Platform.isAndroid == false) return; if (Platform.isAndroid) {
switch (mode) {
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); case AdaptiveThemeMode.light:
final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo; SystemChrome.setSystemUIOverlayStyle(
final int sdk = androidInfo.version.sdkInt; const SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
if (sdk > 28) return; statusBarIconBrightness: Brightness.dark,
switch (mode) { statusBarColor: Palette.transparent,
case AdaptiveThemeMode.light: ),
SystemChrome.setSystemUIOverlayStyle( );
const SystemUiOverlayStyle( case AdaptiveThemeMode.dark:
statusBarBrightness: Brightness.dark, SystemChrome.setSystemUIOverlayStyle(
statusBarIconBrightness: Brightness.dark, const SystemUiOverlayStyle(
statusBarColor: Palette.transparent, statusBarBrightness: Brightness.light,
), statusBarIconBrightness: Brightness.light,
); statusBarColor: Palette.transparent,
case AdaptiveThemeMode.dark: ),
SystemChrome.setSystemUIOverlayStyle( );
const SystemUiOverlayStyle( case AdaptiveThemeMode.system:
statusBarBrightness: Brightness.light, case null:
statusBarIconBrightness: Brightness.light, switch (brightness) {
statusBarColor: Palette.transparent, case Brightness.light:
), SystemChrome.setSystemUIOverlayStyle(
); const SystemUiOverlayStyle(
case AdaptiveThemeMode.system: statusBarBrightness: Brightness.dark,
case null: statusBarIconBrightness: Brightness.dark,
switch (brightness) { statusBarColor: Palette.transparent,
case Brightness.light: ),
SystemChrome.setSystemUIOverlayStyle( );
const SystemUiOverlayStyle( case Brightness.dark:
statusBarBrightness: Brightness.dark, SystemChrome.setSystemUIOverlayStyle(
statusBarIconBrightness: Brightness.dark, const SystemUiOverlayStyle(
statusBarColor: Palette.transparent, statusBarBrightness: Brightness.light,
), statusBarIconBrightness: Brightness.light,
); statusBarColor: Palette.transparent,
case Brightness.dark: ),
SystemChrome.setSystemUIOverlayStyle( );
const SystemUiOverlayStyle( }
statusBarBrightness: Brightness.light, }
statusBarIconBrightness: Brightness.light, } else {
statusBarColor: Palette.transparent, switch (mode) {
), case AdaptiveThemeMode.light:
); SystemChrome.setSystemUIOverlayStyle(
} const SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
statusBarColor: Palette.transparent,
),
);
case AdaptiveThemeMode.dark:
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
statusBarColor: Palette.transparent,
),
);
case AdaptiveThemeMode.system:
case null:
switch (brightness) {
case Brightness.light:
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
statusBarColor: Palette.transparent,
),
);
case Brightness.dark:
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
statusBarColor: Palette.transparent,
),
);
}
}
} }
} }
} }

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.3"

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.1+112
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.3"
dependencies: dependencies:
adaptive_theme: ^3.2.0 adaptive_theme: ^3.2.0