Compare commits

..

11 Commits

35 changed files with 580 additions and 412 deletions

View File

@ -141,7 +141,7 @@ SPEC CHECKSUMS:
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
@ -166,4 +166,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937 PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
COCOAPODS: 1.11.3 COCOAPODS: 1.13.0

View File

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

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
@ -28,11 +29,12 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: _collapseCache.totalHidden(_commentId), collapsedCount: _collapseCache.totalHidden(_commentId),
collapsed: _collapseCache.isCollapsed(_commentId), collapsed: _collapseCache.isCollapsed(_commentId),
hidden: _collapseCache.isHidden(_commentId), hidden: _collapseCache.isHidden(_commentId),
locked: _collapseCache.lockedId == _commentId,
), ),
); );
} }
void collapse() { void collapse({required VoidCallback onStateChanged}) {
if (state.collapsed) { if (state.collapsed) {
_collapseCache.uncollapse(_commentId); _collapseCache.uncollapse(_commentId);
@ -42,7 +44,14 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: 0, collapsedCount: 0,
), ),
); );
onStateChanged();
} else { } else {
if (state.locked) {
emit(state.copyWith(locked: false));
return;
}
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId); final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
emit( emit(
@ -51,6 +60,8 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length, 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 @override
Future<void> close() async { Future<void> close() async {
await _streamSubscription.cancel(); await _streamSubscription.cancel();

View File

@ -4,26 +4,39 @@ class CollapseState extends Equatable {
const CollapseState({ const CollapseState({
required this.collapsed, required this.collapsed,
required this.hidden, required this.hidden,
required this.locked,
required this.collapsedCount, required this.collapsedCount,
}); });
const CollapseState.init() const CollapseState.init()
: collapsed = false, : collapsed = false,
hidden = false, hidden = false,
locked = false,
collapsedCount = 0; collapsedCount = 0;
final bool collapsed; 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; 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; final int collapsedCount;
CollapseState copyWith({ CollapseState copyWith({
bool? collapsed, bool? collapsed,
bool? hidden, bool? hidden,
bool? locked,
int? collapsedCount, int? collapsedCount,
}) { }) {
return CollapseState( return CollapseState(
collapsed: collapsed ?? this.collapsed, collapsed: collapsed ?? this.collapsed,
hidden: hidden ?? this.hidden, hidden: hidden ?? this.hidden,
locked: locked ?? this.locked,
collapsedCount: collapsedCount ?? this.collapsedCount, collapsedCount: collapsedCount ?? this.collapsedCount,
); );
} }
@ -32,6 +45,7 @@ class CollapseState extends Equatable {
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
collapsed, collapsed,
hidden, hidden,
locked,
collapsedCount, collapsedCount,
]; ];
} }

View File

@ -322,7 +322,7 @@ class CommentsCubit extends Cubit<CommentsState> {
} }
} }
void onOrderChanged(CommentsOrder? order) { void updateOrder(CommentsOrder? order) {
if (order == null) return; if (order == null) return;
if (state.order == order) return; if (state.order == order) return;
HapticFeedbackUtil.selection(); HapticFeedbackUtil.selection();
@ -335,7 +335,7 @@ class CommentsCubit extends Cubit<CommentsState> {
init(useCommentCache: true); init(useCommentCache: true);
} }
void onFetchModeChanged(FetchMode? fetchMode) { void updateFetchMode(FetchMode? fetchMode) {
if (fetchMode == null) return; if (fetchMode == null) return;
if (state.fetchMode == fetchMode) return; if (state.fetchMode == fetchMode) return;
_collapseCache.resetCollapsedComments(); _collapseCache.resetCollapsedComments();

View File

@ -1,9 +1,9 @@
export 'context_extension.dart'; export 'context_extension.dart';
export 'date_time_extension.dart'; export 'date_time_extension.dart';
export 'int_extension.dart'; export 'int_extension.dart';
export 'item_action_mixin.dart';
export 'list_extension.dart'; export 'list_extension.dart';
export 'object_extension.dart'; export 'object_extension.dart';
export 'set_extension.dart'; export 'set_extension.dart';
export 'state_extension.dart';
export 'string_extension.dart'; export 'string_extension.dart';
export 'widget_extension.dart'; export 'widget_extension.dart';

View File

@ -12,7 +12,8 @@ import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
extension StateExtension on State { @optionalTypeArgs
mixin ItemActionMixin<T extends StatefulWidget> on State<T> {
void showSnackBar({ void showSnackBar({
required String content, required String content,
VoidCallback? action, VoidCallback? action,

View File

@ -2,21 +2,19 @@ import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart'; import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
extension WidgetModifier on Widget { extension ContextMenuBuilder on Widget {
Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
return Padding(
padding: value,
child: this,
);
}
Widget contextMenuBuilder( Widget contextMenuBuilder(
BuildContext context, BuildContext context,
EditableTextState editableTextState, { EditableTextState editableTextState, {
required Item item, required Item item,
}) { }) {
if (item is! Buildable) {
return const SizedBox.shrink();
}
final int start = editableTextState.textEditingValue.selection.base.offset; final int start = editableTextState.textEditingValue.selection.base.offset;
final int end = editableTextState.textEditingValue.selection.end; final int end = editableTextState.textEditingValue.selection.end;
@ -25,28 +23,11 @@ extension WidgetModifier on Widget {
]; ];
if (start != -1 && end != -1) { if (start != -1 && end != -1) {
String selectedText = item.text.substring(start, end); final String text = (item as Buildable)
.elements
if (item is Buildable) { .map((LinkifyElement e) => e.text)
final Iterable<EmphasisElement> emphasisElements = .reduce((String value, String e) => '$value$e');
(item as Buildable).elements.whereType<EmphasisElement>(); final String selectedText = text.substring(start, end);
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++;
}
}
items.addAll(<ContextMenuButtonItem>[ items.addAll(<ContextMenuButtonItem>[
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,
);
}
}

View File

@ -36,7 +36,7 @@ class HomeScreen extends StatefulWidget {
} }
class _HomeScreenState extends State<HomeScreen> class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin, RouteAware { with SingleTickerProviderStateMixin, RouteAware, ItemActionMixin {
late final TabController tabController; late final TabController tabController;
late final StreamSubscription<String> intentDataStreamSubscription; late final StreamSubscription<String> intentDataStreamSubscription;
late final StreamSubscription<String?> notificationStreamSubscription; late final StreamSubscription<String?> notificationStreamSubscription;

View File

@ -137,7 +137,8 @@ class ItemScreen extends StatefulWidget {
_ItemScreenState createState() => _ItemScreenState(); _ItemScreenState createState() => _ItemScreenState();
} }
class _ItemScreenState extends State<ItemScreen> with RouteAware { class _ItemScreenState extends State<ItemScreen>
with RouteAware, ItemActionMixin {
final TextEditingController commentEditingController = final TextEditingController commentEditingController =
TextEditingController(); TextEditingController();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();

View File

@ -16,7 +16,7 @@ class LoginDialog extends StatefulWidget {
State<LoginDialog> createState() => _LoginDialogState(); State<LoginDialog> createState() => _LoginDialogState();
} }
class _LoginDialogState extends State<LoginDialog> { class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
final TextEditingController usernameController = TextEditingController(); final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController(); final TextEditingController passwordController = TextEditingController();

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart'; import 'package:flutter_fadein/flutter_fadein.dart';
@ -188,9 +189,10 @@ class _ParentItemSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Item item = state.item;
return Semantics( return Semantics(
label: 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( child: Column(
children: <Widget>[ children: <Widget>[
if (!splitViewEnabled) if (!splitViewEnabled)
@ -207,11 +209,11 @@ class _ParentItemSection extends StatelessWidget {
onPressed: (_) { onPressed: (_) {
HapticFeedbackUtil.light(); HapticFeedbackUtil.light();
if (state.item.id != if (item.id !=
context.read<EditCubit>().state.replyingTo?.id) { context.read<EditCubit>().state.replyingTo?.id) {
commentEditingController.clear(); commentEditingController.clear();
} }
context.read<EditCubit>().onReplyTapped(state.item); context.read<EditCubit>().onReplyTapped(item);
}, },
backgroundColor: Palette.orange, backgroundColor: Palette.orange,
foregroundColor: Palette.white, foregroundColor: Palette.white,
@ -219,7 +221,7 @@ class _ParentItemSection extends StatelessWidget {
), ),
SlidableAction( SlidableAction(
onPressed: (BuildContext context) => onPressed: (BuildContext context) =>
onMoreTapped(state.item, context.rect), onMoreTapped(item, context.rect),
backgroundColor: Palette.orange, backgroundColor: Palette.orange,
foregroundColor: Palette.white, foregroundColor: Palette.white,
icon: Icons.more_horiz, icon: Icons.more_horiz,
@ -236,7 +238,7 @@ class _ParentItemSection extends StatelessWidget {
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Text( Text(
state.item.by, item.by,
style: const TextStyle( style: const TextStyle(
color: Palette.orange, color: Palette.orange,
), ),
@ -245,7 +247,7 @@ class _ParentItemSection extends StatelessWidget {
), ),
const Spacer(), const Spacer(),
Text( Text(
state.item.timeAgo, item.timeAgo,
style: const TextStyle( style: const TextStyle(
color: Palette.grey, color: Palette.grey,
), ),
@ -265,12 +267,13 @@ class _ParentItemSection extends StatelessWidget {
BuildContext context, BuildContext context,
PreferenceState prefState, PreferenceState prefState,
) { ) {
final double fontSize = prefState.fontSize.fontSize;
return Column( return Column(
children: <Widget>[ children: <Widget>[
if (state.item is Story) if (item is Story)
InkWell( InkWell(
onTap: () => LinkUtil.launch( onTap: () => LinkUtil.launch(
state.item.url, item.url,
useReader: context useReader: context
.read<PreferenceCubit>() .read<PreferenceCubit>()
.state .state
@ -280,6 +283,13 @@ class _ParentItemSection extends StatelessWidget {
.state .state
.isOfflineReading, .isOfflineReading,
), ),
onLongPress: () => FlutterClipboard.copy(item.url)
.whenComplete(() {
HapticFeedbackUtil.selection();
context.showSnackBar(
content: 'Link copied.',
);
}),
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: Dimens.pt6, left: Dimens.pt6,
@ -291,7 +301,7 @@ class _ParentItemSection extends StatelessWidget {
TextSpan( TextSpan(
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: prefState.fontSize.fontSize, fontSize: fontSize,
color: Theme.of(context) color: Theme.of(context)
.textTheme .textTheme
.bodyLarge .bodyLarge
@ -299,24 +309,22 @@ class _ParentItemSection extends StatelessWidget {
), ),
children: <TextSpan>[ children: <TextSpan>[
TextSpan( TextSpan(
semanticsLabel: state.item.title, semanticsLabel: item.title,
text: state.item.title, text: item.title,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: prefState.fontSize.fontSize, fontSize: fontSize,
color: state.item.url.isNotEmpty color: item.url.isNotEmpty
? Palette.orange ? Palette.orange
: null, : null,
), ),
), ),
if (state.item.url.isNotEmpty) if (item.url.isNotEmpty)
TextSpan( TextSpan(
text: text: ''' (${item.readableUrl})''',
''' (${(state.item as Story).readableUrl})''',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: fontSize: fontSize - 4,
prefState.fontSize.fontSize - 4,
color: Palette.orange, color: Palette.orange,
), ),
), ),
@ -332,7 +340,7 @@ class _ParentItemSection extends StatelessWidget {
const SizedBox( const SizedBox(
height: Dimens.pt6, height: Dimens.pt6,
), ),
if (state.item.text.isNotEmpty) if (item.text.isNotEmpty)
FadeIn( FadeIn(
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
@ -341,9 +349,10 @@ class _ParentItemSection extends StatelessWidget {
left: Dimens.pt8, left: Dimens.pt8,
), ),
child: ItemText( child: ItemText(
item: state.item, item: item,
textScaleFactor: textScaleFactor:
MediaQuery.of(context).textScaleFactor, MediaQuery.of(context).textScaleFactor,
selectable: true,
), ),
), ),
), ),
@ -352,28 +361,27 @@ class _ParentItemSection extends StatelessWidget {
); );
}, },
), ),
if (state.item.isPoll) if (item is Story && item.isPoll)
BlocProvider<PollCubit>( BlocProvider<PollCubit>(
create: (BuildContext context) => create: (BuildContext context) =>
PollCubit(story: state.item as Story)..init(), PollCubit(story: item)..init(),
child: const PollView(), child: const PollView(),
), ),
], ],
), ),
), ),
), ),
if (state.item.text.isNotEmpty) if (item.text.isNotEmpty)
const SizedBox( const SizedBox(
height: Dimens.pt8, height: Dimens.pt8,
), ),
const Divider( const Divider(
height: Dimens.zero, height: Dimens.zero,
), ),
if (state.onlyShowTargetComment) ...<Widget>[ if (state.onlyShowTargetComment && item is Story) ...<Widget>[
Center( Center(
child: TextButton( child: TextButton(
onPressed: () => onPressed: () => context.read<CommentsCubit>().loadAll(item),
context.read<CommentsCubit>().loadAll(state.item as Story),
child: const Text('View all comments'), child: const Text('View all comments'),
), ),
), ),
@ -383,12 +391,12 @@ class _ParentItemSection extends StatelessWidget {
] else ...<Widget>[ ] else ...<Widget>[
Row( Row(
children: <Widget>[ children: <Widget>[
if (state.item is Story) ...<Widget>[ if (item is Story) ...<Widget>[
const SizedBox( const SizedBox(
width: Dimens.pt12, width: Dimens.pt12,
), ),
Text( 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( style: const TextStyle(
fontSize: TextDimens.pt13, fontSize: TextDimens.pt13,
), ),
@ -461,7 +469,7 @@ class _ParentItemSection extends StatelessWidget {
), ),
) )
.toList(), .toList(),
onChanged: context.read<CommentsCubit>().onFetchModeChanged, onChanged: context.read<CommentsCubit>().updateFetchMode,
), ),
const SizedBox( const SizedBox(
width: Dimens.pt6, width: Dimens.pt6,
@ -483,7 +491,7 @@ class _ParentItemSection extends StatelessWidget {
), ),
) )
.toList(), .toList(),
onChanged: context.read<CommentsCubit>().onOrderChanged, onChanged: context.read<CommentsCubit>().updateOrder,
), ),
const SizedBox( const SizedBox(
width: Dimens.pt4, width: Dimens.pt4,

View File

@ -15,7 +15,7 @@ class PollView extends StatefulWidget {
State<PollView> createState() => _PollViewState(); State<PollView> createState() => _PollViewState();
} }
class _PollViewState extends State<PollView> { class _PollViewState extends State<PollView> with ItemActionMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<PollCubit, PollState>( return BlocBuilder<PollCubit, PollState>(

View File

@ -32,7 +32,7 @@ class ReplyBox extends StatefulWidget {
_ReplyBoxState createState() => _ReplyBoxState(); _ReplyBoxState createState() => _ReplyBoxState();
} }
class _ReplyBoxState extends State<ReplyBox> { class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
bool expanded = false; bool expanded = false;
double? expandedHeight; double? expandedHeight;
@ -365,6 +365,7 @@ class _ReplyBoxState extends State<ReplyBox> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: ItemText( child: ItemText(
item: replyingTo, item: replyingTo,
selectable: true,
textScaleFactor: textScaleFactor:
MediaQuery.of(context).textScaleFactor, MediaQuery.of(context).textScaleFactor,
), ),

View File

@ -70,6 +70,7 @@ class TimeMachineDialog extends StatelessWidget {
CommentTile( CommentTile(
comment: c, comment: c,
actionable: false, actionable: false,
collapsable: false,
fetchMode: FetchMode.eager, fetchMode: FetchMode.eager,
), ),
const Divider( const Divider(

View File

@ -26,7 +26,7 @@ class ProfileScreen extends StatefulWidget {
} }
class _ProfileScreenState extends State<ProfileScreen> class _ProfileScreenState extends State<ProfileScreen>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin, ItemActionMixin {
final RefreshController refreshControllerHistory = RefreshController(); final RefreshController refreshControllerHistory = RefreshController();
final RefreshController refreshControllerFav = RefreshController(); final RefreshController refreshControllerFav = RefreshController();
final RefreshController refreshControllerNotification = RefreshController(); final RefreshController refreshControllerNotification = RefreshController();

View File

@ -47,7 +47,7 @@ class Settings extends StatefulWidget {
State<Settings> createState() => _SettingsState(); State<Settings> createState() => _SettingsState();
} }
class _SettingsState extends State<Settings> { class _SettingsState extends State<Settings> with ItemActionMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<PreferenceCubit, PreferenceState>( return BlocBuilder<PreferenceCubit, PreferenceState>(

View File

@ -27,7 +27,7 @@ class SearchScreen extends StatefulWidget {
_SearchScreenState createState() => _SearchScreenState(); _SearchScreenState createState() => _SearchScreenState();
} }
class _SearchScreenState extends State<SearchScreen> { class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
final RefreshController refreshController = RefreshController(); final RefreshController refreshController = RefreshController();
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final Debouncer debouncer = Debouncer(delay: Durations.oneSecond); final Debouncer debouncer = Debouncer(delay: Durations.oneSecond);
@ -57,6 +57,10 @@ class _SearchScreenState extends State<SearchScreen> {
body: Column( body: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ColoredBox(
color: Theme.of(context).canvasColor,
child: Column(
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -185,8 +189,9 @@ class _SearchScreenState extends State<SearchScreen> {
), ),
PostedByFilterChip( PostedByFilterChip(
filter: state.params.get<PostedByFilter>(), filter: state.params.get<PostedByFilter>(),
onChanged: onChanged: context
context.read<SearchCubit>().onPostedByChanged, .read<SearchCubit>()
.onPostedByChanged,
), ),
const SizedBox( const SizedBox(
width: Dimens.pt8, width: Dimens.pt8,
@ -225,8 +230,9 @@ class _SearchScreenState extends State<SearchScreen> {
width: Dimens.pt8, width: Dimens.pt8,
), ),
CustomChip( CustomChip(
onSelected: (_) => onSelected: (_) => context
context.read<SearchCubit>().onToggled(filter), .read<SearchCubit>()
.onToggled(filter),
selected: context selected: context
.read<SearchCubit>() .read<SearchCubit>()
.state .state
@ -239,6 +245,9 @@ class _SearchScreenState extends State<SearchScreen> {
], ],
), ),
), ),
],
),
),
if (state.status == SearchStatus.loading && if (state.status == SearchStatus.loading &&
state.results.isEmpty) ...<Widget>[ state.results.isEmpty) ...<Widget>[
const SizedBox( const SizedBox(
@ -323,6 +332,8 @@ class _SearchScreenState extends State<SearchScreen> {
FadeIn( FadeIn(
child: CommentTile( child: CommentTile(
actionable: false, actionable: false,
collapsable: false,
selectable: false,
comment: e, comment: e,
fetchMode: FetchMode.eager, fetchMode: FetchMode.eager,
onTap: () => goToItemScreen( onTap: () => goToItemScreen(

View File

@ -16,7 +16,7 @@ class SubmitScreen extends StatefulWidget {
_SubmitScreenState createState() => _SubmitScreenState(); _SubmitScreenState createState() => _SubmitScreenState();
} }
class _SubmitScreenState extends State<SubmitScreen> { class _SubmitScreenState extends State<SubmitScreen> with ItemActionMixin {
final TextEditingController titleEditingController = TextEditingController(); final TextEditingController titleEditingController = TextEditingController();
final TextEditingController urlEditingController = TextEditingController(); final TextEditingController urlEditingController = TextEditingController();
final TextEditingController textEditingController = TextEditingController(); final TextEditingController textEditingController = TextEditingController();

View File

@ -23,6 +23,8 @@ class CommentTile extends StatelessWidget {
this.onRightMoreTapped, this.onRightMoreTapped,
this.opUsername, this.opUsername,
this.actionable = true, this.actionable = true,
this.collapsable = true,
this.selectable = true,
this.level = 0, this.level = 0,
this.onTap, this.onTap,
this.itemScrollController, this.itemScrollController,
@ -32,6 +34,8 @@ class CommentTile extends StatelessWidget {
final Comment comment; final Comment comment;
final int level; final int level;
final bool actionable; final bool actionable;
final bool collapsable;
final bool selectable;
final FetchMode fetchMode; final FetchMode fetchMode;
final ItemScrollController? itemScrollController; final ItemScrollController? itemScrollController;
@ -119,7 +123,7 @@ class CommentTile extends StatelessWidget {
: null, : null,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
if (actionable) { if (collapsable) {
_collapse(context); _collapse(context);
} else { } else {
onTap?.call(); onTap?.call();
@ -200,6 +204,7 @@ class CommentTile extends StatelessWidget {
child: ItemText( child: ItemText(
key: ValueKey<int>(comment.id), key: ValueKey<int>(comment.id),
item: comment, item: comment,
selectable: selectable,
textScaleFactor: MediaQuery.of(context) textScaleFactor: MediaQuery.of(context)
.textScaleFactor, .textScaleFactor,
onTap: () { onTap: () {
@ -353,17 +358,20 @@ class CommentTile extends StatelessWidget {
} }
void _collapse(BuildContext context) { void _collapse(BuildContext context) {
HapticFeedbackUtil.selection(); final PreferenceCubit preferenceCubit = context.read<PreferenceCubit>();
context.read<CollapseCubit>().collapse(); final CollapseCubit collapseCubit = context.read<CollapseCubit>()
if (context.read<CollapseCubit>().state.collapsed && ..collapse(onStateChanged: HapticFeedbackUtil.selection);
context.read<PreferenceCubit>().state.autoScrollEnabled) { 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( Future<void>.delayed(
Durations.ms300, Durations.ms300,
() { () {
itemScrollController?.scrollTo( itemScrollController?.scrollTo(
index: index: indexOfNextComment,
context.read<CommentsCubit>().state.comments.indexOf(comment) +
1,
alignment: 0.1, alignment: 0.1,
duration: Durations.ms300, duration: Durations.ms300,
); );
@ -371,4 +379,5 @@ class CommentTile extends StatelessWidget {
); );
} }
} }
}
} }

View File

@ -17,7 +17,7 @@ class CountdownReminder extends StatefulWidget {
} }
class _CountDownReminderState extends State<CountdownReminder> class _CountDownReminderState extends State<CountdownReminder>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin, ItemActionMixin {
late final AnimationController animationController; late final AnimationController animationController;
late final Animation<double> progressAnimation; late final Animation<double> progressAnimation;
late final Animation<double> opacityAnimation; late final Animation<double> opacityAnimation;

View File

@ -1,7 +1,8 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.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/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:hacki/utils/utils.dart';
import 'package:linkify/linkify.dart' hide UrlLinkifier; import 'package:linkify/linkify.dart' hide UrlLinkifier;
@ -139,7 +140,9 @@ const UrlLinkifier _urlLinkifier = UrlLinkifier();
const EmailLinkifier _emailLinkifier = EmailLinkifier(); const EmailLinkifier _emailLinkifier = EmailLinkifier();
const QuoteLinkifier _quoteLinkifier = QuoteLinkifier(); const QuoteLinkifier _quoteLinkifier = QuoteLinkifier();
const EmphasisLinkifier _emphasisLinkifier = EmphasisLinkifier(); const EmphasisLinkifier _emphasisLinkifier = EmphasisLinkifier();
const CodeLinkifier _codeLinkifier = CodeLinkifier();
const List<Linkifier> defaultLinkifiers = <Linkifier>[ const List<Linkifier> defaultLinkifiers = <Linkifier>[
_codeLinkifier,
_urlLinkifier, _urlLinkifier,
_emailLinkifier, _emailLinkifier,
_quoteLinkifier, _quoteLinkifier,
@ -391,6 +394,14 @@ TextSpan buildTextSpan(
text: element.text, text: element.text,
style: style?.copyWith( style: style?.copyWith(
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold,
),
);
} else if (element is CodeElement) {
return TextSpan(
text: element.text,
style: style?.copyWith(
fontFamily: Font.ubuntuMono.name,
), ),
); );
} }

View File

@ -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;
}

View File

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

View File

@ -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'; import 'package:linkify/linkify.dart';
final RegExp _urlRegex = RegExp( final RegExp _urlRegex = RegExp(
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[\/\\\%:\?=&#@;A-Za-z0-9_.~-]*)', r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[\/\\\%:\?=&#@;A-Za-z0-9()+_.,~-]*)',
caseSensitive: false, caseSensitive: false,
dotAll: true, dotAll: true,
); );
@ -62,6 +64,29 @@ class UrlLinkifier extends Linkifier {
originalUrl; 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) || (options.removeWww)) {
if (options.humanize) { if (options.humanize) {
url = url.replaceFirst(RegExp('https?://'), ''); url = url.replaceFirst(RegExp('https?://'), '');
@ -70,15 +95,9 @@ class UrlLinkifier extends Linkifier {
url = url.replaceFirst(RegExp(r'www\.'), ''); url = url.replaceFirst(RegExp(r'www\.'), '');
} }
list.add( list.add(UrlElement(originalUrl, url, originText));
UrlElement(
originalUrl,
url,
originText,
),
);
} else { } else {
list.add(UrlElement(originalUrl, null, originText)); list.add(UrlElement(url, url, originText));
} }
if (end != null) { if (end != null) {

View File

@ -11,13 +11,18 @@ class ItemText extends StatelessWidget {
const ItemText({ const ItemText({
required this.item, required this.item,
required this.textScaleFactor, required this.textScaleFactor,
required this.selectable,
super.key, super.key,
this.onTap, this.onTap,
}); });
final Item item; final Item item;
final VoidCallback? onTap;
final double textScaleFactor; final double textScaleFactor;
final bool selectable;
/// Reserved for collapsing a comment tile when
/// [CollapseModePreference] is enabled;
final VoidCallback? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -30,7 +35,18 @@ class ItemText extends StatelessWidget {
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
color: Palette.orange, 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( return SelectableText.rich(
buildTextSpan( buildTextSpan(
(item as Buildable).elements, (item as Buildable).elements,
@ -40,6 +56,7 @@ class ItemText extends StatelessWidget {
), ),
onTap: onTap, onTap: onTap,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
onSelectionChanged: onSelectionChanged,
contextMenuBuilder: ( contextMenuBuilder: (
BuildContext context, BuildContext context,
EditableTextState editableTextState, EditableTextState editableTextState,
@ -52,24 +69,30 @@ class ItemText extends StatelessWidget {
semanticsLabel: item.text, semanticsLabel: item.text,
); );
} else { } else {
return SelectableLinkify( 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, text: item.text,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
style: style, style: style,
linkStyle: linkStyle, linkStyle: linkStyle,
onOpen: (LinkableElement link) => LinkUtil.launch(link.url), onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
onTap: onTap,
contextMenuBuilder: (
BuildContext context,
EditableTextState editableTextState,
) =>
contextMenuBuilder(
context,
editableTextState,
item: item,
), ),
semanticsLabel: item.text,
); );
} }
} }
}
} }

View File

@ -272,7 +272,7 @@ class _CommentTile extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Text( child: Text(
comment.text, comment.text.trimLeft(),
style: TextStyle( style: TextStyle(
fontSize: fontSize, fontSize: fontSize,
), ),

View File

@ -28,7 +28,8 @@ class StoriesListView extends StatefulWidget {
State<StoriesListView> createState() => _StoriesListViewState(); State<StoriesListView> createState() => _StoriesListViewState();
} }
class _StoriesListViewState extends State<StoriesListView> { class _StoriesListViewState extends State<StoriesListView>
with ItemActionMixin {
final RefreshController refreshController = RefreshController(); final RefreshController refreshController = RefreshController();
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();

View File

@ -6,6 +6,7 @@ class CollapseCache {
final Map<int, Set<int>> _hidden = <int, Set<int>>{}; final Map<int, Set<int>> _hidden = <int, Set<int>>{};
final PublishSubject<Map<int, Set<int>>> _hiddenCommentsSubject = final PublishSubject<Map<int, Set<int>>> _hiddenCommentsSubject =
PublishSubject<Map<int, Set<int>>>(); PublishSubject<Map<int, Set<int>>>();
int? lockedId;
Stream<Map<int, Set<int>>> get hiddenComments => Stream<Map<int, Set<int>>> get hiddenComments =>
_hiddenCommentsSubject.stream; _hiddenCommentsSubject.stream;

View File

@ -39,10 +39,6 @@ abstract class HtmlUtil {
RegExp(r'\<i\>(.*?)\<\/i\>'), RegExp(r'\<i\>(.*?)\<\/i\>'),
(Match match) => '*${match[1]}*', (Match match) => '*${match[1]}*',
) )
.replaceAllMapped(
RegExp(r'\<pre\>\<code\>(.*?)\<\/code\>\<\/pre\>', dotAll: true),
(Match match) => match[1]?.trimRight() ?? '',
)
.replaceAllMapped( .replaceAllMapped(
RegExp(r'\<a href=\"(.*?)\".*?\>.*?\<\/a\>'), RegExp(r'\<a href=\"(.*?)\".*?\>.*?\<\/a\>'),
(Match match) => match[1] ?? '', (Match match) => match[1] ?? '',

View File

@ -1,27 +1,20 @@
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart'; import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart';
import 'package:linkify/linkify.dart' hide UrlLinkifier;
abstract class LinkifierUtil { abstract class LinkifierUtil {
static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false); static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false);
static List<LinkifyElement> linkify(String text) { static List<LinkifyElement> linkify(String text) {
const List<Linkifier> linkifiers = <Linkifier>[
UrlLinkifier(),
EmailLinkifier(),
QuoteLinkifier(),
EmphasisLinkifier(),
];
List<LinkifyElement> list = <LinkifyElement>[TextElement(text)]; List<LinkifyElement> list = <LinkifyElement>[TextElement(text)];
if (text.isEmpty) { if (text.isEmpty) {
return <LinkifyElement>[]; return <LinkifyElement>[];
} }
if (linkifiers.isEmpty) { if (defaultLinkifiers.isEmpty) {
return list; return list;
} }
for (final Linkifier linkifier in linkifiers) { for (final Linkifier linkifier in defaultLinkifiers) {
list = linkifier.parse(list, linkifyOptions); list = linkifier.parse(list, linkifyOptions);
} }

View File

@ -13,10 +13,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: adaptive_theme name: adaptive_theme
sha256: "2d9bfee4240cdfad1b169cb43ac38fb49487e7fe1cc845e2973d4cef1780c0f6" sha256: "28df95a6b86993b38a51ee97d33a9f1d845fd1c7320c21c5d5e2183b5605e152"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0" version: "3.4.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@ -45,10 +45,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: badges name: badges
sha256: "6e7f3ec561ec08f47f912cfe349d4a1707afdc8dda271e17b046aa6d42c89e77" sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "3.1.2"
bloc: bloc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -77,26 +77,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cached_network_image name: cached_network_image
sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.3" version: "3.3.0"
cached_network_image_platform_interface: cached_network_image_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_platform_interface name: cached_network_image_platform_interface
sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "3.0.0"
cached_network_image_web: cached_network_image_web:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_web name: cached_network_image_web
sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.1.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -165,10 +165,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cross_file name: cross_file
sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.3+4" version: "0.3.3+5"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -221,10 +221,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197 sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.3.2" version: "5.3.3"
equatable: equatable:
dependency: "direct main" dependency: "direct main"
description: description:
@ -287,14 +287,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.3" 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: flutter_cache_manager:
dependency: "direct main" dependency: "direct main"
description: description:
@ -312,10 +304,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_email_sender name: flutter_email_sender
sha256: "52b713a67a966be4d9e6f68a323fc0a5bc2da71c567eb451af1aa90d30adbc3a" sha256: "5001e9158f91a8799140fb30a11ad89cd587244f30b4f848d87085985c49b60f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.1" version: "6.0.2"
flutter_fadein: flutter_fadein:
dependency: "direct main" dependency: "direct main"
description: description:
@ -344,10 +336,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_local_notifications name: flutter_local_notifications
sha256: "3cc40fe8c50ab8383f3e053a499f00f975636622ecdc8e20a77418ece3b1e975" sha256: "3002092e5b8ce2f86c3361422e52e6db6776c23ee21e0b2f71b892bf4259ef04"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.1.0+1" version: "15.1.1"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: transitive
description: description:
@ -368,50 +360,50 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_secure_storage name: flutter_secure_storage
sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5" sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.0" version: "9.0.0"
flutter_secure_storage_linux: flutter_secure_storage_linux:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_linux name: flutter_secure_storage_linux
sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.3" version: "1.2.0"
flutter_secure_storage_macos: flutter_secure_storage_macos:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_macos name: flutter_secure_storage_macos
sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50" sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.1"
flutter_secure_storage_platform_interface: flutter_secure_storage_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_platform_interface name: flutter_secure_storage_platform_interface
sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.2"
flutter_secure_storage_web: flutter_secure_storage_web:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_web name: flutter_secure_storage_web
sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
flutter_secure_storage_windows: flutter_secure_storage_windows:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_windows name: flutter_secure_storage_windows
sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "3.0.0"
flutter_siri_suggestions: flutter_siri_suggestions:
dependency: "direct main" dependency: "direct main"
description: description:
@ -471,10 +463,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: get_it name: get_it
sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468" sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.6.0" version: "7.6.4"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -487,10 +479,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "5668e6d3dbcb2d0dfa25f7567554b88c57e1e3f3c440b672b24d4a9477017d5b" sha256: a07c781bf55bf11ae85133338e4850f0b4e33e261c44a66c750fc707d65d8393
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.2" version: "11.1.2"
hive: hive:
dependency: "direct main" dependency: "direct main"
description: description:
@ -603,10 +595,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: logger name: logger
sha256: "66cb048220ca51cf9011da69fa581e4ee2bed4be6e82870d9e9baae75739da49" sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.2+1"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@ -691,10 +683,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: octo_image name: octo_image
sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "2.0.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@ -731,50 +723,50 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
path_provider_android: path_provider_android:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider_android name: path_provider_android
sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.2.0"
path_provider_foundation: path_provider_foundation:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -795,10 +787,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: plugin_platform_interface name: plugin_platform_interface
sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.1.6"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -924,58 +916,58 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
shared_preferences_android: shared_preferences_android:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
shared_preferences_foundation: shared_preferences_foundation:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.3" version: "2.3.4"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_linux name: shared_preferences_linux
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_web name: shared_preferences_web
sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
shared_preferences_windows: shared_preferences_windows:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_windows name: shared_preferences_windows
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@ -1168,66 +1160,66 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.12" version: "6.1.14"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.38" version: "6.1.0"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.4" version: "6.1.5"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_linux name: url_launcher_linux
sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.6"
url_launcher_macos: url_launcher_macos:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.0.7"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_platform_interface name: url_launcher_platform_interface
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.5"
url_launcher_web: url_launcher_web:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.18" version: "2.0.20"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.8"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
@ -1248,10 +1240,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: very_good_analysis name: very_good_analysis
sha256: "5e4ea72d2a9188630f0dd8f120a541de730090ef8863243fedca8267a84508b8" sha256: "9ae7f3a3bd5764fb021b335ca28a34f040cd0ab6eec00a1b213b445dae58a4b8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0+1" version: "5.1.0"
visibility_detector: visibility_detector:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1344,58 +1336,58 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webkit_inspection_protocol name: webkit_inspection_protocol
sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.1"
webview_flutter: webview_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: webview_flutter name: webview_flutter
sha256: "789d52bd789373cc1e100fb634af2127e86c99cf9abde09499743270c5de8d00" sha256: "82f6787d5df55907aa01e49bd9644f4ed1cc82af7a8257dd9947815959d2e755"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.2" version: "4.2.4"
webview_flutter_android: webview_flutter_android:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_android name: webview_flutter_android
sha256: bca797abba472868655b5f1a6029c1132385685ee9db4713cb0e7f33076210c6 sha256: ddc167c6676f57c8b367d19fcbee267d6dc6adf81bd6c3cb87981d30746e0a6d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.3" version: "3.10.1"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_platform_interface name: webview_flutter_platform_interface
sha256: "0ca3cfcc6781a7de701d580917af4a9efc4e3e129f8ead95a80587f0a749480a" sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.0" version: "2.6.0"
webview_flutter_wkwebview: webview_flutter_wkwebview:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_wkwebview name: webview_flutter_wkwebview
sha256: ed749f94ac9e814d04a258a9255cf69cfa4cc6006ff59542aea7fb4590144972 sha256: "485af05f2c5f83c7f78c20e236b170ad02df7153b299ae9917345be43871d29f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.7.3" version: "3.8.0"
win32: win32:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
name: win32 name: win32
sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.6" version: "5.0.9"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:
name: win32_registry name: win32_registry
sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
workmanager: workmanager:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1408,10 +1400,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.3"
xml: xml:
dependency: transitive dependency: transitive
description: description:
@ -1429,5 +1421,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.1.0-185.0.dev <4.0.0" dart: ">=3.1.0 <4.0.0"
flutter: ">=3.13.4" flutter: ">=3.13.6"

View File

@ -1,11 +1,11 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 1.9.2+123 version: 1.9.3+124
publish_to: none publish_to: none
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
flutter: "3.13.4" flutter: "3.13.6"
dependencies: dependencies:
adaptive_theme: ^3.2.0 adaptive_theme: ^3.2.0
@ -32,13 +32,13 @@ dependencies:
flutter_feather_icons: 2.0.0+1 flutter_feather_icons: 2.0.0+1
flutter_inappwebview: ^5.7.2+3 flutter_inappwebview: ^5.7.2+3
flutter_local_notifications: ^15.1.0+1 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_siri_suggestions: ^2.1.0
flutter_slidable: ^3.0.0 flutter_slidable: ^3.0.0
font_awesome_flutter: ^10.3.0 font_awesome_flutter: ^10.3.0
gbk_codec: ^0.4.0 gbk_codec: ^0.4.0
get_it: ^7.2.0 get_it: ^7.2.0
go_router: ^10.1.2 go_router: ^11.1.2
hive: ^2.2.3 hive: ^2.2.3
html: ^0.15.1 html: ^0.15.1
html_unescape: ^2.0.0 html_unescape: ^2.0.0