Compare commits

...

1 Commits

Author SHA1 Message Date
ef557e7b84 fix text scaling and url parsing. (#237) 2023-07-10 10:18:12 -07:00
18 changed files with 176 additions and 27 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

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

View File

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

View File

@ -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,
), ),
), ),
) )

View File

@ -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,
), ),
), ),
), ),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart'; import 'package:hacki/main.dart';
@ -54,17 +53,7 @@ abstract class LinkUtil {
return; return;
} }
Uri rinseLink(String link) { final Uri uri = Uri.parse(link);
final RegExp regex = RegExp(RegExpConstants.linkSuffix);
if (!link.contains('en.wikipedia.org') && link.contains(regex)) {
final String match = regex.stringMatch(link) ?? '';
return Uri.parse(link.replaceAll(match, ''));
}
return Uri.parse(link);
}
final Uri uri = rinseLink(link);
canLaunchUrl(uri).then((bool val) { canLaunchUrl(uri).then((bool val) {
if (val) { if (val) {
if (link.contains('http')) { if (link.contains('http')) {

View File

@ -1,5 +1,5 @@
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart'; import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
import 'package:linkify/linkify.dart'; import 'package:linkify/linkify.dart' hide UrlLinkifier;
abstract class LinkifierUtil { abstract class LinkifierUtil {
static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false); static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false);

View File

@ -1,6 +1,6 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 1.8.0+116 version: 1.8.1+117
publish_to: none publish_to: none
environment: environment: