mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
d1c8eed3de | |||
aa6a2c684c | |||
d4778d9530 | |||
c702e08481 |
@ -50,7 +50,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.jiaqifeng.hacki"
|
||||
minSdkVersion 26
|
||||
minSdkVersion 30
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
@ -64,12 +64,15 @@ android {
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
flutter {
|
||||
|
@ -37,15 +37,6 @@
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<!-- Displays an Android View that continues showing the launch screen
|
||||
Drawable until Flutter paints its first frame, then this splash
|
||||
screen fades out. A splash screen is useful to avoid any visual
|
||||
gap between the end of Android's launch screen and the painting of
|
||||
Flutter's first frame. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
@ -160,6 +160,13 @@ class FavCubit extends Cubit<FavState> {
|
||||
});
|
||||
}
|
||||
|
||||
void removeAll() {
|
||||
_preferenceRepository
|
||||
..clearAllFavs(username: '')
|
||||
..clearAllFavs(username: _authBloc.state.username);
|
||||
emit(FavState.init());
|
||||
}
|
||||
|
||||
void _onItemLoaded(Item item) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
@ -6,6 +6,8 @@ import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
extension ContextExtension on BuildContext {
|
||||
bool get isScreenReaderEnabled => MediaQuery.of(this).accessibleNavigation;
|
||||
|
||||
T? tryRead<T>() {
|
||||
try {
|
||||
return read<T>();
|
||||
|
@ -1,5 +1,5 @@
|
||||
extension DateTimeExtension on DateTime {
|
||||
String toReadableString() {
|
||||
String toTimeAgoString() {
|
||||
final DateTime now = DateTime.now();
|
||||
final Duration diff = now.difference(this);
|
||||
if (diff.inDays > 365) {
|
||||
|
@ -5,4 +5,15 @@ extension ObjectExtension on Object {
|
||||
void log({String identifier = ''}) {
|
||||
locator.get<Logger>().d('$identifier ${toString()}');
|
||||
}
|
||||
|
||||
void logInfo({String identifier = ''}) {
|
||||
locator.get<Logger>().i('$identifier ${toString()}');
|
||||
}
|
||||
|
||||
void logError({
|
||||
String identifier = '',
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
locator.get<Logger>().e(identifier, this, stackTrace ?? StackTrace.current);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
||||
import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
|
||||
extension WidgetModifier on Widget {
|
||||
|
@ -24,7 +24,7 @@ class Comment extends Item {
|
||||
|
||||
final int level;
|
||||
|
||||
String get metadata => '''by $by $postedDate''';
|
||||
String get metadata => '''by $by $timeAgo''';
|
||||
|
||||
Comment copyWith({int? level}) {
|
||||
return Comment(
|
||||
|
@ -82,8 +82,8 @@ class Item extends Equatable {
|
||||
final List<int> kids;
|
||||
final List<int> parts;
|
||||
|
||||
String get postedDate =>
|
||||
DateTime.fromMillisecondsSinceEpoch(time * 1000).toReadableString();
|
||||
String get timeAgo =>
|
||||
DateTime.fromMillisecondsSinceEpoch(time * 1000).toTimeAgoString();
|
||||
|
||||
bool get isPoll => type == 'poll';
|
||||
|
||||
|
@ -43,10 +43,13 @@ class Story extends Item {
|
||||
Story.fromJson(super.json) : super.fromJson();
|
||||
|
||||
String get metadata =>
|
||||
'''$score point${score > 1 ? 's' : ''} by $by $postedDate | $descendants comment${descendants > 1 ? 's' : ''}''';
|
||||
'''$score point${score > 1 ? 's' : ''} by $by $timeAgo | $descendants comment${descendants > 1 ? 's' : ''}''';
|
||||
|
||||
String get screenReaderLabel =>
|
||||
'''$title at $readableUrl by $by $timeAgo. This story has $score point${score > 1 ? 's' : ''} and $descendants comment${descendants > 1 ? 's' : ''}''';
|
||||
|
||||
String get simpleMetadata =>
|
||||
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate''';
|
||||
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $timeAgo''';
|
||||
|
||||
String get readableUrl {
|
||||
final Uri url = Uri.parse(this.url);
|
||||
@ -55,10 +58,5 @@ class Story extends Item {
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
// final String prettyString =
|
||||
// const JsonEncoder.withIndent(' ').convert(this);
|
||||
// return 'Story $prettyString';
|
||||
return 'Story $id';
|
||||
}
|
||||
String toString() => 'Story $id';
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
||||
const NotificationModePreference(),
|
||||
const SwipeGesturePreference(),
|
||||
const CollapseModePreference(),
|
||||
NavigationModePreference(),
|
||||
const NavigationModePreference(),
|
||||
const ReaderModePreference(),
|
||||
const MarkReadStoriesModePreference(),
|
||||
const EyeCandyModePreference(),
|
||||
@ -54,8 +54,7 @@ abstract class IntPreference extends Preference<int> {
|
||||
const bool _notificationModeDefaultValue = true;
|
||||
const bool _swipeGestureModeDefaultValue = false;
|
||||
const bool _displayModeDefaultValue = true;
|
||||
const bool _navigationModeDefaultValueIOS = false;
|
||||
const bool _navigationModeDefaultValueAndroid = false;
|
||||
const bool _navigationModeDefaultValue = false;
|
||||
const bool _eyeCandyModeDefaultValue = false;
|
||||
const bool _trueDarkModeDefaultValue = false;
|
||||
const bool _readerModeDefaultValue = true;
|
||||
@ -193,12 +192,9 @@ class StoryUrlModePreference extends BooleanPreference {
|
||||
/// The value deciding whether or not user should be
|
||||
/// navigated to web view first. Defaults to false.
|
||||
class NavigationModePreference extends BooleanPreference {
|
||||
NavigationModePreference({bool? val})
|
||||
const NavigationModePreference({bool? val})
|
||||
: super(
|
||||
val: val ??
|
||||
(Platform.isAndroid
|
||||
? _navigationModeDefaultValueAndroid
|
||||
: _navigationModeDefaultValueIOS),
|
||||
val: val ?? _navigationModeDefaultValue,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -207,6 +207,23 @@ class PreferenceRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearAllFavs({required String username}) async {
|
||||
final String key = _getFavKey(username);
|
||||
|
||||
if (Platform.isIOS) {
|
||||
await _syncedPrefs.setStringList(
|
||||
key: key,
|
||||
val: <String>[],
|
||||
);
|
||||
} else {
|
||||
final SharedPreferences prefs = await _prefs;
|
||||
await prefs.setStringList(
|
||||
key,
|
||||
<String>[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static String _getFavKey(String username) => 'fav_$username';
|
||||
|
||||
//#endregion
|
||||
|
@ -24,9 +24,7 @@ class LinkIconButton extends StatelessWidget {
|
||||
featureId: Constants.featureOpenStoryInWebView,
|
||||
title: Text('Open in Browser'),
|
||||
description: Text(
|
||||
'Want more than just reading and replying? '
|
||||
'You can tap here to open this story in a '
|
||||
'browser.',
|
||||
'''You can tap here to open this story in browser.''',
|
||||
style: TextStyle(fontSize: TextDimens.pt16),
|
||||
),
|
||||
child: Icon(
|
||||
|
@ -286,7 +286,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.postedDate,
|
||||
state.item.timeAgo,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
@ -25,16 +23,8 @@ class MorePopupMenu extends StatelessWidget {
|
||||
final bool isBlocked;
|
||||
final VoidCallback onLoginTapped;
|
||||
|
||||
static double? _cachedStoryHeight;
|
||||
static double? _cachedCommentHeight;
|
||||
|
||||
static double get storyHeight {
|
||||
return _cachedStoryHeight ??= Platform.isIOS ? 500 : 530;
|
||||
}
|
||||
|
||||
static double get commentHeight {
|
||||
return _cachedCommentHeight ??= Platform.isIOS ? 480 : 520;
|
||||
}
|
||||
static const double _storySheetHeight = 500;
|
||||
static const double _commentSheetHeight = 480;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -80,7 +70,7 @@ class MorePopupMenu extends StatelessWidget {
|
||||
final bool upvoted = voteState.vote == Vote.up;
|
||||
final bool downvoted = voteState.vote == Vote.down;
|
||||
return Container(
|
||||
height: item is Comment ? commentHeight : storyHeight,
|
||||
height: item is Comment ? _commentSheetHeight : _storySheetHeight,
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: Material(
|
||||
color: Palette.transparent,
|
||||
|
@ -118,7 +118,7 @@ class InboxView extends StatelessWidget {
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
e.postedDate,
|
||||
e.timeAgo,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -21,7 +22,6 @@ import 'package:hacki/screens/profile/widgets/tab_bar_settings.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
@ -192,7 +192,7 @@ class _SettingsState extends State<Settings> {
|
||||
.whereType<BooleanPreference>()
|
||||
.where(
|
||||
(Preference<dynamic> e) => e.isDisplayable,
|
||||
))
|
||||
)) ...<Widget>[
|
||||
SwitchListTile(
|
||||
title: Text(preference.title),
|
||||
subtitle: preference.subtitle.isNotEmpty
|
||||
@ -217,6 +217,8 @@ class _SettingsState extends State<Settings> {
|
||||
},
|
||||
activeColor: Palette.orange,
|
||||
),
|
||||
if (preference is StoryUrlModePreference) const Divider(),
|
||||
],
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Font',
|
||||
@ -229,11 +231,24 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
onTap: showThemeSettingDialog,
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Clear Data',
|
||||
'Export Favorites',
|
||||
),
|
||||
onTap: showClearDataDialog,
|
||||
onTap: onExportFavoritesTapped,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Clear Favorites',
|
||||
),
|
||||
onTap: showClearFavoritesDialog,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Clear Cache',
|
||||
),
|
||||
onTap: showClearCacheDialog,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('About'),
|
||||
@ -376,12 +391,12 @@ class _SettingsState extends State<Settings> {
|
||||
);
|
||||
}
|
||||
|
||||
void showClearDataDialog() {
|
||||
void showClearCacheDialog() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
title: const Text('Clear Data?'),
|
||||
title: const Text('Clear Cache?'),
|
||||
content: const Text(
|
||||
'Clear all cached images, stories and comments.',
|
||||
),
|
||||
@ -411,7 +426,7 @@ class _SettingsState extends State<Settings> {
|
||||
DefaultCacheManager().emptyCache,
|
||||
)
|
||||
.whenComplete(() {
|
||||
showSnackBar(content: 'Data cleared!');
|
||||
showSnackBar(content: 'Cache cleared!');
|
||||
});
|
||||
},
|
||||
child: const Text(
|
||||
@ -621,11 +636,64 @@ class _SettingsState extends State<Settings> {
|
||||
LinkUtil.launchInExternalBrowser(Constants.githubIssueLink);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
locator.get<Logger>().e(
|
||||
'Error caught in onGithubTapped',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
error.logError(stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onExportFavoritesTapped() async {
|
||||
final List<int> allFavorites = context.read<FavCubit>().state.favIds;
|
||||
|
||||
if (allFavorites.isEmpty) {
|
||||
showSnackBar(content: "You don't have any favorite item.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await FlutterClipboard.copy(
|
||||
allFavorites.join('\n'),
|
||||
).whenComplete(HapticFeedback.selectionClick);
|
||||
showSnackBar(content: 'Ids of favorites have been copied to clipboard.');
|
||||
} catch (error, stackTrace) {
|
||||
error.logError(stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void showClearFavoritesDialog() {
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Remove all favorites?'),
|
||||
content: const Text(
|
||||
'''This will not effect favorites saved in your Hacker News account.''',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
try {
|
||||
context.read<FavCubit>().removeAll();
|
||||
showSnackBar(content: 'All favorites have been removed.');
|
||||
} catch (error, stackTrace) {
|
||||
error.logError(stackTrace: stackTrace);
|
||||
}
|
||||
},
|
||||
child: const Text(
|
||||
'Confirm',
|
||||
style: TextStyle(
|
||||
color: Palette.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -152,7 +152,7 @@ class CommentTile extends StatelessWidget {
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
comment.postedDate,
|
||||
comment.timeAgo,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
|
@ -5,6 +5,7 @@ import 'package:hacki/styles/palette.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:linkify/linkify.dart';
|
||||
|
||||
export 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
||||
export 'package:linkify/linkify.dart'
|
||||
show
|
||||
LinkifyElement,
|
||||
@ -27,7 +28,7 @@ class Linkify extends StatelessWidget {
|
||||
required this.text,
|
||||
this.linkifiers = defaultLinkifiers,
|
||||
this.onOpen,
|
||||
this.options = const LinkifyOptions(),
|
||||
this.options = LinkifierUtil.linkifyOptions,
|
||||
// TextSpan
|
||||
this.style,
|
||||
this.linkStyle,
|
||||
@ -154,7 +155,7 @@ class SelectableLinkify extends StatelessWidget {
|
||||
required this.text,
|
||||
this.linkifiers = defaultLinkifiers,
|
||||
this.onOpen,
|
||||
this.options = const LinkifyOptions(),
|
||||
this.options = LinkifierUtil.linkifyOptions,
|
||||
// TextSpan
|
||||
this.style,
|
||||
this.linkStyle,
|
||||
@ -316,6 +317,7 @@ class SelectableLinkify extends StatelessWidget {
|
||||
selectionControls: selectionControls,
|
||||
onSelectionChanged: onSelectionChanged,
|
||||
contextMenuBuilder: contextMenuBuilder,
|
||||
semanticsLabel: text,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:linkify/linkify.dart';
|
||||
|
||||
final RegExp _quoteRegex = RegExp(
|
||||
r'(?=^> )(.*?)(?=\n|$)',
|
||||
r'(?=^>)(.*?)(?=\n|$)',
|
||||
multiLine: true,
|
||||
);
|
||||
|
||||
|
@ -47,6 +47,7 @@ class ItemText extends StatelessWidget {
|
||||
editableTextState,
|
||||
item: item,
|
||||
),
|
||||
semanticsLabel: item.text,
|
||||
);
|
||||
} else {
|
||||
return SelectableLinkify(
|
||||
|
@ -200,7 +200,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
e.postedDate,
|
||||
e.timeAgo,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
|
@ -52,15 +52,18 @@ class _OnboardingViewState extends State<OnboardingView> {
|
||||
children: const <Widget>[
|
||||
_PageViewChild(
|
||||
path: Constants.commentTileRightSlidePath,
|
||||
description: 'Swipe right to leave a comment or vote.',
|
||||
description:
|
||||
'''Swipe right to leave a comment, vote, and more.''',
|
||||
),
|
||||
_PageViewChild(
|
||||
path: Constants.commentTileLeftSlidePath,
|
||||
description: 'Swipe left to view all the parent comments.',
|
||||
description:
|
||||
'''Swipe left to view all the ancestor comments.''',
|
||||
),
|
||||
_PageViewChild(
|
||||
path: Constants.commentTileTopTapPath,
|
||||
description: 'Tap on the top of comment tile to collapse.',
|
||||
description:
|
||||
'''Tap on anywhere inside a comment tile to collapse.''',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -33,101 +33,108 @@ class StoryTile extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
if (showWebPreview) {
|
||||
final double height = context.storyTileHeight;
|
||||
return TapDownWrapper(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: LinkPreview(
|
||||
story: story,
|
||||
link: story.url,
|
||||
offlineReading: context.read<StoriesBloc>().state.offlineReading,
|
||||
placeholderWidget: _LinkPreviewPlaceholder(
|
||||
height: height,
|
||||
return Semantics(
|
||||
label: story.screenReaderLabel,
|
||||
child: TapDownWrapper(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: LinkPreview(
|
||||
story: story,
|
||||
link: story.url,
|
||||
offlineReading:
|
||||
context.read<StoriesBloc>().state.offlineReading,
|
||||
placeholderWidget: _LinkPreviewPlaceholder(
|
||||
height: height,
|
||||
),
|
||||
errorImage: Constants.hackerNewsLogoLink,
|
||||
backgroundColor: Palette.transparent,
|
||||
borderRadius: Dimens.zero,
|
||||
removeElevation: true,
|
||||
bodyMaxLines: context.storyTileMaxLines,
|
||||
errorTitle: story.title,
|
||||
titleStyle: TextStyle(
|
||||
color: hasRead
|
||||
? Palette.grey[500]
|
||||
: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
showMetadata: showMetadata,
|
||||
showUrl: showUrl,
|
||||
),
|
||||
errorImage: Constants.hackerNewsLogoLink,
|
||||
backgroundColor: Palette.transparent,
|
||||
borderRadius: Dimens.zero,
|
||||
removeElevation: true,
|
||||
bodyMaxLines: context.storyTileMaxLines,
|
||||
errorTitle: story.title,
|
||||
titleStyle: TextStyle(
|
||||
color: hasRead
|
||||
? Palette.grey[500]
|
||||
: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
showMetadata: showMetadata,
|
||||
showUrl: showUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: Dimens.pt12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: story.title,
|
||||
style: TextStyle(
|
||||
color: hasRead
|
||||
? Palette.grey[500]
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color,
|
||||
fontSize: simpleTileFontSize,
|
||||
),
|
||||
),
|
||||
if (showUrl && story.url.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' (${story.readableUrl})',
|
||||
style: TextStyle(
|
||||
color: Palette.grey[500],
|
||||
fontSize: simpleTileFontSize - 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showMetadata)
|
||||
return Semantics(
|
||||
label: story.screenReaderLabel,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: Dimens.pt12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
story.metadata,
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
fontSize: simpleTileFontSize - 2,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: story.title,
|
||||
style: TextStyle(
|
||||
color: hasRead
|
||||
? Palette.grey[500]
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color,
|
||||
fontSize: simpleTileFontSize,
|
||||
),
|
||||
),
|
||||
if (showUrl && story.url.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' (${story.readableUrl})',
|
||||
style: TextStyle(
|
||||
color: Palette.grey[500],
|
||||
fontSize: simpleTileFontSize - 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
maxLines: 1,
|
||||
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
],
|
||||
if (showMetadata)
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
story.metadata,
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
fontSize: simpleTileFontSize - 2,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -2,8 +2,9 @@ import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart'
|
||||
import 'package:linkify/linkify.dart';
|
||||
|
||||
abstract class LinkifierUtil {
|
||||
static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false);
|
||||
|
||||
static List<LinkifyElement> linkify(String text) {
|
||||
const LinkifyOptions options = LinkifyOptions();
|
||||
const List<Linkifier> linkifiers = <Linkifier>[
|
||||
UrlLinkifier(),
|
||||
EmailLinkifier(),
|
||||
@ -21,7 +22,7 @@ abstract class LinkifierUtil {
|
||||
}
|
||||
|
||||
for (final Linkifier linkifier in linkifiers) {
|
||||
list = linkifier.parse(list, options);
|
||||
list = linkifier.parse(list, linkifyOptions);
|
||||
}
|
||||
|
||||
return list;
|
||||
|
@ -1359,4 +1359,4 @@ packages:
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=2.19.0 <3.0.0"
|
||||
flutter: ">=3.7.5"
|
||||
flutter: ">=3.7.6"
|
||||
|
@ -1,11 +1,11 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 1.2.5+98
|
||||
version: 1.3.2+101
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
flutter: "3.7.5"
|
||||
flutter: "3.7.6"
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.0.0
|
||||
|
Submodule submodules/flutter updated: c07f788888...12cb4eb7a0
Reference in New Issue
Block a user