mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
f03b45a98a | |||
cbe5bba986 | |||
268f4054a3 | |||
988c5d9881 | |||
e748e2f818 | |||
1b0a0dbda9 | |||
64d68389ba | |||
381c99b353 | |||
39ee3137f8 | |||
0d76be8634 | |||
9986f72e11 | |||
ef557e7b84 | |||
ec065c0122 | |||
2960c6e59e | |||
92dac6b932 | |||
20365393a3 |
3
.github/workflows/commit_check.yml
vendored
3
.github/workflows/commit_check.yml
vendored
@ -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
|
||||
|
1
.github/workflows/publish_ios.yml
vendored
1
.github/workflows/publish_ios.yml
vendored
@ -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
|
||||
|
@ -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...
|
||||
|
||||
|
@ -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',
|
||||
);
|
||||
});
|
||||
}
|
1
fastlane/metadata/android/en-US/changelogs/121.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/121.txt
Normal file
@ -0,0 +1 @@
|
||||
- Ability to mark a story as read once scrolling past.
|
@ -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
|
||||
|
@ -291,7 +291,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1330;
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1430;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>[
|
||||
'(๑•̀ㅂ•́)و✧',
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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]));
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -87,6 +87,9 @@ class PreferenceState extends Equatable {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
StoryMarkingMode get storyMarkingMode => StoryMarkingMode.values
|
||||
.elementAt(preferences.singleWhereType<StoryMarkingModePreference>().val);
|
||||
|
||||
FetchMode get fetchMode => FetchMode.values
|
||||
.elementAt(preferences.singleWhereType<FetchModePreference>().val);
|
||||
|
||||
|
@ -1,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();
|
||||
|
||||
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/item/models/models.dart';
|
||||
import 'package:hacki/screens/item/widgets/widgets.dart';
|
||||
@ -36,9 +36,9 @@ extension StateExtension on State {
|
||||
if (splitViewEnabled && !forceNewScreen) {
|
||||
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
||||
} else {
|
||||
return HackiApp.navigatorKey.currentState?.pushNamed(
|
||||
ItemScreen.routeName,
|
||||
arguments: args,
|
||||
context.push(
|
||||
'/${ItemScreen.routeName}',
|
||||
extra: args,
|
||||
);
|
||||
}
|
||||
|
||||
@ -112,12 +112,11 @@ extension StateExtension on State {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
onTap: () => Navigator.pop(context, item.url),
|
||||
onTap: () => context.pop(item.url),
|
||||
title: const Text('Link to article'),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => Navigator.pop(
|
||||
context,
|
||||
onTap: () => context.pop(
|
||||
'https://news.ycombinator.com/item?id=${item.id}',
|
||||
),
|
||||
title: const Text('Link to HN'),
|
||||
@ -155,13 +154,13 @@ extension StateExtension on State {
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () => context.pop(false),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => context.pop(true),
|
||||
child: const Text(
|
||||
'Yes',
|
||||
),
|
||||
@ -193,13 +192,13 @@ extension StateExtension on State {
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () => context.pop(false),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => context.pop(true),
|
||||
child: const Text(
|
||||
'Yes',
|
||||
),
|
||||
|
@ -17,7 +17,6 @@ import 'package:hacki/config/custom_router.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/fetcher.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/theme_util.dart';
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
14
lib/models/export_destination.dart
Normal file
14
lib/models/export_destination.dart
Normal 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;
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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
14
lib/models/status.dart
Normal 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;
|
||||
}
|
21
lib/models/story_marking_mode.dart
Normal file
21
lib/models/story_marking_mode.dart
Normal file
@ -0,0 +1,21 @@
|
||||
/// Used for determining when to mark a story as read.
|
||||
enum StoryMarkingMode {
|
||||
// Mark a story as read after user scrolls past it.
|
||||
scrollPast('scrolling past'),
|
||||
// Mark a story as read after user taps on it.
|
||||
tap('tapping'),
|
||||
// Mark a story as read after user scrolls past or taps on it, whichever
|
||||
// happens the first.
|
||||
scrollPastOrTap('scrolling past or tapping');
|
||||
|
||||
const StoryMarkingMode(this.label);
|
||||
|
||||
final String label;
|
||||
|
||||
bool get shouldDetectScrollingPast =>
|
||||
this == StoryMarkingMode.scrollPast ||
|
||||
this == StoryMarkingMode.scrollPastOrTap;
|
||||
|
||||
bool get shouldDetectTapping =>
|
||||
this == StoryMarkingMode.tap || this == StoryMarkingMode.scrollPastOrTap;
|
||||
}
|
@ -64,7 +64,7 @@ class PostableRepository {
|
||||
validateStatus: validateStatus,
|
||||
),
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
throw ServiceException(e.message);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart' hide Badge;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
@ -30,13 +31,6 @@ class HomeScreen extends StatefulWidget {
|
||||
|
||||
static const String routeName = '/';
|
||||
|
||||
static Route<dynamic> route() {
|
||||
return MaterialPageRoute<HomeScreen>(
|
||||
settings: const RouteSettings(name: routeName),
|
||||
builder: (BuildContext context) => const HomeScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
_HomeScreenState createState() => _HomeScreenState();
|
||||
}
|
||||
@ -209,11 +203,13 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
void onStoryTapped(Story story, {bool isPin = false}) {
|
||||
void onStoryTapped(Story story) {
|
||||
final bool useReader = context.read<PreferenceCubit>().state.readerEnabled;
|
||||
final bool offlineReading =
|
||||
context.read<StoriesBloc>().state.isOfflineReading;
|
||||
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
||||
final StoryMarkingMode storyMarkingMode =
|
||||
context.read<PreferenceCubit>().state.storyMarkingMode;
|
||||
|
||||
// If a story is a job story and it has a link to the job posting,
|
||||
// it would be better to just navigate to the web page.
|
||||
@ -222,23 +218,14 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
if (isJobWithLink) {
|
||||
context.read<ReminderCubit>().removeLastReadStoryId();
|
||||
} else {
|
||||
final ItemScreenArgs args = ItemScreenArgs(
|
||||
item: story,
|
||||
);
|
||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
||||
|
||||
context.read<ReminderCubit>().updateLastReadStoryId(story.id);
|
||||
|
||||
if (splitViewEnabled) {
|
||||
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
||||
} else {
|
||||
HackiApp.navigatorKey.currentState
|
||||
?.pushNamed(
|
||||
ItemScreen.routeName,
|
||||
arguments: args,
|
||||
)
|
||||
.whenComplete(() {
|
||||
context.read<ReminderCubit>().removeLastReadStoryId();
|
||||
});
|
||||
context.push('/${ItemScreen.routeName}', extra: args);
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,11 +237,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
context.read<StoriesBloc>().add(
|
||||
StoryRead(
|
||||
story: story,
|
||||
),
|
||||
);
|
||||
if (storyMarkingMode.shouldDetectTapping) {
|
||||
context.read<StoriesBloc>().add(StoryRead(story: story));
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
FlutterSiriSuggestions.instance.registerActivity(
|
||||
|
@ -16,7 +16,7 @@ class PinnedStories extends StatelessWidget {
|
||||
});
|
||||
|
||||
final PreferenceState preferenceState;
|
||||
final void Function(Story story, {bool isPin}) onStoryTapped;
|
||||
final void Function(Story story) onStoryTapped;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -49,7 +49,7 @@ class PinnedStories extends StatelessWidget {
|
||||
child: StoryTile(
|
||||
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
|
||||
story: story,
|
||||
onTap: () => onStoryTapped(story, isPin: true),
|
||||
onTap: () => onStoryTapped(story),
|
||||
showWebPreview: preferenceState.complexStoryTileEnabled,
|
||||
showMetadata: preferenceState.metadataEnabled,
|
||||
showUrl: preferenceState.urlEnabled,
|
||||
|
@ -76,7 +76,7 @@ class _TabletStoryView extends StatelessWidget {
|
||||
previous.itemScreenArgs != current.itemScreenArgs,
|
||||
builder: (BuildContext context, SplitViewState state) {
|
||||
if (state.itemScreenArgs != null) {
|
||||
return ItemScreen.build(context, state.itemScreenArgs!);
|
||||
return ItemScreen.tablet(context, state.itemScreenArgs!);
|
||||
}
|
||||
|
||||
return Material(
|
||||
|
@ -3,6 +3,7 @@ import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
@ -51,44 +52,39 @@ class ItemScreen extends StatefulWidget {
|
||||
this.splitViewEnabled = false,
|
||||
});
|
||||
|
||||
static const String routeName = '/item';
|
||||
static const String routeName = 'item';
|
||||
|
||||
static Route<dynamic> route(ItemScreenArgs args) {
|
||||
return MaterialPageRoute<ItemScreen>(
|
||||
settings: const RouteSettings(name: routeName),
|
||||
builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
|
||||
create: (_) => CollapseCache(),
|
||||
lazy: false,
|
||||
child: MultiBlocProvider(
|
||||
providers: <BlocProvider<dynamic>>[
|
||||
BlocProvider<CommentsCubit>(
|
||||
create: (BuildContext context) => CommentsCubit(
|
||||
filterCubit: context.read<FilterCubit>(),
|
||||
isOfflineReading:
|
||||
context.read<StoriesBloc>().state.isOfflineReading,
|
||||
item: args.item,
|
||||
collapseCache: context.read<CollapseCache>(),
|
||||
defaultFetchMode:
|
||||
context.read<PreferenceCubit>().state.fetchMode,
|
||||
defaultCommentsOrder:
|
||||
context.read<PreferenceCubit>().state.order,
|
||||
)..init(
|
||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||
targetAncestors: args.targetComments,
|
||||
useCommentCache: args.useCommentCache,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ItemScreen(
|
||||
item: args.item,
|
||||
parentComments: args.targetComments ?? <Comment>[],
|
||||
static Widget phone(ItemScreenArgs args) {
|
||||
return RepositoryProvider<CollapseCache>(
|
||||
create: (_) => CollapseCache(),
|
||||
lazy: false,
|
||||
child: MultiBlocProvider(
|
||||
providers: <BlocProvider<dynamic>>[
|
||||
BlocProvider<CommentsCubit>(
|
||||
create: (BuildContext context) => CommentsCubit(
|
||||
filterCubit: context.read<FilterCubit>(),
|
||||
isOfflineReading:
|
||||
context.read<StoriesBloc>().state.isOfflineReading,
|
||||
item: args.item,
|
||||
collapseCache: context.read<CollapseCache>(),
|
||||
defaultFetchMode: context.read<PreferenceCubit>().state.fetchMode,
|
||||
defaultCommentsOrder: context.read<PreferenceCubit>().state.order,
|
||||
)..init(
|
||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||
targetAncestors: args.targetComments,
|
||||
useCommentCache: args.useCommentCache,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ItemScreen(
|
||||
item: args.item,
|
||||
parentComments: args.targetComments ?? <Comment>[],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget build(BuildContext context, ItemScreenArgs args) {
|
||||
static Widget tablet(BuildContext context, ItemScreenArgs args) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context.read<SplitViewCubit>().state.expanded) {
|
||||
@ -168,7 +164,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
SchedulerBinding.instance
|
||||
..addPostFrameCallback((_) {
|
||||
FeatureDiscovery.discoverFeatures(
|
||||
@ -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,
|
||||
|
@ -43,6 +43,7 @@ class CustomAppBar extends AppBar {
|
||||
fontFamily: FeatherIcons.type.fontFamily,
|
||||
package: FeatherIcons.type.fontPackage,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
onPressed: onFontSizeTap,
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -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,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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> {
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
@ -56,7 +57,7 @@ class TimeMachineDialog extends StatelessWidget {
|
||||
Icons.close,
|
||||
size: Dimens.pt16,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => context.pop(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
|
@ -1,11 +1,13 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/profile/models/models.dart';
|
||||
@ -31,7 +33,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final Throttle throttle = Throttle(delay: Durations.twoSeconds);
|
||||
|
||||
PageType pageType = PageType.notification;
|
||||
PageType? pageType;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@ -45,6 +47,9 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
pageType ??= context.read<AuthBloc>().state.isLoggedIn
|
||||
? PageType.notification
|
||||
: PageType.fav;
|
||||
super.build(context);
|
||||
return BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (BuildContext context, AuthState authState) {
|
||||
@ -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,
|
||||
),
|
||||
|
71
lib/screens/profile/qr_code_scanner_screen.dart
Normal file
71
lib/screens/profile/qr_code_scanner_screen.dart
Normal 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();
|
||||
}
|
||||
}
|
46
lib/screens/profile/qr_code_view_screen.dart
Normal file
46
lib/screens/profile/qr_code_view_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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'),
|
||||
),
|
||||
],
|
||||
|
@ -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.');
|
||||
|
@ -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';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/models/search_params.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
@ -67,13 +68,13 @@ class PostedByFilterChip extends StatelessWidget {
|
||||
child: ButtonBar(
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, filter?.author),
|
||||
onPressed: () => context.pop(filter?.author),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, null),
|
||||
onPressed: () => context.pop(null),
|
||||
child: const Text(
|
||||
'Clear',
|
||||
),
|
||||
@ -81,7 +82,7 @@ class PostedByFilterChip extends StatelessWidget {
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final String text = usernameController.text.trim();
|
||||
Navigator.pop(context, text.isEmpty ? null : text);
|
||||
context.pop(text.isEmpty ? null : text);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
|
@ -1,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,
|
||||
|
@ -10,6 +10,8 @@ class WebViewScreen extends StatefulWidget {
|
||||
super.key,
|
||||
});
|
||||
|
||||
static const String routeName = 'web-view';
|
||||
|
||||
final String url;
|
||||
|
||||
@override
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -1,2 +1,3 @@
|
||||
export 'emphasis_linkifier.dart';
|
||||
export 'quote_linkifier.dart';
|
||||
export 'url_linkifier.dart';
|
||||
|
121
lib/screens/widgets/custom_linkify/linkifiers/url_linkifier.dart
Normal file
121
lib/screens/widgets/custom_linkify/linkifiers/url_linkifier.dart
Normal 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);
|
||||
}
|
@ -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,
|
||||
);
|
||||
|
@ -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),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/context_extension.dart';
|
||||
@ -13,8 +13,8 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
const ItemsListView({
|
||||
required this.showWebPreview,
|
||||
required this.showMetadata,
|
||||
required this.showWebPreviewOnStoryTile,
|
||||
required this.showMetadataOnStoryTile,
|
||||
required this.showUrl,
|
||||
required this.items,
|
||||
required this.onTap,
|
||||
@ -23,7 +23,6 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
this.useCommentTile = false,
|
||||
this.showCommentBy = false,
|
||||
this.enablePullDown = true,
|
||||
this.pinnable = false,
|
||||
this.markReadStories = false,
|
||||
this.useConsistentFontSize = false,
|
||||
this.showOfflineBanner = false,
|
||||
@ -32,33 +31,31 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
this.onPinned,
|
||||
this.header,
|
||||
this.onMoreTapped,
|
||||
}) : assert(
|
||||
!pinnable || (pinnable && onPinned != null),
|
||||
'onPinned cannot be null when pinnable is true',
|
||||
);
|
||||
this.scrollController,
|
||||
this.itemBuilder,
|
||||
});
|
||||
|
||||
final bool useCommentTile;
|
||||
final bool showCommentBy;
|
||||
final bool showWebPreview;
|
||||
final bool showMetadata;
|
||||
final bool showWebPreviewOnStoryTile;
|
||||
final bool showMetadataOnStoryTile;
|
||||
final bool showUrl;
|
||||
final bool enablePullDown;
|
||||
final bool markReadStories;
|
||||
final bool showOfflineBanner;
|
||||
|
||||
/// Whether story tiles can be pinned to the top.
|
||||
final bool pinnable;
|
||||
|
||||
/// Whether to use same font size for comment and story tiles.
|
||||
final bool useConsistentFontSize;
|
||||
|
||||
final List<T> items;
|
||||
final Widget? header;
|
||||
final RefreshController refreshController;
|
||||
final ScrollController? scrollController;
|
||||
final VoidCallback? onRefresh;
|
||||
final VoidCallback? onLoadMore;
|
||||
final ValueChanged<Story>? onPinned;
|
||||
final void Function(T) onTap;
|
||||
final Widget Function(Widget child, T item)? itemBuilder;
|
||||
|
||||
/// Used for home screen.
|
||||
final void Function(Story, Rect?)? onMoreTapped;
|
||||
@ -66,6 +63,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ListView child = ListView(
|
||||
controller: scrollController,
|
||||
children: <Widget>[
|
||||
if (showOfflineBanner)
|
||||
const OfflineBanner(
|
||||
@ -85,51 +83,21 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
? () => onMoreTapped?.call(e, context.rect)
|
||||
: null,
|
||||
child: FadeIn(
|
||||
child: Slidable(
|
||||
enabled: !swipeGestureEnabled,
|
||||
startActionPane: pinnable
|
||||
? ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedbackUtil.light();
|
||||
onPinned?.call(e);
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: showWebPreview
|
||||
? Icons.push_pin_outlined
|
||||
: null,
|
||||
label: showWebPreview ? null : 'Pin to top',
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onMoreTapped?.call(e, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: showWebPreview ? Icons.more_horiz : null,
|
||||
label: showWebPreview ? null : 'More',
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
child: StoryTile(
|
||||
key: ValueKey<int>(e.id),
|
||||
story: e,
|
||||
onTap: () => onTap(e),
|
||||
showWebPreview: showWebPreview,
|
||||
showMetadata: showMetadata,
|
||||
showUrl: showUrl,
|
||||
hasRead: markReadStories && hasRead,
|
||||
simpleTileFontSize: useConsistentFontSize
|
||||
? TextDimens.pt14
|
||||
: TextDimens.pt16,
|
||||
),
|
||||
child: StoryTile(
|
||||
key: ValueKey<int>(e.id),
|
||||
story: e,
|
||||
onTap: () => onTap(e),
|
||||
showWebPreview: showWebPreviewOnStoryTile,
|
||||
showMetadata: showMetadataOnStoryTile,
|
||||
showUrl: showUrl,
|
||||
hasRead: markReadStories && hasRead,
|
||||
simpleTileFontSize: useConsistentFontSize
|
||||
? TextDimens.pt14
|
||||
: TextDimens.pt16,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!showWebPreview)
|
||||
if (!showWebPreviewOnStoryTile)
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
@ -137,14 +105,16 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
} else if (e is Comment) {
|
||||
if (useCommentTile) {
|
||||
return <Widget>[
|
||||
if (showWebPreview)
|
||||
if (showWebPreviewOnStoryTile)
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
_CommentTile(
|
||||
comment: e,
|
||||
onTap: () => onTap(e),
|
||||
fontSize: showWebPreview ? TextDimens.pt14 : TextDimens.pt16,
|
||||
fontSize: showWebPreviewOnStoryTile
|
||||
? TextDimens.pt14
|
||||
: TextDimens.pt16,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
@ -227,7 +197,11 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
}
|
||||
|
||||
return <Widget>[Container()];
|
||||
}).expand((List<Widget> element) => element),
|
||||
}).mapIndexed(
|
||||
(int index, List<Widget> e) => itemBuilder == null
|
||||
? Column(children: e)
|
||||
: itemBuilder!(Column(children: e), items.elementAt(index)),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt40,
|
||||
),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
@ -39,11 +40,11 @@ class OfflineBanner extends StatelessWidget {
|
||||
title: const Text('Exit offline mode?'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
onPressed: () => context.pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
onPressed: () => context.pop(true),
|
||||
child: const Text(
|
||||
'Yes',
|
||||
style: TextStyle(
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
@ -31,7 +32,7 @@ class _OnboardingViewState extends State<OnboardingView> {
|
||||
Icons.close,
|
||||
color: Palette.white,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: context.pop,
|
||||
),
|
||||
),
|
||||
backgroundColor: Theme.of(context).brightness == Brightness.light
|
||||
@ -76,7 +77,7 @@ class _OnboardingViewState extends State<OnboardingView> {
|
||||
onPressed: () {
|
||||
HapticFeedbackUtil.light();
|
||||
if (pageController.page! >= 2) {
|
||||
Navigator.pop(context);
|
||||
context.pop();
|
||||
} else {
|
||||
throttle.run(() {
|
||||
pageController.nextPage(
|
||||
|
23
lib/screens/widgets/optional_wrapper.dart
Normal file
23
lib/screens/widgets/optional_wrapper.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OptionalWrapper extends StatelessWidget {
|
||||
const OptionalWrapper({
|
||||
required this.enabled,
|
||||
required this.wrapper,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool enabled;
|
||||
final Widget Function(Widget) wrapper;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (enabled) {
|
||||
return wrapper(child);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
class StoriesListView extends StatefulWidget {
|
||||
const StoriesListView({
|
||||
@ -26,11 +30,13 @@ class StoriesListView extends StatefulWidget {
|
||||
|
||||
class _StoriesListViewState extends State<StoriesListView> {
|
||||
final RefreshController refreshController = RefreshController();
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
refreshController.dispose();
|
||||
scrollController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -229,7 +229,7 @@ class _LinkPreviewPlaceholder extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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';
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
|
@ -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']!,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
|
320
pubspec.lock
320
pubspec.lock
@ -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"
|
||||
|
20
pubspec.yaml
20
pubspec.yaml
@ -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:
|
||||
|
Submodule submodules/flutter updated: f92f44110e...367f9ea16b
@ -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,
|
||||
),
|
||||
],
|
||||
|
Reference in New Issue
Block a user