mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
f03b45a98a | |||
cbe5bba986 | |||
268f4054a3 | |||
988c5d9881 | |||
e748e2f818 | |||
1b0a0dbda9 | |||
64d68389ba |
1
fastlane/metadata/android/en-US/changelogs/121.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/121.txt
Normal file
@ -0,0 +1 @@
|
||||
- Ability to mark a story as read once scrolling past.
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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',
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
}
|
||||
|
21
lib/models/story_marking_mode.dart
Normal file
21
lib/models/story_marking_mode.dart
Normal 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;
|
||||
}
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -431,6 +431,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
|
@ -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.',
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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'),
|
||||
),
|
||||
],
|
||||
|
@ -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.');
|
||||
|
@ -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:
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -10,6 +10,8 @@ class WebViewScreen extends StatefulWidget {
|
||||
super.key,
|
||||
});
|
||||
|
||||
static const String routeName = 'web-view';
|
||||
|
||||
final String url;
|
||||
|
||||
@override
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
23
lib/screens/widgets/optional_wrapper.dart
Normal file
23
lib/screens/widgets/optional_wrapper.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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';
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
18
pubspec.lock
18
pubspec.lock
@ -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"
|
||||
|
@ -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
|
||||
|
Submodule submodules/flutter updated: ff5b5b5fa6...367f9ea16b
Reference in New Issue
Block a user