mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
9986f72e11 | |||
ef557e7b84 |
@ -133,6 +133,8 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
StoriesRefresh event,
|
StoriesRefresh event,
|
||||||
Emitter<StoriesState> emit,
|
Emitter<StoriesState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
if (state.statusByType[event.type] == StoriesStatus.loading) return;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWithStatusUpdated(
|
state.copyWithStatusUpdated(
|
||||||
type: event.type,
|
type: event.type,
|
||||||
|
@ -39,8 +39,9 @@ abstract class Constants {
|
|||||||
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
|
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
|
||||||
static const String featureLogIn = 'log_in';
|
static const String featureLogIn = 'log_in';
|
||||||
static const String featurePinToTop = 'pin_to_top';
|
static const String featurePinToTop = 'pin_to_top';
|
||||||
static const String featureJumpUpButton = 'jump_up_button';
|
static const String featureJumpUpButton = 'jump_up_button_with_long_press';
|
||||||
static const String featureJumpDownButton = 'jump_down_button';
|
static const String featureJumpDownButton =
|
||||||
|
'jump_down_button_with_long_press';
|
||||||
|
|
||||||
static final String happyFace = <String>[
|
static final String happyFace = <String>[
|
||||||
'(๑•̀ㅂ•́)و✧',
|
'(๑•̀ㅂ•́)و✧',
|
||||||
|
@ -349,8 +349,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
init(useCommentCache: true);
|
init(useCommentCache: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Jump to next root level comment.
|
/// Scroll to next root level comment.
|
||||||
void jump(
|
void scrollToNextRoot(
|
||||||
ItemScrollController itemScrollController,
|
ItemScrollController itemScrollController,
|
||||||
ItemPositionsListener itemPositionsListener,
|
ItemPositionsListener itemPositionsListener,
|
||||||
) {
|
) {
|
||||||
@ -387,8 +387,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Jump to previous root level comment.
|
/// Scroll to previous root level comment.
|
||||||
void jumpUp(
|
void scrollToPreviousRoot(
|
||||||
ItemScrollController itemScrollController,
|
ItemScrollController itemScrollController,
|
||||||
ItemPositionsListener itemPositionsListener,
|
ItemPositionsListener itemPositionsListener,
|
||||||
) {
|
) {
|
||||||
|
@ -27,7 +27,7 @@ class PinCubit extends Cubit<PinState> {
|
|||||||
emit(state.copyWith(pinnedStoriesIds: ids));
|
emit(state.copyWith(pinnedStoriesIds: ids));
|
||||||
|
|
||||||
_storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched);
|
_storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched);
|
||||||
});
|
}).whenComplete(() => emit(state.copyWith(status: Status.loaded)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void pinStory(Story story) {
|
void pinStory(Story story) {
|
||||||
@ -52,7 +52,10 @@ class PinCubit extends Cubit<PinState> {
|
|||||||
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
|
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh() => init();
|
void refresh() {
|
||||||
|
if (state.status == Status.loading) return;
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
void _onStoryFetched(Story story) {
|
void _onStoryFetched(Story story) {
|
||||||
emit(state.copyWith(pinnedStories: <Story>[...state.pinnedStories, story]));
|
emit(state.copyWith(pinnedStories: <Story>[...state.pinnedStories, story]));
|
||||||
|
@ -4,22 +4,27 @@ class PinState extends Equatable {
|
|||||||
const PinState({
|
const PinState({
|
||||||
required this.pinnedStoriesIds,
|
required this.pinnedStoriesIds,
|
||||||
required this.pinnedStories,
|
required this.pinnedStories,
|
||||||
|
required this.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
PinState.init()
|
PinState.init()
|
||||||
: pinnedStoriesIds = <int>[],
|
: pinnedStoriesIds = <int>[],
|
||||||
pinnedStories = <Story>[];
|
pinnedStories = <Story>[],
|
||||||
|
status = Status.idle;
|
||||||
|
|
||||||
final List<int> pinnedStoriesIds;
|
final List<int> pinnedStoriesIds;
|
||||||
final List<Story> pinnedStories;
|
final List<Story> pinnedStories;
|
||||||
|
final Status status;
|
||||||
|
|
||||||
PinState copyWith({
|
PinState copyWith({
|
||||||
List<int>? pinnedStoriesIds,
|
List<int>? pinnedStoriesIds,
|
||||||
List<Story>? pinnedStories,
|
List<Story>? pinnedStories,
|
||||||
|
Status? status,
|
||||||
}) {
|
}) {
|
||||||
return PinState(
|
return PinState(
|
||||||
pinnedStoriesIds: pinnedStoriesIds ?? this.pinnedStoriesIds,
|
pinnedStoriesIds: pinnedStoriesIds ?? this.pinnedStoriesIds,
|
||||||
pinnedStories: pinnedStories ?? this.pinnedStories,
|
pinnedStories: pinnedStories ?? this.pinnedStories,
|
||||||
|
status: status ?? this.status,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,5 +32,6 @@ class PinState extends Equatable {
|
|||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
pinnedStoriesIds,
|
pinnedStoriesIds,
|
||||||
pinnedStories,
|
pinnedStories,
|
||||||
|
status,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -289,7 +289,9 @@ class HackiApp extends StatelessWidget {
|
|||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'Hacki',
|
title: 'Hacki',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: useTrueDark ? trueDarkTheme : theme,
|
theme: (useTrueDark ? trueDarkTheme : theme).copyWith(
|
||||||
|
useMaterial3: false,
|
||||||
|
),
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
navigatorObservers: <NavigatorObserver>[
|
navigatorObservers: <NavigatorObserver>[
|
||||||
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
|
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
|
||||||
|
@ -4,7 +4,8 @@ enum FontSize {
|
|||||||
small('Small', TextDimens.pt15),
|
small('Small', TextDimens.pt15),
|
||||||
regular('Regular', TextDimens.pt16),
|
regular('Regular', TextDimens.pt16),
|
||||||
large('Large', TextDimens.pt17),
|
large('Large', TextDimens.pt17),
|
||||||
xlarge('XLarge', TextDimens.pt18);
|
xlarge('XLarge', TextDimens.pt18),
|
||||||
|
xxlarge('XXLarge', TextDimens.pt19);
|
||||||
|
|
||||||
const FontSize(this.description, this.fontSize);
|
const FontSize(this.description, this.fontSize);
|
||||||
|
|
||||||
|
@ -7,5 +7,6 @@ export 'item/item.dart';
|
|||||||
export 'post_data.dart';
|
export 'post_data.dart';
|
||||||
export 'preference.dart';
|
export 'preference.dart';
|
||||||
export 'search_params.dart';
|
export 'search_params.dart';
|
||||||
|
export 'status.dart';
|
||||||
export 'story_type.dart';
|
export 'story_type.dart';
|
||||||
export 'user.dart';
|
export 'user.dart';
|
||||||
|
6
lib/models/status.dart
Normal file
6
lib/models/status.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
enum Status {
|
||||||
|
idle,
|
||||||
|
loading,
|
||||||
|
loaded,
|
||||||
|
error,
|
||||||
|
}
|
@ -43,6 +43,7 @@ class CustomAppBar extends AppBar {
|
|||||||
fontFamily: FeatherIcons.type.fontFamily,
|
fontFamily: FeatherIcons.type.fontFamily,
|
||||||
package: FeatherIcons.type.fontPackage,
|
package: FeatherIcons.type.fontPackage,
|
||||||
),
|
),
|
||||||
|
textScaleFactor: 1,
|
||||||
),
|
),
|
||||||
onPressed: onFontSizeTap,
|
onPressed: onFontSizeTap,
|
||||||
),
|
),
|
||||||
|
@ -32,30 +32,32 @@ class CustomFloatingActionButton extends StatelessWidget {
|
|||||||
Icons.keyboard_arrow_up,
|
Icons.keyboard_arrow_up,
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
),
|
),
|
||||||
title: const Text('Jump to previous root level comment.'),
|
title: const Text('Shortcut'),
|
||||||
description: const Text(
|
description: const Text(
|
||||||
'''Tapping on this button will take you to the previous off-screen root level comment.''',
|
'''Tapping on this button will take you to the previous off-screen root level comment.\n\nLong press on it to jump to the very beginning of this thread.''',
|
||||||
),
|
),
|
||||||
child: FloatingActionButton.small(
|
child: InkWell(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
onLongPress: () => itemScrollController.scrollTo(
|
||||||
|
index: 0,
|
||||||
|
duration: Durations.ms400,
|
||||||
|
),
|
||||||
|
child: FloatingActionButton.small(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
|
||||||
/// Randomly generated string as heroTag to prevent
|
/// Randomly generated string as heroTag to prevent
|
||||||
/// default [FloatingActionButton] animation.
|
/// default [FloatingActionButton] animation.
|
||||||
heroTag: UniqueKey().hashCode,
|
heroTag: UniqueKey().hashCode,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (state.status == CommentsStatus.loading) return;
|
HapticFeedbackUtil.selection();
|
||||||
|
context.read<CommentsCubit>().scrollToPreviousRoot(
|
||||||
HapticFeedbackUtil.selection();
|
itemScrollController,
|
||||||
context.read<CommentsCubit>().jumpUp(
|
itemPositionsListener,
|
||||||
itemScrollController,
|
);
|
||||||
itemPositionsListener,
|
},
|
||||||
);
|
child: Icon(
|
||||||
},
|
Icons.keyboard_arrow_up,
|
||||||
child: Icon(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
Icons.keyboard_arrow_up,
|
),
|
||||||
color: state.status == CommentsStatus.loading
|
|
||||||
? Palette.grey
|
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -65,29 +67,31 @@ class CustomFloatingActionButton extends StatelessWidget {
|
|||||||
Icons.keyboard_arrow_down,
|
Icons.keyboard_arrow_down,
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
),
|
),
|
||||||
title: const Text('Jump to next root level comment.'),
|
title: const Text('Shortcut'),
|
||||||
description: const Text(
|
description: const Text(
|
||||||
'''Tapping on this button will take you to the next off-screen root level comment.''',
|
'''Tapping on this button will take you to the next off-screen root level comment.\n\nLong press on it to jump to the end of this thread.''',
|
||||||
),
|
),
|
||||||
child: FloatingActionButton.small(
|
child: InkWell(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
onLongPress: () => itemScrollController.scrollTo(
|
||||||
|
index: state.comments.length,
|
||||||
|
duration: Durations.ms400,
|
||||||
|
),
|
||||||
|
child: FloatingActionButton.small(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
|
||||||
/// Same as above.
|
/// Same as above.
|
||||||
heroTag: UniqueKey().hashCode,
|
heroTag: UniqueKey().hashCode,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (state.status == CommentsStatus.loading) return;
|
HapticFeedbackUtil.selection();
|
||||||
|
context.read<CommentsCubit>().scrollToNextRoot(
|
||||||
HapticFeedbackUtil.selection();
|
itemScrollController,
|
||||||
context.read<CommentsCubit>().jump(
|
itemPositionsListener,
|
||||||
itemScrollController,
|
);
|
||||||
itemPositionsListener,
|
},
|
||||||
);
|
child: Icon(
|
||||||
},
|
Icons.keyboard_arrow_down,
|
||||||
child: Icon(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
Icons.keyboard_arrow_down,
|
),
|
||||||
color: state.status == CommentsStatus.loading
|
|
||||||
? Palette.grey
|
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -255,6 +255,8 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Palette.orange,
|
color: Palette.orange,
|
||||||
),
|
),
|
||||||
|
textScaleFactor:
|
||||||
|
MediaQuery.of(context).textScaleFactor,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
@ -262,6 +264,8 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Palette.grey,
|
color: Palette.grey,
|
||||||
),
|
),
|
||||||
|
textScaleFactor:
|
||||||
|
MediaQuery.of(context).textScaleFactor,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -334,9 +338,8 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
textScaleFactor: MediaQuery.of(
|
textScaleFactor:
|
||||||
context,
|
MediaQuery.of(context).textScaleFactor,
|
||||||
).textScaleFactor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -354,6 +357,8 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: ItemText(
|
child: ItemText(
|
||||||
item: state.item,
|
item: state.item,
|
||||||
|
textScaleFactor:
|
||||||
|
MediaQuery.of(context).textScaleFactor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -402,6 +407,7 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: TextDimens.pt13,
|
fontSize: TextDimens.pt13,
|
||||||
),
|
),
|
||||||
|
textScaleFactor: 1,
|
||||||
),
|
),
|
||||||
] else ...<Widget>[
|
] else ...<Widget>[
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@ -444,6 +450,7 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: TextDimens.pt13,
|
fontSize: TextDimens.pt13,
|
||||||
),
|
),
|
||||||
|
textScaleFactor: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -462,6 +469,7 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: TextDimens.pt13,
|
fontSize: TextDimens.pt13,
|
||||||
),
|
),
|
||||||
|
textScaleFactor: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -483,6 +491,7 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: TextDimens.pt13,
|
fontSize: TextDimens.pt13,
|
||||||
),
|
),
|
||||||
|
textScaleFactor: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -345,6 +345,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: ItemText(
|
child: ItemText(
|
||||||
item: replyingTo,
|
item: replyingTo,
|
||||||
|
textScaleFactor:
|
||||||
|
MediaQuery.of(context).textScaleFactor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -143,6 +143,8 @@ class CommentTile extends StatelessWidget {
|
|||||||
? orange
|
? orange
|
||||||
: color,
|
: color,
|
||||||
),
|
),
|
||||||
|
textScaleFactor:
|
||||||
|
MediaQuery.of(context).textScaleFactor,
|
||||||
),
|
),
|
||||||
if (comment.by == opUsername)
|
if (comment.by == opUsername)
|
||||||
const Text(
|
const Text(
|
||||||
@ -157,6 +159,8 @@ class CommentTile extends StatelessWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Palette.grey,
|
color: Palette.grey,
|
||||||
),
|
),
|
||||||
|
textScaleFactor:
|
||||||
|
MediaQuery.of(context).textScaleFactor,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -196,6 +200,8 @@ class CommentTile extends StatelessWidget {
|
|||||||
child: ItemText(
|
child: ItemText(
|
||||||
key: ValueKey<int>(comment.id),
|
key: ValueKey<int>(comment.id),
|
||||||
item: comment,
|
item: comment,
|
||||||
|
textScaleFactor: MediaQuery.of(context)
|
||||||
|
.textScaleFactor,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (onTap == null) {
|
if (onTap == null) {
|
||||||
_onTextTapped(context);
|
_onTextTapped(context);
|
||||||
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
||||||
import 'package:hacki/styles/palette.dart';
|
import 'package:hacki/styles/palette.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
import 'package:linkify/linkify.dart';
|
import 'package:linkify/linkify.dart' hide UrlLinkifier;
|
||||||
|
|
||||||
export 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
export 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
||||||
export 'package:linkify/linkify.dart'
|
export 'package:linkify/linkify.dart'
|
||||||
@ -14,9 +14,7 @@ export 'package:linkify/linkify.dart'
|
|||||||
Linkifier,
|
Linkifier,
|
||||||
LinkifyElement,
|
LinkifyElement,
|
||||||
LinkifyOptions,
|
LinkifyOptions,
|
||||||
TextElement,
|
TextElement;
|
||||||
UrlElement,
|
|
||||||
UrlLinkifier;
|
|
||||||
|
|
||||||
/// Callback clicked link
|
/// Callback clicked link
|
||||||
typedef LinkCallback = void Function(LinkableElement link);
|
typedef LinkCallback = void Function(LinkableElement link);
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export 'emphasis_linkifier.dart';
|
export 'emphasis_linkifier.dart';
|
||||||
export 'quote_linkifier.dart';
|
export 'quote_linkifier.dart';
|
||||||
|
export 'url_linkifier.dart';
|
||||||
|
121
lib/screens/widgets/custom_linkify/linkifiers/url_linkifier.dart
Normal file
121
lib/screens/widgets/custom_linkify/linkifiers/url_linkifier.dart
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:linkify/linkify.dart';
|
||||||
|
|
||||||
|
final RegExp _urlRegex = RegExp(
|
||||||
|
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[\/\\\%:\?=&#@;A-Za-z0-9_.~-]*)',
|
||||||
|
caseSensitive: false,
|
||||||
|
dotAll: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final RegExp _looseUrlRegex = RegExp(
|
||||||
|
r'''^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//="'`]*))''',
|
||||||
|
caseSensitive: false,
|
||||||
|
dotAll: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final RegExp _protocolIdentifierRegex = RegExp(
|
||||||
|
r'^(https?:\/\/)',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
class UrlLinkifier extends Linkifier {
|
||||||
|
const UrlLinkifier();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<LinkifyElement> parse(
|
||||||
|
List<LinkifyElement> elements,
|
||||||
|
LinkifyOptions options,
|
||||||
|
) {
|
||||||
|
final List<LinkifyElement> list = <LinkifyElement>[];
|
||||||
|
|
||||||
|
for (final LinkifyElement element in elements) {
|
||||||
|
if (element is TextElement) {
|
||||||
|
final RegExpMatch? match = options.looseUrl
|
||||||
|
? _looseUrlRegex.firstMatch(element.text)
|
||||||
|
: _urlRegex.firstMatch(element.text);
|
||||||
|
|
||||||
|
if (match == null) {
|
||||||
|
list.add(element);
|
||||||
|
} else {
|
||||||
|
final String text = element.text.replaceFirst(match.group(0)!, '');
|
||||||
|
|
||||||
|
if (match.group(1)?.isNotEmpty ?? false) {
|
||||||
|
list.add(TextElement(match.group(1)!));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.group(2)?.isNotEmpty ?? false) {
|
||||||
|
String originalUrl = match.group(2)!;
|
||||||
|
String originText = originalUrl;
|
||||||
|
String? end;
|
||||||
|
|
||||||
|
if ((options.excludeLastPeriod) &&
|
||||||
|
originalUrl[originalUrl.length - 1] == '.') {
|
||||||
|
end = '.';
|
||||||
|
originText = originText.substring(0, originText.length - 1);
|
||||||
|
originalUrl = originalUrl.substring(0, originalUrl.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = originalUrl;
|
||||||
|
|
||||||
|
if (!originalUrl.startsWith(_protocolIdentifierRegex)) {
|
||||||
|
originalUrl = (options.defaultToHttps ? 'https://' : 'http://') +
|
||||||
|
originalUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((options.humanize) || (options.removeWww)) {
|
||||||
|
if (options.humanize) {
|
||||||
|
url = url.replaceFirst(RegExp('https?://'), '');
|
||||||
|
}
|
||||||
|
if (options.removeWww) {
|
||||||
|
url = url.replaceFirst(RegExp(r'www\.'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
list.add(
|
||||||
|
UrlElement(
|
||||||
|
originalUrl,
|
||||||
|
url,
|
||||||
|
originText,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
list.add(UrlElement(originalUrl, null, originText));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end != null) {
|
||||||
|
list.add(TextElement(end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.isNotEmpty) {
|
||||||
|
list.addAll(parse(<LinkifyElement>[TextElement(text)], options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
list.add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents an element containing a link
|
||||||
|
@immutable
|
||||||
|
class UrlElement extends LinkableElement {
|
||||||
|
UrlElement(String url, [String? text, String? originText])
|
||||||
|
: super(text, url, originText);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "LinkElement: '$url' ($text)";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => equals(other);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(text, originText, url);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool equals(dynamic other) => other is UrlElement && super.equals(other);
|
||||||
|
}
|
@ -10,12 +10,14 @@ import 'package:hacki/utils/utils.dart';
|
|||||||
class ItemText extends StatelessWidget {
|
class ItemText extends StatelessWidget {
|
||||||
const ItemText({
|
const ItemText({
|
||||||
required this.item,
|
required this.item,
|
||||||
|
required this.textScaleFactor,
|
||||||
super.key,
|
super.key,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Item item;
|
final Item item;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
final double textScaleFactor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -37,7 +39,7 @@ class ItemText extends StatelessWidget {
|
|||||||
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
textScaleFactor: textScaleFactor,
|
||||||
contextMenuBuilder: (
|
contextMenuBuilder: (
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
EditableTextState editableTextState,
|
EditableTextState editableTextState,
|
||||||
@ -52,7 +54,7 @@ class ItemText extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
return SelectableLinkify(
|
return SelectableLinkify(
|
||||||
text: item.text,
|
text: item.text,
|
||||||
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
textScaleFactor: textScaleFactor,
|
||||||
style: style,
|
style: style,
|
||||||
linkStyle: linkStyle,
|
linkStyle: linkStyle,
|
||||||
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
||||||
|
@ -33,6 +33,7 @@ abstract class TextDimens {
|
|||||||
static const double pt16 = 16;
|
static const double pt16 = 16;
|
||||||
static const double pt17 = 17;
|
static const double pt17 = 17;
|
||||||
static const double pt18 = 18;
|
static const double pt18 = 18;
|
||||||
|
static const double pt19 = 19;
|
||||||
static const double pt20 = 20;
|
static const double pt20 = 20;
|
||||||
static const double pt24 = 24;
|
static const double pt24 = 24;
|
||||||
static const double pt26 = 26;
|
static const double pt26 = 26;
|
||||||
|
@ -2,7 +2,6 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/main.dart';
|
import 'package:hacki/main.dart';
|
||||||
@ -54,17 +53,7 @@ abstract class LinkUtil {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri rinseLink(String link) {
|
final Uri uri = Uri.parse(link);
|
||||||
final RegExp regex = RegExp(RegExpConstants.linkSuffix);
|
|
||||||
if (!link.contains('en.wikipedia.org') && link.contains(regex)) {
|
|
||||||
final String match = regex.stringMatch(link) ?? '';
|
|
||||||
return Uri.parse(link.replaceAll(match, ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Uri.parse(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Uri uri = rinseLink(link);
|
|
||||||
canLaunchUrl(uri).then((bool val) {
|
canLaunchUrl(uri).then((bool val) {
|
||||||
if (val) {
|
if (val) {
|
||||||
if (link.contains('http')) {
|
if (link.contains('http')) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
||||||
import 'package:linkify/linkify.dart';
|
import 'package:linkify/linkify.dart' hide UrlLinkifier;
|
||||||
|
|
||||||
abstract class LinkifierUtil {
|
abstract class LinkifierUtil {
|
||||||
static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false);
|
static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false);
|
||||||
|
@ -1414,4 +1414,4 @@ packages:
|
|||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.0.0 <4.0.0"
|
dart: ">=3.0.0 <4.0.0"
|
||||||
flutter: ">=3.10.5"
|
flutter: ">=3.10.6"
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 1.8.0+116
|
version: 1.8.2+118
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
flutter: "3.10.5"
|
flutter: "3.10.6"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
adaptive_theme: ^3.2.0
|
adaptive_theme: ^3.2.0
|
||||||
|
Submodule submodules/flutter updated: 796c8ef792...f468f3366c
Reference in New Issue
Block a user