Compare commits

...

8 Commits

59 changed files with 760 additions and 277 deletions

View File

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

BIN
assets/hacki-github.xcf Normal file

Binary file not shown.

BIN
assets/hacki.xcf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

View File

@ -12,8 +12,7 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart';
abstract class InAppReviewPlatform extends PlatformInterface { abstract class InAppReviewPlatform extends PlatformInterface {
InAppReviewPlatform() : super(token: _token); InAppReviewPlatform() : super(token: _token);
static InAppReviewPlatform _instance = static InAppReviewPlatform _instance = MethodChannelInAppReview();
MethodChannelInAppReview() as InAppReviewPlatform;
static final Object _token = Object(); static final Object _token = Object();

View File

@ -27,12 +27,16 @@ PODS:
- Flutter - Flutter
- integration_test (0.0.1): - integration_test (0.0.1):
- Flutter - Flutter
- MTBBarcodeScanner (5.0.11)
- OrderedSet (5.0.0) - OrderedSet (5.0.0)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- qr_code_scanner (0.2.0):
- Flutter
- MTBBarcodeScanner
- ReachabilitySwift (5.0.0) - ReachabilitySwift (5.0.0)
- receive_sharing_intent (0.0.1): - receive_sharing_intent (0.0.1):
- Flutter - Flutter
@ -68,6 +72,7 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - 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`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@ -81,6 +86,7 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- FMDB - FMDB
- MTBBarcodeScanner
- OrderedSet - OrderedSet
- ReachabilitySwift - ReachabilitySwift
@ -109,6 +115,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios"
receive_sharing_intent: receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios" :path: ".symlinks/plugins/receive_sharing_intent/ios"
share_plus: share_plus:
@ -140,9 +148,11 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
integration_test: 13825b8a9334a850581300559b8839134b124670 integration_test: 13825b8a9334a850581300559b8839134b124670
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1 receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
@ -156,4 +166,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937 PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
COCOAPODS: 1.12.0 COCOAPODS: 1.11.3

View File

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

View File

@ -133,6 +133,8 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesRefresh event, StoriesRefresh event,
Emitter<StoriesState> emit, Emitter<StoriesState> emit,
) async { ) async {
if (state.statusByType[event.type] == StoriesStatus.loading) return;
emit( emit(
state.copyWithStatusUpdated( state.copyWithStatusUpdated(
type: event.type, type: event.type,

View File

@ -78,3 +78,15 @@ abstract class RegExpConstants {
static const String linkSuffix = r'(\)|]|,|\*)(.)*$'; static const String linkSuffix = r'(\)|]|,|\*)(.)*$';
static const String number = '[0-9]+'; static const String number = '[0-9]+';
} }
abstract class Durations {
static const Duration ms100 = Duration(milliseconds: 100);
static const Duration ms200 = Duration(milliseconds: 200);
static const Duration ms300 = Duration(milliseconds: 300);
static const Duration ms400 = Duration(milliseconds: 400);
static const Duration ms500 = Duration(milliseconds: 500);
static const Duration ms600 = Duration(milliseconds: 600);
static const Duration oneSecond = Duration(seconds: 1);
static const Duration twoSeconds = Duration(seconds: 2);
static const Duration tenSeconds = Duration(seconds: 10);
}

View File

@ -11,10 +11,14 @@ class CustomRouter {
switch (settings.name) { switch (settings.name) {
case HomeScreen.routeName: case HomeScreen.routeName:
return HomeScreen.route(); return HomeScreen.route();
case ItemScreen.routeName:
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
case SubmitScreen.routeName: case SubmitScreen.routeName:
return SubmitScreen.route(); return SubmitScreen.route();
case QrCodeScannerScreen.routeName:
return QrCodeScannerScreen.route();
case ItemScreen.routeName:
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
case QrCodeViewScreen.routeName:
return QrCodeViewScreen.route(data: settings.arguments! as String);
default: default:
return _errorRoute(); return _errorRoute();
} }

View File

@ -5,6 +5,7 @@ import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/main.dart'; import 'package:hacki/main.dart';
@ -348,6 +349,7 @@ class CommentsCubit extends Cubit<CommentsState> {
init(useCommentCache: true); init(useCommentCache: true);
} }
/// Jump to next root level comment.
void jump( void jump(
ItemScrollController itemScrollController, ItemScrollController itemScrollController,
ItemPositionsListener itemPositionsListener, ItemPositionsListener itemPositionsListener,
@ -378,13 +380,14 @@ class CommentsCubit extends Cubit<CommentsState> {
itemScrollController.scrollTo( itemScrollController.scrollTo(
index: i + 1, index: i + 1,
alignment: 0.15, alignment: 0.15,
duration: const Duration(milliseconds: 400), duration: Durations.ms400,
); );
return; return;
} }
} }
} }
/// Jump to previous root level comment.
void jumpUp( void jumpUp(
ItemScrollController itemScrollController, ItemScrollController itemScrollController,
ItemPositionsListener itemPositionsListener, ItemPositionsListener itemPositionsListener,
@ -416,7 +419,7 @@ class CommentsCubit extends Cubit<CommentsState> {
itemScrollController.scrollTo( itemScrollController.scrollTo(
index: i + 1, index: i + 1,
alignment: 0.15, alignment: 0.15,
duration: const Duration(milliseconds: 400), duration: Durations.ms400,
); );
return; return;
} }

View File

@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -11,7 +12,7 @@ part 'edit_state.dart';
class EditCubit extends HydratedCubit<EditState> { class EditCubit extends HydratedCubit<EditState> {
EditCubit({DraftCache? draftCache}) EditCubit({DraftCache? draftCache})
: _draftCache = draftCache ?? locator.get<DraftCache>(), : _draftCache = draftCache ?? locator.get<DraftCache>(),
_debouncer = Debouncer(delay: const Duration(seconds: 1)), _debouncer = Debouncer(delay: Durations.oneSecond),
super(const EditState.init()); super(const EditState.init());
final DraftCache _draftCache; final DraftCache _draftCache;

View File

@ -4,6 +4,7 @@ import 'dart:math';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -31,7 +32,7 @@ class NotificationCubit extends Cubit<NotificationState> {
if (authState.isLoggedIn && authState.username != _username) { if (authState.isLoggedIn && authState.username != _username) {
// Get the user setting. // Get the user setting.
if (_preferenceCubit.state.notificationEnabled) { if (_preferenceCubit.state.notificationEnabled) {
Future<void>.delayed(const Duration(seconds: 2), init); Future<void>.delayed(Durations.twoSeconds, init);
} }
// Listen for setting changes in the future. // Listen for setting changes in the future.

View File

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

View File

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

View File

@ -20,9 +20,6 @@ class PostCubit extends Cubit<PostState> {
text: text, text: text,
); );
// final successful =
// await Future<bool>.delayed(const Duration(seconds: 2), () => true);
if (successful) { if (successful) {
emit(state.copyWith(status: PostStatus.successful)); emit(state.copyWith(status: PostStatus.successful));
} else { } else {

View File

@ -68,6 +68,8 @@ class PreferenceState extends Equatable {
bool get swipeGestureEnabled => _isOn<SwipeGesturePreference>(); bool get swipeGestureEnabled => _isOn<SwipeGesturePreference>();
bool get autoScrollEnabled => _isOn<AutoScrollModePreference>();
List<StoryType> get tabs { List<StoryType> get tabs {
final String result = final String result =
preferences.singleWhereType<TabOrderPreference>().val.toString(); preferences.singleWhereType<TabOrderPreference>().val.toString();

View File

@ -48,3 +48,11 @@ class SearchState extends Equatable {
params, params,
]; ];
} }
extension SearchStateExtension on SearchState {
bool get showDateRangeShortcutChips {
return hasDateFilter &&
dateFilter?.startTime != null &&
dateFilter?.endTime != null;
}
}

View File

@ -7,6 +7,7 @@ import 'package:equatable/equatable.dart';
import 'package:feature_discovery/feature_discovery.dart'; import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
@ -268,8 +269,8 @@ class HackiApp extends StatelessWidget {
AsyncSnapshot<AdaptiveThemeMode?> snapshot, AsyncSnapshot<AdaptiveThemeMode?> snapshot,
) { ) {
final AdaptiveThemeMode? mode = snapshot.data; final AdaptiveThemeMode? mode = snapshot.data;
ThemeUtil.updateAndroidStatusBarSetting( ThemeUtil.updateStatusBarSetting(
Theme.of(context).brightness, SchedulerBinding.instance.platformDispatcher.platformBrightness,
mode, mode,
); );
return BlocBuilder<PreferenceCubit, PreferenceState>( return BlocBuilder<PreferenceCubit, PreferenceState>(

View File

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
const StoryUrlModePreference(), const StoryUrlModePreference(),
const NotificationModePreference(), const NotificationModePreference(),
const SwipeGesturePreference(), const SwipeGesturePreference(),
const AutoScrollModePreference(),
const CollapseModePreference(), const CollapseModePreference(),
const ReaderModePreference(), const ReaderModePreference(),
const MarkReadStoriesModePreference(), const MarkReadStoriesModePreference(),
@ -54,12 +55,13 @@ const bool _notificationModeDefaultValue = true;
const bool _swipeGestureModeDefaultValue = false; const bool _swipeGestureModeDefaultValue = false;
const bool _displayModeDefaultValue = true; const bool _displayModeDefaultValue = true;
const bool _eyeCandyModeDefaultValue = false; const bool _eyeCandyModeDefaultValue = false;
const bool _trueDarkModeDefaultValue = false; const bool _trueDarkModeDefaultValue = true;
const bool _readerModeDefaultValue = true; const bool _readerModeDefaultValue = true;
const bool _markReadStoriesModeDefaultValue = true; const bool _markReadStoriesModeDefaultValue = true;
const bool _metadataModeDefaultValue = true; const bool _metadataModeDefaultValue = true;
const bool _storyUrlModeDefaultValue = true; const bool _storyUrlModeDefaultValue = true;
const bool _collapseModeDefaultValue = true; const bool _collapseModeDefaultValue = true;
const bool _autoScrollModeDefaultValue = true;
final int _fetchModeDefaultValue = FetchMode.eager.index; final int _fetchModeDefaultValue = FetchMode.eager.index;
final int _commentsOrderDefaultValue = CommentsOrder.natural.index; final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
final int _fontSizeDefaultValue = FontSize.regular.index; final int _fontSizeDefaultValue = FontSize.regular.index;
@ -127,6 +129,26 @@ class CollapseModePreference extends BooleanPreference {
'''if disabled, tap on the top of comment tile to collapse.'''; '''if disabled, tap on the top of comment tile to collapse.''';
} }
class AutoScrollModePreference extends BooleanPreference {
const AutoScrollModePreference({bool? val})
: super(val: val ?? _autoScrollModeDefaultValue);
@override
AutoScrollModePreference copyWith({required bool? val}) {
return AutoScrollModePreference(val: val);
}
@override
String get key => 'autoScrollMode';
@override
String get title => 'Auto-scroll on collapsing';
@override
String get subtitle =>
'''automatically scroll to next comment when you collapse a comment.''';
}
/// The value deciding whether or not the story /// The value deciding whether or not the story
/// tile should display link preview. Defaults to true. /// tile should display link preview. Defaults to true.
class DisplayModePreference extends BooleanPreference { class DisplayModePreference extends BooleanPreference {

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

@ -0,0 +1,6 @@
enum Status {
idle,
loading,
loaded,
error,
}

View File

@ -57,7 +57,7 @@ class _HomeScreenState extends State<HomeScreen>
DeviceScreenType.mobile) { DeviceScreenType.mobile) {
locator.get<Logger>().i('resetting comments in CommentCache'); locator.get<Logger>().i('resetting comments in CommentCache');
Future<void>.delayed( Future<void>.delayed(
const Duration(milliseconds: 500), Durations.ms500,
locator.get<CommentCache>().resetComments, locator.get<CommentCache>().resetComments,
); );
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart' hide Badge; import 'package:flutter/material.dart' hide Badge;
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
@ -36,7 +37,7 @@ class TabletHomeScreen extends StatelessWidget {
top: Dimens.zero, top: Dimens.zero,
bottom: Dimens.zero, bottom: Dimens.zero,
width: homeScreenWidth, width: homeScreenWidth,
duration: const Duration(milliseconds: 300), duration: Durations.ms300,
curve: Curves.elasticOut, curve: Curves.elasticOut,
child: homeScreen, child: homeScreen,
), ),
@ -52,7 +53,7 @@ class TabletHomeScreen extends StatelessWidget {
top: Dimens.zero, top: Dimens.zero,
bottom: Dimens.zero, bottom: Dimens.zero,
left: state.expanded ? Dimens.zero : homeScreenWidth, left: state.expanded ? Dimens.zero : homeScreenWidth,
duration: const Duration(milliseconds: 300), duration: Durations.ms300,
curve: Curves.elasticOut, curve: Curves.elasticOut,
child: const _TabletStoryView(), child: const _TabletStoryView(),
), ),

View File

@ -153,9 +153,9 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
); );
final GlobalKey fontSizeIconButtonKey = GlobalKey(); final GlobalKey fontSizeIconButtonKey = GlobalKey();
static const Duration _storyLinkTapThrottleDelay = Duration(seconds: 2); static const Duration _storyLinkTapThrottleDelay = Durations.twoSeconds;
static const Duration _featureDiscoveryDismissThrottleDelay = static const Duration _featureDiscoveryDismissThrottleDelay =
Duration(seconds: 1); Durations.oneSecond;
@override @override
void didPop() { void didPop() {

View File

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

View File

@ -139,6 +139,7 @@ class MainView extends StatelessWidget {
}, },
onMoreTapped: onMoreTapped, onMoreTapped: onMoreTapped,
onRightMoreTapped: onRightMoreTapped, onRightMoreTapped: onRightMoreTapped,
itemScrollController: itemScrollController,
), ),
); );
}, },
@ -254,6 +255,8 @@ class _ParentItemSection extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
color: Palette.orange, color: Palette.orange,
), ),
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
), ),
const Spacer(), const Spacer(),
Text( Text(
@ -261,6 +264,8 @@ class _ParentItemSection extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
color: Palette.grey, color: Palette.grey,
), ),
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
), ),
], ],
), ),
@ -333,9 +338,8 @@ class _ParentItemSection extends StatelessWidget {
], ],
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
textScaleFactor: MediaQuery.of( textScaleFactor:
context, MediaQuery.of(context).textScaleFactor,
).textScaleFactor,
), ),
), ),
) )
@ -353,6 +357,8 @@ class _ParentItemSection extends StatelessWidget {
), ),
child: ItemText( child: ItemText(
item: state.item, item: state.item,
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
), ),
), ),
), ),
@ -401,6 +407,7 @@ class _ParentItemSection extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontSize: TextDimens.pt13, fontSize: TextDimens.pt13,
), ),
textScaleFactor: 1,
), ),
] else ...<Widget>[ ] else ...<Widget>[
const SizedBox( const SizedBox(
@ -443,6 +450,7 @@ class _ParentItemSection extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: TextDimens.pt13, fontSize: TextDimens.pt13,
), ),
textScaleFactor: 1,
), ),
), ),
), ),
@ -461,6 +469,7 @@ class _ParentItemSection extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontSize: TextDimens.pt13, fontSize: TextDimens.pt13,
), ),
textScaleFactor: 1,
), ),
), ),
) )
@ -482,6 +491,7 @@ class _ParentItemSection extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontSize: TextDimens.pt13, fontSize: TextDimens.pt13,
), ),
textScaleFactor: 1,
), ),
), ),
) )

View File

@ -2,6 +2,7 @@ import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/item/item.dart'; import 'package:hacki/models/item/item.dart';
@ -60,7 +61,7 @@ class _ReplyBoxState extends State<ReplyBox> {
), ),
child: AnimatedContainer( child: AnimatedContainer(
height: expanded ? expandedHeight : _collapsedHeight, height: expanded ? expandedHeight : _collapsedHeight,
duration: const Duration(milliseconds: 200), duration: Durations.ms200,
decoration: BoxDecoration( decoration: BoxDecoration(
boxShadow: <BoxShadow>[ boxShadow: <BoxShadow>[
if (!context.read<SplitViewCubit>().state.enabled) if (!context.read<SplitViewCubit>().state.enabled)
@ -79,7 +80,7 @@ class _ReplyBoxState extends State<ReplyBox> {
), ),
AnimatedContainer( AnimatedContainer(
height: expanded ? Dimens.pt36 : Dimens.zero, height: expanded ? Dimens.pt36 : Dimens.zero,
duration: const Duration(milliseconds: 200), duration: Durations.ms200,
), ),
Row( Row(
children: <Widget>[ children: <Widget>[
@ -107,7 +108,7 @@ class _ReplyBoxState extends State<ReplyBox> {
AnimatedOpacity( AnimatedOpacity(
opacity: opacity:
expanded ? NumSwitch.on : NumSwitch.off, expanded ? NumSwitch.on : NumSwitch.off,
duration: const Duration(milliseconds: 300), duration: Durations.ms300,
child: IconButton( child: IconButton(
key: const Key('quote'), key: const Key('quote'),
icon: const Icon( icon: const Icon(
@ -344,6 +345,8 @@ class _ReplyBoxState extends State<ReplyBox> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: ItemText( child: ItemText(
item: replyingTo, item: replyingTo,
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
), ),
), ),
), ),

View File

@ -29,7 +29,7 @@ class _ProfileScreenState extends State<ProfileScreen>
final RefreshController refreshControllerFav = RefreshController(); final RefreshController refreshControllerFav = RefreshController();
final RefreshController refreshControllerNotification = RefreshController(); final RefreshController refreshControllerNotification = RefreshController();
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final Throttle throttle = Throttle(delay: const Duration(seconds: 2)); final Throttle throttle = Throttle(delay: Durations.twoSeconds);
PageType pageType = PageType.notification; PageType pageType = PageType.notification;

View File

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:hacki/main.dart';
import 'package:hacki/styles/styles.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class QrCodeScannerScreen extends StatefulWidget {
const QrCodeScannerScreen({super.key});
static const String routeName = '/qr-code-scanner';
static Route<dynamic> route() {
return MaterialPageRoute<String?>(
settings: const RouteSettings(name: routeName),
builder: (_) => const QrCodeScannerScreen(),
);
}
@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();
HackiApp.navigatorKey.currentState?.pop(scanData.code);
});
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,55 @@
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 Route<dynamic> route({required String data}) {
return MaterialPageRoute<QrCodeViewScreen>(
settings: const RouteSettings(name: routeName),
builder: (_) => QrCodeViewScreen(
data: data,
),
);
}
static const int qrCodeVersion = 4;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Palette.transparent,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: QrImageView(
data: data,
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Theme.of(context).colorScheme.onSurface,
),
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Theme.of(context).colorScheme.onSurface,
),
version: qrCodeVersion,
size: 300,
),
),
],
),
);
}
}

View File

@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart';
@ -13,9 +15,12 @@ import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/profile/models/page_type.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/offline_list_tile.dart';
import 'package:hacki/screens/profile/widgets/tab_bar_settings.dart'; import 'package:hacki/screens/profile/widgets/tab_bar_settings.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
@ -243,6 +248,13 @@ class _SettingsState extends State<Settings> {
), ),
onTap: onExportFavoritesTapped, onTap: onExportFavoritesTapped,
), ),
ListTile(
title: const Text(
'Import Favorites',
),
onTap: () =>
onImportFavoritesTapped(context.read<FavCubit>()),
),
ListTile( ListTile(
title: const Text( title: const Text(
'Clear Favorites', 'Clear Favorites',
@ -404,8 +416,9 @@ class _SettingsState extends State<Settings> {
AdaptiveTheme.of(context).setSystem(); AdaptiveTheme.of(context).setSystem();
} }
final Brightness brightness = Theme.of(context).brightness; final Brightness brightness =
ThemeUtil.updateAndroidStatusBarSetting(brightness, val); SchedulerBinding.instance.platformDispatcher.platformBrightness;
ThemeUtil.updateStatusBarSetting(brightness, val);
} }
void showClearCacheDialog() { void showClearCacheDialog() {
@ -752,20 +765,68 @@ class _SettingsState extends State<Settings> {
} }
Future<void> onExportFavoritesTapped() async { 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: () => Navigator.pop<ExportDestination>(context, e),
),
),
],
),
);
},
).then(
(ExportDestination? destination) => exportFavorites(to: destination),
);
}
Future<void> onImportFavoritesTapped(FavCubit favCubit) async {
final String? res = await HackiApp.navigatorKey.currentState
?.pushNamed(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) { if (allFavorites.isEmpty) {
showSnackBar(content: "You don't have any favorite item."); showSnackBar(content: "You don't have any favorite item.");
return; return;
} }
final String allFavoritesStr = allFavorites.join('\n');
try { switch (destination) {
await FlutterClipboard.copy( case ExportDestination.qrCode:
allFavorites.join('\n'), await HackiApp.navigatorKey.currentState?.pushNamed(
).whenComplete(HapticFeedbackUtil.selection); QrCodeViewScreen.routeName,
showSnackBar(content: 'Ids of favorites have been copied to clipboard.'); arguments: allFavoritesStr,
} catch (error, stackTrace) { );
error.logError(stackTrace: stackTrace); 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);
}
} }
} }

View File

@ -1,6 +1,8 @@
export 'home/home_screen.dart'; export 'home/home_screen.dart';
export 'item/item_screen.dart'; export 'item/item_screen.dart';
export 'profile/profile_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 'search/search_screen.dart';
export 'submit/submit_screen.dart'; export 'submit/submit_screen.dart';
export 'web_view/web_view_screen.dart'; export 'web_view/web_view_screen.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart'; import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -30,31 +30,9 @@ class SearchScreen extends StatefulWidget {
class _SearchScreenState extends State<SearchScreen> { class _SearchScreenState extends State<SearchScreen> {
final RefreshController refreshController = RefreshController(); final RefreshController refreshController = RefreshController();
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final Debouncer debouncer = Debouncer(delay: const Duration(seconds: 1)); final Debouncer debouncer = Debouncer(delay: Durations.oneSecond);
bool showChips = true;
bool shouldOffStageChips = false;
static const Duration chipsAnimationDuration = Duration(milliseconds: 300); static const Duration chipsAnimationDuration = Durations.ms300;
@override
void initState() {
super.initState();
scrollController.addListener(() {
if (scrollController.position.userScrollDirection ==
ScrollDirection.reverse &&
showChips) {
setState(() {
showChips = false;
});
} else if (scrollController.position.userScrollDirection ==
ScrollDirection.forward &&
!showChips) {
setState(() {
showChips = true;
});
}
});
}
@override @override
void dispose() { void dispose() {
@ -107,83 +85,13 @@ class _SearchScreenState extends State<SearchScreen> {
), ),
AnimatedCrossFade( AnimatedCrossFade(
duration: chipsAnimationDuration, duration: chipsAnimationDuration,
crossFadeState: showChips crossFadeState: state.showDateRangeShortcutChips
? CrossFadeState.showSecond ? CrossFadeState.showSecond
: CrossFadeState.showFirst, : CrossFadeState.showFirst,
firstChild: SizedBox.fromSize(), firstChild: SizedBox.fromSize(),
secondChild: Column( secondChild: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
if (state.hasDateFilter &&
state.dateFilter?.startTime != null &&
state.dateFilter?.endTime != null)
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.dayBefore(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.dayAfter(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.weekBefore(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.weekAfter(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.monthBefore(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.monthAfter(
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter!.startTime!,
endDate: state.dateFilter!.endTime!,
),
],
),
),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
@ -191,78 +99,143 @@ class _SearchScreenState extends State<SearchScreen> {
const SizedBox( const SizedBox(
width: Dimens.pt8, width: Dimens.pt8,
), ),
DateTimeRangeFilterChip( DateTimeShortcutChip.dayBefore(
filter: state.dateFilter,
initialStartDate: state.dateFilter?.startTime,
initialEndDate: state.dateFilter?.endTime,
onDateTimeRangeUpdated: context onDateTimeRangeUpdated: context
.read<SearchCubit>() .read<SearchCubit>()
.onDateTimeRangeUpdated, .onDateTimeRangeUpdated,
onDateTimeRangeRemoved: context startDate: state.dateFilter?.startTime,
endDate: state.dateFilter?.endTime,
),
const SizedBox(
width: Dimens.pt8,
),
DateTimeShortcutChip.dayAfter(
onDateTimeRangeUpdated: context
.read<SearchCubit>() .read<SearchCubit>()
.removeFilter<DateTimeRangeFilter>, .onDateTimeRangeUpdated,
startDate: state.dateFilter?.startTime,
endDate: state.dateFilter?.endTime,
), ),
const SizedBox( const SizedBox(
width: Dimens.pt8, width: Dimens.pt8,
), ),
PostedByFilterChip( DateTimeShortcutChip.weekBefore(
filter: state.params.get<PostedByFilter>(), onDateTimeRangeUpdated: context
onChanged: context
.read<SearchCubit>() .read<SearchCubit>()
.onPostedByChanged, .onDateTimeRangeUpdated,
startDate: state.dateFilter?.startTime,
endDate: state.dateFilter?.endTime,
), ),
const SizedBox( const SizedBox(
width: Dimens.pt8, width: Dimens.pt8,
), ),
CustomChip( DateTimeShortcutChip.weekAfter(
onSelected: (_) => onDateTimeRangeUpdated: context
context.read<SearchCubit>().onSortToggled(), .read<SearchCubit>()
selected: state.params.sorted, .onDateTimeRangeUpdated,
label: '''newest first''', startDate: state.dateFilter?.startTime,
endDate: state.dateFilter?.endTime,
), ),
const SizedBox( const SizedBox(
width: Dimens.pt8, width: Dimens.pt8,
), ),
for (final CustomDateTimeRange range DateTimeShortcutChip.monthBefore(
in CustomDateTimeRange.values) ...<Widget>[ onDateTimeRangeUpdated: context
CustomRangeFilterChip( .read<SearchCubit>()
range: range, .onDateTimeRangeUpdated,
onTap: context startDate: state.dateFilter?.startTime,
.read<SearchCubit>() endDate: state.dateFilter?.endTime,
.onDateTimeRangeUpdated, ),
), const SizedBox(
const SizedBox( width: Dimens.pt8,
width: Dimens.pt8, ),
), DateTimeShortcutChip.monthAfter(
], onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
startDate: state.dateFilter?.startTime,
endDate: state.dateFilter?.endTime,
),
], ],
), ),
), ),
SingleChildScrollView( ],
scrollDirection: Axis.horizontal, ),
child: Row( ),
children: <Widget>[ SingleChildScrollView(
for (final TypeTagFilter filter scrollDirection: Axis.horizontal,
in TypeTagFilter.all) ...<Widget>[ child: Row(
const SizedBox( children: <Widget>[
width: Dimens.pt8, const SizedBox(
), width: Dimens.pt8,
CustomChip(
onSelected: (_) => context
.read<SearchCubit>()
.onToggled(filter),
selected: context
.read<SearchCubit>()
.state
.params
.get<TypeTagFilter>() ==
filter,
label: filter.query,
),
],
],
),
), ),
DateTimeRangeFilterChip(
filter: state.dateFilter,
initialStartDate: state.dateFilter?.startTime,
initialEndDate: state.dateFilter?.endTime,
onDateTimeRangeUpdated: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
onDateTimeRangeRemoved: context
.read<SearchCubit>()
.removeFilter<DateTimeRangeFilter>,
),
const SizedBox(
width: Dimens.pt8,
),
PostedByFilterChip(
filter: state.params.get<PostedByFilter>(),
onChanged:
context.read<SearchCubit>().onPostedByChanged,
),
const SizedBox(
width: Dimens.pt8,
),
CustomChip(
onSelected: (_) =>
context.read<SearchCubit>().onSortToggled(),
selected: state.params.sorted,
label: '''newest first''',
),
const SizedBox(
width: Dimens.pt8,
),
for (final CustomDateTimeRange range
in CustomDateTimeRange.values) ...<Widget>[
CustomRangeFilterChip(
range: range,
onTap: context
.read<SearchCubit>()
.onDateTimeRangeUpdated,
),
const SizedBox(
width: Dimens.pt8,
),
],
],
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[
for (final TypeTagFilter filter
in TypeTagFilter.all) ...<Widget>[
const SizedBox(
width: Dimens.pt8,
),
CustomChip(
onSelected: (_) =>
context.read<SearchCubit>().onToggled(filter),
selected: context
.read<SearchCubit>()
.state
.params
.get<TypeTagFilter>() ==
filter,
label: filter.query,
),
],
], ],
), ),
), ),

View File

@ -68,8 +68,8 @@ class DateTimeShortcutChip extends StatelessWidget {
_calculator = ((DateTime date) => date.add(const Duration(days: 30))); _calculator = ((DateTime date) => date.add(const Duration(days: 30)));
final void Function(DateTime, DateTime) onDateTimeRangeUpdated; final void Function(DateTime, DateTime) onDateTimeRangeUpdated;
final DateTime startDate; final DateTime? startDate;
final DateTime endDate; final DateTime? endDate;
final String label; final String label;
final Calculator _calculator; final Calculator _calculator;
@ -77,8 +77,9 @@ class DateTimeShortcutChip extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomChip( return CustomChip(
onSelected: (bool value) { onSelected: (bool value) {
final DateTime updatedStartDate = _calculator(startDate); if (startDate == null || endDate == null) return;
final DateTime updatedEndDate = _calculator(endDate); final DateTime updatedStartDate = _calculator(startDate!);
final DateTime updatedEndDate = _calculator(endDate!);
onDateTimeRangeUpdated(updatedStartDate, updatedEndDate); onDateTimeRangeUpdated(updatedStartDate, updatedEndDate);
}, },
selected: false, selected: false,

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -9,6 +10,7 @@ import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class CommentTile extends StatelessWidget { class CommentTile extends StatelessWidget {
const CommentTile({ const CommentTile({
@ -23,6 +25,7 @@ class CommentTile extends StatelessWidget {
this.actionable = true, this.actionable = true,
this.level = 0, this.level = 0,
this.onTap, this.onTap,
this.itemScrollController,
}); });
final String? opUsername; final String? opUsername;
@ -30,6 +33,7 @@ class CommentTile extends StatelessWidget {
final int level; final int level;
final bool actionable; final bool actionable;
final FetchMode fetchMode; final FetchMode fetchMode;
final ItemScrollController? itemScrollController;
final void Function(Comment)? onReplyTapped; final void Function(Comment)? onReplyTapped;
final void Function(Comment, Rect?)? onMoreTapped; final void Function(Comment, Rect?)? onMoreTapped;
@ -116,8 +120,7 @@ class CommentTile extends StatelessWidget {
child: InkWell( child: InkWell(
onTap: () { onTap: () {
if (actionable) { if (actionable) {
HapticFeedbackUtil.selection(); _collapse(context);
context.read<CollapseCubit>().collapse();
} else { } else {
onTap?.call(); onTap?.call();
} }
@ -140,6 +143,8 @@ class CommentTile extends StatelessWidget {
? orange ? orange
: color, : color,
), ),
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
), ),
if (comment.by == opUsername) if (comment.by == opUsername)
const Text( const Text(
@ -154,12 +159,14 @@ class CommentTile extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
color: Palette.grey, color: Palette.grey,
), ),
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
), ),
], ],
), ),
), ),
AnimatedSize( AnimatedSize(
duration: const Duration(milliseconds: 200), duration: Durations.ms200,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@ -193,6 +200,8 @@ class CommentTile extends StatelessWidget {
child: ItemText( child: ItemText(
key: ValueKey<int>(comment.id), key: ValueKey<int>(comment.id),
item: comment, item: comment,
textScaleFactor: MediaQuery.of(context)
.textScaleFactor,
onTap: () { onTap: () {
if (onTap == null) { if (onTap == null) {
_onTextTapped(context); _onTextTapped(context);
@ -339,8 +348,27 @@ class CommentTile extends StatelessWidget {
void _onTextTapped(BuildContext context) { void _onTextTapped(BuildContext context) {
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) { if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
HapticFeedbackUtil.selection(); _collapse(context);
context.read<CollapseCubit>().collapse(); }
}
void _collapse(BuildContext context) {
HapticFeedbackUtil.selection();
context.read<CollapseCubit>().collapse();
if (context.read<CollapseCubit>().state.collapsed &&
context.read<PreferenceCubit>().state.autoScrollEnabled) {
Future<void>.delayed(
Durations.ms300,
() {
itemScrollController?.scrollTo(
index:
context.read<CommentsCubit>().state.comments.indexOf(comment) +
1,
alignment: 0.1,
duration: Durations.ms300,
);
},
);
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -66,7 +66,7 @@ class _CustomTabBarState extends State<CustomTabBar> {
currentIndex == i ? TextDimens.pt14 : TextDimens.pt10, currentIndex == i ? TextDimens.pt14 : TextDimens.pt10,
color: currentIndex == i ? Palette.orange : Palette.grey, color: currentIndex == i ? Palette.orange : Palette.grey,
), ),
duration: const Duration(milliseconds: 200), duration: Durations.ms200,
child: Text( child: Text(
state.tabs.elementAt(i).label, state.tabs.elementAt(i).label,
key: ValueKey<String>( key: ValueKey<String>(

View File

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

View File

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

View File

@ -233,7 +233,7 @@ class _LinkPreviewState extends State<LinkPreview> {
secondChild: loadedWidget, secondChild: loadedWidget,
crossFadeState: crossFadeState:
_loading ? CrossFadeState.showFirst : CrossFadeState.showSecond, _loading ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: const Duration(milliseconds: 500), duration: Durations.ms500,
); );
} }
} }

View File

@ -237,7 +237,7 @@ class LinkView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
SizedBox( SizedBox(
height: isUsingSerifFont! ? Dimens.pt2 : Dimens.pt4, height: isUsingSerifFont! ? Dimens.zero : Dimens.pt4,
), ),
Text( Text(
title, title,

View File

@ -15,7 +15,7 @@ class _OnboardingViewState extends State<OnboardingView> {
final PageController pageController = PageController(); final PageController pageController = PageController();
final Throttle throttle = Throttle(delay: _throttleDelay); final Throttle throttle = Throttle(delay: _throttleDelay);
static const Duration _throttleDelay = Duration(milliseconds: 100); static const Duration _throttleDelay = Durations.ms100;
static const double _screenshotHeight = 550; static const double _screenshotHeight = 550;
@override @override
@ -80,7 +80,7 @@ class _OnboardingViewState extends State<OnboardingView> {
} else { } else {
throttle.run(() { throttle.run(() {
pageController.nextPage( pageController.nextPage(
duration: const Duration(milliseconds: 600), duration: Durations.ms600,
curve: SpringCurve.underDamped, curve: SpringCurve.underDamped,
); );
}); });

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart';
class TapDownWrapper extends StatefulWidget { class TapDownWrapper extends StatefulWidget {
const TapDownWrapper({ const TapDownWrapper({
@ -22,7 +23,7 @@ class _TapDownWrapperState extends State<TapDownWrapper>
@override @override
void initState() { void initState() {
controller = AnimationController( controller = AnimationController(
duration: const Duration(milliseconds: 100), duration: Durations.ms100,
vsync: this, vsync: this,
); );

View File

@ -5,6 +5,7 @@ import 'dart:io';
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:fast_gbk/fast_gbk.dart'; import 'package:fast_gbk/fast_gbk.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -321,7 +322,7 @@ class WebAnalyzer {
final Uri uri = Uri.parse(url); final Uri uri = Uri.parse(url);
final HttpClient ioClient = HttpClient() final HttpClient ioClient = HttpClient()
..badCertificateCallback = _certificateCheck ..badCertificateCallback = _certificateCheck
..connectionTimeout = const Duration(seconds: 2); ..connectionTimeout = Durations.twoSeconds;
final IOClient client = IOClient(ioClient); final IOClient client = IOClient(ioClient);
final BaseRequest request = Request('GET', uri) final BaseRequest request = Request('GET', uri)
..followRedirects = true ..followRedirects = true
@ -337,7 +338,7 @@ class WebAnalyzer {
try { try {
final IOStreamedResponse stream = final IOStreamedResponse stream =
await client.send(request).timeout(const Duration(seconds: 10)); await client.send(request).timeout(Durations.tenSeconds);
if (stream.statusCode == HttpStatus.movedTemporarily || if (stream.statusCode == HttpStatus.movedTemporarily ||
stream.statusCode == HttpStatus.movedPermanently) { stream.statusCode == HttpStatus.movedPermanently) {

View File

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

View File

@ -2,7 +2,6 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart'; import 'package:hacki/main.dart';
@ -54,17 +53,7 @@ abstract class LinkUtil {
return; return;
} }
Uri rinseLink(String link) { final Uri uri = Uri.parse(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);
canLaunchUrl(uri).then((bool val) { canLaunchUrl(uri).then((bool val) {
if (val) { if (val) {
if (link.contains('http')) { if (link.contains('http')) {

View File

@ -1,5 +1,5 @@
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart'; 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 { abstract class LinkifierUtil {
static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false); static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false);

View File

@ -1,61 +1,92 @@
import 'dart:io'; import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
abstract class ThemeUtil { abstract class ThemeUtil {
/// Temp fix for the issue: static Future<void> updateStatusBarSetting(
/// https://github.com/flutter/flutter/issues/119465
static Future<void> updateAndroidStatusBarSetting(
Brightness brightness, Brightness brightness,
AdaptiveThemeMode? mode, AdaptiveThemeMode? mode,
) async { ) async {
if (Platform.isAndroid == false) return; if (Platform.isAndroid) {
switch (mode) {
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); case AdaptiveThemeMode.light:
final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo; SystemChrome.setSystemUIOverlayStyle(
final int sdk = androidInfo.version.sdkInt; const SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
if (sdk > 28) return; statusBarIconBrightness: Brightness.dark,
switch (mode) { statusBarColor: Palette.transparent,
case AdaptiveThemeMode.light: ),
SystemChrome.setSystemUIOverlayStyle( );
const SystemUiOverlayStyle( case AdaptiveThemeMode.dark:
statusBarBrightness: Brightness.dark, SystemChrome.setSystemUIOverlayStyle(
statusBarIconBrightness: Brightness.dark, const SystemUiOverlayStyle(
statusBarColor: Palette.transparent, statusBarBrightness: Brightness.light,
), statusBarIconBrightness: Brightness.light,
); statusBarColor: Palette.transparent,
case AdaptiveThemeMode.dark: ),
SystemChrome.setSystemUIOverlayStyle( );
const SystemUiOverlayStyle( case AdaptiveThemeMode.system:
statusBarBrightness: Brightness.light, case null:
statusBarIconBrightness: Brightness.light, switch (brightness) {
statusBarColor: Palette.transparent, case Brightness.light:
), SystemChrome.setSystemUIOverlayStyle(
); const SystemUiOverlayStyle(
case AdaptiveThemeMode.system: statusBarBrightness: Brightness.dark,
case null: statusBarIconBrightness: Brightness.dark,
switch (brightness) { statusBarColor: Palette.transparent,
case Brightness.light: ),
SystemChrome.setSystemUIOverlayStyle( );
const SystemUiOverlayStyle( case Brightness.dark:
statusBarBrightness: Brightness.dark, SystemChrome.setSystemUIOverlayStyle(
statusBarIconBrightness: Brightness.dark, const SystemUiOverlayStyle(
statusBarColor: Palette.transparent, statusBarBrightness: Brightness.light,
), statusBarIconBrightness: Brightness.light,
); statusBarColor: Palette.transparent,
case Brightness.dark: ),
SystemChrome.setSystemUIOverlayStyle( );
const SystemUiOverlayStyle( }
statusBarBrightness: Brightness.light, }
statusBarIconBrightness: Brightness.light, } else {
statusBarColor: Palette.transparent, switch (mode) {
), case AdaptiveThemeMode.light:
); SystemChrome.setSystemUIOverlayStyle(
} const SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
statusBarColor: Palette.transparent,
),
);
case AdaptiveThemeMode.dark:
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
statusBarColor: Palette.transparent,
),
);
case AdaptiveThemeMode.system:
case null:
switch (brightness) {
case Brightness.light:
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
statusBarColor: Palette.transparent,
),
);
case Brightness.dark:
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
statusBarColor: Palette.transparent,
),
);
}
}
} }
} }
} }

View File

@ -840,6 +840,30 @@ packages:
url: "https://github.com/livinglist/flutter_pulltorefresh" url: "https://github.com/livinglist/flutter_pulltorefresh"
source: git source: git
version: "2.0.0" 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: receive_sharing_intent:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1390,4 +1414,4 @@ packages:
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.0.0 <4.0.0" dart: ">=3.0.0 <4.0.0"
flutter: ">=3.10.2" flutter: ">=3.10.5"

View File

@ -1,11 +1,11 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 1.7.0+111 version: 1.8.1+117
publish_to: none publish_to: none
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
flutter: "3.10.2" flutter: "3.10.5"
dependencies: dependencies:
adaptive_theme: ^3.2.0 adaptive_theme: ^3.2.0
@ -58,6 +58,8 @@ dependencies:
git: git:
url: https://github.com/livinglist/flutter_pulltorefresh url: https://github.com/livinglist/flutter_pulltorefresh
ref: master ref: master
qr_code_scanner: ^1.0.1
qr_flutter: ^4.1.0
receive_sharing_intent: ^1.4.5 receive_sharing_intent: ^1.4.5
responsive_builder: ^0.7.0 responsive_builder: ^0.7.0
rxdart: ^0.27.7 rxdart: ^0.27.7