mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-26 20:50:20 +08:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
f2bc48f980 | |||
d56697c57c | |||
320ec41aae | |||
d85b3535d5 | |||
f8cd1cbba0 | |||
817ec208d6 | |||
554a165789 | |||
0c680370ef | |||
59541d2fcc | |||
32083c3564 | |||
258dbc4b8b | |||
6c8047ebac | |||
00a0135867 | |||
1db7be7a2c | |||
ff400f9c40 | |||
f03b45a98a | |||
cbe5bba986 |
@ -141,7 +141,7 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||
flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
|
||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
||||
@ -166,4 +166,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
COCOAPODS: 1.13.0
|
||||
|
@ -1050,4 +1050,4 @@
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
}
|
@ -77,10 +77,8 @@
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
<key>FLTEnableWideGamut</key>
|
||||
<false/>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -34,15 +34,6 @@ abstract class Constants {
|
||||
static const String logFilename = 'hacki_log.txt';
|
||||
static const String previousLogFileName = 'old_hacki_log.txt';
|
||||
|
||||
/// Feature ids for feature discovery.
|
||||
static const String featureAddStoryToFavList = 'add_story_to_fav_list';
|
||||
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
|
||||
static const String featureLogIn = 'log_in';
|
||||
static const String featurePinToTop = 'pin_to_top';
|
||||
static const String featureJumpUpButton = 'jump_up_button_with_long_press';
|
||||
static const String featureJumpDownButton =
|
||||
'jump_down_button_with_long_press';
|
||||
|
||||
static final String happyFace = <String>[
|
||||
'(๑•̀ㅂ•́)و✧',
|
||||
'( ͡• ͜ʖ ͡•)',
|
||||
|
@ -1,12 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
|
||||
final GoRouter router = GoRouter(
|
||||
observers: <NavigatorObserver>[
|
||||
RouteObserver<ModalRoute<dynamic>>(),
|
||||
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
|
||||
],
|
||||
initialLocation: HomeScreen.routeName,
|
||||
routes: <RouteBase>[
|
||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
|
||||
@ -28,11 +29,12 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
collapsedCount: _collapseCache.totalHidden(_commentId),
|
||||
collapsed: _collapseCache.isCollapsed(_commentId),
|
||||
hidden: _collapseCache.isHidden(_commentId),
|
||||
locked: _collapseCache.lockedId == _commentId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void collapse() {
|
||||
void collapse({required VoidCallback onStateChanged}) {
|
||||
if (state.collapsed) {
|
||||
_collapseCache.uncollapse(_commentId);
|
||||
|
||||
@ -42,7 +44,14 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
collapsedCount: 0,
|
||||
),
|
||||
);
|
||||
|
||||
onStateChanged();
|
||||
} else {
|
||||
if (state.locked) {
|
||||
emit(state.copyWith(locked: false));
|
||||
return;
|
||||
}
|
||||
|
||||
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
|
||||
|
||||
emit(
|
||||
@ -51,6 +60,8 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length,
|
||||
),
|
||||
);
|
||||
|
||||
onStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,6 +96,13 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Prevent the item to be able to collapse, used when the comment
|
||||
/// text is selected.
|
||||
void lock() {
|
||||
_collapseCache.lockedId = _commentId;
|
||||
emit(state.copyWith(locked: true));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _streamSubscription.cancel();
|
||||
|
@ -4,26 +4,39 @@ class CollapseState extends Equatable {
|
||||
const CollapseState({
|
||||
required this.collapsed,
|
||||
required this.hidden,
|
||||
required this.locked,
|
||||
required this.collapsedCount,
|
||||
});
|
||||
|
||||
const CollapseState.init()
|
||||
: collapsed = false,
|
||||
hidden = false,
|
||||
locked = false,
|
||||
collapsedCount = 0;
|
||||
|
||||
final bool collapsed;
|
||||
|
||||
/// The value determining whether or not the comment should show up in the
|
||||
/// screen, this is true when the comment's parent is collapsed.
|
||||
final bool hidden;
|
||||
|
||||
/// The value determining whether or not the comment is collapsable.
|
||||
/// If [locked] is true then the comment is not collapsable and vice versa.
|
||||
final bool locked;
|
||||
|
||||
/// The number of children under this collapsed comment.
|
||||
final int collapsedCount;
|
||||
|
||||
CollapseState copyWith({
|
||||
bool? collapsed,
|
||||
bool? hidden,
|
||||
bool? locked,
|
||||
int? collapsedCount,
|
||||
}) {
|
||||
return CollapseState(
|
||||
collapsed: collapsed ?? this.collapsed,
|
||||
hidden: hidden ?? this.hidden,
|
||||
locked: locked ?? this.locked,
|
||||
collapsedCount: collapsedCount ?? this.collapsedCount,
|
||||
);
|
||||
}
|
||||
@ -32,6 +45,7 @@ class CollapseState extends Equatable {
|
||||
List<Object?> get props => <Object?>[
|
||||
collapsed,
|
||||
hidden,
|
||||
locked,
|
||||
collapsedCount,
|
||||
];
|
||||
}
|
||||
|
@ -322,7 +322,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
}
|
||||
|
||||
void onOrderChanged(CommentsOrder? order) {
|
||||
void updateOrder(CommentsOrder? order) {
|
||||
if (order == null) return;
|
||||
if (state.order == order) return;
|
||||
HapticFeedbackUtil.selection();
|
||||
@ -335,7 +335,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
init(useCommentCache: true);
|
||||
}
|
||||
|
||||
void onFetchModeChanged(FetchMode? fetchMode) {
|
||||
void updateFetchMode(FetchMode? fetchMode) {
|
||||
if (fetchMode == null) return;
|
||||
if (state.fetchMode == fetchMode) return;
|
||||
_collapseCache.resetCollapsedComments();
|
||||
|
@ -18,6 +18,8 @@ class EditCubit extends HydratedCubit<EditState> {
|
||||
final DraftCache _draftCache;
|
||||
final Debouncer _debouncer;
|
||||
|
||||
void reset() => emit(const EditState.init());
|
||||
|
||||
void onReplyTapped(Item item) {
|
||||
emit(
|
||||
EditState(
|
||||
@ -36,14 +38,6 @@ class EditCubit extends HydratedCubit<EditState> {
|
||||
);
|
||||
}
|
||||
|
||||
void onReplyBoxClosed() {
|
||||
emit(const EditState.init());
|
||||
}
|
||||
|
||||
void onScrolled() {
|
||||
emit(const EditState.init());
|
||||
}
|
||||
|
||||
void onReplySubmittedSuccessfully() {
|
||||
if (state.replyingTo != null) {
|
||||
_draftCache.removeDraft(replyingTo: state.replyingTo!.id);
|
||||
@ -65,9 +59,14 @@ class EditCubit extends HydratedCubit<EditState> {
|
||||
}
|
||||
}
|
||||
|
||||
void deleteDraft() => clear();
|
||||
|
||||
bool called = false;
|
||||
void deleteDraft() {
|
||||
// Remove draft in storage.
|
||||
clear();
|
||||
// Reset cached state.
|
||||
_cachedState = const EditState.init();
|
||||
// Reset to init state;
|
||||
reset();
|
||||
}
|
||||
|
||||
@override
|
||||
EditState? fromJson(Map<String, dynamic> json) {
|
||||
@ -96,6 +95,7 @@ class EditCubit extends HydratedCubit<EditState> {
|
||||
Map<String, dynamic>? toJson(EditState state) {
|
||||
EditState selected = state;
|
||||
|
||||
// Override previous draft only when current draft is not empty.
|
||||
if (state.replyingTo == null ||
|
||||
(state.replyingTo?.id != _cachedState.replyingTo?.id &&
|
||||
state.text.isNullOrEmpty)) {
|
||||
|
@ -16,6 +16,7 @@ class PostCubit extends Cubit<PostState> {
|
||||
|
||||
Future<void> post({required String text, required int to}) async {
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
final bool successful = await _postRepository.comment(
|
||||
parentId: to,
|
||||
text: text,
|
||||
@ -42,4 +43,13 @@ class PostCubit extends Cubit<PostState> {
|
||||
void reset() {
|
||||
emit(state.copyWith(status: Status.idle));
|
||||
}
|
||||
|
||||
@Deprecated('For debugging only')
|
||||
Future<bool> getFakeResult() async {
|
||||
final bool result = await Future<bool>.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() => true,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
export 'context_extension.dart';
|
||||
export 'date_time_extension.dart';
|
||||
export 'int_extension.dart';
|
||||
export 'item_action_mixin.dart';
|
||||
export 'list_extension.dart';
|
||||
export 'object_extension.dart';
|
||||
export 'set_extension.dart';
|
||||
export 'state_extension.dart';
|
||||
export 'string_extension.dart';
|
||||
export 'widget_extension.dart';
|
||||
|
@ -12,7 +12,8 @@ import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
extension StateExtension on State {
|
||||
@optionalTypeArgs
|
||||
mixin ItemActionMixin<T extends StatefulWidget> on State<T> {
|
||||
void showSnackBar({
|
||||
required String content,
|
||||
VoidCallback? action,
|
||||
@ -36,10 +37,7 @@ extension StateExtension on State {
|
||||
if (splitViewEnabled && !forceNewScreen) {
|
||||
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
||||
} else {
|
||||
context.push(
|
||||
'/${ItemScreen.routeName}',
|
||||
extra: args,
|
||||
);
|
||||
context.push('/${ItemScreen.routeName}', extra: args);
|
||||
}
|
||||
|
||||
return Future<void>.value();
|
@ -2,21 +2,19 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
|
||||
extension WidgetModifier on Widget {
|
||||
Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
|
||||
return Padding(
|
||||
padding: value,
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
extension ContextMenuBuilder on Widget {
|
||||
Widget contextMenuBuilder(
|
||||
BuildContext context,
|
||||
EditableTextState editableTextState, {
|
||||
required Item item,
|
||||
}) {
|
||||
if (item is! Buildable) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final int start = editableTextState.textEditingValue.selection.base.offset;
|
||||
final int end = editableTextState.textEditingValue.selection.end;
|
||||
|
||||
@ -25,28 +23,11 @@ extension WidgetModifier on Widget {
|
||||
];
|
||||
|
||||
if (start != -1 && end != -1) {
|
||||
String selectedText = item.text.substring(start, end);
|
||||
|
||||
if (item is Buildable) {
|
||||
final Iterable<EmphasisElement> emphasisElements =
|
||||
(item as Buildable).elements.whereType<EmphasisElement>();
|
||||
|
||||
int count = 1;
|
||||
while (selectedText.contains(' ') && count <= emphasisElements.length) {
|
||||
final int s = (start + count * 2).clamp(0, item.text.length);
|
||||
final int e = (end + count * 2).clamp(0, item.text.length);
|
||||
selectedText = item.text.substring(s, e);
|
||||
count++;
|
||||
}
|
||||
|
||||
count = 1;
|
||||
while (selectedText.contains(' ') && count <= emphasisElements.length) {
|
||||
final int s = (start - count * 2).clamp(0, item.text.length);
|
||||
final int e = (end - count * 2).clamp(0, item.text.length);
|
||||
selectedText = item.text.substring(s, e);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
final String text = (item as Buildable)
|
||||
.elements
|
||||
.map((LinkifyElement e) => e.text)
|
||||
.reduce((String value, String e) => '$value$e');
|
||||
final String selectedText = text.substring(start, end);
|
||||
|
||||
items.addAll(<ContextMenuButtonItem>[
|
||||
ContextMenuButtonItem(
|
||||
@ -70,3 +51,14 @@ extension WidgetModifier on Widget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension WidgetModifier on Widget {
|
||||
Widget padded([
|
||||
EdgeInsetsGeometry value = const EdgeInsets.all(Dimens.pt12),
|
||||
]) {
|
||||
return Padding(
|
||||
padding: value,
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/custom_router.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
@ -26,6 +27,7 @@ import 'package:logger/logger.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
|
||||
// For receiving payload event from local notifications.
|
||||
@ -138,11 +140,13 @@ Future<void> main({bool testing = false}) async {
|
||||
prefs.getInt(FontPreference().key) ?? Font.roboto.index,
|
||||
);
|
||||
|
||||
//Uncomment this line to log events from bloc/cubit.
|
||||
//Bloc.observer = CustomBlocObserver();
|
||||
// Uncomment this line to log events from bloc/cubit.
|
||||
// Bloc.observer = CustomBlocObserver();
|
||||
|
||||
HydratedBloc.storage = storage;
|
||||
|
||||
VisibilityDetectorController.instance.updateInterval = Durations.ms200;
|
||||
|
||||
runApp(
|
||||
HackiApp(
|
||||
savedThemeMode: savedThemeMode,
|
||||
|
47
lib/models/discoverable_feature.dart
Normal file
47
lib/models/discoverable_feature.dart
Normal file
@ -0,0 +1,47 @@
|
||||
enum DiscoverableFeature {
|
||||
addStoryToFavList(
|
||||
featureId: 'add_story_to_fav_list',
|
||||
title: 'Fav a Story',
|
||||
description: '''Add it to your favorites''',
|
||||
),
|
||||
openStoryInWebView(
|
||||
featureId: 'open_story_in_web_view',
|
||||
title: 'Open in Browser',
|
||||
description: '''You can tap here to open this story in browser.''',
|
||||
),
|
||||
login(
|
||||
featureId: 'log_in',
|
||||
title: 'Log in for more',
|
||||
description:
|
||||
'''Log in using your Hacker News account to check out stories and comments you have posted in the past, and get in-app notification when there is new reply to your comments or stories.''',
|
||||
),
|
||||
pinToTop(
|
||||
featureId: 'pin_to_top',
|
||||
title: 'Pin a Story',
|
||||
description:
|
||||
'''Pin this story to the top of your home screen so that you can come back later.''',
|
||||
),
|
||||
jumpUpButton(
|
||||
featureId: 'jump_up_button_with_long_press',
|
||||
title: 'Shortcut',
|
||||
description:
|
||||
'''Tapping on this button will take you to the previous off-screen root level comment.\n\nLong press on it to jump to the very beginning of this thread.''',
|
||||
),
|
||||
jumpDownButton(
|
||||
featureId: 'jump_down_button_with_long_press',
|
||||
title: 'Shortcut',
|
||||
description:
|
||||
'''Tapping on this button will take you to the next off-screen root level comment.\n\nLong press on it to jump to the end of this thread.''',
|
||||
);
|
||||
|
||||
const DiscoverableFeature({
|
||||
required this.featureId,
|
||||
required this.title,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
/// Feature ids for feature discovery.
|
||||
final String featureId;
|
||||
final String title;
|
||||
final String description;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export 'comments_order.dart';
|
||||
export 'discoverable_feature.dart';
|
||||
export 'export_destination.dart';
|
||||
export 'fetch_mode.dart';
|
||||
export 'font.dart';
|
||||
|
@ -36,7 +36,7 @@ class HomeScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen>
|
||||
with SingleTickerProviderStateMixin, RouteAware {
|
||||
with SingleTickerProviderStateMixin, RouteAware, ItemActionMixin {
|
||||
late final TabController tabController;
|
||||
late final StreamSubscription<String> intentDataStreamSubscription;
|
||||
late final StreamSubscription<String?> notificationStreamSubscription;
|
||||
@ -61,14 +61,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// This is for testing only.
|
||||
// FeatureDiscovery.clearPreferences(context, <String>[
|
||||
// Constants.featureLogIn,
|
||||
// Constants.featureAddStoryToFavList,
|
||||
// Constants.featureOpenStoryInWebView,
|
||||
// Constants.featurePinToTop,
|
||||
// ]);
|
||||
|
||||
ReceiveSharingIntent.getInitialText().then(onShareExtensionTapped);
|
||||
|
||||
intentDataStreamSubscription =
|
||||
@ -89,7 +81,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
FeatureDiscovery.discoverFeatures(
|
||||
context,
|
||||
<String>{
|
||||
Constants.featureLogIn,
|
||||
DiscoverableFeature.login.featureId,
|
||||
},
|
||||
);
|
||||
})
|
||||
@ -316,4 +308,14 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('For debugging only')
|
||||
void clearFeatureDiscoveryPreferences(BuildContext context) {
|
||||
FeatureDiscovery.clearPreferences(context, <String>[
|
||||
DiscoverableFeature.login.featureId,
|
||||
DiscoverableFeature.addStoryToFavList.featureId,
|
||||
DiscoverableFeature.openStoryInWebView.featureId,
|
||||
DiscoverableFeature.pinToTop.featureId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ class _TabletStoryView extends StatelessWidget {
|
||||
child: ColoredBox(
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: const Center(
|
||||
child: Text('Tap on story tile to view comments.'),
|
||||
child: Text('Tap on story tile to view its comments.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -30,7 +32,7 @@ class ItemScreenArgs extends Equatable {
|
||||
final bool onlyShowTargetComment;
|
||||
final List<Comment>? targetComments;
|
||||
|
||||
/// when a user is trying to view a sub-thread from a main thread, we don't
|
||||
/// when the user is trying to view a sub-thread from a main thread, we don't
|
||||
/// need to fetch comments from [StoriesRepository] since we have some, if not
|
||||
/// all, comments cached in [CommentCache].
|
||||
final bool useCommentCache;
|
||||
@ -135,12 +137,16 @@ class ItemScreen extends StatefulWidget {
|
||||
_ItemScreenState createState() => _ItemScreenState();
|
||||
}
|
||||
|
||||
class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
class _ItemScreenState extends State<ItemScreen>
|
||||
with RouteAware, ItemActionMixin {
|
||||
final TextEditingController commentEditingController =
|
||||
TextEditingController();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final ItemScrollController itemScrollController = ItemScrollController();
|
||||
final ItemPositionsListener itemPositionsListener =
|
||||
ItemPositionsListener.create();
|
||||
final ScrollOffsetListener scrollOffsetListener =
|
||||
ScrollOffsetListener.create();
|
||||
final Throttle storyLinkTapThrottle = Throttle(
|
||||
delay: _storyLinkTapThrottleDelay,
|
||||
);
|
||||
@ -148,6 +154,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
delay: _featureDiscoveryDismissThrottleDelay,
|
||||
);
|
||||
final GlobalKey fontSizeIconButtonKey = GlobalKey();
|
||||
StreamSubscription<double>? scrollOffsetSubscription;
|
||||
|
||||
static const Duration _storyLinkTapThrottleDelay = Durations.twoSeconds;
|
||||
static const Duration _featureDiscoveryDismissThrottleDelay =
|
||||
@ -157,10 +164,16 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
void didPop() {
|
||||
super.didPop();
|
||||
if (context.read<EditCubit>().state.text.isNullOrEmpty) {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
context.read<EditCubit>().reset();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didPushNext() {
|
||||
super.didPushNext();
|
||||
focusNode.unfocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -169,11 +182,11 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
FeatureDiscovery.discoverFeatures(
|
||||
context,
|
||||
<String>{
|
||||
Constants.featurePinToTop,
|
||||
Constants.featureAddStoryToFavList,
|
||||
Constants.featureOpenStoryInWebView,
|
||||
Constants.featureJumpUpButton,
|
||||
Constants.featureJumpDownButton,
|
||||
DiscoverableFeature.pinToTop.featureId,
|
||||
DiscoverableFeature.addStoryToFavList.featureId,
|
||||
DiscoverableFeature.openStoryInWebView.featureId,
|
||||
DiscoverableFeature.jumpUpButton.featureId,
|
||||
DiscoverableFeature.jumpDownButton.featureId,
|
||||
},
|
||||
);
|
||||
})
|
||||
@ -187,6 +200,9 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
.subscribe(this, route);
|
||||
});
|
||||
|
||||
scrollOffsetSubscription =
|
||||
scrollOffsetListener.changes.listen(removeReplyBoxFocusOnScroll);
|
||||
|
||||
commentEditingController.text = context.read<EditCubit>().state.text ?? '';
|
||||
}
|
||||
|
||||
@ -195,6 +211,8 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
commentEditingController.dispose();
|
||||
storyLinkTapThrottle.dispose();
|
||||
featureDiscoveryDismissThrottle.dispose();
|
||||
focusNode.dispose();
|
||||
scrollOffsetSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -209,7 +227,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
BlocListener<PostCubit, PostState>(
|
||||
listener: (BuildContext context, PostState postState) {
|
||||
if (postState.status == Status.success) {
|
||||
context.pop();
|
||||
final String verb =
|
||||
context.read<EditCubit>().state.replyingTo == null
|
||||
? 'updated'
|
||||
@ -220,7 +237,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
context.read<EditCubit>().onReplySubmittedSuccessfully();
|
||||
context.read<PostCubit>().reset();
|
||||
} else if (postState.status == Status.failure) {
|
||||
context.pop();
|
||||
showErrorSnackBar();
|
||||
context.read<PostCubit>().reset();
|
||||
}
|
||||
@ -258,13 +274,13 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
child: MainView(
|
||||
itemScrollController: itemScrollController,
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
scrollOffsetListener: scrollOffsetListener,
|
||||
commentEditingController: commentEditingController,
|
||||
authState: authState,
|
||||
topPadding: topPadding,
|
||||
splitViewEnabled: widget.splitViewEnabled,
|
||||
onMoreTapped: onMoreTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
onReplyTapped: showReplyBox,
|
||||
),
|
||||
),
|
||||
BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||
@ -303,6 +319,18 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: Dimens.zero,
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: ReplyBox(
|
||||
splitViewEnabled: true,
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onChanged: context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
@ -319,18 +347,24 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
body: MainView(
|
||||
itemScrollController: itemScrollController,
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
scrollOffsetListener: scrollOffsetListener,
|
||||
commentEditingController: commentEditingController,
|
||||
authState: authState,
|
||||
topPadding: topPadding,
|
||||
splitViewEnabled: widget.splitViewEnabled,
|
||||
onMoreTapped: onMoreTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
onReplyTapped: showReplyBox,
|
||||
),
|
||||
floatingActionButton: CustomFloatingActionButton(
|
||||
itemScrollController: itemScrollController,
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
),
|
||||
bottomSheet: ReplyBox(
|
||||
textEditingController: commentEditingController,
|
||||
focusNode: focusNode,
|
||||
onSendTapped: onSendTapped,
|
||||
onChanged: context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -338,31 +372,11 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
);
|
||||
}
|
||||
|
||||
void showReplyBox() {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
enableDrag: false,
|
||||
builder: (BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ReplyBox(
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
},
|
||||
onChanged: context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
void removeReplyBoxFocusOnScroll(double _) {
|
||||
focusNode.unfocus();
|
||||
if (commentEditingController.text.isEmpty) {
|
||||
context.read<EditCubit>().reset();
|
||||
}
|
||||
}
|
||||
|
||||
void onFontSizeTapped() {
|
||||
|
@ -3,8 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/screens/widgets/custom_described_feature_overlay.dart';
|
||||
import 'package:hacki/styles/palette.dart';
|
||||
import 'package:hacki/models/discoverable_feature.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
@ -22,80 +23,88 @@ class CustomFloatingActionButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CommentsCubit, CommentsState>(
|
||||
builder: (BuildContext context, CommentsState state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CustomDescribedFeatureOverlay(
|
||||
featureId: Constants.featureJumpUpButton,
|
||||
contentLocation: ContentLocation.above,
|
||||
tapTarget: const Icon(
|
||||
Icons.keyboard_arrow_up,
|
||||
color: Palette.white,
|
||||
),
|
||||
title: const Text('Shortcut'),
|
||||
description: const Text(
|
||||
'''Tapping on this button will take you to the previous off-screen root level comment.\n\nLong press on it to jump to the very beginning of this thread.''',
|
||||
),
|
||||
child: InkWell(
|
||||
onLongPress: () => itemScrollController.scrollTo(
|
||||
index: 0,
|
||||
duration: Durations.ms400,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
return BlocBuilder<EditCubit, EditState>(
|
||||
buildWhen: (EditState previous, EditState current) =>
|
||||
previous.showReplyBox != current.showReplyBox,
|
||||
builder: (BuildContext context, EditState editState) {
|
||||
return AnimatedPadding(
|
||||
padding: editState.showReplyBox
|
||||
? const EdgeInsets.only(
|
||||
bottom: Dimens.replyBoxCollapsedHeight,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
duration: Durations.ms200,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CustomDescribedFeatureOverlay(
|
||||
feature: DiscoverableFeature.jumpUpButton,
|
||||
contentLocation: ContentLocation.above,
|
||||
tapTarget: const Icon(
|
||||
Icons.keyboard_arrow_up,
|
||||
color: Palette.white,
|
||||
),
|
||||
child: InkWell(
|
||||
onLongPress: () => itemScrollController.scrollTo(
|
||||
index: 0,
|
||||
duration: Durations.ms400,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
|
||||
/// Randomly generated string as heroTag to prevent
|
||||
/// default [FloatingActionButton] animation.
|
||||
heroTag: UniqueKey().hashCode,
|
||||
onPressed: () {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<CommentsCubit>().scrollToPreviousRoot(
|
||||
itemScrollController,
|
||||
itemPositionsListener,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_up,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
/// Randomly generated string as heroTag to prevent
|
||||
/// default [FloatingActionButton] animation.
|
||||
heroTag: UniqueKey().hashCode,
|
||||
onPressed: () {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<CommentsCubit>().scrollToPreviousRoot(
|
||||
itemScrollController,
|
||||
itemPositionsListener,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_up,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomDescribedFeatureOverlay(
|
||||
featureId: Constants.featureJumpDownButton,
|
||||
tapTarget: const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Palette.white,
|
||||
),
|
||||
title: const Text('Shortcut'),
|
||||
description: const Text(
|
||||
'''Tapping on this button will take you to the next off-screen root level comment.\n\nLong press on it to jump to the end of this thread.''',
|
||||
),
|
||||
child: InkWell(
|
||||
onLongPress: () => itemScrollController.scrollTo(
|
||||
index: state.comments.length,
|
||||
duration: Durations.ms400,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
CustomDescribedFeatureOverlay(
|
||||
feature: DiscoverableFeature.jumpDownButton,
|
||||
tapTarget: const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Palette.white,
|
||||
),
|
||||
child: InkWell(
|
||||
onLongPress: () => itemScrollController.scrollTo(
|
||||
index: state.comments.length,
|
||||
duration: Durations.ms400,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
|
||||
/// Same as above.
|
||||
heroTag: UniqueKey().hashCode,
|
||||
onPressed: () {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<CommentsCubit>().scrollToNextRoot(
|
||||
itemScrollController,
|
||||
itemPositionsListener,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
/// Same as above.
|
||||
heroTag: UniqueKey().hashCode,
|
||||
onPressed: () {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<CommentsCubit>().scrollToNextRoot(
|
||||
itemScrollController,
|
||||
itemPositionsListener,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/discoverable_feature.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
@ -26,12 +26,7 @@ class FavIconButton extends StatelessWidget {
|
||||
isFav ? Icons.favorite : Icons.favorite_border,
|
||||
color: Palette.white,
|
||||
),
|
||||
featureId: Constants.featureAddStoryToFavList,
|
||||
title: const Text('Fav a Story'),
|
||||
description: const Text(
|
||||
'Add it to your favorites.',
|
||||
style: TextStyle(fontSize: TextDimens.pt16),
|
||||
),
|
||||
feature: DiscoverableFeature.addStoryToFavList,
|
||||
child: Icon(
|
||||
isFav ? Icons.favorite : Icons.favorite_border,
|
||||
color: isFav ? Palette.orange : Theme.of(context).iconTheme.color,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/models/discoverable_feature.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
@ -21,12 +21,7 @@ class LinkIconButton extends StatelessWidget {
|
||||
Icons.stream,
|
||||
color: Palette.white,
|
||||
),
|
||||
featureId: Constants.featureOpenStoryInWebView,
|
||||
title: Text('Open in Browser'),
|
||||
description: Text(
|
||||
'''You can tap here to open this story in browser.''',
|
||||
style: TextStyle(fontSize: TextDimens.pt16),
|
||||
),
|
||||
feature: DiscoverableFeature.openStoryInWebView,
|
||||
child: Icon(
|
||||
Icons.stream,
|
||||
),
|
||||
|
@ -16,7 +16,7 @@ class LoginDialog extends StatefulWidget {
|
||||
State<LoginDialog> createState() => _LoginDialogState();
|
||||
}
|
||||
|
||||
class _LoginDialogState extends State<LoginDialog> {
|
||||
class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
|
||||
final TextEditingController usernameController = TextEditingController();
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
@ -19,25 +20,25 @@ class MainView extends StatelessWidget {
|
||||
const MainView({
|
||||
required this.itemScrollController,
|
||||
required this.itemPositionsListener,
|
||||
required this.scrollOffsetListener,
|
||||
required this.commentEditingController,
|
||||
required this.authState,
|
||||
required this.topPadding,
|
||||
required this.splitViewEnabled,
|
||||
required this.onMoreTapped,
|
||||
required this.onRightMoreTapped,
|
||||
required this.onReplyTapped,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ItemScrollController itemScrollController;
|
||||
final ItemPositionsListener itemPositionsListener;
|
||||
final ScrollOffsetListener scrollOffsetListener;
|
||||
final TextEditingController commentEditingController;
|
||||
final AuthState authState;
|
||||
final double topPadding;
|
||||
final bool splitViewEnabled;
|
||||
final void Function(Item item, Rect? rect) onMoreTapped;
|
||||
final ValueChanged<Comment> onRightMoreTapped;
|
||||
final VoidCallback onReplyTapped;
|
||||
|
||||
static const int _loadingIndicatorOpacityAnimationDuration = 300;
|
||||
static const double _trailingBoxHeight = 240;
|
||||
@ -49,101 +50,90 @@ class MainView extends StatelessWidget {
|
||||
Positioned.fill(
|
||||
child: BlocBuilder<CommentsCubit, CommentsState>(
|
||||
builder: (BuildContext context, CommentsState state) {
|
||||
return Scrollbar(
|
||||
interactive: true,
|
||||
child: RefreshIndicator(
|
||||
displacement: 100,
|
||||
onRefresh: () async {
|
||||
HapticFeedbackUtil.light();
|
||||
return RefreshIndicator(
|
||||
displacement: 100,
|
||||
onRefresh: () async {
|
||||
HapticFeedbackUtil.light();
|
||||
|
||||
if (context.read<StoriesBloc>().state.isOfflineReading ==
|
||||
false &&
|
||||
state.onlyShowTargetComment == false) {
|
||||
unawaited(context.read<CommentsCubit>().refresh());
|
||||
if (context.read<StoriesBloc>().state.isOfflineReading ==
|
||||
false &&
|
||||
state.onlyShowTargetComment == false) {
|
||||
unawaited(context.read<CommentsCubit>().refresh());
|
||||
|
||||
if (state.item.isPoll) {
|
||||
context.read<PollCubit>().refresh();
|
||||
if (state.item.isPoll) {
|
||||
context.read<PollCubit>().refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: ScrollablePositionedList.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemScrollController: itemScrollController,
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
itemCount: state.comments.length + 2,
|
||||
padding: EdgeInsets.only(top: topPadding),
|
||||
scrollOffsetListener: scrollOffsetListener,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == 0) {
|
||||
return _ParentItemSection(
|
||||
commentEditingController: commentEditingController,
|
||||
state: state,
|
||||
authState: authState,
|
||||
topPadding: topPadding,
|
||||
splitViewEnabled: splitViewEnabled,
|
||||
onMoreTapped: onMoreTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
);
|
||||
} else if (index == state.comments.length + 1) {
|
||||
if ((state.status == CommentsStatus.allLoaded &&
|
||||
state.comments.isNotEmpty) ||
|
||||
state.onlyShowTargetComment) {
|
||||
return SizedBox(
|
||||
height: _trailingBoxHeight,
|
||||
child: Center(
|
||||
child: Text(Constants.happyFace),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: ScrollablePositionedList.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemScrollController: itemScrollController,
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
itemCount: state.comments.length + 2,
|
||||
padding: EdgeInsets.only(top: topPadding),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == 0) {
|
||||
return _ParentItemSection(
|
||||
commentEditingController: commentEditingController,
|
||||
state: state,
|
||||
authState: authState,
|
||||
topPadding: topPadding,
|
||||
splitViewEnabled: splitViewEnabled,
|
||||
onMoreTapped: onMoreTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
onReplyTapped: onReplyTapped,
|
||||
);
|
||||
} else if (index == state.comments.length + 1) {
|
||||
if ((state.status == CommentsStatus.allLoaded &&
|
||||
state.comments.isNotEmpty) ||
|
||||
state.onlyShowTargetComment) {
|
||||
return SizedBox(
|
||||
height: _trailingBoxHeight,
|
||||
child: Center(
|
||||
child: Text(Constants.happyFace),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
index = index - 1;
|
||||
final Comment comment = state.comments.elementAt(index);
|
||||
return FadeIn(
|
||||
key: ValueKey<String>('${comment.id}-FadeIn'),
|
||||
child: CommentTile(
|
||||
comment: comment,
|
||||
level: comment.level,
|
||||
opUsername: state.item.by,
|
||||
fetchMode: state.fetchMode,
|
||||
onReplyTapped: (Comment cmt) {
|
||||
HapticFeedbackUtil.light();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
index = index - 1;
|
||||
final Comment comment = state.comments.elementAt(index);
|
||||
return FadeIn(
|
||||
key: ValueKey<String>('${comment.id}-FadeIn'),
|
||||
child: CommentTile(
|
||||
comment: comment,
|
||||
level: comment.level,
|
||||
opUsername: state.item.by,
|
||||
fetchMode: state.fetchMode,
|
||||
onReplyTapped: (Comment cmt) {
|
||||
HapticFeedbackUtil.light();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmt.id !=
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.state
|
||||
.replyingTo
|
||||
?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
|
||||
context.read<EditCubit>().onReplyTapped(cmt);
|
||||
|
||||
onReplyTapped();
|
||||
},
|
||||
onEditTapped: (Comment cmt) {
|
||||
HapticFeedbackUtil.light();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
if (cmt.id !=
|
||||
context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
context.read<EditCubit>().onEditTapped(cmt);
|
||||
}
|
||||
|
||||
onReplyTapped();
|
||||
},
|
||||
onMoreTapped: onMoreTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
itemScrollController: itemScrollController,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
context.read<EditCubit>().onReplyTapped(cmt);
|
||||
},
|
||||
onEditTapped: (Comment cmt) {
|
||||
HapticFeedbackUtil.light();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
commentEditingController.clear();
|
||||
context.read<EditCubit>().onEditTapped(cmt);
|
||||
},
|
||||
onMoreTapped: onMoreTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
itemScrollController: itemScrollController,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -184,7 +174,6 @@ class _ParentItemSection extends StatelessWidget {
|
||||
required this.splitViewEnabled,
|
||||
required this.onMoreTapped,
|
||||
required this.onRightMoreTapped,
|
||||
required this.onReplyTapped,
|
||||
});
|
||||
|
||||
final TextEditingController commentEditingController;
|
||||
@ -194,16 +183,16 @@ class _ParentItemSection extends StatelessWidget {
|
||||
final bool splitViewEnabled;
|
||||
final void Function(Item item, Rect? rect) onMoreTapped;
|
||||
final ValueChanged<Comment> onRightMoreTapped;
|
||||
final VoidCallback onReplyTapped;
|
||||
|
||||
static const double _viewParentButtonWidth = 100;
|
||||
static const double _viewRootButtonWidth = 80;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Item item = state.item;
|
||||
return Semantics(
|
||||
label:
|
||||
'''Posted by ${state.item.by} ${state.item.timeAgo}, ${state.item.title}. ${state.item.text}''',
|
||||
'''Posted by ${item.by} ${item.timeAgo}, ${item.title}. ${item.text}''',
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (!splitViewEnabled)
|
||||
@ -220,13 +209,11 @@ class _ParentItemSection extends StatelessWidget {
|
||||
onPressed: (_) {
|
||||
HapticFeedbackUtil.light();
|
||||
|
||||
if (state.item.id !=
|
||||
if (item.id !=
|
||||
context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
context.read<EditCubit>().onReplyTapped(state.item);
|
||||
|
||||
onReplyTapped();
|
||||
context.read<EditCubit>().onReplyTapped(item);
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
@ -234,7 +221,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(state.item, context.rect),
|
||||
onMoreTapped(item, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
@ -251,7 +238,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.item.by,
|
||||
item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
@ -260,7 +247,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.timeAgo,
|
||||
item.timeAgo,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
@ -280,12 +267,13 @@ class _ParentItemSection extends StatelessWidget {
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
final double fontSize = prefState.fontSize.fontSize;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (state.item is Story)
|
||||
if (item is Story)
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launch(
|
||||
state.item.url,
|
||||
item.url,
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
@ -295,6 +283,13 @@ class _ParentItemSection extends StatelessWidget {
|
||||
.state
|
||||
.isOfflineReading,
|
||||
),
|
||||
onLongPress: () => FlutterClipboard.copy(item.url)
|
||||
.whenComplete(() {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.showSnackBar(
|
||||
content: 'Link copied.',
|
||||
);
|
||||
}),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
@ -306,7 +301,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
fontSize: fontSize,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
@ -314,24 +309,22 @@ class _ParentItemSection extends StatelessWidget {
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
semanticsLabel: state.item.title,
|
||||
text: state.item.title,
|
||||
semanticsLabel: item.title,
|
||||
text: item.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: state.item.url.isNotEmpty
|
||||
fontSize: fontSize,
|
||||
color: item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (state.item.url.isNotEmpty)
|
||||
if (item.url.isNotEmpty)
|
||||
TextSpan(
|
||||
text:
|
||||
''' (${(state.item as Story).readableUrl})''',
|
||||
text: ''' (${item.readableUrl})''',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize:
|
||||
prefState.fontSize.fontSize - 4,
|
||||
fontSize: fontSize - 4,
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
@ -347,18 +340,19 @@ class _ParentItemSection extends StatelessWidget {
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
if (item.text.isNotEmpty)
|
||||
FadeIn(
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt8,
|
||||
),
|
||||
child: ItemText(
|
||||
item: state.item,
|
||||
item: item,
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
selectable: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -367,28 +361,27 @@ class _ParentItemSection extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
if (state.item.isPoll)
|
||||
if (item is Story && item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)..init(),
|
||||
PollCubit(story: item)..init(),
|
||||
child: const PollView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
if (item.text.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
if (state.onlyShowTargetComment) ...<Widget>[
|
||||
if (state.onlyShowTargetComment && item is Story) ...<Widget>[
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () =>
|
||||
context.read<CommentsCubit>().loadAll(state.item as Story),
|
||||
onPressed: () => context.read<CommentsCubit>().loadAll(item),
|
||||
child: const Text('View all comments'),
|
||||
),
|
||||
),
|
||||
@ -398,12 +391,12 @@ class _ParentItemSection extends StatelessWidget {
|
||||
] else ...<Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story) ...<Widget>[
|
||||
if (item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text(
|
||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||
'''${item.score} karma, ${item.descendants} comment${item.descendants > 1 ? 's' : ''}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
@ -476,7 +469,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onFetchModeChanged,
|
||||
onChanged: context.read<CommentsCubit>().updateFetchMode,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt6,
|
||||
@ -498,7 +491,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onOrderChanged,
|
||||
onChanged: context.read<CommentsCubit>().updateOrder,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
|
@ -2,7 +2,6 @@ import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
@ -33,14 +32,7 @@ class PinIconButton extends StatelessWidget {
|
||||
pinned ? Icons.push_pin : Icons.push_pin_outlined,
|
||||
color: Palette.white,
|
||||
),
|
||||
featureId: Constants.featurePinToTop,
|
||||
title: const Text('Pin a Story'),
|
||||
description: const Text(
|
||||
'Pin this story to the top of your '
|
||||
'home screen so that you can come'
|
||||
' back later.',
|
||||
style: TextStyle(fontSize: TextDimens.pt16),
|
||||
),
|
||||
feature: DiscoverableFeature.pinToTop,
|
||||
child: Icon(
|
||||
pinned ? Icons.push_pin : Icons.push_pin_outlined,
|
||||
color: pinned
|
||||
|
@ -15,7 +15,7 @@ class PollView extends StatefulWidget {
|
||||
State<PollView> createState() => _PollViewState();
|
||||
}
|
||||
|
||||
class _PollViewState extends State<PollView> {
|
||||
class _PollViewState extends State<PollView> with ItemActionMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<PollCubit, PollState>(
|
||||
|
@ -14,25 +14,25 @@ import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class ReplyBox extends StatefulWidget {
|
||||
const ReplyBox({
|
||||
required this.focusNode,
|
||||
required this.textEditingController,
|
||||
required this.onSendTapped,
|
||||
required this.onCloseTapped,
|
||||
required this.onChanged,
|
||||
super.key,
|
||||
this.splitViewEnabled = false,
|
||||
});
|
||||
|
||||
final bool splitViewEnabled;
|
||||
final FocusNode focusNode;
|
||||
final TextEditingController textEditingController;
|
||||
final VoidCallback onSendTapped;
|
||||
final VoidCallback onCloseTapped;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
@override
|
||||
_ReplyBoxState createState() => _ReplyBoxState();
|
||||
}
|
||||
|
||||
class _ReplyBoxState extends State<ReplyBox> {
|
||||
class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
|
||||
bool expanded = false;
|
||||
double? expandedHeight;
|
||||
|
||||
@ -40,9 +40,17 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
expandedHeight ??= MediaQuery.of(context).size.height -
|
||||
MediaQuery.of(context).viewInsets.bottom;
|
||||
return BlocBuilder<EditCubit, EditState>(
|
||||
expandedHeight ??= MediaQuery.of(context).size.height;
|
||||
return BlocConsumer<EditCubit, EditState>(
|
||||
listenWhen: (EditState previous, EditState current) =>
|
||||
previous.showReplyBox != current.showReplyBox,
|
||||
listener: (BuildContext context, EditState editState) {
|
||||
if (editState.showReplyBox) {
|
||||
widget.focusNode.requestFocus();
|
||||
} else {
|
||||
widget.focusNode.unfocus();
|
||||
}
|
||||
},
|
||||
buildWhen: (EditState previous, EditState current) =>
|
||||
previous.showReplyBox != current.showReplyBox ||
|
||||
previous.itemBeingEdited != current.itemBeingEdited ||
|
||||
@ -62,7 +70,9 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
: Dimens.zero,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
height: expanded ? expandedHeight : collapsedHeight,
|
||||
height: editState.showReplyBox
|
||||
? (expanded ? expandedHeight : collapsedHeight)
|
||||
: Dimens.zero,
|
||||
duration: Durations.ms200,
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: <BoxShadow>[
|
||||
@ -82,8 +92,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
height: Dimens.zero,
|
||||
),
|
||||
AnimatedContainer(
|
||||
height: expanded ? Dimens.pt36 : Dimens.zero,
|
||||
duration: Durations.ms200,
|
||||
height: expanded ? Dimens.pt40 : Dimens.zero,
|
||||
duration: Durations.ms300,
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
@ -145,7 +155,9 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
color: Palette.orange,
|
||||
),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
setState(() {
|
||||
expanded = false;
|
||||
});
|
||||
|
||||
final EditState state =
|
||||
context.read<EditCubit>().state;
|
||||
@ -153,17 +165,13 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
state.text.isNotNullOrEmpty) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) =>
|
||||
AlertDialog(
|
||||
title: const Text('Save draft?'),
|
||||
title: const Text('Abort editing?'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.deleteDraft();
|
||||
context.pop();
|
||||
},
|
||||
onPressed: context.pop,
|
||||
child: const Text(
|
||||
'No',
|
||||
style: TextStyle(
|
||||
@ -172,15 +180,18 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
onCloseTapped();
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
onCloseTapped();
|
||||
}
|
||||
widget.onCloseTapped();
|
||||
expanded = false;
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -214,39 +225,32 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: Dimens.pt16,
|
||||
right: Dimens.pt16,
|
||||
bottom: expanded
|
||||
// This padding here prevents keyboard
|
||||
// overlapping with TextField.
|
||||
? MediaQuery.of(context).viewInsets.bottom +
|
||||
Dimens.pt16
|
||||
: Dimens.zero,
|
||||
),
|
||||
child: TextField(
|
||||
controller: widget.textEditingController,
|
||||
autofocus: true,
|
||||
expands: true,
|
||||
maxLines: null,
|
||||
decoration: const InputDecoration(
|
||||
alignLabelWithHint: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: '...',
|
||||
hintStyle: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
focusedBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
child: TextField(
|
||||
focusNode: widget.focusNode,
|
||||
controller: widget.textEditingController,
|
||||
expands: true,
|
||||
maxLines: null,
|
||||
decoration: const InputDecoration(
|
||||
alignLabelWithHint: true,
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: Dimens.pt10,
|
||||
),
|
||||
keyboardType: TextInputType.multiline,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
textInputAction: TextInputAction.newline,
|
||||
onChanged: widget.onChanged,
|
||||
hintText: '...',
|
||||
hintStyle: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
focusedBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
keyboardType: TextInputType.multiline,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
textInputAction: TextInputAction.newline,
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -258,10 +262,22 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
);
|
||||
}
|
||||
|
||||
void onCloseTapped() {
|
||||
context.read<EditCubit>().deleteDraft();
|
||||
widget.textEditingController.clear();
|
||||
}
|
||||
|
||||
void showTextPopup() {
|
||||
final Item? replyingTo = context.read<EditCubit>().state.replyingTo;
|
||||
|
||||
if (replyingTo == null) return;
|
||||
if (replyingTo == null) {
|
||||
return;
|
||||
} else if (replyingTo is Story) {
|
||||
final ItemScreenArgs args = ItemScreenArgs(item: replyingTo);
|
||||
context.push('/${ItemScreen.routeName}', extra: args);
|
||||
expanded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
@ -349,6 +365,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
child: SingleChildScrollView(
|
||||
child: ItemText(
|
||||
item: replyingTo,
|
||||
selectable: true,
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
),
|
||||
|
@ -70,6 +70,7 @@ class TimeMachineDialog extends StatelessWidget {
|
||||
CommentTile(
|
||||
comment: c,
|
||||
actionable: false,
|
||||
collapsable: false,
|
||||
fetchMode: FetchMode.eager,
|
||||
),
|
||||
const Divider(
|
||||
|
@ -26,7 +26,7 @@ class ProfileScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ProfileScreenState extends State<ProfileScreen>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
with AutomaticKeepAliveClientMixin, ItemActionMixin {
|
||||
final RefreshController refreshControllerHistory = RefreshController();
|
||||
final RefreshController refreshControllerFav = RefreshController();
|
||||
final RefreshController refreshControllerNotification = RefreshController();
|
||||
|
@ -47,7 +47,7 @@ class Settings extends StatefulWidget {
|
||||
State<Settings> createState() => _SettingsState();
|
||||
}
|
||||
|
||||
class _SettingsState extends State<Settings> {
|
||||
class _SettingsState extends State<Settings> with ItemActionMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
|
@ -27,7 +27,7 @@ class SearchScreen extends StatefulWidget {
|
||||
_SearchScreenState createState() => _SearchScreenState();
|
||||
}
|
||||
|
||||
class _SearchScreenState extends State<SearchScreen> {
|
||||
class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
||||
final RefreshController refreshController = RefreshController();
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final Debouncer debouncer = Debouncer(delay: Durations.oneSecond);
|
||||
@ -58,40 +58,114 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: TextField(
|
||||
cursorColor: Palette.orange,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search Hacker News',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Palette.orange),
|
||||
),
|
||||
),
|
||||
onChanged: (String val) {
|
||||
if (val.isNotEmpty) {
|
||||
debouncer.run(() {
|
||||
context.read<SearchCubit>().search(val);
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
duration: chipsAnimationDuration,
|
||||
crossFadeState: state.showDateRangeShortcutChips
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
firstChild: SizedBox.fromSize(),
|
||||
secondChild: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
ColoredBox(
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: TextField(
|
||||
cursorColor: Palette.orange,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search Hacker News',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Palette.orange),
|
||||
),
|
||||
),
|
||||
onChanged: (String val) {
|
||||
if (val.isNotEmpty) {
|
||||
debouncer.run(() {
|
||||
context.read<SearchCubit>().search(val);
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
duration: chipsAnimationDuration,
|
||||
crossFadeState: state.showDateRangeShortcutChips
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
firstChild: SizedBox.fromSize(),
|
||||
secondChild: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
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(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
@ -99,143 +173,78 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
DateTimeShortcutChip.dayBefore(
|
||||
DateTimeRangeFilterChip(
|
||||
filter: state.dateFilter,
|
||||
initialStartDate: state.dateFilter?.startTime,
|
||||
initialEndDate: state.dateFilter?.endTime,
|
||||
onDateTimeRangeUpdated: context
|
||||
.read<SearchCubit>()
|
||||
.onDateTimeRangeUpdated,
|
||||
startDate: state.dateFilter?.startTime,
|
||||
endDate: state.dateFilter?.endTime,
|
||||
onDateTimeRangeRemoved: context
|
||||
.read<SearchCubit>()
|
||||
.removeFilter<DateTimeRangeFilter>,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
DateTimeShortcutChip.dayAfter(
|
||||
onDateTimeRangeUpdated: context
|
||||
PostedByFilterChip(
|
||||
filter: state.params.get<PostedByFilter>(),
|
||||
onChanged: context
|
||||
.read<SearchCubit>()
|
||||
.onDateTimeRangeUpdated,
|
||||
startDate: state.dateFilter?.startTime,
|
||||
endDate: state.dateFilter?.endTime,
|
||||
.onPostedByChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
DateTimeShortcutChip.weekBefore(
|
||||
onDateTimeRangeUpdated: context
|
||||
.read<SearchCubit>()
|
||||
.onDateTimeRangeUpdated,
|
||||
startDate: state.dateFilter?.startTime,
|
||||
endDate: state.dateFilter?.endTime,
|
||||
CustomChip(
|
||||
onSelected: (_) =>
|
||||
context.read<SearchCubit>().onSortToggled(),
|
||||
selected: state.params.sorted,
|
||||
label: '''newest first''',
|
||||
),
|
||||
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,
|
||||
),
|
||||
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>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
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,
|
||||
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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -323,6 +332,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
FadeIn(
|
||||
child: CommentTile(
|
||||
actionable: false,
|
||||
collapsable: false,
|
||||
selectable: false,
|
||||
comment: e,
|
||||
fetchMode: FetchMode.eager,
|
||||
onTap: () => goToItemScreen(
|
||||
|
@ -16,7 +16,7 @@ class SubmitScreen extends StatefulWidget {
|
||||
_SubmitScreenState createState() => _SubmitScreenState();
|
||||
}
|
||||
|
||||
class _SubmitScreenState extends State<SubmitScreen> {
|
||||
class _SubmitScreenState extends State<SubmitScreen> with ItemActionMixin {
|
||||
final TextEditingController titleEditingController = TextEditingController();
|
||||
final TextEditingController urlEditingController = TextEditingController();
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
|
@ -23,6 +23,8 @@ class CommentTile extends StatelessWidget {
|
||||
this.onRightMoreTapped,
|
||||
this.opUsername,
|
||||
this.actionable = true,
|
||||
this.collapsable = true,
|
||||
this.selectable = true,
|
||||
this.level = 0,
|
||||
this.onTap,
|
||||
this.itemScrollController,
|
||||
@ -32,6 +34,8 @@ class CommentTile extends StatelessWidget {
|
||||
final Comment comment;
|
||||
final int level;
|
||||
final bool actionable;
|
||||
final bool collapsable;
|
||||
final bool selectable;
|
||||
final FetchMode fetchMode;
|
||||
final ItemScrollController? itemScrollController;
|
||||
|
||||
@ -119,7 +123,7 @@ class CommentTile extends StatelessWidget {
|
||||
: null,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (actionable) {
|
||||
if (collapsable) {
|
||||
_collapse(context);
|
||||
} else {
|
||||
onTap?.call();
|
||||
@ -200,6 +204,7 @@ class CommentTile extends StatelessWidget {
|
||||
child: ItemText(
|
||||
key: ValueKey<int>(comment.id),
|
||||
item: comment,
|
||||
selectable: selectable,
|
||||
textScaleFactor: MediaQuery.of(context)
|
||||
.textScaleFactor,
|
||||
onTap: () {
|
||||
@ -353,22 +358,26 @@ class CommentTile extends StatelessWidget {
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
final PreferenceCubit preferenceCubit = context.read<PreferenceCubit>();
|
||||
final CollapseCubit collapseCubit = context.read<CollapseCubit>()
|
||||
..collapse(onStateChanged: HapticFeedbackUtil.selection);
|
||||
if (collapseCubit.state.collapsed &&
|
||||
preferenceCubit.state.autoScrollEnabled) {
|
||||
final List<Comment> comments =
|
||||
context.read<CommentsCubit>().state.comments;
|
||||
final int indexOfNextComment = comments.indexOf(comment) + 1;
|
||||
if (indexOfNextComment < comments.length) {
|
||||
Future<void>.delayed(
|
||||
Durations.ms300,
|
||||
() {
|
||||
itemScrollController?.scrollTo(
|
||||
index: indexOfNextComment,
|
||||
alignment: 0.1,
|
||||
duration: Durations.ms300,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class CountdownReminder extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CountDownReminderState extends State<CountdownReminder>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with SingleTickerProviderStateMixin, ItemActionMixin {
|
||||
late final AnimationController animationController;
|
||||
late final Animation<double> progressAnimation;
|
||||
late final Animation<double> opacityAnimation;
|
||||
|
@ -2,24 +2,22 @@ import 'dart:async';
|
||||
|
||||
import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/models/discoverable_feature.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class CustomDescribedFeatureOverlay extends StatelessWidget {
|
||||
const CustomDescribedFeatureOverlay({
|
||||
required this.featureId,
|
||||
required this.feature,
|
||||
required this.child,
|
||||
required this.tapTarget,
|
||||
required this.title,
|
||||
required this.description,
|
||||
super.key,
|
||||
this.contentLocation = ContentLocation.trivial,
|
||||
this.onComplete,
|
||||
});
|
||||
|
||||
final String featureId;
|
||||
final DiscoverableFeature feature;
|
||||
final Widget tapTarget;
|
||||
final Widget title;
|
||||
final Widget description;
|
||||
final Widget child;
|
||||
final ContentLocation contentLocation;
|
||||
final VoidCallback? onComplete;
|
||||
@ -27,12 +25,15 @@ class CustomDescribedFeatureOverlay extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DescribedFeatureOverlay(
|
||||
featureId: featureId,
|
||||
featureId: feature.featureId,
|
||||
overflowMode: OverflowMode.extendBackground,
|
||||
targetColor: Theme.of(context).primaryColor,
|
||||
tapTarget: tapTarget,
|
||||
title: title,
|
||||
description: description,
|
||||
title: Text(feature.title),
|
||||
description: Text(
|
||||
feature.description,
|
||||
style: const TextStyle(fontSize: TextDimens.pt16),
|
||||
),
|
||||
barrierDismissible: false,
|
||||
contentLocation: contentLocation,
|
||||
onBackgroundTap: () {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
||||
import 'package:hacki/styles/palette.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:linkify/linkify.dart' hide UrlLinkifier;
|
||||
|
||||
@ -139,7 +140,9 @@ const UrlLinkifier _urlLinkifier = UrlLinkifier();
|
||||
const EmailLinkifier _emailLinkifier = EmailLinkifier();
|
||||
const QuoteLinkifier _quoteLinkifier = QuoteLinkifier();
|
||||
const EmphasisLinkifier _emphasisLinkifier = EmphasisLinkifier();
|
||||
const CodeLinkifier _codeLinkifier = CodeLinkifier();
|
||||
const List<Linkifier> defaultLinkifiers = <Linkifier>[
|
||||
_codeLinkifier,
|
||||
_urlLinkifier,
|
||||
_emailLinkifier,
|
||||
_quoteLinkifier,
|
||||
@ -391,6 +394,14 @@ TextSpan buildTextSpan(
|
||||
text: element.text,
|
||||
style: style?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
} else if (element is CodeElement) {
|
||||
return TextSpan(
|
||||
text: element.text,
|
||||
style: style?.copyWith(
|
||||
fontFamily: Font.ubuntuMono.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:linkify/linkify.dart';
|
||||
|
||||
final RegExp _codeRegex =
|
||||
RegExp(r'\<pre\>\<code\>(.*?)\<\/code\>\<\/pre\>', dotAll: true);
|
||||
|
||||
class CodeLinkifier extends Linkifier {
|
||||
const CodeLinkifier();
|
||||
|
||||
static const String _openTag = '<pre><code>';
|
||||
static const String _closeTag = '</code></pre>';
|
||||
|
||||
@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 = _codeRegex.firstMatch(
|
||||
element.text.trimLeft(),
|
||||
);
|
||||
|
||||
if (match == null || match.group(0) == null || match.group(1) == null) {
|
||||
list.add(element);
|
||||
} else {
|
||||
final String matchedText = match.group(0)!;
|
||||
final num pos = element.text.indexOf(matchedText);
|
||||
final List<String> splitTexts = element.text.split(matchedText);
|
||||
|
||||
int curPos = 0;
|
||||
bool added = false;
|
||||
|
||||
for (final String text in splitTexts) {
|
||||
list.addAll(parse(<LinkifyElement>[TextElement(text)], options));
|
||||
|
||||
curPos += text.length;
|
||||
|
||||
if (!added && curPos >= pos) {
|
||||
added = true;
|
||||
final String trimmedText = matchedText
|
||||
.replaceFirst(_openTag, '')
|
||||
.replaceFirst(_closeTag, '');
|
||||
list.add(CodeElement(trimmedText));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
list.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an element that is wrapped by <code> tag.
|
||||
@immutable
|
||||
class CodeElement extends LinkifyElement {
|
||||
CodeElement(super.text);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "CodeElement: '$text'";
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => equals(other);
|
||||
|
||||
@override
|
||||
bool equals(dynamic other) => other is CodeElement && super.equals(other);
|
||||
|
||||
@override
|
||||
int get hashCode => text.hashCode;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export 'code_linkifier.dart';
|
||||
export 'emphasis_linkifier.dart';
|
||||
export 'quote_linkifier.dart';
|
||||
export 'url_linkifier.dart';
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/widgets.dart' show StringCharacters, immutable;
|
||||
import 'package:linkify/linkify.dart';
|
||||
|
||||
final RegExp _urlRegex = RegExp(
|
||||
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[\/\\\%:\?=&#@;A-Za-z0-9_.~-]*)',
|
||||
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[\/\\\%:\?=&#@;A-Za-z0-9()+_.,~-]*)',
|
||||
caseSensitive: false,
|
||||
dotAll: true,
|
||||
);
|
||||
@ -62,6 +64,29 @@ class UrlLinkifier extends Linkifier {
|
||||
originalUrl;
|
||||
}
|
||||
|
||||
if (url.contains(')')) {
|
||||
int openCount = 0;
|
||||
int closeCount = 0;
|
||||
for (final String c in url.characters) {
|
||||
if (c == '(') {
|
||||
openCount++;
|
||||
} else if (c == ')') {
|
||||
closeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (openCount != closeCount) {
|
||||
final int index = max(0, url.lastIndexOf(')'));
|
||||
url = url.substring(0, index);
|
||||
end = originalUrl.substring(index);
|
||||
}
|
||||
}
|
||||
|
||||
if (url.endsWith(',')) {
|
||||
url = url.substring(0, max(0, url.length - 1));
|
||||
end = '$end,';
|
||||
}
|
||||
|
||||
if ((options.humanize) || (options.removeWww)) {
|
||||
if (options.humanize) {
|
||||
url = url.replaceFirst(RegExp('https?://'), '');
|
||||
@ -70,15 +95,9 @@ class UrlLinkifier extends Linkifier {
|
||||
url = url.replaceFirst(RegExp(r'www\.'), '');
|
||||
}
|
||||
|
||||
list.add(
|
||||
UrlElement(
|
||||
originalUrl,
|
||||
url,
|
||||
originText,
|
||||
),
|
||||
);
|
||||
list.add(UrlElement(originalUrl, url, originText));
|
||||
} else {
|
||||
list.add(UrlElement(originalUrl, null, originText));
|
||||
list.add(UrlElement(url, url, originText));
|
||||
}
|
||||
|
||||
if (end != null) {
|
||||
|
@ -83,16 +83,7 @@ class _CustomTabBarState extends State<CustomTabBar> {
|
||||
size: TextDimens.pt16,
|
||||
color: Palette.white,
|
||||
),
|
||||
featureId: Constants.featureLogIn,
|
||||
title: const Text('Log in for more'),
|
||||
description: const Text(
|
||||
'Log in using your Hacker News account '
|
||||
'to check out stories and comments you have '
|
||||
'posted in the past, and get in-app '
|
||||
'notification when there is new reply to '
|
||||
'your comments or stories.',
|
||||
style: TextStyle(fontSize: TextDimens.pt16),
|
||||
),
|
||||
feature: DiscoverableFeature.login,
|
||||
child: BlocBuilder<NotificationCubit, NotificationState>(
|
||||
buildWhen: (
|
||||
NotificationState previous,
|
||||
|
@ -11,13 +11,18 @@ class ItemText extends StatelessWidget {
|
||||
const ItemText({
|
||||
required this.item,
|
||||
required this.textScaleFactor,
|
||||
required this.selectable,
|
||||
super.key,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final Item item;
|
||||
final VoidCallback? onTap;
|
||||
final double textScaleFactor;
|
||||
final bool selectable;
|
||||
|
||||
/// Reserved for collapsing a comment tile when
|
||||
/// [CollapseModePreference] is enabled;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -30,7 +35,18 @@ class ItemText extends StatelessWidget {
|
||||
decoration: TextDecoration.underline,
|
||||
color: Palette.orange,
|
||||
);
|
||||
if (item is Buildable) {
|
||||
|
||||
void onSelectionChanged(
|
||||
TextSelection selection,
|
||||
SelectionChangedCause? cause,
|
||||
) {
|
||||
if (cause == SelectionChangedCause.longPress &&
|
||||
selection.baseOffset != selection.extentOffset) {
|
||||
context.tryRead<CollapseCubit>()?.lock();
|
||||
}
|
||||
}
|
||||
|
||||
if (selectable && item is Buildable) {
|
||||
return SelectableText.rich(
|
||||
buildTextSpan(
|
||||
(item as Buildable).elements,
|
||||
@ -40,6 +56,7 @@ class ItemText extends StatelessWidget {
|
||||
),
|
||||
onTap: onTap,
|
||||
textScaleFactor: textScaleFactor,
|
||||
onSelectionChanged: onSelectionChanged,
|
||||
contextMenuBuilder: (
|
||||
BuildContext context,
|
||||
EditableTextState editableTextState,
|
||||
@ -52,24 +69,30 @@ class ItemText extends StatelessWidget {
|
||||
semanticsLabel: item.text,
|
||||
);
|
||||
} else {
|
||||
return SelectableLinkify(
|
||||
text: item.text,
|
||||
textScaleFactor: textScaleFactor,
|
||||
style: style,
|
||||
linkStyle: linkStyle,
|
||||
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
||||
onTap: onTap,
|
||||
contextMenuBuilder: (
|
||||
BuildContext context,
|
||||
EditableTextState editableTextState,
|
||||
) =>
|
||||
contextMenuBuilder(
|
||||
context,
|
||||
editableTextState,
|
||||
item: item,
|
||||
),
|
||||
semanticsLabel: item.text,
|
||||
);
|
||||
if (item is Buildable) {
|
||||
return InkWell(
|
||||
child: Text.rich(
|
||||
buildTextSpan(
|
||||
(item as Buildable).elements,
|
||||
style: style,
|
||||
linkStyle: linkStyle,
|
||||
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
||||
),
|
||||
textScaleFactor: textScaleFactor,
|
||||
semanticsLabel: item.text,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return InkWell(
|
||||
child: Linkify(
|
||||
text: item.text,
|
||||
textScaleFactor: textScaleFactor,
|
||||
style: style,
|
||||
linkStyle: linkStyle,
|
||||
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -272,7 +272,7 @@ class _CommentTile extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
comment.text,
|
||||
comment.text.trimLeft(),
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
),
|
||||
|
@ -28,7 +28,8 @@ class StoriesListView extends StatefulWidget {
|
||||
State<StoriesListView> createState() => _StoriesListViewState();
|
||||
}
|
||||
|
||||
class _StoriesListViewState extends State<StoriesListView> {
|
||||
class _StoriesListViewState extends State<StoriesListView>
|
||||
with ItemActionMixin {
|
||||
final RefreshController refreshController = RefreshController();
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
@ -130,10 +131,11 @@ class _StoriesListViewState extends State<StoriesListView> {
|
||||
),
|
||||
child: OptionalWrapper(
|
||||
enabled: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.storyMarkingMode
|
||||
.shouldDetectScrollingPast,
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.storyMarkingMode
|
||||
.shouldDetectScrollingPast &&
|
||||
!context.read<StoriesBloc>().hasRead(story),
|
||||
wrapper: (Widget child) => VisibilityDetector(
|
||||
key: ValueKey<int>(story.id),
|
||||
onVisibilityChanged: (VisibilityInfo info) {
|
||||
|
@ -6,6 +6,7 @@ class CollapseCache {
|
||||
final Map<int, Set<int>> _hidden = <int, Set<int>>{};
|
||||
final PublishSubject<Map<int, Set<int>>> _hiddenCommentsSubject =
|
||||
PublishSubject<Map<int, Set<int>>>();
|
||||
int? lockedId;
|
||||
|
||||
Stream<Map<int, Set<int>>> get hiddenComments =>
|
||||
_hiddenCommentsSubject.stream;
|
||||
|
@ -20,6 +20,8 @@ abstract class Dimens {
|
||||
static const double pt64 = 64;
|
||||
static const double pt100 = 100;
|
||||
static const double pt120 = 120;
|
||||
|
||||
static const double replyBoxCollapsedHeight = 140;
|
||||
}
|
||||
|
||||
abstract class TextDimens {
|
||||
|
@ -39,10 +39,6 @@ abstract class HtmlUtil {
|
||||
RegExp(r'\<i\>(.*?)\<\/i\>'),
|
||||
(Match match) => '*${match[1]}*',
|
||||
)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'\<pre\>\<code\>(.*?)\<\/code\>\<\/pre\>', dotAll: true),
|
||||
(Match match) => match[1]?.trimRight() ?? '',
|
||||
)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'\<a href=\"(.*?)\".*?\>.*?\<\/a\>'),
|
||||
(Match match) => match[1] ?? '',
|
||||
|
@ -1,27 +1,20 @@
|
||||
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
||||
import 'package:linkify/linkify.dart' hide UrlLinkifier;
|
||||
import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart';
|
||||
|
||||
abstract class LinkifierUtil {
|
||||
static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false);
|
||||
|
||||
static List<LinkifyElement> linkify(String text) {
|
||||
const List<Linkifier> linkifiers = <Linkifier>[
|
||||
UrlLinkifier(),
|
||||
EmailLinkifier(),
|
||||
QuoteLinkifier(),
|
||||
EmphasisLinkifier(),
|
||||
];
|
||||
List<LinkifyElement> list = <LinkifyElement>[TextElement(text)];
|
||||
|
||||
if (text.isEmpty) {
|
||||
return <LinkifyElement>[];
|
||||
}
|
||||
|
||||
if (linkifiers.isEmpty) {
|
||||
if (defaultLinkifiers.isEmpty) {
|
||||
return list;
|
||||
}
|
||||
|
||||
for (final Linkifier linkifier in linkifiers) {
|
||||
for (final Linkifier linkifier in defaultLinkifiers) {
|
||||
list = linkifier.parse(list, linkifyOptions);
|
||||
}
|
||||
|
||||
|
212
pubspec.lock
212
pubspec.lock
@ -13,10 +13,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: adaptive_theme
|
||||
sha256: "2d9bfee4240cdfad1b169cb43ac38fb49487e7fe1cc845e2973d4cef1780c0f6"
|
||||
sha256: "28df95a6b86993b38a51ee97d33a9f1d845fd1c7320c21c5d5e2183b5605e152"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
version: "3.4.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -45,10 +45,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: badges
|
||||
sha256: "6e7f3ec561ec08f47f912cfe349d4a1707afdc8dda271e17b046aa6d42c89e77"
|
||||
sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -77,26 +77,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15
|
||||
sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
version: "3.3.0"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7
|
||||
sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "3.0.0"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0
|
||||
sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.1.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -165,10 +165,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9"
|
||||
sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+4"
|
||||
version: "0.3.3+5"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -221,10 +221,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197
|
||||
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.2"
|
||||
version: "5.3.3"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -287,14 +287,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.3"
|
||||
flutter_blurhash:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_blurhash
|
||||
sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
flutter_cache_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -312,10 +304,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_email_sender
|
||||
sha256: "52b713a67a966be4d9e6f68a323fc0a5bc2da71c567eb451af1aa90d30adbc3a"
|
||||
sha256: "5001e9158f91a8799140fb30a11ad89cd587244f30b4f848d87085985c49b60f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
version: "6.0.2"
|
||||
flutter_fadein:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -344,10 +336,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "3cc40fe8c50ab8383f3e053a499f00f975636622ecdc8e20a77418ece3b1e975"
|
||||
sha256: "3002092e5b8ce2f86c3361422e52e6db6776c23ee21e0b2f71b892bf4259ef04"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.1.0+1"
|
||||
version: "15.1.1"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -368,50 +360,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5"
|
||||
sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
version: "9.0.0"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3"
|
||||
sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
version: "1.2.0"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50"
|
||||
sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b
|
||||
sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.0.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe"
|
||||
sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee
|
||||
sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "3.0.0"
|
||||
flutter_siri_suggestions:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -471,10 +463,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: get_it
|
||||
sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468"
|
||||
sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.6.0"
|
||||
version: "7.6.4"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -487,10 +479,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "5668e6d3dbcb2d0dfa25f7567554b88c57e1e3f3c440b672b24d4a9477017d5b"
|
||||
sha256: a07c781bf55bf11ae85133338e4850f0b4e33e261c44a66c750fc707d65d8393
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.1.2"
|
||||
version: "11.1.2"
|
||||
hive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -603,10 +595,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: "66cb048220ca51cf9011da69fa581e4ee2bed4be6e82870d9e9baae75739da49"
|
||||
sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "2.0.2+1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -691,10 +683,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: octo_image
|
||||
sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143"
|
||||
sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "2.0.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -731,50 +723,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0"
|
||||
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
path_provider_android:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8"
|
||||
sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.2.0"
|
||||
path_provider_foundation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5"
|
||||
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84
|
||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da
|
||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -795,10 +787,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd"
|
||||
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.1.6"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -924,58 +916,58 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1"
|
||||
sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
shared_preferences_android:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076
|
||||
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
shared_preferences_foundation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef
|
||||
sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "2.3.4"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1"
|
||||
sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1"
|
||||
sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a"
|
||||
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d
|
||||
sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1168,66 +1160,66 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e"
|
||||
sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.12"
|
||||
version: "6.1.14"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025"
|
||||
sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.38"
|
||||
version: "6.1.0"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
|
||||
sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "6.1.5"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5"
|
||||
sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.6"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1"
|
||||
sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.0.7"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea
|
||||
sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.5"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4
|
||||
sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.18"
|
||||
version: "2.0.20"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422"
|
||||
sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "3.0.8"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1248,10 +1240,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: very_good_analysis
|
||||
sha256: "5e4ea72d2a9188630f0dd8f120a541de730090ef8863243fedca8267a84508b8"
|
||||
sha256: "9ae7f3a3bd5764fb021b335ca28a34f040cd0ab6eec00a1b213b445dae58a4b8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0+1"
|
||||
version: "5.1.0"
|
||||
visibility_detector:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1344,58 +1336,58 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d"
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
webview_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_flutter
|
||||
sha256: "789d52bd789373cc1e100fb634af2127e86c99cf9abde09499743270c5de8d00"
|
||||
sha256: "82f6787d5df55907aa01e49bd9644f4ed1cc82af7a8257dd9947815959d2e755"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.2"
|
||||
version: "4.2.4"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: bca797abba472868655b5f1a6029c1132385685ee9db4713cb0e7f33076210c6
|
||||
sha256: ddc167c6676f57c8b367d19fcbee267d6dc6adf81bd6c3cb87981d30746e0a6d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.3"
|
||||
version: "3.10.1"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: "0ca3cfcc6781a7de701d580917af4a9efc4e3e129f8ead95a80587f0a749480a"
|
||||
sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
version: "2.6.0"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: ed749f94ac9e814d04a258a9255cf69cfa4cc6006ff59542aea7fb4590144972
|
||||
sha256: "485af05f2c5f83c7f78c20e236b170ad02df7153b299ae9917345be43871d29f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.3"
|
||||
version: "3.8.0"
|
||||
win32:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: win32
|
||||
sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0
|
||||
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.6"
|
||||
version: "5.0.9"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9
|
||||
sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
workmanager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1408,10 +1400,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247
|
||||
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.0.3"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1429,5 +1421,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.1.0-185.0.dev <4.0.0"
|
||||
flutter: ">=3.13.2"
|
||||
dart: ">=3.1.0 <4.0.0"
|
||||
flutter: ">=3.13.6"
|
||||
|
@ -1,11 +1,11 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 1.9.0+121
|
||||
version: 1.9.3+124
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
flutter: "3.13.2"
|
||||
flutter: "3.13.6"
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.2.0
|
||||
@ -32,13 +32,13 @@ dependencies:
|
||||
flutter_feather_icons: 2.0.0+1
|
||||
flutter_inappwebview: ^5.7.2+3
|
||||
flutter_local_notifications: ^15.1.0+1
|
||||
flutter_secure_storage: ^8.0.0
|
||||
flutter_secure_storage: ^9.0.0
|
||||
flutter_siri_suggestions: ^2.1.0
|
||||
flutter_slidable: ^3.0.0
|
||||
font_awesome_flutter: ^10.3.0
|
||||
gbk_codec: ^0.4.0
|
||||
get_it: ^7.2.0
|
||||
go_router: ^10.1.2
|
||||
go_router: ^11.1.2
|
||||
hive: ^2.2.3
|
||||
html: ^0.15.1
|
||||
html_unescape: ^2.0.0
|
||||
|
Submodule submodules/flutter updated: 2524052335...ead455963c
Reference in New Issue
Block a user