Compare commits

...

7 Commits

Author SHA1 Message Date
f03b45a98a update pubspec.lock (#255) 2023-09-17 17:59:29 -07:00
cbe5bba986 bump flutter version. (#254) 2023-09-17 17:38:44 -07:00
268f4054a3 improve story marking. (#253) 2023-09-11 20:42:33 -07:00
988c5d9881 add haptic feedback. (#252) 2023-09-11 18:08:21 -07:00
e748e2f818 allow swipe gesture in fav screen. (#251) 2023-09-11 17:01:42 -07:00
1b0a0dbda9 add changelog. (#250) 2023-09-11 15:22:32 -07:00
64d68389ba migrate from Navigator to GoRouter (#249) 2023-09-10 22:26:46 -07:00
36 changed files with 483 additions and 340 deletions

View File

@ -0,0 +1 @@
- Ability to mark a story as read once scrolling past.

View File

@ -1,53 +1,76 @@
import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/screens/screens.dart';
/// Custom router.
///
/// Handle named routing.
class CustomRouter {
/// Top level routing.
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case HomeScreen.routeName:
return HomeScreen.route();
case SubmitScreen.routeName:
return SubmitScreen.route();
case QrCodeScannerScreen.routeName:
return QrCodeScannerScreen.route();
case ItemScreen.routeName:
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
case QrCodeViewScreen.routeName:
return QrCodeViewScreen.route(data: settings.arguments! as String);
default:
return _errorRoute();
}
}
/// Nested routing for bottom navigation bar.
static Route<dynamic> onGenerateNestedRoute(RouteSettings settings) {
switch (settings.name) {
case ItemScreen.routeName:
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
case SubmitScreen.routeName:
return SubmitScreen.route();
default:
return _errorRoute();
}
}
/// Error route.
static Route<dynamic> _errorRoute() {
return MaterialPageRoute<dynamic>(
settings: const RouteSettings(name: '/error'),
builder: (_) => Scaffold(
appBar: AppBar(
title: const Text('Error'),
),
body: Center(
child: Text(Constants.errorMessage),
final GoRouter router = GoRouter(
observers: <NavigatorObserver>[
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
],
initialLocation: HomeScreen.routeName,
routes: <RouteBase>[
GoRoute(
path: HomeScreen.routeName,
builder: (_, __) => const HomeScreen(),
routes: <RouteBase>[
GoRoute(
path: ItemScreen.routeName,
builder: (_, GoRouterState state) {
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
if (args == null) {
throw GoError("args can't be null");
}
return ItemScreen.phone(args);
},
),
],
),
GoRoute(
path: '/${ItemScreen.routeName}',
builder: (_, GoRouterState state) {
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
if (args == null) {
throw GoError("args can't be null");
}
return ItemScreen.phone(args);
},
),
GoRoute(
path: '/${SubmitScreen.routeName}',
builder: (_, __) => BlocProvider<SubmitCubit>(
create: (_) => SubmitCubit(),
child: const SubmitScreen(),
),
);
}
}
),
GoRoute(
path: '/${QrCodeScannerScreen.routeName}',
builder: (_, __) => const QrCodeScannerScreen(),
),
GoRoute(
path: '/${QrCodeViewScreen.routeName}',
builder: (_, GoRouterState state) {
final String? data = state.extra as String?;
if (data == null) {
throw GoError("data can't be null");
}
return QrCodeViewScreen(
data: data,
);
},
),
GoRoute(
path: '/${WebViewScreen.routeName}',
builder: (_, GoRouterState state) {
final String? link = state.extra as String?;
if (link == null) {
throw GoError("link can't be null");
}
return WebViewScreen(
url: link,
);
},
),
],
);

View File

@ -6,9 +6,9 @@ import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart';
@ -286,9 +286,9 @@ class CommentsCubit extends Cubit<CommentsState> {
if (parent == null) {
return;
} else {
await HackiApp.navigatorKey.currentState?.pushNamed(
ItemScreen.routeName,
arguments: ItemScreenArgs(item: parent),
await router.push(
'/${ItemScreen.routeName}',
extra: ItemScreenArgs(item: parent),
);
emit(
@ -309,9 +309,9 @@ class CommentsCubit extends Cubit<CommentsState> {
if (parent == null) {
return;
} else {
await HackiApp.navigatorKey.currentState?.pushNamed(
ItemScreen.routeName,
arguments: ItemScreenArgs(item: parent),
await router.push(
'/${ItemScreen.routeName}',
extra: ItemScreenArgs(item: parent),
);
emit(

View File

@ -87,6 +87,9 @@ class PreferenceState extends Equatable {
return tabs;
}
StoryMarkingMode get storyMarkingMode => StoryMarkingMode.values
.elementAt(preferences.singleWhereType<StoryMarkingModePreference>().val);
FetchMode get fetchMode => FetchMode.values
.elementAt(preferences.singleWhereType<FetchModePreference>().val);

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/auth/auth_bloc.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/item/models/models.dart';
import 'package:hacki/screens/item/widgets/widgets.dart';
@ -36,9 +36,9 @@ extension StateExtension on State {
if (splitViewEnabled && !forceNewScreen) {
context.read<SplitViewCubit>().updateItemScreenArgs(args);
} else {
return HackiApp.navigatorKey.currentState?.pushNamed(
ItemScreen.routeName,
arguments: args,
context.push(
'/${ItemScreen.routeName}',
extra: args,
);
}
@ -112,12 +112,11 @@ extension StateExtension on State {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
onTap: () => Navigator.pop(context, item.url),
onTap: () => context.pop(item.url),
title: const Text('Link to article'),
),
ListTile(
onTap: () => Navigator.pop(
context,
onTap: () => context.pop(
'https://news.ycombinator.com/item?id=${item.id}',
),
title: const Text('Link to HN'),
@ -155,13 +154,13 @@ extension StateExtension on State {
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, false),
onPressed: () => context.pop(false),
child: const Text(
'Cancel',
),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
onPressed: () => context.pop(true),
child: const Text(
'Yes',
),
@ -193,13 +192,13 @@ extension StateExtension on State {
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, false),
onPressed: () => context.pop(false),
child: const Text(
'Cancel',
),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
onPressed: () => context.pop(true),
child: const Text(
'Yes',
),

View File

@ -17,7 +17,6 @@ import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/fetcher.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/theme_util.dart';
@ -139,8 +138,8 @@ Future<void> main({bool testing = false}) async {
prefs.getInt(FontPreference().key) ?? Font.roboto.index,
);
//Uncomment this line to log events from bloc/cubit.
//Bloc.observer = CustomBlocObserver();
// Uncomment this line to log events from bloc/cubit.
// Bloc.observer = CustomBlocObserver();
HydratedBloc.storage = storage;
@ -165,9 +164,6 @@ class HackiApp extends StatelessWidget {
final Font font;
final bool trueDarkMode;
static final GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@ -286,18 +282,13 @@ class HackiApp extends StatelessWidget {
.platformBrightness ==
Brightness.dark));
return FeatureDiscovery(
child: MaterialApp(
child: MaterialApp.router(
title: 'Hacki',
debugShowCheckedModeBanner: false,
theme: (useTrueDark ? trueDarkTheme : theme).copyWith(
useMaterial3: false,
),
navigatorKey: navigatorKey,
navigatorObservers: <NavigatorObserver>[
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
],
onGenerateRoute: CustomRouter.onGenerateRoute,
initialRoute: HomeScreen.routeName,
routerConfig: router,
),
);
},

View File

@ -8,5 +8,6 @@ export 'post_data.dart';
export 'preference.dart';
export 'search_params.dart';
export 'status.dart';
export 'story_marking_mode.dart';
export 'story_type.dart';
export 'user.dart';

View File

@ -23,17 +23,18 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
FontPreference(),
FontSizePreference(),
TabOrderPreference(),
StoryMarkingModePreference(),
// Order of items below matters and
// reflects the order on settings screen.
const DisplayModePreference(),
const MetadataModePreference(),
const StoryUrlModePreference(),
const MarkReadStoriesModePreference(),
const NotificationModePreference(),
const SwipeGesturePreference(),
const AutoScrollModePreference(),
const CollapseModePreference(),
const ReaderModePreference(),
const MarkReadStoriesModePreference(),
const EyeCandyModePreference(),
const TrueDarkModePreference(),
],
@ -68,6 +69,8 @@ final int _fontSizeDefaultValue = FontSize.regular.index;
final int _fontDefaultValue = Font.roboto.index;
final int _tabOrderDefaultValue =
StoryType.convertToSettingsValue(StoryType.values);
final int _markStoriesAsReadWhenPreferenceDefaultValue =
StoryMarkingMode.tap.index;
class SwipeGesturePreference extends BooleanPreference {
const SwipeGesturePreference({bool? val})
@ -364,3 +367,19 @@ class TabOrderPreference extends IntPreference {
@override
String get title => 'Tab order';
}
class StoryMarkingModePreference extends IntPreference {
StoryMarkingModePreference({int? val})
: super(val: val ?? _markStoriesAsReadWhenPreferenceDefaultValue);
@override
StoryMarkingModePreference copyWith({required int? val}) {
return StoryMarkingModePreference(val: val);
}
@override
String get key => 'storyMarkingMode';
@override
String get title => 'Mark a Story as Read on';
}

View File

@ -0,0 +1,21 @@
/// Used for determining when to mark a story as read.
enum StoryMarkingMode {
// Mark a story as read after user scrolls past it.
scrollPast('scrolling past'),
// Mark a story as read after user taps on it.
tap('tapping'),
// Mark a story as read after user scrolls past or taps on it, whichever
// happens the first.
scrollPastOrTap('scrolling past or tapping');
const StoryMarkingMode(this.label);
final String label;
bool get shouldDetectScrollingPast =>
this == StoryMarkingMode.scrollPast ||
this == StoryMarkingMode.scrollPastOrTap;
bool get shouldDetectTapping =>
this == StoryMarkingMode.tap || this == StoryMarkingMode.scrollPastOrTap;
}

View File

@ -7,6 +7,7 @@ import 'package:flutter/material.dart' hide Badge;
import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart';
@ -30,13 +31,6 @@ class HomeScreen extends StatefulWidget {
static const String routeName = '/';
static Route<dynamic> route() {
return MaterialPageRoute<HomeScreen>(
settings: const RouteSettings(name: routeName),
builder: (BuildContext context) => const HomeScreen(),
);
}
@override
_HomeScreenState createState() => _HomeScreenState();
}
@ -209,11 +203,13 @@ class _HomeScreenState extends State<HomeScreen>
);
}
void onStoryTapped(Story story, {bool isPin = false}) {
void onStoryTapped(Story story) {
final bool useReader = context.read<PreferenceCubit>().state.readerEnabled;
final bool offlineReading =
context.read<StoriesBloc>().state.isOfflineReading;
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
final StoryMarkingMode storyMarkingMode =
context.read<PreferenceCubit>().state.storyMarkingMode;
// If a story is a job story and it has a link to the job posting,
// it would be better to just navigate to the web page.
@ -222,23 +218,14 @@ class _HomeScreenState extends State<HomeScreen>
if (isJobWithLink) {
context.read<ReminderCubit>().removeLastReadStoryId();
} else {
final ItemScreenArgs args = ItemScreenArgs(
item: story,
);
final ItemScreenArgs args = ItemScreenArgs(item: story);
context.read<ReminderCubit>().updateLastReadStoryId(story.id);
if (splitViewEnabled) {
context.read<SplitViewCubit>().updateItemScreenArgs(args);
} else {
HackiApp.navigatorKey.currentState
?.pushNamed(
ItemScreen.routeName,
arguments: args,
)
.whenComplete(() {
context.read<ReminderCubit>().removeLastReadStoryId();
});
context.push('/${ItemScreen.routeName}', extra: args);
}
}
@ -250,11 +237,9 @@ class _HomeScreenState extends State<HomeScreen>
);
}
context.read<StoriesBloc>().add(
StoryRead(
story: story,
),
);
if (storyMarkingMode.shouldDetectTapping) {
context.read<StoriesBloc>().add(StoryRead(story: story));
}
if (Platform.isIOS) {
FlutterSiriSuggestions.instance.registerActivity(

View File

@ -16,7 +16,7 @@ class PinnedStories extends StatelessWidget {
});
final PreferenceState preferenceState;
final void Function(Story story, {bool isPin}) onStoryTapped;
final void Function(Story story) onStoryTapped;
@override
Widget build(BuildContext context) {
@ -49,7 +49,7 @@ class PinnedStories extends StatelessWidget {
child: StoryTile(
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
story: story,
onTap: () => onStoryTapped(story, isPin: true),
onTap: () => onStoryTapped(story),
showWebPreview: preferenceState.complexStoryTileEnabled,
showMetadata: preferenceState.metadataEnabled,
showUrl: preferenceState.urlEnabled,

View File

@ -76,7 +76,7 @@ class _TabletStoryView extends StatelessWidget {
previous.itemScreenArgs != current.itemScreenArgs,
builder: (BuildContext context, SplitViewState state) {
if (state.itemScreenArgs != null) {
return ItemScreen.build(context, state.itemScreenArgs!);
return ItemScreen.tablet(context, state.itemScreenArgs!);
}
return Material(

View File

@ -3,6 +3,7 @@ import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart';
@ -51,44 +52,39 @@ class ItemScreen extends StatefulWidget {
this.splitViewEnabled = false,
});
static const String routeName = '/item';
static const String routeName = 'item';
static Route<dynamic> route(ItemScreenArgs args) {
return MaterialPageRoute<ItemScreen>(
settings: const RouteSettings(name: routeName),
builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
create: (_) => CollapseCache(),
lazy: false,
child: MultiBlocProvider(
providers: <BlocProvider<dynamic>>[
BlocProvider<CommentsCubit>(
create: (BuildContext context) => CommentsCubit(
filterCubit: context.read<FilterCubit>(),
isOfflineReading:
context.read<StoriesBloc>().state.isOfflineReading,
item: args.item,
collapseCache: context.read<CollapseCache>(),
defaultFetchMode:
context.read<PreferenceCubit>().state.fetchMode,
defaultCommentsOrder:
context.read<PreferenceCubit>().state.order,
)..init(
onlyShowTargetComment: args.onlyShowTargetComment,
targetAncestors: args.targetComments,
useCommentCache: args.useCommentCache,
),
),
],
child: ItemScreen(
item: args.item,
parentComments: args.targetComments ?? <Comment>[],
static Widget phone(ItemScreenArgs args) {
return RepositoryProvider<CollapseCache>(
create: (_) => CollapseCache(),
lazy: false,
child: MultiBlocProvider(
providers: <BlocProvider<dynamic>>[
BlocProvider<CommentsCubit>(
create: (BuildContext context) => CommentsCubit(
filterCubit: context.read<FilterCubit>(),
isOfflineReading:
context.read<StoriesBloc>().state.isOfflineReading,
item: args.item,
collapseCache: context.read<CollapseCache>(),
defaultFetchMode: context.read<PreferenceCubit>().state.fetchMode,
defaultCommentsOrder: context.read<PreferenceCubit>().state.order,
)..init(
onlyShowTargetComment: args.onlyShowTargetComment,
targetAncestors: args.targetComments,
useCommentCache: args.useCommentCache,
),
),
],
child: ItemScreen(
item: args.item,
parentComments: args.targetComments ?? <Comment>[],
),
),
);
}
static Widget build(BuildContext context, ItemScreenArgs args) {
static Widget tablet(BuildContext context, ItemScreenArgs args) {
return WillPopScope(
onWillPop: () async {
if (context.read<SplitViewCubit>().state.expanded) {
@ -168,7 +164,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
@override
void initState() {
super.initState();
SchedulerBinding.instance
..addPostFrameCallback((_) {
FeatureDiscovery.discoverFeatures(
@ -214,11 +209,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
BlocListener<PostCubit, PostState>(
listener: (BuildContext context, PostState postState) {
if (postState.status == Status.success) {
Navigator.popUntil(
context,
(Route<dynamic> route) =>
route.settings.name == ItemScreen.routeName,
);
context.pop();
final String verb =
context.read<EditCubit>().state.replyingTo == null
? 'updated'
@ -229,11 +220,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
context.read<EditCubit>().onReplySubmittedSuccessfully();
context.read<PostCubit>().reset();
} else if (postState.status == Status.failure) {
Navigator.popUntil(
context,
(Route<dynamic> route) =>
route.settings.name == ItemScreen.routeName,
);
context.pop();
showErrorSnackBar();
context.read<PostCubit>().reset();
}
@ -445,7 +432,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
leading: const Icon(Icons.av_timer),
title: const Text('View ancestors'),
onTap: () {
Navigator.pop(context);
context.pop();
onTimeMachineActivated(comment);
},
enabled:
@ -456,8 +443,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
title: const Text('View in separate thread'),
onTap: () {
locator.get<AppReviewService>().requestReview();
Navigator.pop(context);
context.pop();
goToItemScreen(
args: ItemScreenArgs(
item: comment,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
@ -24,7 +25,7 @@ class _LoginDialogState extends State<LoginDialog> {
return BlocConsumer<AuthBloc, AuthState>(
listener: (BuildContext context, AuthState state) {
if (state.isLoggedIn) {
Navigator.pop(context);
context.pop();
showSnackBar(
content: 'Logged in successfully! ${Constants.happyFace}',
);
@ -153,7 +154,7 @@ class _LoginDialogState extends State<LoginDialog> {
children: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context);
context.pop();
context.read<AuthBloc>().add(AuthInitialize());
},
child: const Text(

View File

@ -431,6 +431,7 @@ class _ParentItemSection extends StatelessWidget {
style: TextStyle(
fontSize: TextDimens.pt13,
),
textScaleFactor: 1,
),
),
),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
@ -60,10 +61,7 @@ class MorePopupMenu extends StatelessWidget {
);
}
Navigator.pop(
context,
MenuAction.upvote,
);
context.pop(MenuAction.upvote);
},
builder: (BuildContext context, VoteState voteState) {
final bool upvoted = voteState.vote == Vote.up;
@ -91,7 +89,7 @@ class MorePopupMenu extends StatelessWidget {
state.user.description,
),
onTap: () {
Navigator.pop(context);
context.pop();
showDialog<void>(
context: context,
builder: (BuildContext context) => AlertDialog(
@ -130,7 +128,7 @@ class MorePopupMenu extends StatelessWidget {
locator
.get<AppReviewService>()
.requestReview();
Navigator.pop(context);
context.pop();
onSearchUserTapped(context);
},
child: const Text(
@ -142,7 +140,7 @@ class MorePopupMenu extends StatelessWidget {
locator
.get<AppReviewService>()
.requestReview();
Navigator.pop(context);
context.pop();
},
child: const Text(
'Okay',
@ -196,10 +194,7 @@ class MorePopupMenu extends StatelessWidget {
title: Text(
isFav ? 'Unfavorite' : 'Favorite',
),
onTap: () => Navigator.pop(
context,
MenuAction.fav,
),
onTap: () => context.pop(MenuAction.fav),
);
},
),
@ -208,20 +203,14 @@ class MorePopupMenu extends StatelessWidget {
title: const Text(
'Share',
),
onTap: () => Navigator.pop(
context,
MenuAction.share,
),
onTap: () => context.pop(MenuAction.share),
),
ListTile(
leading: const Icon(Icons.local_police),
title: const Text(
'Flag',
),
onTap: () => Navigator.pop(
context,
MenuAction.flag,
),
onTap: () => context.pop(MenuAction.flag),
),
ListTile(
leading: Icon(
@ -230,20 +219,14 @@ class MorePopupMenu extends StatelessWidget {
title: Text(
isBlocked ? 'Unblock' : 'Block',
),
onTap: () => Navigator.pop(
context,
MenuAction.block,
),
onTap: () => context.pop(MenuAction.block),
),
ListTile(
leading: const Icon(Icons.close),
title: const Text(
'Cancel',
),
onTap: () => Navigator.pop(
context,
MenuAction.cancel,
),
onTap: () => context.pop(MenuAction.cancel),
),
],
),

View File

@ -2,6 +2,7 @@ import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
@ -144,7 +145,7 @@ class _ReplyBoxState extends State<ReplyBox> {
color: Palette.orange,
),
onPressed: () {
Navigator.pop(context);
context.pop();
final EditState state =
context.read<EditCubit>().state;
@ -161,7 +162,7 @@ class _ReplyBoxState extends State<ReplyBox> {
context
.read<EditCubit>()
.deleteDraft();
Navigator.pop(context);
context.pop();
},
child: const Text(
'No',
@ -171,8 +172,7 @@ class _ReplyBoxState extends State<ReplyBox> {
),
),
TextButton(
onPressed: () =>
Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text('Yes'),
),
],
@ -306,12 +306,6 @@ class _ReplyBoxState extends State<ReplyBox> {
setState(() {
expanded = false;
});
Navigator.popUntil(
context,
(Route<dynamic> route) =>
route.settings.name == ItemScreen.routeName ||
route.isFirst,
);
goToItemScreen(
args: ItemScreenArgs(
item: replyingTo,
@ -338,7 +332,7 @@ class _ReplyBoxState extends State<ReplyBox> {
color: Palette.orange,
size: TextDimens.pt18,
),
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
),
],
),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart';
@ -56,7 +57,7 @@ class TimeMachineDialog extends StatelessWidget {
Icons.close,
size: Dimens.pt16,
),
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
padding: EdgeInsets.zero,
),
],

View File

@ -1,11 +1,13 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/profile/models/models.dart';
@ -31,7 +33,7 @@ class _ProfileScreenState extends State<ProfileScreen>
final ScrollController scrollController = ScrollController();
final Throttle throttle = Throttle(delay: Durations.twoSeconds);
PageType pageType = PageType.notification;
PageType? pageType;
@override
void dispose() {
@ -45,6 +47,9 @@ class _ProfileScreenState extends State<ProfileScreen>
@override
Widget build(BuildContext context) {
pageType ??= context.read<AuthBloc>().state.isLoggedIn
? PageType.notification
: PageType.fav;
super.build(context);
return BlocBuilder<AuthBloc, AuthState>(
builder: (BuildContext context, AuthState authState) {
@ -91,8 +96,8 @@ class _ProfileScreenState extends State<ProfileScreen>
}
return ItemsListView<Item>(
showWebPreview: false,
showMetadata: false,
showWebPreviewOnStoryTile: false,
showMetadataOnStoryTile: false,
showUrl: false,
useConsistentFontSize: true,
refreshController: refreshControllerHistory,
@ -157,8 +162,10 @@ class _ProfileScreenState extends State<ProfileScreen>
PreferenceState prefState,
) {
return ItemsListView<Item>(
showWebPreview: prefState.complexStoryTileEnabled,
showMetadata: prefState.metadataEnabled,
showWebPreviewOnStoryTile:
prefState.complexStoryTileEnabled,
showMetadataOnStoryTile:
prefState.metadataEnabled,
showUrl: prefState.urlEnabled,
useCommentTile: true,
refreshController: refreshControllerFav,
@ -173,6 +180,28 @@ class _ProfileScreenState extends State<ProfileScreen>
onTap: (Item item) => goToItemScreen(
args: ItemScreenArgs(item: item),
),
itemBuilder: (Widget child, Item item) {
return Slidable(
dragStartBehavior: DragStartBehavior.start,
startActionPane: ActionPane(
motion: const BehindMotion(),
children: <Widget>[
SlidableAction(
onPressed: (_) {
HapticFeedbackUtil.light();
context
.read<FavCubit>()
.removeFav(item.id);
},
backgroundColor: Palette.red,
foregroundColor: Palette.white,
icon: Icons.close,
),
],
),
child: child,
);
},
);
},
);
@ -246,8 +275,7 @@ class _ProfileScreenState extends State<ProfileScreen>
selected: false,
onSelected: (bool val) {
if (authState.isLoggedIn) {
HackiApp.navigatorKey.currentState
?.pushNamed(SubmitScreen.routeName);
context.push('/${SubmitScreen.routeName}');
} else {
showSnackBar(
content: 'You need to log in first.',

View File

@ -1,19 +1,12 @@
import 'package:flutter/material.dart';
import 'package:hacki/main.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/styles/styles.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class QrCodeScannerScreen extends StatefulWidget {
const QrCodeScannerScreen({super.key});
static const String routeName = '/qr-code-scanner';
static Route<dynamic> route() {
return MaterialPageRoute<String?>(
settings: const RouteSettings(name: routeName),
builder: (_) => const QrCodeScannerScreen(),
);
}
static const String routeName = 'qr-code-scanner';
@override
State<QrCodeScannerScreen> createState() => _QrCodeScannerScreenState();
@ -66,7 +59,7 @@ class _QrCodeScannerScreenState extends State<QrCodeScannerScreen> {
});
controller.scannedDataStream.listen((Barcode scanData) {
controller.stopCamera();
HackiApp.navigatorKey.currentState?.pop(scanData.code);
context.pop(scanData.code);
});
}

View File

@ -10,16 +10,7 @@ class QrCodeViewScreen extends StatelessWidget {
final String data;
static const String routeName = '/qr-code-view';
static Route<dynamic> route({required String data}) {
return MaterialPageRoute<QrCodeViewScreen>(
settings: const RouteSettings(name: routeName),
builder: (_) => QrCodeViewScreen(
data: data,
),
);
}
static const String routeName = 'qr-code-view';
static const int qrCodeVersion = 4;

View File

@ -1,6 +1,7 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
@ -64,15 +65,15 @@ class OfflineListTile extends StatelessWidget {
title: const Text('Abort downloading?'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, false),
onPressed: () => context.pop(false),
child: const Text('No'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
onPressed: () => context.pop(true),
child: const Text('Yes'),
),
],
@ -93,15 +94,15 @@ class OfflineListTile extends StatelessWidget {
content: const Text('It will take longer time.'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, false),
onPressed: () => context.pop(false),
child: const Text('No'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
onPressed: () => context.pop(true),
child: const Text('Yes'),
),
],

View File

@ -10,12 +10,13 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/profile/models/page_type.dart';
@ -40,7 +41,7 @@ class Settings extends StatefulWidget {
final AuthState authState;
final String magicWord;
final PageType pageType;
final PageType? pageType;
@override
State<Settings> createState() => _SettingsState();
@ -221,6 +222,51 @@ class _SettingsState extends State<Settings> {
},
activeColor: Palette.orange,
),
if (preference
is MarkReadStoriesModePreference) ...<Widget>[
ListTile(
title: Text(
StoryMarkingModePreference().title,
style: TextStyle(
color: !preferenceState.markReadStoriesEnabled
? Palette.grey
: null,
),
),
trailing: DropdownButton<StoryMarkingMode>(
value: preferenceState.storyMarkingMode,
underline: const SizedBox.shrink(),
items: StoryMarkingMode.values
.map(
(StoryMarkingMode val) =>
DropdownMenuItem<StoryMarkingMode>(
value: val,
child: Text(
val.label,
style: TextStyle(
fontSize: TextDimens.pt16,
color: !preferenceState
.markReadStoriesEnabled
? Palette.grey
: null,
),
),
),
)
.toList(),
onChanged: (StoryMarkingMode? storyMarkingMode) {
if (storyMarkingMode != null) {
HapticFeedbackUtil.selection();
context.read<PreferenceCubit>().update(
StoryMarkingModePreference(),
to: storyMarkingMode.index,
);
}
},
),
),
const Divider(),
],
if (preference is StoryUrlModePreference) const Divider(),
],
ListTile(
@ -300,14 +346,14 @@ class _SettingsState extends State<Settings> {
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text(
'Cancel',
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
context.pop();
context.read<AuthBloc>().add(AuthLogout());
context.read<HistoryCubit>().reset();
},
@ -432,7 +478,7 @@ class _SettingsState extends State<Settings> {
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text(
'Cancel',
style: TextStyle(
@ -442,7 +488,7 @@ class _SettingsState extends State<Settings> {
),
TextButton(
onPressed: () {
Navigator.pop(context);
context.pop();
locator
.get<SembastRepository>()
.deleteAllCachedComments()
@ -719,7 +765,7 @@ class _SettingsState extends State<Settings> {
),
),
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text(
'Okay',
),
@ -742,7 +788,7 @@ class _SettingsState extends State<Settings> {
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text(
'Cancel',
),
@ -752,7 +798,7 @@ class _SettingsState extends State<Settings> {
final String keyword = controller.text.trim();
if (keyword.isEmpty) return;
context.read<FilterCubit>().addKeyword(keyword.toLowerCase());
Navigator.pop(context);
context.pop();
},
child: const Text(
'Confirm',
@ -776,7 +822,7 @@ class _SettingsState extends State<Settings> {
(ExportDestination e) => ListTile(
leading: Icon(e.icon),
title: Text(e.label),
onTap: () => Navigator.pop<ExportDestination>(context, e),
onTap: () => context.pop<ExportDestination>(e),
),
),
],
@ -789,8 +835,8 @@ class _SettingsState extends State<Settings> {
}
Future<void> onImportFavoritesTapped(FavCubit favCubit) async {
final String? res = await HackiApp.navigatorKey.currentState
?.pushNamed(QrCodeScannerScreen.routeName) as String?;
final String? res =
await router.push('/${QrCodeScannerScreen.routeName}') as String?;
final List<int>? ids =
res?.split('\n').map(int.tryParse).whereType<int>().toList();
if (ids == null) return;
@ -813,9 +859,9 @@ class _SettingsState extends State<Settings> {
switch (destination) {
case ExportDestination.qrCode:
await HackiApp.navigatorKey.currentState?.pushNamed(
QrCodeViewScreen.routeName,
arguments: allFavoritesStr,
await router.push(
'/${QrCodeViewScreen.routeName}',
extra: allFavoritesStr,
);
case ExportDestination.clipBoard:
try {
@ -841,14 +887,14 @@ class _SettingsState extends State<Settings> {
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text(
'Cancel',
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
context.pop();
try {
context.read<FavCubit>().removeAll();
showSnackBar(content: 'All favorites have been removed.');

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/models/search_params.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
@ -67,13 +68,13 @@ class PostedByFilterChip extends StatelessWidget {
child: ButtonBar(
children: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, filter?.author),
onPressed: () => context.pop(filter?.author),
child: const Text(
'Cancel',
),
),
TextButton(
onPressed: () => Navigator.pop(context, null),
onPressed: () => context.pop(null),
child: const Text(
'Clear',
),
@ -81,7 +82,7 @@ class PostedByFilterChip extends StatelessWidget {
ElevatedButton(
onPressed: () {
final String text = usernameController.text.trim();
Navigator.pop(context, text.isEmpty ? null : text);
context.pop(text.isEmpty ? null : text);
},
style: ButtonStyle(
backgroundColor:

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
@ -9,17 +10,7 @@ import 'package:hacki/utils/utils.dart';
class SubmitScreen extends StatefulWidget {
const SubmitScreen({super.key});
static const String routeName = '/submit';
static Route<dynamic> route() {
return MaterialPageRoute<SubmitScreen>(
settings: const RouteSettings(name: routeName),
builder: (BuildContext context) => BlocProvider<SubmitCubit>(
create: (BuildContext context) => SubmitCubit(),
child: const SubmitScreen(),
),
);
}
static const String routeName = 'submit';
@override
_SubmitScreenState createState() => _SubmitScreenState();
@ -45,7 +36,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
previous.status != current.status,
listener: (BuildContext context, SubmitState state) {
if (state.status == Status.success) {
Navigator.pop(context);
context.pop();
HapticFeedbackUtil.light();
showSnackBar(
content: 'Post submitted successfully.',
@ -70,11 +61,11 @@ class _SubmitScreenState extends State<SubmitScreen> {
title: const Text('Quit editing?'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
onPressed: () => context.pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
onPressed: () => context.pop(true),
child: const Text(
'Yes',
style: TextStyle(
@ -87,7 +78,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
},
).then((bool? value) {
if (value ?? false) {
Navigator.of(context).pop();
context.pop();
}
});
},

View File

@ -10,6 +10,8 @@ class WebViewScreen extends StatefulWidget {
super.key,
});
static const String routeName = 'web-view';
final String url;
@override

View File

@ -1,7 +1,7 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/context_extension.dart';
@ -13,8 +13,8 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
class ItemsListView<T extends Item> extends StatelessWidget {
const ItemsListView({
required this.showWebPreview,
required this.showMetadata,
required this.showWebPreviewOnStoryTile,
required this.showMetadataOnStoryTile,
required this.showUrl,
required this.items,
required this.onTap,
@ -23,7 +23,6 @@ class ItemsListView<T extends Item> extends StatelessWidget {
this.useCommentTile = false,
this.showCommentBy = false,
this.enablePullDown = true,
this.pinnable = false,
this.markReadStories = false,
this.useConsistentFontSize = false,
this.showOfflineBanner = false,
@ -32,33 +31,31 @@ class ItemsListView<T extends Item> extends StatelessWidget {
this.onPinned,
this.header,
this.onMoreTapped,
}) : assert(
!pinnable || (pinnable && onPinned != null),
'onPinned cannot be null when pinnable is true',
);
this.scrollController,
this.itemBuilder,
});
final bool useCommentTile;
final bool showCommentBy;
final bool showWebPreview;
final bool showMetadata;
final bool showWebPreviewOnStoryTile;
final bool showMetadataOnStoryTile;
final bool showUrl;
final bool enablePullDown;
final bool markReadStories;
final bool showOfflineBanner;
/// Whether story tiles can be pinned to the top.
final bool pinnable;
/// Whether to use same font size for comment and story tiles.
final bool useConsistentFontSize;
final List<T> items;
final Widget? header;
final RefreshController refreshController;
final ScrollController? scrollController;
final VoidCallback? onRefresh;
final VoidCallback? onLoadMore;
final ValueChanged<Story>? onPinned;
final void Function(T) onTap;
final Widget Function(Widget child, T item)? itemBuilder;
/// Used for home screen.
final void Function(Story, Rect?)? onMoreTapped;
@ -66,6 +63,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ListView child = ListView(
controller: scrollController,
children: <Widget>[
if (showOfflineBanner)
const OfflineBanner(
@ -85,51 +83,21 @@ class ItemsListView<T extends Item> extends StatelessWidget {
? () => onMoreTapped?.call(e, context.rect)
: null,
child: FadeIn(
child: Slidable(
enabled: !swipeGestureEnabled,
startActionPane: pinnable
? ActionPane(
motion: const BehindMotion(),
children: <Widget>[
SlidableAction(
onPressed: (_) {
HapticFeedbackUtil.light();
onPinned?.call(e);
},
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: showWebPreview
? Icons.push_pin_outlined
: null,
label: showWebPreview ? null : 'Pin to top',
),
SlidableAction(
onPressed: (_) =>
onMoreTapped?.call(e, context.rect),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: showWebPreview ? Icons.more_horiz : null,
label: showWebPreview ? null : 'More',
),
],
)
: null,
child: StoryTile(
key: ValueKey<int>(e.id),
story: e,
onTap: () => onTap(e),
showWebPreview: showWebPreview,
showMetadata: showMetadata,
showUrl: showUrl,
hasRead: markReadStories && hasRead,
simpleTileFontSize: useConsistentFontSize
? TextDimens.pt14
: TextDimens.pt16,
),
child: StoryTile(
key: ValueKey<int>(e.id),
story: e,
onTap: () => onTap(e),
showWebPreview: showWebPreviewOnStoryTile,
showMetadata: showMetadataOnStoryTile,
showUrl: showUrl,
hasRead: markReadStories && hasRead,
simpleTileFontSize: useConsistentFontSize
? TextDimens.pt14
: TextDimens.pt16,
),
),
),
if (!showWebPreview)
if (!showWebPreviewOnStoryTile)
const Divider(
height: Dimens.zero,
),
@ -137,14 +105,16 @@ class ItemsListView<T extends Item> extends StatelessWidget {
} else if (e is Comment) {
if (useCommentTile) {
return <Widget>[
if (showWebPreview)
if (showWebPreviewOnStoryTile)
const Divider(
height: Dimens.zero,
),
_CommentTile(
comment: e,
onTap: () => onTap(e),
fontSize: showWebPreview ? TextDimens.pt14 : TextDimens.pt16,
fontSize: showWebPreviewOnStoryTile
? TextDimens.pt14
: TextDimens.pt16,
),
const Divider(
height: Dimens.zero,
@ -227,7 +197,11 @@ class ItemsListView<T extends Item> extends StatelessWidget {
}
return <Widget>[Container()];
}).expand((List<Widget> element) => element),
}).mapIndexed(
(int index, List<Widget> e) => itemBuilder == null
? Column(children: e)
: itemBuilder!(Column(children: e), items.elementAt(index)),
),
const SizedBox(
height: Dimens.pt40,
),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/services/services.dart';
@ -39,11 +40,11 @@ class OfflineBanner extends StatelessWidget {
title: const Text('Exit offline mode?'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
onPressed: () => context.pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
onPressed: () => context.pop(true),
child: const Text(
'Yes',
style: TextStyle(

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
@ -31,7 +32,7 @@ class _OnboardingViewState extends State<OnboardingView> {
Icons.close,
color: Palette.white,
),
onPressed: () => Navigator.pop(context),
onPressed: context.pop,
),
),
backgroundColor: Theme.of(context).brightness == Brightness.light
@ -76,7 +77,7 @@ class _OnboardingViewState extends State<OnboardingView> {
onPressed: () {
HapticFeedbackUtil.light();
if (pageController.page! >= 2) {
Navigator.pop(context);
context.pop();
} else {
throttle.run(() {
pageController.nextPage(

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class OptionalWrapper extends StatelessWidget {
const OptionalWrapper({
required this.enabled,
required this.wrapper,
required this.child,
super.key,
});
final bool enabled;
final Widget Function(Widget) wrapper;
final Widget child;
@override
Widget build(BuildContext context) {
if (enabled) {
return wrapper(child);
} else {
return child;
}
}
}

View File

@ -1,12 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:visibility_detector/visibility_detector.dart';
class StoriesListView extends StatefulWidget {
const StoriesListView({
@ -26,11 +30,13 @@ class StoriesListView extends StatefulWidget {
class _StoriesListViewState extends State<StoriesListView> {
final RefreshController refreshController = RefreshController();
final ScrollController scrollController = ScrollController();
@override
void dispose() {
super.dispose();
refreshController.dispose();
scrollController.dispose();
}
@override
@ -63,14 +69,15 @@ class _StoriesListViewState extends State<StoriesListView> {
(previous.readStoriesIds.length != current.readStoriesIds.length),
builder: (BuildContext context, StoriesState state) {
return ItemsListView<Story>(
pinnable: true,
showOfflineBanner: true,
markReadStories:
context.read<PreferenceCubit>().state.markReadStoriesEnabled,
showWebPreview: preferenceState.complexStoryTileEnabled,
showMetadata: preferenceState.metadataEnabled,
showWebPreviewOnStoryTile:
preferenceState.complexStoryTileEnabled,
showMetadataOnStoryTile: preferenceState.metadataEnabled,
showUrl: preferenceState.urlEnabled,
refreshController: refreshController,
scrollController: scrollController,
items: state.storiesByType[storyType]!,
onRefresh: () {
HapticFeedbackUtil.light();
@ -88,6 +95,64 @@ class _StoriesListViewState extends State<StoriesListView> {
onPinned: context.read<PinCubit>().pinStory,
header: state.isOfflineReading ? null : header,
onMoreTapped: onMoreTapped,
itemBuilder: (Widget child, Story story) {
return Slidable(
enabled: !preferenceState.swipeGestureEnabled,
startActionPane: ActionPane(
motion: const BehindMotion(),
children: <Widget>[
SlidableAction(
onPressed: (_) {
HapticFeedbackUtil.light();
context.read<PinCubit>().pinStory(story);
},
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: preferenceState.complexStoryTileEnabled
? Icons.push_pin_outlined
: null,
label: preferenceState.complexStoryTileEnabled
? null
: 'Pin to top',
),
SlidableAction(
onPressed: (_) => onMoreTapped(story, context.rect),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: preferenceState.complexStoryTileEnabled
? Icons.more_horiz
: null,
label: preferenceState.complexStoryTileEnabled
? null
: 'More',
),
],
),
child: OptionalWrapper(
enabled: context
.read<PreferenceCubit>()
.state
.storyMarkingMode
.shouldDetectScrollingPast,
wrapper: (Widget child) => VisibilityDetector(
key: ValueKey<int>(story.id),
onVisibilityChanged: (VisibilityInfo info) {
if (info.visibleFraction == 0 &&
mounted &&
scrollController.position.userScrollDirection ==
ScrollDirection.reverse &&
!state.readStoriesIds.contains(story.id)) {
context
.read<StoriesBloc>()
.add(StoryRead(story: story));
}
},
child: child,
),
child: child,
),
);
},
);
},
);

View File

@ -14,6 +14,7 @@ export 'items_list_view.dart';
export 'link_preview/link_preview.dart';
export 'offline_banner.dart';
export 'onboarding_view.dart';
export 'optional_wrapper.dart';
export 'spring_curve.dart';
export 'stories_list_view.dart';
export 'story_tile.dart';

View File

@ -1,10 +1,9 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart'
@ -37,10 +36,9 @@ abstract class LinkUtil {
.hasCachedWebPage(url: link)
.then((bool cached) {
if (cached) {
HackiApp.navigatorKey.currentState?.push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) => WebViewScreen(url: link),
),
router.push(
'/${WebViewScreen.routeName}',
extra: link,
);
}
});
@ -87,9 +85,9 @@ abstract class LinkUtil {
.fetchItem(id: id)
.then((Item? item) {
if (item != null) {
HackiApp.navigatorKey.currentState!.pushNamed(
ItemScreen.routeName,
arguments: ItemScreenArgs(item: item),
router.push(
'/${ItemScreen.routeName}',
extra: ItemScreenArgs(item: item),
);
}
});

View File

@ -483,6 +483,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "5668e6d3dbcb2d0dfa25f7567554b88c57e1e3f3c440b672b24d4a9477017d5b"
url: "https://pub.dev"
source: hosted
version: "10.1.2"
hive:
dependency: "direct main"
description:
@ -1244,6 +1252,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0+1"
visibility_detector:
dependency: "direct main"
description:
name: visibility_detector
sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420
url: "https://pub.dev"
source: hosted
version: "0.4.0+2"
vm_service:
dependency: transitive
description:
@ -1414,4 +1430,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.13.2"
flutter: ">=3.13.4"

View File

@ -1,11 +1,11 @@
name: hacki
description: A Hacker News reader.
version: 1.8.4+120
version: 1.9.1+122
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: "3.13.2"
flutter: "3.13.4"
dependencies:
adaptive_theme: ^3.2.0
@ -38,6 +38,7 @@ dependencies:
font_awesome_flutter: ^10.3.0
gbk_codec: ^0.4.0
get_it: ^7.2.0
go_router: ^10.1.2
hive: ^2.2.3
html: ^0.15.1
html_unescape: ^2.0.0
@ -74,6 +75,7 @@ dependencies:
path: components/synced_shared_preferences
universal_platform: ^1.0.0+1
url_launcher: ^6.1.9
visibility_detector: ^0.4.0+2
wakelock: ^0.6.2
webview_flutter: ^4.0.2
workmanager: ^0.5.1