Compare commits

...

16 Commits

91 changed files with 1291 additions and 888 deletions

View File

@ -9,13 +9,14 @@ on:
jobs:
releases:
name: Check commit
runs-on: ubuntu-latest
runs-on: macos-latest
timeout-minutes: 30
steps:
- name: checkout all the submodules
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- run: submodules/flutter/bin/flutter doctor
- run: submodules/flutter/bin/flutter pub get
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test

View File

@ -23,6 +23,7 @@ jobs:
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- run: submodules/flutter/bin/flutter doctor
- run: submodules/flutter/bin/flutter pub get
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test

View File

@ -29,6 +29,7 @@ Features:
- Download stories and comments for offline reading.
- Pick up where you left off.
- Synced favorites and pins across devices. (iOS only)
- Export or import your favorites.
- Launch from system share sheet.
- And more...

View File

@ -1,121 +0,0 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_review_platform_interface/method_channel_in_app_review.dart';
import 'package:platform/platform.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late MethodChannelInAppReview methodChannelInAppReview;
late List<MethodCall> log = <MethodCall>[];
const MethodChannel channel = MethodChannel('dev.britannio.in_app_review');
setUp(() {
methodChannelInAppReview = MethodChannelInAppReview();
methodChannelInAppReview.channel = channel;
log = <MethodCall>[];
});
tearDown(() {
log.clear();
});
group('isAvailable', () {
test(
'should invoke the isAvailable method channel',
() async {
// ACT
final bool result = await methodChannelInAppReview.isAvailable();
// ASSERT
expect(log, <Matcher>[isMethodCall('isAvailable', arguments: null)]);
expect(result, isTrue);
},
);
});
group('requestReview', () {
test(
'should invoke the requestReview method channel',
() async {
// ACT
await methodChannelInAppReview.requestReview();
// ASSERT
expect(log, <Matcher>[isMethodCall('requestReview', arguments: null)]);
},
);
});
group('openStoreListing', () {
test(
'should invoke the openStoreListing method channel on Android',
() async {
// ARRANGE
methodChannelInAppReview.platform =
FakePlatform(operatingSystem: 'android');
// ACT
await methodChannelInAppReview.openStoreListing();
// ASSERT
expect(
log,
<Matcher>[isMethodCall('openStoreListing', arguments: null)],
);
},
);
test(
'should invoke the openStoreListing method channel on iOS',
() async {
// ARRANGE
methodChannelInAppReview.platform =
FakePlatform(operatingSystem: 'ios');
final String appStoreId = "store_id";
// ACT
await methodChannelInAppReview.openStoreListing(appStoreId: appStoreId);
// ASSERT
expect(log,
<Matcher>[isMethodCall('openStoreListing', arguments: appStoreId)]);
},
);
test(
'should invoke the openStoreListing method channel on MacOS',
() async {
// ARRANGE
methodChannelInAppReview.platform =
FakePlatform(operatingSystem: 'macos');
final String appStoreId = "store_id";
// ACT
await methodChannelInAppReview.openStoreListing(appStoreId: appStoreId);
// ASSERT
expect(log,
<Matcher>[isMethodCall('openStoreListing', arguments: appStoreId)]);
},
);
test(
'should invoke the openStoreListing method channel on Windows',
() async {
// ARRANGE
methodChannelInAppReview.platform =
FakePlatform(operatingSystem: 'windows');
final String microsoftStoreId = 'store_id';
// ACT
await methodChannelInAppReview.openStoreListing(
microsoftStoreId: microsoftStoreId,
);
// ASSERT
expect(log, <Matcher>[
isMethodCall('openStoreListing', arguments: microsoftStoreId)
]);
},
skip:
'The windows uwp implementation still uses the url_launcher package',
);
});
}

View File

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

View File

@ -27,12 +27,16 @@ PODS:
- Flutter
- integration_test (0.0.1):
- Flutter
- MTBBarcodeScanner (5.0.11)
- OrderedSet (5.0.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- qr_code_scanner (0.2.0):
- Flutter
- MTBBarcodeScanner
- ReachabilitySwift (5.0.0)
- receive_sharing_intent (0.0.1):
- Flutter
@ -68,6 +72,7 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@ -81,6 +86,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- FMDB
- MTBBarcodeScanner
- OrderedSet
- ReachabilitySwift
@ -109,6 +115,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
share_plus:
@ -140,13 +148,15 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
integration_test: 13825b8a9334a850581300559b8839134b124670
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4

View File

@ -291,7 +291,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1330;
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -76,5 +76,11 @@
<false/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>FLTEnableWideGamut</key>
<false/>
</dict>
</plist>

View File

@ -52,14 +52,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
state.copyWith(
isLoggedIn: true,
user: user,
status: AuthStatus.loaded,
status: Status.success,
),
);
} else {
emit(
state.copyWith(
isLoggedIn: false,
status: AuthStatus.loaded,
status: Status.success,
),
);
}
@ -81,7 +81,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
Future<void> onLogin(AuthLogin event, Emitter<AuthState> emit) async {
emit(state.copyWith(status: AuthStatus.loading));
emit(state.copyWith(status: Status.inProgress));
final bool successful = await _authRepository.login(
username: event.username,
@ -94,11 +94,11 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
state.copyWith(
user: user ?? User.emptyWithId(event.username),
isLoggedIn: true,
status: AuthStatus.loaded,
status: Status.success,
),
);
} else {
emit(state.copyWith(status: AuthStatus.failure));
emit(state.copyWith(status: Status.failure));
}
}

View File

@ -1,11 +1,5 @@
part of 'auth_bloc.dart';
enum AuthStatus {
loading,
loaded,
failure,
}
class AuthState extends Equatable {
const AuthState({
required this.user,
@ -17,13 +11,13 @@ class AuthState extends Equatable {
const AuthState.init()
: user = const User.empty(),
isLoggedIn = false,
status = AuthStatus.loaded,
status = Status.success,
agreedToEULA = false;
final User user;
final bool isLoggedIn;
final bool agreedToEULA;
final AuthStatus status;
final Status status;
String get username => user.id;
@ -31,7 +25,7 @@ class AuthState extends Equatable {
User? user,
bool? isLoggedIn,
bool? agreedToEULA,
AuthStatus? status,
Status? status,
}) {
return AuthState(
user: user ?? this.user,

View File

@ -79,7 +79,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
const StoriesState.init().copyWith(
isOfflineReading: hasCachedStories &&
// Only go into offline mode in the next session.
state.downloadStatus == StoriesDownloadStatus.initial,
state.downloadStatus == StoriesDownloadStatus.idle,
currentPageSize: pageSize,
downloadStatus: state.downloadStatus,
storiesDownloaded: state.storiesDownloaded,
@ -133,10 +133,12 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesRefresh event,
Emitter<StoriesState> emit,
) async {
if (state.statusByType[event.type] == Status.inProgress) return;
emit(
state.copyWithStatusUpdated(
type: event.type,
to: StoriesStatus.loading,
to: Status.inProgress,
),
);
@ -144,7 +146,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
emit(
state.copyWithStatusUpdated(
type: event.type,
to: StoriesStatus.loaded,
to: Status.success,
),
);
} else {
@ -157,7 +159,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
emit(
state.copyWithStatusUpdated(
type: event.type,
to: StoriesStatus.loading,
to: Status.inProgress,
),
);
@ -216,7 +218,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
emit(
state.copyWithStatusUpdated(
type: event.type,
to: StoriesStatus.loaded,
to: Status.success,
),
);
}
@ -243,7 +245,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) {
emit(
state.copyWithStatusUpdated(type: event.type, to: StoriesStatus.loaded),
state.copyWithStatusUpdated(type: event.type, to: Status.success),
);
}

View File

@ -1,13 +1,7 @@
part of 'stories_bloc.dart';
enum StoriesStatus {
initial,
loading,
loaded,
}
enum StoriesDownloadStatus {
initial,
idle,
downloading,
finished,
failure,
@ -43,12 +37,12 @@ class StoriesState extends Equatable {
StoryType.ask: <int>[],
StoryType.show: <int>[],
},
this.statusByType = const <StoryType, StoriesStatus>{
StoryType.top: StoriesStatus.initial,
StoryType.best: StoriesStatus.initial,
StoryType.latest: StoriesStatus.initial,
StoryType.ask: StoriesStatus.initial,
StoryType.show: StoriesStatus.initial,
this.statusByType = const <StoryType, Status>{
StoryType.top: Status.idle,
StoryType.best: Status.idle,
StoryType.latest: Status.idle,
StoryType.ask: Status.idle,
StoryType.show: Status.idle,
},
this.currentPageByType = const <StoryType, int>{
StoryType.top: 0,
@ -58,7 +52,7 @@ class StoriesState extends Equatable {
StoryType.show: 0,
},
}) : isOfflineReading = false,
downloadStatus = StoriesDownloadStatus.initial,
downloadStatus = StoriesDownloadStatus.idle,
currentPageSize = 0,
readStoriesIds = const <int>{},
storiesDownloaded = 0,
@ -66,7 +60,7 @@ class StoriesState extends Equatable {
final Map<StoryType, List<Story>> storiesByType;
final Map<StoryType, List<int>> storyIdsByType;
final Map<StoryType, StoriesStatus> statusByType;
final Map<StoryType, Status> statusByType;
final Map<StoryType, int> currentPageByType;
final Set<int> readStoriesIds;
final StoriesDownloadStatus downloadStatus;
@ -78,7 +72,7 @@ class StoriesState extends Equatable {
StoriesState copyWith({
Map<StoryType, List<Story>>? storiesByType,
Map<StoryType, List<int>>? storyIdsByType,
Map<StoryType, StoriesStatus>? statusByType,
Map<StoryType, Status>? statusByType,
Map<StoryType, int>? currentPageByType,
Set<int>? readStoriesIds,
StoriesDownloadStatus? downloadStatus,
@ -133,10 +127,10 @@ class StoriesState extends Equatable {
StoriesState copyWithStatusUpdated({
required StoryType type,
required StoriesStatus to,
required Status to,
}) {
final Map<StoryType, StoriesStatus> newMap =
Map<StoryType, StoriesStatus>.from(statusByType);
final Map<StoryType, Status> newMap =
Map<StoryType, Status>.from(statusByType);
newMap[type] = to;
return copyWith(
statusByType: newMap,
@ -162,9 +156,9 @@ class StoriesState extends Equatable {
final Map<StoryType, List<int>> newStoryIdsMap =
Map<StoryType, List<int>>.from(storyIdsByType);
newStoryIdsMap[type] = <int>[];
final Map<StoryType, StoriesStatus> newStatusMap =
Map<StoryType, StoriesStatus>.from(statusByType);
newStatusMap[type] = StoriesStatus.loading;
final Map<StoryType, Status> newStatusMap =
Map<StoryType, Status>.from(statusByType);
newStatusMap[type] = Status.inProgress;
final Map<StoryType, int> newCurrentPageMap =
Map<StoryType, int>.from(currentPageByType);
newCurrentPageMap[type] = 0;

View File

@ -39,8 +39,9 @@ abstract class Constants {
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
static const String featureLogIn = 'log_in';
static const String featurePinToTop = 'pin_to_top';
static const String featureJumpUpButton = 'jump_up_button';
static const String featureJumpDownButton = 'jump_down_button';
static const String featureJumpUpButton = 'jump_up_button_with_long_press';
static const String featureJumpDownButton =
'jump_down_button_with_long_press';
static final String happyFace = <String>[
'(๑•̀ㅂ•́)و✧',

View File

@ -2,7 +2,7 @@ import 'package:logger/logger.dart';
class CustomLogFilter extends LogFilter {
@override
Level? get level => Level.verbose;
Level? get level => Level.trace;
/// The minimal level allowed in production.
static const Level _minimalLevel = Level.info;

View File

@ -1,49 +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 ItemScreen.routeName:
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
case SubmitScreen.routeName:
return SubmitScreen.route();
default:
return _errorRoute();
}
}
/// Nested routing for bottom navigation bar.
static Route<dynamic> onGenerateNestedRoute(RouteSettings settings) {
switch (settings.name) {
case ItemScreen.routeName:
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
case SubmitScreen.routeName:
return SubmitScreen.route();
default:
return _errorRoute();
}
}
/// Error route.
static Route<dynamic> _errorRoute() {
return MaterialPageRoute<dynamic>(
settings: const RouteSettings(name: '/error'),
builder: (_) => Scaffold(
appBar: AppBar(
title: const Text('Error'),
),
body: Center(
child: Text(Constants.errorMessage),
final GoRouter router = GoRouter(
observers: <NavigatorObserver>[
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
],
initialLocation: HomeScreen.routeName,
routes: <RouteBase>[
GoRoute(
path: HomeScreen.routeName,
builder: (_, __) => const HomeScreen(),
routes: <RouteBase>[
GoRoute(
path: ItemScreen.routeName,
builder: (_, GoRouterState state) {
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
if (args == null) {
throw GoError("args can't be null");
}
return ItemScreen.phone(args);
},
),
],
),
GoRoute(
path: '/${ItemScreen.routeName}',
builder: (_, GoRouterState state) {
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
if (args == null) {
throw GoError("args can't be null");
}
return ItemScreen.phone(args);
},
),
GoRoute(
path: '/${SubmitScreen.routeName}',
builder: (_, __) => BlocProvider<SubmitCubit>(
create: (_) => SubmitCubit(),
child: const SubmitScreen(),
),
);
}
}
),
GoRoute(
path: '/${QrCodeScannerScreen.routeName}',
builder: (_, __) => const QrCodeScannerScreen(),
),
GoRoute(
path: '/${QrCodeViewScreen.routeName}',
builder: (_, GoRouterState state) {
final String? data = state.extra as String?;
if (data == null) {
throw GoError("data can't be null");
}
return QrCodeViewScreen(
data: data,
);
},
),
GoRoute(
path: '/${WebViewScreen.routeName}',
builder: (_, GoRouterState state) {
final String? link = state.extra as String?;
if (link == null) {
throw GoError("link can't be null");
}
return WebViewScreen(
url: link,
);
},
),
],
);

View File

@ -20,7 +20,7 @@ class CustomFileOutput extends LogOutput {
IOSink? _sink;
@override
void init() {
Future<void> init() async {
_sink = file.openWrite(
mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend,
encoding: encoding,

View File

@ -6,9 +6,9 @@ import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart';
@ -106,7 +106,7 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(
state.copyWith(
status: CommentsStatus.loading,
status: CommentsStatus.inProgress,
comments: <Comment>[],
currentPage: 0,
),
@ -150,7 +150,7 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<void> refresh() async {
emit(
state.copyWith(
status: CommentsStatus.loading,
status: CommentsStatus.inProgress,
),
);
@ -224,7 +224,7 @@ class CommentsCubit extends Cubit<CommentsState> {
void Function(Comment)? onCommentFetched,
VoidCallback? onDone,
}) {
if (comment == null && state.status == CommentsStatus.loading) return;
if (comment == null && state.status == CommentsStatus.inProgress) return;
switch (state.fetchMode) {
case FetchMode.lazy:
@ -269,7 +269,7 @@ class CommentsCubit extends Cubit<CommentsState> {
_streamSubscriptions[comment.id] = streamSubscription;
case FetchMode.eager:
if (_streamSubscription != null) {
emit(state.copyWith(status: CommentsStatus.loading));
emit(state.copyWith(status: CommentsStatus.inProgress));
_streamSubscription
?..resume()
..onData(onCommentFetched);
@ -279,16 +279,16 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<void> loadParentThread() async {
HapticFeedbackUtil.light();
emit(state.copyWith(fetchParentStatus: CommentsStatus.loading));
emit(state.copyWith(fetchParentStatus: CommentsStatus.inProgress));
final Item? parent =
await _storiesRepository.fetchItem(id: state.item.parent);
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(
@ -301,7 +301,7 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<void> loadRootThread() async {
HapticFeedbackUtil.light();
emit(state.copyWith(fetchRootStatus: CommentsStatus.loading));
emit(state.copyWith(fetchRootStatus: CommentsStatus.inProgress));
final Story? parent = await _storiesRepository
.fetchParentStory(id: state.item.id)
.then(_toBuildableStory);
@ -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(
@ -349,8 +349,8 @@ class CommentsCubit extends Cubit<CommentsState> {
init(useCommentCache: true);
}
/// Jump to next root level comment.
void jump(
/// Scroll to next root level comment.
void scrollToNextRoot(
ItemScrollController itemScrollController,
ItemPositionsListener itemPositionsListener,
) {
@ -387,8 +387,8 @@ class CommentsCubit extends Cubit<CommentsState> {
}
}
/// Jump to previous root level comment.
void jumpUp(
/// Scroll to previous root level comment.
void scrollToPreviousRoot(
ItemScrollController itemScrollController,
ItemPositionsListener itemPositionsListener,
) {

View File

@ -1,11 +1,11 @@
part of 'comments_cubit.dart';
enum CommentsStatus {
init,
loading,
idle,
inProgress,
loaded,
allLoaded,
failure,
error,
}
class CommentsState extends Equatable {
@ -28,9 +28,9 @@ class CommentsState extends Equatable {
required this.fetchMode,
required this.order,
}) : comments = <Comment>[],
status = CommentsStatus.init,
fetchParentStatus = CommentsStatus.init,
fetchRootStatus = CommentsStatus.init,
status = CommentsStatus.idle,
fetchParentStatus = CommentsStatus.idle,
fetchRootStatus = CommentsStatus.idle,
onlyShowTargetComment = false,
currentPage = 0;

View File

@ -51,7 +51,7 @@ class FavCubit extends Cubit<FavState> {
.onDone(() {
emit(
state.copyWith(
status: FavStatus.loaded,
status: Status.success,
),
);
});
@ -107,7 +107,7 @@ class FavCubit extends Cubit<FavState> {
}
void loadMore() {
emit(state.copyWith(status: FavStatus.loading));
emit(state.copyWith(status: Status.inProgress));
final int currentPage = state.currentPage;
final int len = state.favIds.length;
emit(state.copyWith(currentPage: currentPage + 1));
@ -128,10 +128,10 @@ class FavCubit extends Cubit<FavState> {
)
.listen(_onItemLoaded)
.onDone(() {
emit(state.copyWith(status: FavStatus.loaded));
emit(state.copyWith(status: Status.success));
});
} else {
emit(state.copyWith(status: FavStatus.loaded));
emit(state.copyWith(status: Status.success));
}
}
@ -140,7 +140,7 @@ class FavCubit extends Cubit<FavState> {
emit(
state.copyWith(
status: FavStatus.loading,
status: Status.inProgress,
currentPage: 0,
favItems: <Item>[],
favIds: <int>[],
@ -155,7 +155,7 @@ class FavCubit extends Cubit<FavState> {
)
.listen(_onItemLoaded)
.onDone(() {
emit(state.copyWith(status: FavStatus.loaded));
emit(state.copyWith(status: Status.success));
});
});
}

View File

@ -1,12 +1,5 @@
part of 'fav_cubit.dart';
enum FavStatus {
init,
loading,
loaded,
failure,
}
class FavState extends Equatable {
const FavState({
required this.favIds,
@ -18,18 +11,18 @@ class FavState extends Equatable {
FavState.init()
: favIds = <int>[],
favItems = <Item>[],
status = FavStatus.init,
status = Status.idle,
currentPage = 0;
final List<int> favIds;
final List<Item> favItems;
final FavStatus status;
final Status status;
final int currentPage;
FavState copyWith({
List<int>? favIds,
List<Item>? favItems,
FavStatus? status,
Status? status,
int? currentPage,
}) {
return FavState(

View File

@ -54,7 +54,7 @@ class HistoryCubit extends Cubit<HistoryState> {
}
void loadMore() {
emit(state.copyWith(status: HistoryStatus.loading));
emit(state.copyWith(status: Status.inProgress));
final int currentPage = state.currentPage;
final int len = state.submittedIds.length;
emit(state.copyWith(currentPage: currentPage + 1));
@ -75,10 +75,10 @@ class HistoryCubit extends Cubit<HistoryState> {
)
.listen(_onItemLoaded)
.onDone(() {
emit(state.copyWith(status: HistoryStatus.loaded));
emit(state.copyWith(status: Status.success));
});
} else {
emit(state.copyWith(status: HistoryStatus.loaded));
emit(state.copyWith(status: Status.success));
}
}
@ -86,7 +86,7 @@ class HistoryCubit extends Cubit<HistoryState> {
final String username = _authBloc.state.username;
emit(
state.copyWith(
status: HistoryStatus.loading,
status: Status.inProgress,
currentPage: 0,
submittedIds: <int>[],
submittedItems: <Item>[],
@ -107,7 +107,7 @@ class HistoryCubit extends Cubit<HistoryState> {
)
.listen(_onItemLoaded)
.onDone(() {
emit(state.copyWith(status: HistoryStatus.loaded));
emit(state.copyWith(status: Status.success));
});
}
});

View File

@ -1,12 +1,5 @@
part of 'history_cubit.dart';
enum HistoryStatus {
init,
loading,
loaded,
failure,
}
class HistoryState extends Equatable {
const HistoryState({
required this.submittedIds,
@ -18,18 +11,18 @@ class HistoryState extends Equatable {
HistoryState.init()
: submittedIds = <int>[],
submittedItems = <Item>[],
status = HistoryStatus.init,
status = Status.idle,
currentPage = 0;
final List<int> submittedIds;
final List<Item> submittedItems;
final HistoryStatus status;
final Status status;
final int currentPage;
HistoryState copyWith({
List<int>? submittedIds,
List<Item>? submittedItems,
HistoryStatus? status,
Status? status,
int? currentPage,
}) {
return HistoryState(

View File

@ -100,7 +100,7 @@ class NotificationCubit extends Cubit<NotificationState> {
void markAsRead(int id) {
Future.doWhile(() {
if (state.status != NotificationStatus.loading) {
if (state.status != Status.inProgress) {
if (state.unreadCommentsIds.contains(id)) {
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
..remove(id);
@ -116,7 +116,7 @@ class NotificationCubit extends Cubit<NotificationState> {
void markAllAsRead() {
Future.doWhile(() {
if (state.status != NotificationStatus.loading) {
if (state.status != Status.inProgress) {
emit(state.copyWith(unreadCommentsIds: <int>[]));
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
return false;
@ -131,7 +131,7 @@ class NotificationCubit extends Cubit<NotificationState> {
_preferenceCubit.state.notificationEnabled) {
emit(
state.copyWith(
status: NotificationStatus.loading,
status: Status.inProgress,
),
);
@ -141,14 +141,14 @@ class NotificationCubit extends Cubit<NotificationState> {
} else {
emit(
state.copyWith(
status: NotificationStatus.loaded,
status: Status.success,
),
);
}
}
Future<void> loadMore() async {
emit(state.copyWith(status: NotificationStatus.loading));
emit(state.copyWith(status: Status.inProgress));
final int currentPage = state.currentPage + 1;
final int lower = currentPage * _pageSize + state.offset;
@ -169,7 +169,7 @@ class NotificationCubit extends Cubit<NotificationState> {
emit(
state.copyWith(
status: NotificationStatus.loaded,
status: Status.success,
currentPage: currentPage,
),
);
@ -237,7 +237,7 @@ class NotificationCubit extends Cubit<NotificationState> {
}
}).whenComplete(
() => emit(
state.copyWith(status: NotificationStatus.loaded),
state.copyWith(status: Status.success),
),
);
}

View File

@ -1,12 +1,5 @@
part of 'notification_cubit.dart';
enum NotificationStatus {
initial,
loading,
loaded,
failure,
}
class NotificationState extends Equatable {
const NotificationState({
required this.comments,
@ -23,14 +16,14 @@ class NotificationState extends Equatable {
allCommentsIds = <int>[],
currentPage = 0,
offset = 0,
status = NotificationStatus.initial;
status = Status.idle;
final List<Comment> comments;
final List<int> allCommentsIds;
final List<int> unreadCommentsIds;
final int currentPage;
final int offset;
final NotificationStatus status;
final Status status;
NotificationState copyWith({
List<Comment>? comments,
@ -38,7 +31,7 @@ class NotificationState extends Equatable {
List<int>? unreadCommentsIds,
int? currentPage,
int? offset,
NotificationStatus? status,
Status? status,
}) {
return NotificationState(
comments: comments ?? this.comments,

View File

@ -27,7 +27,7 @@ class PinCubit extends Cubit<PinState> {
emit(state.copyWith(pinnedStoriesIds: ids));
_storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched);
});
}).whenComplete(() => emit(state.copyWith(status: Status.success)));
}
void pinStory(Story story) {
@ -52,7 +52,10 @@ class PinCubit extends Cubit<PinState> {
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
}
void refresh() => init();
void refresh() {
if (state.status.isLoading) return;
init();
}
void _onStoryFetched(Story story) {
emit(state.copyWith(pinnedStories: <Story>[...state.pinnedStories, story]));

View File

@ -4,22 +4,27 @@ class PinState extends Equatable {
const PinState({
required this.pinnedStoriesIds,
required this.pinnedStories,
required this.status,
});
PinState.init()
: pinnedStoriesIds = <int>[],
pinnedStories = <Story>[];
pinnedStories = <Story>[],
status = Status.idle;
final List<int> pinnedStoriesIds;
final List<Story> pinnedStories;
final Status status;
PinState copyWith({
List<int>? pinnedStoriesIds,
List<Story>? pinnedStories,
Status? status,
}) {
return PinState(
pinnedStoriesIds: pinnedStoriesIds ?? this.pinnedStoriesIds,
pinnedStories: pinnedStories ?? this.pinnedStories,
status: status ?? this.status,
);
}
@ -27,5 +32,6 @@ class PinState extends Equatable {
List<Object?> get props => <Object?>[
pinnedStoriesIds,
pinnedStories,
status,
];
}

View File

@ -27,7 +27,7 @@ class PollCubit extends Cubit<PollState> {
emit(PollState.init());
}
emit(state.copyWith(status: PollStatus.loading));
emit(state.copyWith(status: Status.inProgress));
List<int> pollOptionsIds = _story.parts;
@ -42,7 +42,7 @@ class PollCubit extends Cubit<PollState> {
// If pollOptionsIds is still empty, exit loading state.
if (pollOptionsIds.isEmpty) {
emit(state.copyWith(status: PollStatus.loaded));
emit(state.copyWith(status: Status.success));
return;
}
@ -72,7 +72,7 @@ class PollCubit extends Cubit<PollState> {
);
}
emit(state.copyWith(status: PollStatus.loaded));
emit(state.copyWith(status: Status.success));
}
}

View File

@ -1,12 +1,5 @@
part of 'poll_cubit.dart';
enum PollStatus {
initial,
loading,
loaded,
failure,
}
class PollState extends Equatable {
const PollState({
required this.totalVotes,
@ -19,18 +12,18 @@ class PollState extends Equatable {
: totalVotes = 0,
selections = <int>{},
pollOptions = <PollOption>[],
status = PollStatus.initial;
status = Status.idle;
final int totalVotes;
final Set<int> selections;
final List<PollOption> pollOptions;
final PollStatus status;
final Status status;
PollState copyWith({
int? totalVotes,
Set<int>? selections,
List<PollOption>? pollOptions,
PollStatus? status,
Status? status,
}) {
return PollState(
totalVotes: totalVotes ?? this.totalVotes,

View File

@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
part 'post_state.dart';
@ -14,31 +15,31 @@ class PostCubit extends Cubit<PostState> {
final PostRepository _postRepository;
Future<void> post({required String text, required int to}) async {
emit(state.copyWith(status: PostStatus.loading));
emit(state.copyWith(status: Status.inProgress));
final bool successful = await _postRepository.comment(
parentId: to,
text: text,
);
if (successful) {
emit(state.copyWith(status: PostStatus.successful));
emit(state.copyWith(status: Status.success));
} else {
emit(state.copyWith(status: PostStatus.failure));
emit(state.copyWith(status: Status.failure));
}
}
Future<void> edit({required String text, required int id}) async {
emit(state.copyWith(status: PostStatus.loading));
emit(state.copyWith(status: Status.inProgress));
final bool successful = await _postRepository.edit(id: id, text: text);
if (successful) {
emit(state.copyWith(status: PostStatus.successful));
emit(state.copyWith(status: Status.success));
} else {
emit(state.copyWith(status: PostStatus.failure));
emit(state.copyWith(status: Status.failure));
}
}
void reset() {
emit(state.copyWith(status: PostStatus.init));
emit(state.copyWith(status: Status.idle));
}
}

View File

@ -1,20 +1,13 @@
part of 'post_cubit.dart';
enum PostStatus {
init,
loading,
successful,
failure,
}
class PostState extends Equatable {
const PostState({required this.status});
const PostState.init() : status = PostStatus.init;
const PostState.init() : status = Status.idle;
final PostStatus status;
final Status status;
PostState copyWith({PostStatus? status}) {
PostState copyWith({Status? status}) {
return PostState(
status: status ?? this.status,
);

View File

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

View File

@ -1,7 +1,7 @@
part of 'search_cubit.dart';
enum SearchStatus {
initial,
idle,
loading,
loadingMore,
loaded,
@ -15,7 +15,7 @@ class SearchState extends Equatable {
});
SearchState.init()
: status = SearchStatus.initial,
: status = SearchStatus.idle,
results = <Item>[],
params = SearchParams.init();

View File

@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/post_repository.dart';
part 'submit_state.dart';
@ -25,7 +26,7 @@ class SubmitCubit extends Cubit<SubmitState> {
}
void onSubmitTapped() {
emit(state.copyWith(status: SubmitStatus.submitting));
emit(state.copyWith(status: Status.inProgress));
if (state.title?.isNotEmpty ?? false) {
_postRepository
@ -35,9 +36,9 @@ class SubmitCubit extends Cubit<SubmitState> {
text: state.text,
)
.then((bool successful) {
emit(state.copyWith(status: SubmitStatus.submitted));
emit(state.copyWith(status: Status.success));
}).onError((Object? error, StackTrace stackTrace) {
emit(state.copyWith(status: SubmitStatus.failure));
emit(state.copyWith(status: Status.failure));
});
}
}

View File

@ -1,12 +1,5 @@
part of 'submit_cubit.dart';
enum SubmitStatus {
initial,
submitting,
submitted,
failure,
}
class SubmitState extends Equatable {
const SubmitState({
required this.title,
@ -19,18 +12,18 @@ class SubmitState extends Equatable {
: title = null,
url = null,
text = null,
status = SubmitStatus.initial;
status = Status.idle;
final String? title;
final String? url;
final String? text;
final SubmitStatus status;
final Status status;
SubmitState copyWith({
String? title,
String? url,
String? text,
SubmitStatus? status,
Status? status,
}) {
return SubmitState(
title: title ?? this.title,

View File

@ -15,16 +15,16 @@ class UserCubit extends Cubit<UserState> {
final StoriesRepository _storiesRepository;
void init({required String userId}) {
emit(state.copyWith(status: UserStatus.loading));
emit(state.copyWith(status: Status.inProgress));
_storiesRepository.fetchUser(id: userId).then((User? user) {
emit(
state.copyWith(
user: user ?? User.emptyWithId(userId),
status: UserStatus.loaded,
status: Status.success,
),
);
}).onError((_, __) {
emit(state.copyWith(status: UserStatus.failure));
emit(state.copyWith(status: Status.failure));
return;
});
}

View File

@ -1,12 +1,5 @@
part of 'user_cubit.dart';
enum UserStatus {
initial,
loading,
loaded,
failure,
}
class UserState extends Equatable {
const UserState({
required this.user,
@ -15,14 +8,14 @@ class UserState extends Equatable {
const UserState.init()
: user = const User.empty(),
status = UserStatus.initial;
status = Status.idle;
final User user;
final UserStatus status;
final Status status;
UserState copyWith({
User? user,
UserStatus? status,
Status? status,
}) {
return UserState(
user: user ?? this.user,

View File

@ -6,7 +6,7 @@ enum Vote {
}
enum VoteStatus {
initial,
idle,
canceled,
submitted,
failureBeHumble,
@ -24,7 +24,7 @@ class VoteState extends Equatable {
const VoteState.init({required this.item})
: vote = null,
status = VoteStatus.initial;
status = VoteStatus.idle;
/// Null means user has not voted,
/// True means user voted up,

View File

@ -14,6 +14,10 @@ extension ObjectExtension on Object {
String identifier = '',
StackTrace? stackTrace,
}) {
locator.get<Logger>().e(identifier, this, stackTrace ?? StackTrace.current);
locator.get<Logger>().e(
identifier,
error: this,
stackTrace: stackTrace ?? StackTrace.current,
);
}
}

View File

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

View File

@ -17,7 +17,6 @@ import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/fetcher.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/theme_util.dart';
@ -58,8 +57,8 @@ Future<void> main({bool testing = false}) async {
FlutterError.onError = (FlutterErrorDetails details) {
locator.get<Logger>().e(
details.summary,
details.exceptionAsString(),
details.stack,
error: details.exceptionAsString(),
stackTrace: details.stack,
);
};
@ -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(
@ -241,7 +237,7 @@ class HackiApp extends StatelessWidget {
create: (BuildContext context) => TabCubit(
preferenceCubit: context.read<PreferenceCubit>(),
)..init(),
)
),
],
child: AdaptiveTheme(
light: ThemeData(
@ -286,16 +282,13 @@ class HackiApp extends StatelessWidget {
.platformBrightness ==
Brightness.dark));
return FeatureDiscovery(
child: MaterialApp(
child: MaterialApp.router(
title: 'Hacki',
debugShowCheckedModeBanner: false,
theme: useTrueDark ? trueDarkTheme : theme,
navigatorKey: navigatorKey,
navigatorObservers: <NavigatorObserver>[
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
],
onGenerateRoute: CustomRouter.onGenerateRoute,
initialRoute: HomeScreen.routeName,
theme: (useTrueDark ? trueDarkTheme : theme).copyWith(
useMaterial3: false,
),
routerConfig: router,
),
);
},

View File

@ -0,0 +1,14 @@
import 'package:flutter/material.dart' show IconData, Icons;
enum ExportDestination {
qrCode('QR code', icon: Icons.qr_code),
clipBoard('ClipBoard', icon: Icons.copy);
const ExportDestination(
this.label, {
required this.icon,
});
final String label;
final IconData icon;
}

View File

@ -4,7 +4,8 @@ enum FontSize {
small('Small', TextDimens.pt15),
regular('Regular', TextDimens.pt16),
large('Large', TextDimens.pt17),
xlarge('XLarge', TextDimens.pt18);
xlarge('XLarge', TextDimens.pt18),
xxlarge('XXLarge', TextDimens.pt19);
const FontSize(this.description, this.fontSize);

View File

@ -1,4 +1,5 @@
export 'comments_order.dart';
export 'export_destination.dart';
export 'fetch_mode.dart';
export 'font.dart';
export 'font_size.dart';
@ -6,5 +7,7 @@ export 'item/item.dart';
export 'post_data.dart';
export 'preference.dart';
export 'search_params.dart';
export 'status.dart';
export 'story_marking_mode.dart';
export 'story_type.dart';
export 'user.dart';

View File

@ -23,17 +23,18 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
FontPreference(),
FontSizePreference(),
TabOrderPreference(),
StoryMarkingModePreference(),
// Order of items below matters and
// reflects the order on settings screen.
const DisplayModePreference(),
const MetadataModePreference(),
const StoryUrlModePreference(),
const MarkReadStoriesModePreference(),
const NotificationModePreference(),
const SwipeGesturePreference(),
const AutoScrollModePreference(),
const CollapseModePreference(),
const ReaderModePreference(),
const MarkReadStoriesModePreference(),
const EyeCandyModePreference(),
const TrueDarkModePreference(),
],
@ -68,6 +69,8 @@ final int _fontSizeDefaultValue = FontSize.regular.index;
final int _fontDefaultValue = Font.roboto.index;
final int _tabOrderDefaultValue =
StoryType.convertToSettingsValue(StoryType.values);
final int _markStoriesAsReadWhenPreferenceDefaultValue =
StoryMarkingMode.tap.index;
class SwipeGesturePreference extends BooleanPreference {
const SwipeGesturePreference({bool? val})
@ -146,7 +149,7 @@ class AutoScrollModePreference extends BooleanPreference {
@override
String get subtitle =>
'''Automatically scroll to next comment when you collapse a comment.''';
'''automatically scroll to next comment when you collapse a comment.''';
}
/// The value deciding whether or not the story
@ -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';
}

14
lib/models/status.dart Normal file
View File

@ -0,0 +1,14 @@
enum Status {
idle,
inProgress,
success,
failure,
}
extension StatusExtension on Status {
bool get isLoading => this == Status.inProgress;
bool get isSuccessful => this == Status.success;
bool get hasError => this == Status.failure;
}

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

@ -64,7 +64,7 @@ class PostableRepository {
validateStatus: validateStatus,
),
);
} on DioError catch (e) {
} on DioException catch (e) {
throw ServiceException(e.message);
}
}

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart';
@ -51,44 +52,39 @@ class ItemScreen extends StatefulWidget {
this.splitViewEnabled = false,
});
static const String routeName = '/item';
static const String routeName = 'item';
static Route<dynamic> route(ItemScreenArgs args) {
return MaterialPageRoute<ItemScreen>(
settings: const RouteSettings(name: routeName),
builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
create: (_) => CollapseCache(),
lazy: false,
child: MultiBlocProvider(
providers: <BlocProvider<dynamic>>[
BlocProvider<CommentsCubit>(
create: (BuildContext context) => CommentsCubit(
filterCubit: context.read<FilterCubit>(),
isOfflineReading:
context.read<StoriesBloc>().state.isOfflineReading,
item: args.item,
collapseCache: context.read<CollapseCache>(),
defaultFetchMode:
context.read<PreferenceCubit>().state.fetchMode,
defaultCommentsOrder:
context.read<PreferenceCubit>().state.order,
)..init(
onlyShowTargetComment: args.onlyShowTargetComment,
targetAncestors: args.targetComments,
useCommentCache: args.useCommentCache,
),
),
],
child: ItemScreen(
item: args.item,
parentComments: args.targetComments ?? <Comment>[],
static Widget phone(ItemScreenArgs args) {
return RepositoryProvider<CollapseCache>(
create: (_) => CollapseCache(),
lazy: false,
child: MultiBlocProvider(
providers: <BlocProvider<dynamic>>[
BlocProvider<CommentsCubit>(
create: (BuildContext context) => CommentsCubit(
filterCubit: context.read<FilterCubit>(),
isOfflineReading:
context.read<StoriesBloc>().state.isOfflineReading,
item: args.item,
collapseCache: context.read<CollapseCache>(),
defaultFetchMode: context.read<PreferenceCubit>().state.fetchMode,
defaultCommentsOrder: context.read<PreferenceCubit>().state.order,
)..init(
onlyShowTargetComment: args.onlyShowTargetComment,
targetAncestors: args.targetComments,
useCommentCache: args.useCommentCache,
),
),
],
child: ItemScreen(
item: args.item,
parentComments: args.targetComments ?? <Comment>[],
),
),
);
}
static Widget build(BuildContext context, ItemScreenArgs args) {
static Widget tablet(BuildContext context, ItemScreenArgs args) {
return WillPopScope(
onWillPop: () async {
if (context.read<SplitViewCubit>().state.expanded) {
@ -168,7 +164,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
@override
void initState() {
super.initState();
SchedulerBinding.instance
..addPostFrameCallback((_) {
FeatureDiscovery.discoverFeatures(
@ -213,12 +208,8 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
listeners: <BlocListener<dynamic, dynamic>>[
BlocListener<PostCubit, PostState>(
listener: (BuildContext context, PostState postState) {
if (postState.status == PostStatus.successful) {
Navigator.popUntil(
context,
(Route<dynamic> route) =>
route.settings.name == ItemScreen.routeName,
);
if (postState.status == Status.success) {
context.pop();
final String verb =
context.read<EditCubit>().state.replyingTo == null
? 'updated'
@ -228,12 +219,8 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
showSnackBar(content: msg);
context.read<EditCubit>().onReplySubmittedSuccessfully();
context.read<PostCubit>().reset();
} else if (postState.status == PostStatus.failure) {
Navigator.popUntil(
context,
(Route<dynamic> route) =>
route.settings.name == ItemScreen.routeName,
);
} else if (postState.status == Status.failure) {
context.pop();
showErrorSnackBar();
context.read<PostCubit>().reset();
}
@ -355,6 +342,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
enableDrag: false,
builder: (BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
@ -370,7 +358,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
),
SizedBox(
height: MediaQuery.of(context).viewInsets.bottom,
)
),
],
);
},
@ -444,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:
@ -455,8 +443,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
title: const Text('View in separate thread'),
onTap: () {
locator.get<AppReviewService>().requestReview();
Navigator.pop(context);
context.pop();
goToItemScreen(
args: ItemScreenArgs(
item: comment,

View File

@ -43,6 +43,7 @@ class CustomAppBar extends AppBar {
fontFamily: FeatherIcons.type.fontFamily,
package: FeatherIcons.type.fontPackage,
),
textScaleFactor: 1,
),
onPressed: onFontSizeTap,
),

View File

@ -32,30 +32,32 @@ class CustomFloatingActionButton extends StatelessWidget {
Icons.keyboard_arrow_up,
color: Palette.white,
),
title: const Text('Jump to previous root level comment.'),
title: const Text('Shortcut'),
description: const Text(
'''Tapping on this button will take you to the previous off-screen root level comment.''',
'''Tapping on this button will take you to the previous off-screen root level comment.\n\nLong press on it to jump to the very beginning of this thread.''',
),
child: FloatingActionButton.small(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
child: InkWell(
onLongPress: () => itemScrollController.scrollTo(
index: 0,
duration: Durations.ms400,
),
child: FloatingActionButton.small(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
/// Randomly generated string as heroTag to prevent
/// default [FloatingActionButton] animation.
heroTag: UniqueKey().hashCode,
onPressed: () {
if (state.status == CommentsStatus.loading) return;
HapticFeedbackUtil.selection();
context.read<CommentsCubit>().jumpUp(
itemScrollController,
itemPositionsListener,
);
},
child: Icon(
Icons.keyboard_arrow_up,
color: state.status == CommentsStatus.loading
? Palette.grey
: Theme.of(context).colorScheme.primary,
/// Randomly generated string as heroTag to prevent
/// default [FloatingActionButton] animation.
heroTag: UniqueKey().hashCode,
onPressed: () {
HapticFeedbackUtil.selection();
context.read<CommentsCubit>().scrollToPreviousRoot(
itemScrollController,
itemPositionsListener,
);
},
child: Icon(
Icons.keyboard_arrow_up,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
@ -65,29 +67,31 @@ class CustomFloatingActionButton extends StatelessWidget {
Icons.keyboard_arrow_down,
color: Palette.white,
),
title: const Text('Jump to next root level comment.'),
title: const Text('Shortcut'),
description: const Text(
'''Tapping on this button will take you to the next off-screen root level comment.''',
'''Tapping on this button will take you to the next off-screen root level comment.\n\nLong press on it to jump to the end of this thread.''',
),
child: FloatingActionButton.small(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
child: InkWell(
onLongPress: () => itemScrollController.scrollTo(
index: state.comments.length,
duration: Durations.ms400,
),
child: FloatingActionButton.small(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
/// Same as above.
heroTag: UniqueKey().hashCode,
onPressed: () {
if (state.status == CommentsStatus.loading) return;
HapticFeedbackUtil.selection();
context.read<CommentsCubit>().jump(
itemScrollController,
itemPositionsListener,
);
},
child: Icon(
Icons.keyboard_arrow_down,
color: state.status == CommentsStatus.loading
? Palette.grey
: Theme.of(context).colorScheme.primary,
/// Same as above.
heroTag: UniqueKey().hashCode,
onPressed: () {
HapticFeedbackUtil.selection();
context.read<CommentsCubit>().scrollToNextRoot(
itemScrollController,
itemPositionsListener,
);
},
child: Icon(
Icons.keyboard_arrow_down,
color: Theme.of(context).colorScheme.primary,
),
),
),
),

View File

@ -1,8 +1,10 @@
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';
import 'package:hacki/models/status.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
@ -23,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}',
);
@ -32,7 +34,7 @@ class _LoginDialogState extends State<LoginDialog> {
builder: (BuildContext context, AuthState state) {
return SimpleDialog(
children: <Widget>[
if (state.status == AuthStatus.loading)
if (state.status.isLoading)
const SizedBox(
height: Dimens.pt36,
width: Dimens.pt36,
@ -82,7 +84,7 @@ class _LoginDialogState extends State<LoginDialog> {
const SizedBox(
height: Dimens.pt16,
),
if (state.status == AuthStatus.failure)
if (state.status == Status.failure)
Padding(
padding: const EdgeInsets.only(
left: Dimens.pt18,
@ -141,7 +143,7 @@ class _LoginDialogState extends State<LoginDialog> {
),
],
),
)
),
],
),
Padding(
@ -152,7 +154,7 @@ class _LoginDialogState extends State<LoginDialog> {
children: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context);
context.pop();
context.read<AuthBloc>().add(AuthInitialize());
},
child: const Text(

View File

@ -159,7 +159,7 @@ class MainView extends StatelessWidget {
prev.status != current.status,
builder: (BuildContext context, CommentsState state) {
return AnimatedOpacity(
opacity: state.status == CommentsStatus.loading
opacity: state.status == CommentsStatus.inProgress
? NumSwitch.on
: NumSwitch.off,
duration: const Duration(
@ -255,6 +255,8 @@ class _ParentItemSection extends StatelessWidget {
style: const TextStyle(
color: Palette.orange,
),
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
),
const Spacer(),
Text(
@ -262,6 +264,8 @@ class _ParentItemSection extends StatelessWidget {
style: const TextStyle(
color: Palette.grey,
),
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
),
],
),
@ -334,9 +338,8 @@ class _ParentItemSection extends StatelessWidget {
],
),
textAlign: TextAlign.center,
textScaleFactor: MediaQuery.of(
context,
).textScaleFactor,
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
),
),
)
@ -354,6 +357,8 @@ class _ParentItemSection extends StatelessWidget {
),
child: ItemText(
item: state.item,
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
),
),
),
@ -402,6 +407,7 @@ class _ParentItemSection extends StatelessWidget {
style: const TextStyle(
fontSize: TextDimens.pt13,
),
textScaleFactor: 1,
),
] else ...<Widget>[
const SizedBox(
@ -411,27 +417,29 @@ class _ParentItemSection extends StatelessWidget {
width: _viewParentButtonWidth,
child: TextButton(
onPressed: context.read<CommentsCubit>().loadParentThread,
child: state.fetchParentStatus == CommentsStatus.loading
? const SizedBox(
height: Dimens.pt12,
width: Dimens.pt12,
child: CustomCircularProgressIndicator(
strokeWidth: Dimens.pt2,
),
)
: const Text(
'View parent',
style: TextStyle(
fontSize: TextDimens.pt13,
),
),
child:
state.fetchParentStatus == CommentsStatus.inProgress
? const SizedBox(
height: Dimens.pt12,
width: Dimens.pt12,
child: CustomCircularProgressIndicator(
strokeWidth: Dimens.pt2,
),
)
: const Text(
'View parent',
style: TextStyle(
fontSize: TextDimens.pt13,
),
textScaleFactor: 1,
),
),
),
SizedBox(
width: _viewRootButtonWidth,
child: TextButton(
onPressed: context.read<CommentsCubit>().loadRootThread,
child: state.fetchRootStatus == CommentsStatus.loading
child: state.fetchRootStatus == CommentsStatus.inProgress
? const SizedBox(
height: Dimens.pt12,
width: Dimens.pt12,
@ -444,6 +452,7 @@ class _ParentItemSection extends StatelessWidget {
style: TextStyle(
fontSize: TextDimens.pt13,
),
textScaleFactor: 1,
),
),
),
@ -462,6 +471,7 @@ class _ParentItemSection extends StatelessWidget {
style: const TextStyle(
fontSize: TextDimens.pt13,
),
textScaleFactor: 1,
),
),
)
@ -483,6 +493,7 @@ class _ParentItemSection extends StatelessWidget {
style: const TextStyle(
fontSize: TextDimens.pt13,
),
textScaleFactor: 1,
),
),
)

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
@ -60,10 +61,7 @@ class MorePopupMenu extends StatelessWidget {
);
}
Navigator.pop(
context,
MenuAction.upvote,
);
context.pop(MenuAction.upvote);
},
builder: (BuildContext context, VoteState voteState) {
final bool upvoted = voteState.vote == Vote.up;
@ -81,7 +79,7 @@ class MorePopupMenu extends StatelessWidget {
child: BlocBuilder<UserCubit, UserState>(
builder: (BuildContext context, UserState state) {
return Semantics(
excludeSemantics: state.status == UserStatus.loading,
excludeSemantics: state.status == Status.inProgress,
child: ListTile(
leading: const Icon(
Icons.account_circle,
@ -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),
),
],
),
@ -285,7 +268,7 @@ class MorePopupMenu extends StatelessWidget {
child: SearchScreen(
fromUserDialog: true,
),
)
),
],
),
),

View File

@ -25,7 +25,7 @@ class _PollViewState extends State<PollView> {
const SizedBox(
height: Dimens.pt24,
),
if (state.status == PollStatus.loading) ...<Widget>[
if (state.status == Status.inProgress) ...<Widget>[
const LinearProgressIndicator(),
const SizedBox(
height: Dimens.pt24,
@ -134,7 +134,7 @@ class _PollViewState extends State<PollView> {
),
],
),
)
),
],
),
);

View File

@ -2,10 +2,11 @@ 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';
import 'package:hacki/models/item/item.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
@ -35,11 +36,12 @@ class _ReplyBoxState extends State<ReplyBox> {
bool expanded = false;
double? expandedHeight;
static const double _collapsedHeight = 100;
static const double collapsedHeight = 140;
@override
Widget build(BuildContext context) {
expandedHeight ??= MediaQuery.of(context).size.height;
expandedHeight ??= MediaQuery.of(context).size.height -
MediaQuery.of(context).viewInsets.bottom;
return BlocBuilder<EditCubit, EditState>(
buildWhen: (EditState previous, EditState current) =>
previous.showReplyBox != current.showReplyBox ||
@ -49,7 +51,7 @@ class _ReplyBoxState extends State<ReplyBox> {
return BlocBuilder<PostCubit, PostState>(
builder: (BuildContext context, PostState postState) {
final Item? replyingTo = editState.replyingTo;
final bool isLoading = postState.status == PostStatus.loading;
final bool isLoading = postState.status.isLoading;
return Padding(
padding: EdgeInsets.only(
@ -60,7 +62,7 @@ class _ReplyBoxState extends State<ReplyBox> {
: Dimens.zero,
),
child: AnimatedContainer(
height: expanded ? expandedHeight : _collapsedHeight,
height: expanded ? expandedHeight : collapsedHeight,
duration: Durations.ms200,
decoration: BoxDecoration(
boxShadow: <BoxShadow>[
@ -73,6 +75,7 @@ class _ReplyBoxState extends State<ReplyBox> {
),
child: Material(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (context.read<SplitViewCubit>().state.enabled)
const Divider(
@ -142,7 +145,7 @@ class _ReplyBoxState extends State<ReplyBox> {
color: Palette.orange,
),
onPressed: () {
Navigator.pop(context);
context.pop();
final EditState state =
context.read<EditCubit>().state;
@ -159,7 +162,7 @@ class _ReplyBoxState extends State<ReplyBox> {
context
.read<EditCubit>()
.deleteDraft();
Navigator.pop(context);
context.pop();
},
child: const Text(
'No',
@ -169,8 +172,7 @@ class _ReplyBoxState extends State<ReplyBox> {
),
),
TextButton(
onPressed: () =>
Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text('Yes'),
),
],
@ -213,13 +215,21 @@ class _ReplyBoxState extends State<ReplyBox> {
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt16,
padding: EdgeInsets.only(
left: Dimens.pt16,
right: Dimens.pt16,
bottom: expanded
// This padding here prevents keyboard
// overlapping with TextField.
? MediaQuery.of(context).viewInsets.bottom +
Dimens.pt16
: Dimens.zero,
),
child: TextField(
autofocus: true,
controller: widget.textEditingController,
maxLines: 100,
autofocus: true,
expands: true,
maxLines: null,
decoration: const InputDecoration(
alignLabelWithHint: true,
contentPadding: EdgeInsets.zero,
@ -296,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,
@ -328,7 +332,7 @@ class _ReplyBoxState extends State<ReplyBox> {
color: Palette.orange,
size: TextDimens.pt18,
),
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
),
],
),
@ -345,6 +349,8 @@ class _ReplyBoxState extends State<ReplyBox> {
child: SingleChildScrollView(
child: ItemText(
item: replyingTo,
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
),
),
),

View File

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

View File

@ -1,11 +1,13 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/profile/models/models.dart';
@ -31,7 +33,7 @@ class _ProfileScreenState extends State<ProfileScreen>
final ScrollController scrollController = ScrollController();
final Throttle throttle = Throttle(delay: Durations.twoSeconds);
PageType pageType = PageType.notification;
PageType? pageType;
@override
void dispose() {
@ -45,6 +47,9 @@ class _ProfileScreenState extends State<ProfileScreen>
@override
Widget build(BuildContext context) {
pageType ??= context.read<AuthBloc>().state.isLoggedIn
? PageType.notification
: PageType.fav;
super.build(context);
return BlocBuilder<AuthBloc, AuthState>(
builder: (BuildContext context, AuthState authState) {
@ -53,7 +58,7 @@ class _ProfileScreenState extends State<ProfileScreen>
previous.status != current.status,
listener:
(BuildContext context, NotificationState notificationState) {
if (notificationState.status == NotificationStatus.loaded) {
if (notificationState.status == Status.success) {
refreshControllerNotification
..refreshCompleted()
..loadComplete();
@ -71,7 +76,7 @@ class _ProfileScreenState extends State<ProfileScreen>
BuildContext context,
HistoryState historyState,
) {
if (historyState.status == HistoryStatus.loaded) {
if (historyState.status == Status.success) {
refreshControllerHistory
..refreshCompleted()
..loadComplete();
@ -83,7 +88,7 @@ class _ProfileScreenState extends State<ProfileScreen>
) {
if ((!authState.isLoggedIn ||
historyState.submittedItems.isEmpty) &&
historyState.status != HistoryStatus.loading) {
historyState.status != Status.inProgress) {
return const CenteredMessageView(
content: 'Your past comments and stories will '
'show up here.',
@ -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,
@ -126,7 +131,7 @@ class _ProfileScreenState extends State<ProfileScreen>
visible: pageType == PageType.fav,
child: BlocConsumer<FavCubit, FavState>(
listener: (BuildContext context, FavState favState) {
if (favState.status == FavStatus.loaded) {
if (favState.status == Status.success) {
refreshControllerFav
..refreshCompleted()
..loadComplete();
@ -134,7 +139,7 @@ class _ProfileScreenState extends State<ProfileScreen>
},
builder: (BuildContext context, FavState favState) {
if (favState.favItems.isEmpty &&
favState.status != FavStatus.loading) {
favState.status != Status.inProgress) {
return const CenteredMessageView(
content: 'Your favorite stories will show up here.'
'\nThey will be synced to your Hacker '
@ -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.',
@ -363,7 +391,7 @@ class _ProfileScreenState extends State<ProfileScreen>
? <Comment>[comment]
: <Comment>[
...children,
comment.copyWith(level: children.length)
comment.copyWith(level: children.length),
],
onlyShowTargetComment: true,
),

View File

@ -0,0 +1,71 @@
import 'package:flutter/material.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';
@override
State<QrCodeScannerScreen> createState() => _QrCodeScannerScreenState();
}
class _QrCodeScannerScreenState extends State<QrCodeScannerScreen> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
QRViewController? controller;
bool isFlashOn = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Palette.transparent,
actions: <Widget>[
IconButton(
icon: Icon(isFlashOn ? Icons.flash_off : Icons.flash_on),
onPressed: () {
controller?.toggleFlash();
setState(() {
isFlashOn = !isFlashOn;
});
},
),
IconButton(
icon: const Icon(Icons.cameraswitch_outlined),
onPressed: controller?.flipCamera,
),
],
),
extendBodyBehindAppBar: true,
body: Column(
children: <Widget>[
Expanded(
child: QRView(
key: qrKey,
onQRViewCreated: onQRViewCreated,
),
),
],
),
);
}
void onQRViewCreated(QRViewController controller) {
setState(() {
this.controller = controller;
});
controller.scannedDataStream.listen((Barcode scanData) {
controller.stopCamera();
context.pop(scanData.code);
});
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:hacki/styles/palette.dart';
import 'package:qr_flutter/qr_flutter.dart';
class QrCodeViewScreen extends StatelessWidget {
const QrCodeViewScreen({
required this.data,
super.key,
});
final String data;
static const String routeName = 'qr-code-view';
static const int qrCodeVersion = 4;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Palette.transparent,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: QrImageView(
data: data,
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Theme.of(context).colorScheme.onSurface,
),
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Theme.of(context).colorScheme.onSurface,
),
version: qrCodeVersion,
size: 300,
),
),
],
),
);
}
}

View File

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

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart';
@ -9,14 +10,18 @@ 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/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/profile/models/page_type.dart';
import 'package:hacki/screens/profile/qr_code_scanner_screen.dart';
import 'package:hacki/screens/profile/qr_code_view_screen.dart';
import 'package:hacki/screens/profile/widgets/offline_list_tile.dart';
import 'package:hacki/screens/profile/widgets/tab_bar_settings.dart';
import 'package:hacki/screens/widgets/widgets.dart';
@ -36,7 +41,7 @@ class Settings extends StatefulWidget {
final AuthState authState;
final String magicWord;
final PageType pageType;
final PageType? pageType;
@override
State<Settings> createState() => _SettingsState();
@ -217,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(
@ -244,6 +294,13 @@ class _SettingsState extends State<Settings> {
),
onTap: onExportFavoritesTapped,
),
ListTile(
title: const Text(
'Import Favorites',
),
onTap: () =>
onImportFavoritesTapped(context.read<FavCubit>()),
),
ListTile(
title: const Text(
'Clear Favorites',
@ -289,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();
},
@ -350,7 +407,7 @@ class _SettingsState extends State<Settings> {
),
Spacer(),
],
)
),
],
),
);
@ -421,7 +478,7 @@ class _SettingsState extends State<Settings> {
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text(
'Cancel',
style: TextStyle(
@ -431,7 +488,7 @@ class _SettingsState extends State<Settings> {
),
TextButton(
onPressed: () {
Navigator.pop(context);
context.pop();
locator
.get<SembastRepository>()
.deleteAllCachedComments()
@ -708,7 +765,7 @@ class _SettingsState extends State<Settings> {
),
),
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text(
'Okay',
),
@ -731,7 +788,7 @@ class _SettingsState extends State<Settings> {
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text(
'Cancel',
),
@ -741,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',
@ -754,20 +811,68 @@ class _SettingsState extends State<Settings> {
}
Future<void> onExportFavoritesTapped() async {
final List<int> allFavorites = context.read<FavCubit>().state.favIds;
return showModalBottomSheet<ExportDestination>(
context: context,
builder: (BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
...ExportDestination.values.map(
(ExportDestination e) => ListTile(
leading: Icon(e.icon),
title: Text(e.label),
onTap: () => context.pop<ExportDestination>(e),
),
),
],
),
);
},
).then(
(ExportDestination? destination) => exportFavorites(to: destination),
);
}
Future<void> onImportFavoritesTapped(FavCubit favCubit) async {
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;
for (final int id in ids) {
await favCubit.addFav(id);
}
showSnackBar(content: 'Favorites imported successfully.');
}
Future<void> exportFavorites({required ExportDestination? to}) async {
final ExportDestination? destination = to;
if (destination == null) return;
final List<int> allFavorites = context.read<FavCubit>().state.favIds;
if (allFavorites.isEmpty) {
showSnackBar(content: "You don't have any favorite item.");
return;
}
final String allFavoritesStr = allFavorites.join('\n');
try {
await FlutterClipboard.copy(
allFavorites.join('\n'),
).whenComplete(HapticFeedbackUtil.selection);
showSnackBar(content: 'Ids of favorites have been copied to clipboard.');
} catch (error, stackTrace) {
error.logError(stackTrace: stackTrace);
switch (destination) {
case ExportDestination.qrCode:
await router.push(
'/${QrCodeViewScreen.routeName}',
extra: allFavoritesStr,
);
case ExportDestination.clipBoard:
try {
await FlutterClipboard.copy(allFavoritesStr)
.whenComplete(HapticFeedbackUtil.selection);
showSnackBar(
content: 'Ids of favorites have been copied to clipboard.',
);
} catch (error, stackTrace) {
error.logError(stackTrace: stackTrace);
}
}
}
@ -782,14 +887,14 @@ class _SettingsState extends State<Settings> {
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
child: const Text(
'Cancel',
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
context.pop();
try {
context.read<FavCubit>().removeAll();
showSnackBar(content: 'All favorites have been removed.');

View File

@ -1,6 +1,8 @@
export 'home/home_screen.dart';
export 'item/item_screen.dart';
export 'profile/profile_screen.dart';
export 'profile/qr_code_scanner_screen.dart';
export 'profile/qr_code_view_screen.dart';
export 'search/search_screen.dart';
export 'submit/submit_screen.dart';
export 'web_view/web_view_screen.dart';

View File

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

View File

@ -1,24 +1,16 @@
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';
import 'package:hacki/styles/styles.dart';
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();
@ -43,13 +35,13 @@ class _SubmitScreenState extends State<SubmitScreen> {
listenWhen: (SubmitState previous, SubmitState current) =>
previous.status != current.status,
listener: (BuildContext context, SubmitState state) {
if (state.status == SubmitStatus.submitted) {
Navigator.pop(context);
if (state.status == Status.success) {
context.pop();
HapticFeedbackUtil.light();
showSnackBar(
content: 'Post submitted successfully.',
);
} else if (state.status == SubmitStatus.failure) {
} else if (state.status == Status.failure) {
showErrorSnackBar();
}
},
@ -69,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(
@ -86,7 +78,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
},
).then((bool? value) {
if (value ?? false) {
Navigator.of(context).pop();
context.pop();
}
});
},
@ -95,7 +87,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
'Submit',
),
actions: <Widget>[
if (state.status == SubmitStatus.submitting)
if (state.status == Status.inProgress)
const Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.pt18,

View File

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

View File

@ -143,6 +143,8 @@ class CommentTile extends StatelessWidget {
? orange
: color,
),
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
),
if (comment.by == opUsername)
const Text(
@ -157,6 +159,8 @@ class CommentTile extends StatelessWidget {
style: const TextStyle(
color: Palette.grey,
),
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
),
],
),
@ -196,6 +200,8 @@ class CommentTile extends StatelessWidget {
child: ItemText(
key: ValueKey<int>(comment.id),
item: comment,
textScaleFactor: MediaQuery.of(context)
.textScaleFactor,
onTap: () {
if (onTap == null) {
_onTextTapped(context);

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
import 'package:hacki/styles/palette.dart';
import 'package:hacki/utils/utils.dart';
import 'package:linkify/linkify.dart';
import 'package:linkify/linkify.dart' hide UrlLinkifier;
export 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
export 'package:linkify/linkify.dart'
@ -14,9 +14,7 @@ export 'package:linkify/linkify.dart'
Linkifier,
LinkifyElement,
LinkifyOptions,
TextElement,
UrlElement,
UrlLinkifier;
TextElement;
/// Callback clicked link
typedef LinkCallback = void Function(LinkableElement link);

View File

@ -1,2 +1,3 @@
export 'emphasis_linkifier.dart';
export 'quote_linkifier.dart';
export 'url_linkifier.dart';

View File

@ -0,0 +1,121 @@
import 'package:flutter/foundation.dart';
import 'package:linkify/linkify.dart';
final RegExp _urlRegex = RegExp(
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[\/\\\%:\?=&#@;A-Za-z0-9_.~-]*)',
caseSensitive: false,
dotAll: true,
);
final RegExp _looseUrlRegex = RegExp(
r'''^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//="'`]*))''',
caseSensitive: false,
dotAll: true,
);
final RegExp _protocolIdentifierRegex = RegExp(
r'^(https?:\/\/)',
caseSensitive: false,
);
class UrlLinkifier extends Linkifier {
const UrlLinkifier();
@override
List<LinkifyElement> parse(
List<LinkifyElement> elements,
LinkifyOptions options,
) {
final List<LinkifyElement> list = <LinkifyElement>[];
for (final LinkifyElement element in elements) {
if (element is TextElement) {
final RegExpMatch? match = options.looseUrl
? _looseUrlRegex.firstMatch(element.text)
: _urlRegex.firstMatch(element.text);
if (match == null) {
list.add(element);
} else {
final String text = element.text.replaceFirst(match.group(0)!, '');
if (match.group(1)?.isNotEmpty ?? false) {
list.add(TextElement(match.group(1)!));
}
if (match.group(2)?.isNotEmpty ?? false) {
String originalUrl = match.group(2)!;
String originText = originalUrl;
String? end;
if ((options.excludeLastPeriod) &&
originalUrl[originalUrl.length - 1] == '.') {
end = '.';
originText = originText.substring(0, originText.length - 1);
originalUrl = originalUrl.substring(0, originalUrl.length - 1);
}
String url = originalUrl;
if (!originalUrl.startsWith(_protocolIdentifierRegex)) {
originalUrl = (options.defaultToHttps ? 'https://' : 'http://') +
originalUrl;
}
if ((options.humanize) || (options.removeWww)) {
if (options.humanize) {
url = url.replaceFirst(RegExp('https?://'), '');
}
if (options.removeWww) {
url = url.replaceFirst(RegExp(r'www\.'), '');
}
list.add(
UrlElement(
originalUrl,
url,
originText,
),
);
} else {
list.add(UrlElement(originalUrl, null, originText));
}
if (end != null) {
list.add(TextElement(end));
}
}
if (text.isNotEmpty) {
list.addAll(parse(<LinkifyElement>[TextElement(text)], options));
}
}
} else {
list.add(element);
}
}
return list;
}
}
/// Represents an element containing a link
@immutable
class UrlElement extends LinkableElement {
UrlElement(String url, [String? text, String? originText])
: super(text, url, originText);
@override
String toString() {
return "LinkElement: '$url' ($text)";
}
@override
bool operator ==(Object other) => equals(other);
@override
int get hashCode => Object.hash(text, originText, url);
@override
bool equals(dynamic other) => other is UrlElement && super.equals(other);
}

View File

@ -17,7 +17,7 @@ class DeviceGestureWrapper extends StatelessWidget {
Widget build(BuildContext context) {
return MediaQuery(
data: const MediaQueryData(
gestureSettings: DeviceGestureSettings(touchSlop: 7.9),
gestureSettings: DeviceGestureSettings(touchSlop: 12),
),
child: child,
);

View File

@ -10,12 +10,14 @@ import 'package:hacki/utils/utils.dart';
class ItemText extends StatelessWidget {
const ItemText({
required this.item,
required this.textScaleFactor,
super.key,
this.onTap,
});
final Item item;
final VoidCallback? onTap;
final double textScaleFactor;
@override
Widget build(BuildContext context) {
@ -37,7 +39,7 @@ class ItemText extends StatelessWidget {
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
),
onTap: onTap,
textScaleFactor: MediaQuery.of(context).textScaleFactor,
textScaleFactor: textScaleFactor,
contextMenuBuilder: (
BuildContext context,
EditableTextState editableTextState,
@ -52,7 +54,7 @@ class ItemText extends StatelessWidget {
} else {
return SelectableLinkify(
text: item.text,
textScaleFactor: MediaQuery.of(context).textScaleFactor,
textScaleFactor: textScaleFactor,
style: style,
linkStyle: linkStyle,
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -229,7 +229,7 @@ class _LinkPreviewPlaceholder extends StatelessWidget {
],
),
),
)
),
],
),
),

View File

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

View File

@ -273,7 +273,7 @@ class WebAnalyzer {
info.title,
info.description,
info.icon,
info.image
info.image,
];
} else if (info is WebVideoInfo) {
return <dynamic>['1', info.image];

View File

@ -33,6 +33,7 @@ abstract class TextDimens {
static const double pt16 = 16;
static const double pt17 = 17;
static const double pt18 = 18;
static const double pt19 = 19;
static const double pt20 = 20;
static const double pt24 = 24;
static const double pt26 = 26;

View File

@ -26,8 +26,8 @@ abstract class HtmlUtil {
.querySelectorAll("input[type='hidden']");
return <String, String>{
if (hiddenInputs != null)
for (dom.Element hiddenInput in hiddenInputs)
hiddenInput.attributes['name']!: hiddenInput.attributes['value']!
for (final dom.Element hiddenInput in hiddenInputs)
hiddenInput.attributes['name']!: hiddenInput.attributes['value']!,
};
}

View File

@ -1,11 +1,9 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hacki/config/constants.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'
@ -38,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,
);
}
});
@ -54,17 +51,7 @@ abstract class LinkUtil {
return;
}
Uri rinseLink(String link) {
final RegExp regex = RegExp(RegExpConstants.linkSuffix);
if (!link.contains('en.wikipedia.org') && link.contains(regex)) {
final String match = regex.stringMatch(link) ?? '';
return Uri.parse(link.replaceAll(match, ''));
}
return Uri.parse(link);
}
final Uri uri = rinseLink(link);
final Uri uri = Uri.parse(link);
canLaunchUrl(uri).then((bool val) {
if (val) {
if (link.contains('http')) {
@ -98,9 +85,9 @@ abstract class LinkUtil {
.fetchItem(id: id)
.then((Item? item) {
if (item != null) {
HackiApp.navigatorKey.currentState!.pushNamed(
ItemScreen.routeName,
arguments: ItemScreenArgs(item: item),
router.push(
'/${ItemScreen.routeName}',
extra: ItemScreenArgs(item: item),
);
}
});

View File

@ -1,5 +1,5 @@
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
import 'package:linkify/linkify.dart';
import 'package:linkify/linkify.dart' hide UrlLinkifier;
abstract class LinkifierUtil {
static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false);

View File

@ -5,34 +5,34 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "405666cd3cf0ee0a48d21ec67e65406aad2c726d9fa58840d3375e7bdcd32a07"
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
url: "https://pub.dev"
source: hosted
version: "60.0.0"
version: "61.0.0"
adaptive_theme:
dependency: "direct main"
description:
name: adaptive_theme
sha256: "3568bb526d4823c7bb35f9ce3604af15e04cc0e9cc4f257da3604fe6b48d74ae"
sha256: "2d9bfee4240cdfad1b169cb43ac38fb49487e7fe1cc845e2973d4cef1780c0f6"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.3.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "1952250bd005bacb895a01bf1b4dc00e3ba1c526cf47dca54dfe24979c65f5b3"
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
url: "https://pub.dev"
source: hosted
version: "5.12.0"
version: "5.13.0"
args:
dependency: transitive
description:
name: args
sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.2"
async:
dependency: transitive
description:
@ -53,18 +53,18 @@ packages:
dependency: "direct main"
description:
name: bloc
sha256: "658a5ae59edcf1e58aac98b000a71c762ad8f46f1394c34a52050cafb3e11a80"
sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49"
url: "https://pub.dev"
source: hosted
version: "8.1.1"
version: "8.1.2"
bloc_test:
dependency: "direct dev"
description:
name: bloc_test
sha256: ffbb60c17ee3d8e3784cb78071088e353199057233665541e8ac6cd438dca8ad
sha256: af0de1a1e16a7536e95dcd7491e0a6d6078e11d2d691988e862280b74f5c7968
url: "https://pub.dev"
source: hosted
version: "9.1.1"
version: "9.1.4"
boolean_selector:
dependency: transitive
description:
@ -125,18 +125,18 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev"
source: hosted
version: "1.17.1"
version: "1.17.2"
connectivity_plus:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "45262924896ff72a8cd92b722bb7e3d5020f9e0724531a3e10e22ddae2005991"
sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "4.0.2"
connectivity_plus_platform_interface:
dependency: transitive
description:
@ -181,10 +181,10 @@ packages:
dependency: transitive
description:
name: csslib
sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
url: "https://pub.dev"
source: hosted
version: "0.17.2"
version: "1.0.0"
dbus:
dependency: transitive
description:
@ -197,10 +197,10 @@ packages:
dependency: "direct main"
description:
name: device_info_plus
sha256: "9b1a0c32b2a503f8fe9f8764fac7b5fcd4f6bd35d8f49de5350bccf9e2a33b8a"
sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659"
url: "https://pub.dev"
source: hosted
version: "9.0.0"
version: "9.0.3"
device_info_plus_platform_interface:
dependency: transitive
description:
@ -221,10 +221,10 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: "347d56c26d63519552ef9a569f2a593dda99a81fdbdff13c584b7197cfe05059"
sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197
url: "https://pub.dev"
source: hosted
version: "5.1.2"
version: "5.3.2"
equatable:
dependency: "direct main"
description:
@ -262,10 +262,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
version: "2.1.0"
file:
dependency: transitive
description:
@ -283,10 +283,10 @@ packages:
dependency: "direct main"
description:
name: flutter_bloc
sha256: "434951eea948dbe87f737b674281465f610b8259c16c097b8163ce138749a775"
sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae
url: "https://pub.dev"
source: hosted
version: "8.1.2"
version: "8.1.3"
flutter_blurhash:
dependency: transitive
description:
@ -299,10 +299,10 @@ packages:
dependency: "direct main"
description:
name: flutter_cache_manager
sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3"
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
url: "https://pub.dev"
source: hosted
version: "3.3.0"
version: "3.3.1"
flutter_driver:
dependency: "direct dev"
description: flutter
@ -312,10 +312,10 @@ packages:
dependency: "direct main"
description:
name: flutter_email_sender
sha256: "9e253c69617f43d4cb5de672e93a7a19c12a21fb6a75e66c6ce7626336c4c1bc"
sha256: "52b713a67a966be4d9e6f68a323fc0a5bc2da71c567eb451af1aa90d30adbc3a"
url: "https://pub.dev"
source: hosted
version: "5.2.0"
version: "6.0.1"
flutter_fadein:
dependency: "direct main"
description:
@ -344,10 +344,10 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "20c92629902b125cb624efdbacbbe98806b3c0b01adb3d84d1c72198b3eafb1a"
sha256: "3cc40fe8c50ab8383f3e053a499f00f975636622ecdc8e20a77418ece3b1e975"
url: "https://pub.dev"
source: hosted
version: "14.0.1"
version: "15.1.0+1"
flutter_local_notifications_linux:
dependency: transitive
description:
@ -442,10 +442,10 @@ packages:
dependency: "direct main"
description:
name: font_awesome_flutter
sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206"
sha256: "5fb789145cae1f4c3245c58b3f8fb287d055c26323879eab57a7bf0cfd1e45f3"
url: "https://pub.dev"
source: hosted
version: "10.4.0"
version: "10.5.0"
frontend_server_client:
dependency: transitive
description:
@ -479,10 +479,18 @@ packages:
dependency: transitive
description:
name: glob
sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c"
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
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:
@ -495,10 +503,10 @@ packages:
dependency: "direct main"
description:
name: html
sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8"
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
url: "https://pub.dev"
source: hosted
version: "0.15.3"
version: "0.15.4"
html_unescape:
dependency: "direct main"
description:
@ -511,10 +519,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
url: "https://pub.dev"
source: hosted
version: "0.13.6"
version: "1.1.0"
http_multi_server:
dependency: transitive
description:
@ -535,10 +543,10 @@ packages:
dependency: "direct main"
description:
name: hydrated_bloc
sha256: eb92d88061b6b911c48779b08a91c8a9f3a3aa8475f80d9380045375d9876536
sha256: "24994e61f64904d911683cce1a31dc4ef611619da5253f1de2b7b8fc6f79a118"
url: "https://pub.dev"
source: hosted
version: "9.1.0"
version: "9.1.2"
in_app_review:
dependency: "direct main"
description:
@ -595,34 +603,34 @@ packages:
dependency: "direct main"
description:
name: logger
sha256: db2ff852ed77090ba9f62d3611e4208a3d11dfa35991a81ae724c113fcb3e3f7
sha256: "66cb048220ca51cf9011da69fa581e4ee2bed4be6e82870d9e9baae75739da49"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "2.0.1"
logging:
dependency: transitive
description:
name: logging
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.2.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev"
source: hosted
version: "0.12.15"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.5.0"
memoize:
dependency: "direct main"
description:
@ -651,10 +659,10 @@ packages:
dependency: "direct dev"
description:
name: mocktail
sha256: "80a996cd9a69284b3dc521ce185ffe9150cde69767c2d3a0720147d93c0cef53"
sha256: "9503969a7c2c78c7292022c70c0289ed6241df7a9ba720010c0b215af29a5a58"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
version: "1.0.0"
nested:
dependency: transitive
description:
@ -699,10 +707,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: d39e8fbff4c5aef4592737e25ad6ac500df006ce7a7a8e1f838ce1256e167542
sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "4.1.0"
package_info_plus_platform_interface:
dependency: transitive
description:
@ -723,58 +731,50 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0"
url: "https://pub.dev"
source: hosted
version: "2.0.15"
version: "2.1.0"
path_provider_android:
dependency: "direct main"
description:
name: path_provider_android
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8"
url: "https://pub.dev"
source: hosted
version: "2.0.27"
version: "2.1.0"
path_provider_foundation:
dependency: "direct main"
description:
name: path_provider_foundation
sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3"
sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
version: "2.3.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1"
sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3
url: "https://pub.dev"
source: hosted
version: "2.1.10"
version: "2.2.0"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84
url: "https://pub.dev"
source: hosted
version: "2.0.6"
version: "2.1.0"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da
url: "https://pub.dev"
source: hosted
version: "2.1.6"
pedantic:
dependency: transitive
description:
name: pedantic
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "2.2.0"
petitparser:
dependency: transitive
description:
@ -795,10 +795,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.5"
pool:
dependency: transitive
description:
@ -840,6 +840,30 @@ packages:
url: "https://github.com/livinglist/flutter_pulltorefresh"
source: git
version: "2.0.0"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
qr_code_scanner:
dependency: "direct main"
description:
name: qr_code_scanner
sha256: f23b68d893505a424f0bd2e324ebea71ed88465d572d26bb8d2e78a4749591fd
url: "https://pub.dev"
source: hosted
version: "1.0.1"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
receive_sharing_intent:
dependency: "direct main"
description:
@ -876,82 +900,82 @@ packages:
dependency: "direct main"
description:
name: sembast
sha256: a784dbcf313ff38a7f57249694c64a6bcf79f704dbec127958459a7737716830
sha256: "85ff944434f7b566fdc388be4f85b23e954736b7d6e51f965f4f419d966c15b1"
url: "https://pub.dev"
source: hosted
version: "3.4.4"
version: "3.5.0+1"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: "322a1ec9d9fe07e2e2252c098ce93d12dbd06133cc4c00ffe6a4ef505c295c17"
sha256: "6cec740fa0943a826951223e76218df002804adb588235a8910dc3d6b0654e11"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
version: "7.1.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981"
sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.3.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022"
sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.2.0"
shared_preferences_android:
dependency: "direct main"
description:
name: shared_preferences_android
sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749"
sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
shared_preferences_foundation:
dependency: "direct main"
description:
name: shared_preferences_foundation
sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb
sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef
url: "https://pub.dev"
source: hosted
version: "2.2.2"
version: "2.3.3"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa"
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.0"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d
sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.0"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5"
sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.2.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173"
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.0"
shelf:
dependency: transitive
description:
@ -988,10 +1012,10 @@ packages:
dependency: "direct main"
description:
name: shimmer
sha256: "1f1009b5845a1f88f1c5630212279540486f97409e9fc3f63883e71070d107bf"
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
@ -1017,26 +1041,26 @@ packages:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.10.0"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
url: "https://pub.dev"
source: hosted
version: "2.2.8+4"
version: "2.3.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555
sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a"
url: "https://pub.dev"
source: hosted
version: "2.4.5"
version: "2.5.0"
stack_trace:
dependency: transitive
description:
@ -1096,26 +1120,26 @@ packages:
dependency: transitive
description:
name: test
sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4"
sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46"
url: "https://pub.dev"
source: hosted
version: "1.24.1"
version: "1.24.3"
test_api:
dependency: transitive
description:
name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "0.6.0"
test_core:
dependency: transitive
description:
name: test_core
sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93"
sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "0.5.3"
timezone:
dependency: transitive
description:
@ -1144,18 +1168,18 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e"
url: "https://pub.dev"
source: hosted
version: "6.1.11"
version: "6.1.12"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "1a5848f598acc5b7d8f7c18b8cb834ab667e59a13edc3c93e9d09cf38cc6bc87"
sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025"
url: "https://pub.dev"
source: hosted
version: "6.0.34"
version: "6.0.38"
url_launcher_ios:
dependency: transitive
description:
@ -1176,34 +1200,34 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e"
sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.6"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370"
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa"
sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4
url: "https://pub.dev"
source: hosted
version: "2.0.16"
version: "2.0.18"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771"
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.0.7"
uuid:
dependency: transitive
description:
@ -1224,18 +1248,26 @@ packages:
dependency: "direct dev"
description:
name: very_good_analysis
sha256: "5f77d7c00d6010d8ad93ac5d91ecc851c216bcc1e7a51e56c3c01b27152453bb"
sha256: "5e4ea72d2a9188630f0dd8f120a541de730090ef8863243fedca8267a84508b8"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
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:
name: vm_service
sha256: f6deed8ed625c52864792459709183da231ebf66ff0cf09e69b573227c377efe
sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f
url: "https://pub.dev"
source: hosted
version: "11.3.0"
version: "11.7.1"
wakelock:
dependency: "direct main"
description:
@ -1284,6 +1316,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
web_socket_channel:
dependency: transitive
description:
@ -1312,42 +1352,42 @@ packages:
dependency: "direct main"
description:
name: webview_flutter
sha256: "1a37bdbaaf5fbe09ad8579ab09ecfd473284ce482f900b5aea27cf834386a567"
sha256: "789d52bd789373cc1e100fb634af2127e86c99cf9abde09499743270c5de8d00"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
version: "4.2.2"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: "1acea8def62592123e2fbbca164ed8681a98a890bdcbb88f916d5b4a22687759"
sha256: bca797abba472868655b5f1a6029c1132385685ee9db4713cb0e7f33076210c6
url: "https://pub.dev"
source: hosted
version: "3.7.0"
version: "3.9.3"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "78715dc442b7849dbde74e92bb67de1cecf5addf95531c5fb474e72f5fe9a507"
sha256: "0ca3cfcc6781a7de701d580917af4a9efc4e3e129f8ead95a80587f0a749480a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.5.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "4646bb68297803bdbb96d46853e8fcb560d6cb5e04153fa64581535767875dfe"
sha256: ed749f94ac9e814d04a258a9255cf69cfa4cc6006ff59542aea7fb4590144972
url: "https://pub.dev"
source: hosted
version: "3.4.3"
version: "3.7.3"
win32:
dependency: "direct overridden"
description:
name: win32
sha256: "6ca3aaab1790eeb1f5cad232e33d9c53ba66e884dd3e7686c4e730bffc45f1a3"
sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0
url: "https://pub.dev"
source: hosted
version: "5.0.2"
version: "5.0.6"
win32_registry:
dependency: transitive
description:
@ -1368,10 +1408,10 @@ packages:
dependency: transitive
description:
name: xdg_directories
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247
url: "https://pub.dev"
source: hosted
version: "0.2.0+3"
version: "1.0.2"
xml:
dependency: transitive
description:
@ -1389,5 +1429,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.0.0 <4.0.0"
flutter: ">=3.10.3"
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.13.4"

View File

@ -1,11 +1,11 @@
name: hacki
description: A Hacker News reader.
version: 1.7.2+113
version: 1.9.1+122
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: "3.10.3"
flutter: "3.13.4"
dependencies:
adaptive_theme: ^3.2.0
@ -27,27 +27,28 @@ dependencies:
sdk: flutter
flutter_bloc: ^8.1.2
flutter_cache_manager: ^3.3.0
flutter_email_sender: ^5.2.0
flutter_email_sender: ^6.0.1
flutter_fadein: ^2.0.0
flutter_feather_icons: 2.0.0+1
flutter_inappwebview: ^5.7.2+3
flutter_local_notifications: ^14.0.1
flutter_local_notifications: ^15.1.0+1
flutter_secure_storage: ^8.0.0
flutter_siri_suggestions: ^2.1.0
flutter_slidable: ^3.0.0
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
http: ^0.13.5
http: ^1.1.0
hydrated_bloc: ^9.1.0
in_app_review:
path: components/in_app_review
intl: ^0.18.0
linkify: ^5.0.0
logger: ^1.3.0
logger: ^2.0.1
memoize: ^3.0.0
package_info_plus: ^4.0.0
path: ^1.8.2
@ -58,6 +59,8 @@ dependencies:
git:
url: https://github.com/livinglist/flutter_pulltorefresh
ref: master
qr_code_scanner: ^1.0.1
qr_flutter: ^4.1.0
receive_sharing_intent: ^1.4.5
responsive_builder: ^0.7.0
rxdart: ^0.27.7
@ -67,11 +70,12 @@ dependencies:
shared_preferences: ^2.0.17
shared_preferences_android: ^2.0.15
shared_preferences_foundation: ^2.1.3
shimmer: ^2.0.0
shimmer: ^3.0.0
synced_shared_preferences:
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
@ -87,7 +91,7 @@ dev_dependencies:
sdk: flutter
integration_test:
sdk: flutter
mocktail: ^0.3.0
mocktail: ^1.0.0
very_good_analysis: ^5.0.0
flutter:

View File

@ -85,7 +85,7 @@ void main() {
},
expect: () => <AuthState>[
const AuthState.init().copyWith(
status: AuthStatus.loaded,
status: Status.success,
),
],
verify: (_) {
@ -125,25 +125,25 @@ void main() {
const AuthState(
user: User.empty(),
isLoggedIn: false,
status: AuthStatus.loaded,
status: Status.success,
agreedToEULA: false,
),
const AuthState(
user: User.empty(),
isLoggedIn: false,
status: AuthStatus.loaded,
status: Status.success,
agreedToEULA: true,
),
const AuthState(
user: User.empty(),
isLoggedIn: false,
status: AuthStatus.loading,
status: Status.inProgress,
agreedToEULA: true,
),
const AuthState(
user: tUser,
isLoggedIn: true,
status: AuthStatus.loaded,
status: Status.success,
agreedToEULA: true,
),
],