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: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'; import 'package:hacki/screens/screens.dart';
/// Custom router. final GoRouter router = GoRouter(
/// observers: <NavigatorObserver>[
/// Handle named routing. locator.get<RouteObserver<ModalRoute<dynamic>>>(),
class CustomRouter { ],
/// Top level routing. initialLocation: HomeScreen.routeName,
static Route<dynamic> onGenerateRoute(RouteSettings settings) { routes: <RouteBase>[
switch (settings.name) { GoRoute(
case HomeScreen.routeName: path: HomeScreen.routeName,
return HomeScreen.route(); builder: (_, __) => const HomeScreen(),
case SubmitScreen.routeName: routes: <RouteBase>[
return SubmitScreen.route(); GoRoute(
case QrCodeScannerScreen.routeName: path: ItemScreen.routeName,
return QrCodeScannerScreen.route(); builder: (_, GoRouterState state) {
case ItemScreen.routeName: final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
return ItemScreen.route(settings.arguments! as ItemScreenArgs); if (args == null) {
case QrCodeViewScreen.routeName: throw GoError("args can't be null");
return QrCodeViewScreen.route(data: settings.arguments! as String); }
default: return ItemScreen.phone(args);
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),
), ),
],
),
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:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/config/custom_router.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/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
@ -286,9 +286,9 @@ class CommentsCubit extends Cubit<CommentsState> {
if (parent == null) { if (parent == null) {
return; return;
} else { } else {
await HackiApp.navigatorKey.currentState?.pushNamed( await router.push(
ItemScreen.routeName, '/${ItemScreen.routeName}',
arguments: ItemScreenArgs(item: parent), extra: ItemScreenArgs(item: parent),
); );
emit( emit(
@ -309,9 +309,9 @@ class CommentsCubit extends Cubit<CommentsState> {
if (parent == null) { if (parent == null) {
return; return;
} else { } else {
await HackiApp.navigatorKey.currentState?.pushNamed( await router.push(
ItemScreen.routeName, '/${ItemScreen.routeName}',
arguments: ItemScreenArgs(item: parent), extra: ItemScreenArgs(item: parent),
); );
emit( emit(

View File

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

View File

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

View File

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

View File

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

View File

@ -23,17 +23,18 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
FontPreference(), FontPreference(),
FontSizePreference(), FontSizePreference(),
TabOrderPreference(), TabOrderPreference(),
StoryMarkingModePreference(),
// Order of items below matters and // Order of items below matters and
// reflects the order on settings screen. // reflects the order on settings screen.
const DisplayModePreference(), const DisplayModePreference(),
const MetadataModePreference(), const MetadataModePreference(),
const StoryUrlModePreference(), const StoryUrlModePreference(),
const MarkReadStoriesModePreference(),
const NotificationModePreference(), const NotificationModePreference(),
const SwipeGesturePreference(), const SwipeGesturePreference(),
const AutoScrollModePreference(), const AutoScrollModePreference(),
const CollapseModePreference(), const CollapseModePreference(),
const ReaderModePreference(), const ReaderModePreference(),
const MarkReadStoriesModePreference(),
const EyeCandyModePreference(), const EyeCandyModePreference(),
const TrueDarkModePreference(), const TrueDarkModePreference(),
], ],
@ -68,6 +69,8 @@ final int _fontSizeDefaultValue = FontSize.regular.index;
final int _fontDefaultValue = Font.roboto.index; final int _fontDefaultValue = Font.roboto.index;
final int _tabOrderDefaultValue = final int _tabOrderDefaultValue =
StoryType.convertToSettingsValue(StoryType.values); StoryType.convertToSettingsValue(StoryType.values);
final int _markStoriesAsReadWhenPreferenceDefaultValue =
StoryMarkingMode.tap.index;
class SwipeGesturePreference extends BooleanPreference { class SwipeGesturePreference extends BooleanPreference {
const SwipeGesturePreference({bool? val}) const SwipeGesturePreference({bool? val})
@ -364,3 +367,19 @@ class TabOrderPreference extends IntPreference {
@override @override
String get title => 'Tab order'; 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/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.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/blocs/blocs.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
@ -30,13 +31,6 @@ class HomeScreen extends StatefulWidget {
static const String routeName = '/'; static const String routeName = '/';
static Route<dynamic> route() {
return MaterialPageRoute<HomeScreen>(
settings: const RouteSettings(name: routeName),
builder: (BuildContext context) => const HomeScreen(),
);
}
@override @override
_HomeScreenState createState() => _HomeScreenState(); _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 useReader = context.read<PreferenceCubit>().state.readerEnabled;
final bool offlineReading = final bool offlineReading =
context.read<StoriesBloc>().state.isOfflineReading; context.read<StoriesBloc>().state.isOfflineReading;
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled; 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, // 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. // it would be better to just navigate to the web page.
@ -222,23 +218,14 @@ class _HomeScreenState extends State<HomeScreen>
if (isJobWithLink) { if (isJobWithLink) {
context.read<ReminderCubit>().removeLastReadStoryId(); context.read<ReminderCubit>().removeLastReadStoryId();
} else { } else {
final ItemScreenArgs args = ItemScreenArgs( final ItemScreenArgs args = ItemScreenArgs(item: story);
item: story,
);
context.read<ReminderCubit>().updateLastReadStoryId(story.id); context.read<ReminderCubit>().updateLastReadStoryId(story.id);
if (splitViewEnabled) { if (splitViewEnabled) {
context.read<SplitViewCubit>().updateItemScreenArgs(args); context.read<SplitViewCubit>().updateItemScreenArgs(args);
} else { } else {
HackiApp.navigatorKey.currentState context.push('/${ItemScreen.routeName}', extra: args);
?.pushNamed(
ItemScreen.routeName,
arguments: args,
)
.whenComplete(() {
context.read<ReminderCubit>().removeLastReadStoryId();
});
} }
} }
@ -250,11 +237,9 @@ class _HomeScreenState extends State<HomeScreen>
); );
} }
context.read<StoriesBloc>().add( if (storyMarkingMode.shouldDetectTapping) {
StoryRead( context.read<StoriesBloc>().add(StoryRead(story: story));
story: story, }
),
);
if (Platform.isIOS) { if (Platform.isIOS) {
FlutterSiriSuggestions.instance.registerActivity( FlutterSiriSuggestions.instance.registerActivity(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -1,11 +1,13 @@
import 'package:flutter/gestures.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_slidable/flutter_slidable.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.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/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/profile/models/models.dart'; import 'package:hacki/screens/profile/models/models.dart';
@ -31,7 +33,7 @@ class _ProfileScreenState extends State<ProfileScreen>
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final Throttle throttle = Throttle(delay: Durations.twoSeconds); final Throttle throttle = Throttle(delay: Durations.twoSeconds);
PageType pageType = PageType.notification; PageType? pageType;
@override @override
void dispose() { void dispose() {
@ -45,6 +47,9 @@ class _ProfileScreenState extends State<ProfileScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
pageType ??= context.read<AuthBloc>().state.isLoggedIn
? PageType.notification
: PageType.fav;
super.build(context); super.build(context);
return BlocBuilder<AuthBloc, AuthState>( return BlocBuilder<AuthBloc, AuthState>(
builder: (BuildContext context, AuthState authState) { builder: (BuildContext context, AuthState authState) {
@ -91,8 +96,8 @@ class _ProfileScreenState extends State<ProfileScreen>
} }
return ItemsListView<Item>( return ItemsListView<Item>(
showWebPreview: false, showWebPreviewOnStoryTile: false,
showMetadata: false, showMetadataOnStoryTile: false,
showUrl: false, showUrl: false,
useConsistentFontSize: true, useConsistentFontSize: true,
refreshController: refreshControllerHistory, refreshController: refreshControllerHistory,
@ -157,8 +162,10 @@ class _ProfileScreenState extends State<ProfileScreen>
PreferenceState prefState, PreferenceState prefState,
) { ) {
return ItemsListView<Item>( return ItemsListView<Item>(
showWebPreview: prefState.complexStoryTileEnabled, showWebPreviewOnStoryTile:
showMetadata: prefState.metadataEnabled, prefState.complexStoryTileEnabled,
showMetadataOnStoryTile:
prefState.metadataEnabled,
showUrl: prefState.urlEnabled, showUrl: prefState.urlEnabled,
useCommentTile: true, useCommentTile: true,
refreshController: refreshControllerFav, refreshController: refreshControllerFav,
@ -173,6 +180,28 @@ class _ProfileScreenState extends State<ProfileScreen>
onTap: (Item item) => goToItemScreen( onTap: (Item item) => goToItemScreen(
args: ItemScreenArgs(item: item), 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, selected: false,
onSelected: (bool val) { onSelected: (bool val) {
if (authState.isLoggedIn) { if (authState.isLoggedIn) {
HackiApp.navigatorKey.currentState context.push('/${SubmitScreen.routeName}');
?.pushNamed(SubmitScreen.routeName);
} else { } else {
showSnackBar( showSnackBar(
content: 'You need to log in first.', content: 'You need to log in first.',

View File

@ -1,19 +1,12 @@
import 'package:flutter/material.dart'; 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:hacki/styles/styles.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart';
class QrCodeScannerScreen extends StatefulWidget { class QrCodeScannerScreen extends StatefulWidget {
const QrCodeScannerScreen({super.key}); const QrCodeScannerScreen({super.key});
static const String routeName = '/qr-code-scanner'; static const String routeName = 'qr-code-scanner';
static Route<dynamic> route() {
return MaterialPageRoute<String?>(
settings: const RouteSettings(name: routeName),
builder: (_) => const QrCodeScannerScreen(),
);
}
@override @override
State<QrCodeScannerScreen> createState() => _QrCodeScannerScreenState(); State<QrCodeScannerScreen> createState() => _QrCodeScannerScreenState();
@ -66,7 +59,7 @@ class _QrCodeScannerScreenState extends State<QrCodeScannerScreen> {
}); });
controller.scannedDataStream.listen((Barcode scanData) { controller.scannedDataStream.listen((Barcode scanData) {
controller.stopCamera(); 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; final String data;
static const String routeName = '/qr-code-view'; 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 int qrCodeVersion = 4; static const int qrCodeVersion = 4;

View File

@ -1,6 +1,7 @@
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.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:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
@ -64,15 +65,15 @@ class OfflineListTile extends StatelessWidget {
title: const Text('Abort downloading?'), title: const Text('Abort downloading?'),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => context.pop(),
child: const Text('Cancel'), child: const Text('Cancel'),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () => context.pop(false),
child: const Text('No'), child: const Text('No'),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => context.pop(true),
child: const Text('Yes'), child: const Text('Yes'),
), ),
], ],
@ -93,15 +94,15 @@ class OfflineListTile extends StatelessWidget {
content: const Text('It will take longer time.'), content: const Text('It will take longer time.'),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => context.pop(),
child: const Text('Cancel'), child: const Text('Cancel'),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () => context.pop(false),
child: const Text('No'), child: const Text('No'),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => context.pop(true),
child: const Text('Yes'), 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_email_sender/flutter_email_sender.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/blocs/blocs.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/config/custom_router.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/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/profile/models/page_type.dart'; import 'package:hacki/screens/profile/models/page_type.dart';
@ -40,7 +41,7 @@ class Settings extends StatefulWidget {
final AuthState authState; final AuthState authState;
final String magicWord; final String magicWord;
final PageType pageType; final PageType? pageType;
@override @override
State<Settings> createState() => _SettingsState(); State<Settings> createState() => _SettingsState();
@ -221,6 +222,51 @@ class _SettingsState extends State<Settings> {
}, },
activeColor: Palette.orange, 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(), if (preference is StoryUrlModePreference) const Divider(),
], ],
ListTile( ListTile(
@ -300,14 +346,14 @@ class _SettingsState extends State<Settings> {
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => context.pop(),
child: const Text( child: const Text(
'Cancel', 'Cancel',
), ),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); context.pop();
context.read<AuthBloc>().add(AuthLogout()); context.read<AuthBloc>().add(AuthLogout());
context.read<HistoryCubit>().reset(); context.read<HistoryCubit>().reset();
}, },
@ -432,7 +478,7 @@ class _SettingsState extends State<Settings> {
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => context.pop(),
child: const Text( child: const Text(
'Cancel', 'Cancel',
style: TextStyle( style: TextStyle(
@ -442,7 +488,7 @@ class _SettingsState extends State<Settings> {
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); context.pop();
locator locator
.get<SembastRepository>() .get<SembastRepository>()
.deleteAllCachedComments() .deleteAllCachedComments()
@ -719,7 +765,7 @@ class _SettingsState extends State<Settings> {
), ),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => context.pop(),
child: const Text( child: const Text(
'Okay', 'Okay',
), ),
@ -742,7 +788,7 @@ class _SettingsState extends State<Settings> {
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => context.pop(),
child: const Text( child: const Text(
'Cancel', 'Cancel',
), ),
@ -752,7 +798,7 @@ class _SettingsState extends State<Settings> {
final String keyword = controller.text.trim(); final String keyword = controller.text.trim();
if (keyword.isEmpty) return; if (keyword.isEmpty) return;
context.read<FilterCubit>().addKeyword(keyword.toLowerCase()); context.read<FilterCubit>().addKeyword(keyword.toLowerCase());
Navigator.pop(context); context.pop();
}, },
child: const Text( child: const Text(
'Confirm', 'Confirm',
@ -776,7 +822,7 @@ class _SettingsState extends State<Settings> {
(ExportDestination e) => ListTile( (ExportDestination e) => ListTile(
leading: Icon(e.icon), leading: Icon(e.icon),
title: Text(e.label), 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 { Future<void> onImportFavoritesTapped(FavCubit favCubit) async {
final String? res = await HackiApp.navigatorKey.currentState final String? res =
?.pushNamed(QrCodeScannerScreen.routeName) as String?; await router.push('/${QrCodeScannerScreen.routeName}') as String?;
final List<int>? ids = final List<int>? ids =
res?.split('\n').map(int.tryParse).whereType<int>().toList(); res?.split('\n').map(int.tryParse).whereType<int>().toList();
if (ids == null) return; if (ids == null) return;
@ -813,9 +859,9 @@ class _SettingsState extends State<Settings> {
switch (destination) { switch (destination) {
case ExportDestination.qrCode: case ExportDestination.qrCode:
await HackiApp.navigatorKey.currentState?.pushNamed( await router.push(
QrCodeViewScreen.routeName, '/${QrCodeViewScreen.routeName}',
arguments: allFavoritesStr, extra: allFavoritesStr,
); );
case ExportDestination.clipBoard: case ExportDestination.clipBoard:
try { try {
@ -841,14 +887,14 @@ class _SettingsState extends State<Settings> {
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => context.pop(),
child: const Text( child: const Text(
'Cancel', 'Cancel',
), ),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); context.pop();
try { try {
context.read<FavCubit>().removeAll(); context.read<FavCubit>().removeAll();
showSnackBar(content: 'All favorites have been removed.'); showSnackBar(content: 'All favorites have been removed.');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
@ -31,7 +32,7 @@ class _OnboardingViewState extends State<OnboardingView> {
Icons.close, Icons.close,
color: Palette.white, color: Palette.white,
), ),
onPressed: () => Navigator.pop(context), onPressed: context.pop,
), ),
), ),
backgroundColor: Theme.of(context).brightness == Brightness.light backgroundColor: Theme.of(context).brightness == Brightness.light
@ -76,7 +77,7 @@ class _OnboardingViewState extends State<OnboardingView> {
onPressed: () { onPressed: () {
HapticFeedbackUtil.light(); HapticFeedbackUtil.light();
if (pageController.page! >= 2) { if (pageController.page! >= 2) {
Navigator.pop(context); context.pop();
} else { } else {
throttle.run(() { throttle.run(() {
pageController.nextPage( 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/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.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';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:visibility_detector/visibility_detector.dart';
class StoriesListView extends StatefulWidget { class StoriesListView extends StatefulWidget {
const StoriesListView({ const StoriesListView({
@ -26,11 +30,13 @@ class StoriesListView extends StatefulWidget {
class _StoriesListViewState extends State<StoriesListView> { class _StoriesListViewState extends State<StoriesListView> {
final RefreshController refreshController = RefreshController(); final RefreshController refreshController = RefreshController();
final ScrollController scrollController = ScrollController();
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
refreshController.dispose(); refreshController.dispose();
scrollController.dispose();
} }
@override @override
@ -63,14 +69,15 @@ class _StoriesListViewState extends State<StoriesListView> {
(previous.readStoriesIds.length != current.readStoriesIds.length), (previous.readStoriesIds.length != current.readStoriesIds.length),
builder: (BuildContext context, StoriesState state) { builder: (BuildContext context, StoriesState state) {
return ItemsListView<Story>( return ItemsListView<Story>(
pinnable: true,
showOfflineBanner: true, showOfflineBanner: true,
markReadStories: markReadStories:
context.read<PreferenceCubit>().state.markReadStoriesEnabled, context.read<PreferenceCubit>().state.markReadStoriesEnabled,
showWebPreview: preferenceState.complexStoryTileEnabled, showWebPreviewOnStoryTile:
showMetadata: preferenceState.metadataEnabled, preferenceState.complexStoryTileEnabled,
showMetadataOnStoryTile: preferenceState.metadataEnabled,
showUrl: preferenceState.urlEnabled, showUrl: preferenceState.urlEnabled,
refreshController: refreshController, refreshController: refreshController,
scrollController: scrollController,
items: state.storiesByType[storyType]!, items: state.storiesByType[storyType]!,
onRefresh: () { onRefresh: () {
HapticFeedbackUtil.light(); HapticFeedbackUtil.light();
@ -88,6 +95,64 @@ class _StoriesListViewState extends State<StoriesListView> {
onPinned: context.read<PinCubit>().pinStory, onPinned: context.read<PinCubit>().pinStory,
header: state.isOfflineReading ? null : header, header: state.isOfflineReading ? null : header,
onMoreTapped: onMoreTapped, 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 'link_preview/link_preview.dart';
export 'offline_banner.dart'; export 'offline_banner.dart';
export 'onboarding_view.dart'; export 'onboarding_view.dart';
export 'optional_wrapper.dart';
export 'spring_curve.dart'; export 'spring_curve.dart';
export 'stories_list_view.dart'; export 'stories_list_view.dart';
export 'story_tile.dart'; export 'story_tile.dart';

View File

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

View File

@ -483,6 +483,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" 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: hive:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1244,6 +1252,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0+1" 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: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -1414,4 +1430,4 @@ packages:
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.1.0-185.0.dev <4.0.0" 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 name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 1.8.4+120 version: 1.9.1+122
publish_to: none publish_to: none
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
flutter: "3.13.2" flutter: "3.13.4"
dependencies: dependencies:
adaptive_theme: ^3.2.0 adaptive_theme: ^3.2.0
@ -38,6 +38,7 @@ dependencies:
font_awesome_flutter: ^10.3.0 font_awesome_flutter: ^10.3.0
gbk_codec: ^0.4.0 gbk_codec: ^0.4.0
get_it: ^7.2.0 get_it: ^7.2.0
go_router: ^10.1.2
hive: ^2.2.3 hive: ^2.2.3
html: ^0.15.1 html: ^0.15.1
html_unescape: ^2.0.0 html_unescape: ^2.0.0
@ -74,6 +75,7 @@ dependencies:
path: components/synced_shared_preferences path: components/synced_shared_preferences
universal_platform: ^1.0.0+1 universal_platform: ^1.0.0+1
url_launcher: ^6.1.9 url_launcher: ^6.1.9
visibility_detector: ^0.4.0+2
wakelock: ^0.6.2 wakelock: ^0.6.2
webview_flutter: ^4.0.2 webview_flutter: ^4.0.2
workmanager: ^0.5.1 workmanager: ^0.5.1