mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-14 01:58:29 +08:00
Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
ef557e7b84 | |||
ec065c0122 | |||
2960c6e59e | |||
92dac6b932 | |||
20365393a3 | |||
8d238744c7 | |||
e33ff417fb | |||
d8922c2641 |
@ -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
BIN
assets/hacki-github.xcf
Normal file
Binary file not shown.
BIN
assets/hacki.xcf
Normal file
BIN
assets/hacki.xcf
Normal file
Binary file not shown.
BIN
assets/screenshots/hacki-1.png
Normal file
BIN
assets/screenshots/hacki-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 890 KiB |
BIN
assets/screenshots/hacki-2.png
Normal file
BIN
assets/screenshots/hacki-2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 873 KiB |
BIN
assets/screenshots/hacki-3.png
Normal file
BIN
assets/screenshots/hacki-3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 770 KiB |
BIN
assets/screenshots/hacki-4.png
Normal file
BIN
assets/screenshots/hacki-4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 517 KiB |
@ -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();
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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.
|
||||||
|
@ -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]));
|
||||||
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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>(
|
||||||
|
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),
|
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);
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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
6
lib/models/status.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
enum Status {
|
||||||
|
idle,
|
||||||
|
loading,
|
||||||
|
loaded,
|
||||||
|
error,
|
||||||
|
}
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
),
|
),
|
||||||
|
@ -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() {
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
78
lib/screens/profile/qr_code_scanner_screen.dart
Normal file
78
lib/screens/profile/qr_code_scanner_screen.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
55
lib/screens/profile/qr_code_view_screen.dart
Normal file
55
lib/screens/profile/qr_code_view_screen.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export 'emphasis_linkifier.dart';
|
export 'emphasis_linkifier.dart';
|
||||||
export 'quote_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);
|
||||||
|
}
|
@ -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>(
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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')) {
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
pubspec.lock
26
pubspec.lock
@ -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"
|
||||||
|
@ -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
|
||||||
|
Submodule submodules/flutter updated: 9cd3d0d9ff...796c8ef792
Reference in New Issue
Block a user