mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
ab1e90ccad | |||
0ca3e96d91 | |||
d1c8eed3de |
@ -50,7 +50,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.jiaqifeng.hacki"
|
||||
minSdkVersion 30
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
@ -2,6 +2,8 @@ PODS:
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_email_sender (0.0.1):
|
||||
- Flutter
|
||||
@ -53,6 +55,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
|
||||
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
||||
@ -81,6 +84,8 @@ SPEC REPOS:
|
||||
EXTERNAL SOURCES:
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_email_sender:
|
||||
@ -120,6 +125,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||
|
@ -74,7 +74,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
||||
emit(
|
||||
const StoriesState.init().copyWith(
|
||||
offlineReading: hasCachedStories &&
|
||||
isOfflineReading: hasCachedStories &&
|
||||
// Only go into offline mode in the next session.
|
||||
state.downloadStatus == StoriesDownloadStatus.initial,
|
||||
currentPageSize: pageSize,
|
||||
@ -92,7 +92,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
required StoryType type,
|
||||
required Emitter<StoriesState> emit,
|
||||
}) async {
|
||||
if (state.offlineReading) {
|
||||
if (state.isOfflineReading) {
|
||||
final List<int> ids =
|
||||
await _offlineRepository.getCachedStoryIds(type: type);
|
||||
emit(
|
||||
@ -137,7 +137,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
),
|
||||
);
|
||||
|
||||
if (state.offlineReading) {
|
||||
if (state.isOfflineReading) {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
@ -172,7 +172,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
upper = len;
|
||||
}
|
||||
|
||||
if (state.offlineReading) {
|
||||
if (state.isOfflineReading) {
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: state.storyIdsByType[event.type]!.sublist(
|
||||
@ -440,7 +440,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
await _offlineRepository.deleteAllWebPages();
|
||||
emit(state.copyWith(offlineReading: false));
|
||||
emit(state.copyWith(isOfflineReading: false));
|
||||
add(StoriesInitialize());
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ class StoriesState extends Equatable {
|
||||
required this.statusByType,
|
||||
required this.currentPageByType,
|
||||
required this.readStoriesIds,
|
||||
required this.offlineReading,
|
||||
required this.isOfflineReading,
|
||||
required this.downloadStatus,
|
||||
required this.currentPageSize,
|
||||
required this.storiesDownloaded,
|
||||
@ -57,7 +57,7 @@ class StoriesState extends Equatable {
|
||||
StoryType.ask: 0,
|
||||
StoryType.show: 0,
|
||||
},
|
||||
}) : offlineReading = false,
|
||||
}) : isOfflineReading = false,
|
||||
downloadStatus = StoriesDownloadStatus.initial,
|
||||
currentPageSize = 0,
|
||||
readStoriesIds = const <int>{},
|
||||
@ -70,7 +70,7 @@ class StoriesState extends Equatable {
|
||||
final Map<StoryType, int> currentPageByType;
|
||||
final Set<int> readStoriesIds;
|
||||
final StoriesDownloadStatus downloadStatus;
|
||||
final bool offlineReading;
|
||||
final bool isOfflineReading;
|
||||
final int currentPageSize;
|
||||
final int storiesDownloaded;
|
||||
final int storiesToBeDownloaded;
|
||||
@ -82,7 +82,7 @@ class StoriesState extends Equatable {
|
||||
Map<StoryType, int>? currentPageByType,
|
||||
Set<int>? readStoriesIds,
|
||||
StoriesDownloadStatus? downloadStatus,
|
||||
bool? offlineReading,
|
||||
bool? isOfflineReading,
|
||||
int? currentPageSize,
|
||||
int? storiesDownloaded,
|
||||
int? storiesToBeDownloaded,
|
||||
@ -93,7 +93,7 @@ class StoriesState extends Equatable {
|
||||
statusByType: statusByType ?? this.statusByType,
|
||||
currentPageByType: currentPageByType ?? this.currentPageByType,
|
||||
readStoriesIds: readStoriesIds ?? this.readStoriesIds,
|
||||
offlineReading: offlineReading ?? this.offlineReading,
|
||||
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
|
||||
downloadStatus: downloadStatus ?? this.downloadStatus,
|
||||
currentPageSize: currentPageSize ?? this.currentPageSize,
|
||||
storiesDownloaded: storiesDownloaded ?? this.storiesDownloaded,
|
||||
@ -183,7 +183,7 @@ class StoriesState extends Equatable {
|
||||
statusByType,
|
||||
currentPageByType,
|
||||
readStoriesIds,
|
||||
offlineReading,
|
||||
isOfflineReading,
|
||||
downloadStatus,
|
||||
currentPageSize,
|
||||
storiesDownloaded,
|
||||
|
@ -26,7 +26,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
StoriesRepository? storiesRepository,
|
||||
SembastRepository? sembastRepository,
|
||||
Logger? logger,
|
||||
required bool offlineReading,
|
||||
required bool isOfflineReading,
|
||||
required Item item,
|
||||
required FetchMode defaultFetchMode,
|
||||
required CommentsOrder defaultCommentsOrder,
|
||||
@ -41,7 +41,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(
|
||||
CommentsState.init(
|
||||
offlineReading: offlineReading,
|
||||
isOfflineReading: isOfflineReading,
|
||||
item: item,
|
||||
fetchMode: defaultFetchMode,
|
||||
order: defaultCommentsOrder,
|
||||
@ -109,7 +109,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
);
|
||||
|
||||
final Item item = state.item;
|
||||
final Item updatedItem = state.offlineReading
|
||||
final Item updatedItem = state.isOfflineReading
|
||||
? item
|
||||
: await _storiesRepository.fetchItem(id: item.id).then(_toBuildable) ??
|
||||
item;
|
||||
@ -119,7 +119,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
late final Stream<Comment> commentStream;
|
||||
|
||||
if (state.offlineReading) {
|
||||
if (state.isOfflineReading) {
|
||||
commentStream = _offlineRepository.getCachedCommentsStream(ids: kids);
|
||||
} else {
|
||||
switch (state.fetchMode) {
|
||||
@ -152,7 +152,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
),
|
||||
);
|
||||
|
||||
if (state.offlineReading) {
|
||||
if (state.isOfflineReading) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.allLoaded,
|
||||
|
@ -17,12 +17,12 @@ class CommentsState extends Equatable {
|
||||
required this.order,
|
||||
required this.fetchMode,
|
||||
required this.onlyShowTargetComment,
|
||||
required this.offlineReading,
|
||||
required this.isOfflineReading,
|
||||
required this.currentPage,
|
||||
});
|
||||
|
||||
CommentsState.init({
|
||||
required this.offlineReading,
|
||||
required this.isOfflineReading,
|
||||
required this.item,
|
||||
required this.fetchMode,
|
||||
required this.order,
|
||||
@ -39,7 +39,7 @@ class CommentsState extends Equatable {
|
||||
final CommentsOrder order;
|
||||
final FetchMode fetchMode;
|
||||
final bool onlyShowTargetComment;
|
||||
final bool offlineReading;
|
||||
final bool isOfflineReading;
|
||||
final int currentPage;
|
||||
|
||||
CommentsState copyWith({
|
||||
@ -50,7 +50,7 @@ class CommentsState extends Equatable {
|
||||
CommentsOrder? order,
|
||||
FetchMode? fetchMode,
|
||||
bool? onlyShowTargetComment,
|
||||
bool? offlineReading,
|
||||
bool? isOfflineReading,
|
||||
int? currentPage,
|
||||
}) {
|
||||
return CommentsState(
|
||||
@ -62,7 +62,7 @@ class CommentsState extends Equatable {
|
||||
fetchMode: fetchMode ?? this.fetchMode,
|
||||
onlyShowTargetComment:
|
||||
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
||||
offlineReading: offlineReading ?? this.offlineReading,
|
||||
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
);
|
||||
}
|
||||
@ -77,7 +77,7 @@ class CommentsState extends Equatable {
|
||||
order,
|
||||
fetchMode,
|
||||
onlyShowTargetComment,
|
||||
offlineReading,
|
||||
isOfflineReading,
|
||||
currentPage,
|
||||
comments,
|
||||
];
|
||||
|
@ -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) {
|
||||
|
@ -2,7 +2,7 @@ import 'package:hacki/config/locator.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
extension ObjectExtension on Object {
|
||||
void log({String identifier = ''}) {
|
||||
void log([String identifier = '']) {
|
||||
locator.get<Logger>().d('$identifier ${toString()}');
|
||||
}
|
||||
|
||||
|
@ -58,10 +58,12 @@ extension StateExtension on State {
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return MorePopupMenu(
|
||||
item: item,
|
||||
isBlocked: isBlocked,
|
||||
onLoginTapped: onLoginTapped,
|
||||
return SafeArea(
|
||||
child: MorePopupMenu(
|
||||
item: item,
|
||||
isBlocked: isBlocked,
|
||||
onLoginTapped: onLoginTapped,
|
||||
),
|
||||
);
|
||||
},
|
||||
).then((MenuAction? action) {
|
||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -110,13 +111,27 @@ Future<void> main({bool testing = false}) async {
|
||||
},
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Palette.transparent,
|
||||
systemNavigationBarColor: Palette.transparent,
|
||||
systemNavigationBarDividerColor: Palette.transparent,
|
||||
),
|
||||
);
|
||||
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
|
||||
final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
|
||||
final int sdk = androidInfo.version.sdkInt;
|
||||
|
||||
if (sdk > 28) {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Palette.transparent,
|
||||
systemNavigationBarColor: Palette.transparent,
|
||||
systemNavigationBarDividerColor: Palette.transparent,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarBrightness: Brightness.light,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarColor: Colors.transparent,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -214,7 +214,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
context.read<PreferenceCubit>().state.webFirstEnabled;
|
||||
final bool useReader = context.read<PreferenceCubit>().state.readerEnabled;
|
||||
final bool offlineReading =
|
||||
context.read<StoriesBloc>().state.offlineReading;
|
||||
context.read<StoriesBloc>().state.isOfflineReading;
|
||||
final bool hasRead = isPin || context.read<StoriesBloc>().hasRead(story);
|
||||
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
||||
|
||||
@ -225,7 +225,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
if (isJobWithLink) {
|
||||
context.read<ReminderCubit>().removeLastReadStoryId();
|
||||
} else {
|
||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
||||
final ItemScreenArgs args = ItemScreenArgs(
|
||||
item: story,
|
||||
);
|
||||
|
||||
context.read<ReminderCubit>().updateLastReadStoryId(story.id);
|
||||
|
||||
|
@ -58,14 +58,14 @@ class ItemScreen extends StatefulWidget {
|
||||
return MaterialPageRoute<ItemScreen>(
|
||||
settings: const RouteSettings(name: routeName),
|
||||
builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
|
||||
create: (BuildContext context) => CollapseCache(),
|
||||
create: (_) => CollapseCache(),
|
||||
lazy: false,
|
||||
child: MultiBlocProvider(
|
||||
providers: <BlocProvider<dynamic>>[
|
||||
BlocProvider<CommentsCubit>(
|
||||
create: (BuildContext context) => CommentsCubit(
|
||||
offlineReading:
|
||||
context.read<StoriesBloc>().state.offlineReading,
|
||||
isOfflineReading:
|
||||
context.read<StoriesBloc>().state.isOfflineReading,
|
||||
item: args.item,
|
||||
collapseCache: context.read<CollapseCache>(),
|
||||
defaultFetchMode:
|
||||
@ -99,15 +99,15 @@ class ItemScreen extends StatefulWidget {
|
||||
}
|
||||
},
|
||||
child: RepositoryProvider<CollapseCache>(
|
||||
create: (BuildContext context) => CollapseCache(),
|
||||
create: (_) => CollapseCache(),
|
||||
lazy: false,
|
||||
child: MultiBlocProvider(
|
||||
key: ValueKey<ItemScreenArgs>(args),
|
||||
providers: <BlocProvider<dynamic>>[
|
||||
BlocProvider<CommentsCubit>(
|
||||
create: (BuildContext context) => CommentsCubit(
|
||||
offlineReading:
|
||||
context.read<StoriesBloc>().state.offlineReading,
|
||||
isOfflineReading:
|
||||
context.read<StoriesBloc>().state.isOfflineReading,
|
||||
item: args.item,
|
||||
collapseCache: context.read<CollapseCache>(),
|
||||
defaultFetchMode:
|
||||
|
@ -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(
|
||||
|
@ -87,7 +87,7 @@ class MainView extends StatelessWidget {
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (context.read<StoriesBloc>().state.offlineReading) {
|
||||
if (context.read<StoriesBloc>().state.isOfflineReading) {
|
||||
refreshController.refreshCompleted();
|
||||
} else {
|
||||
context.read<CommentsCubit>().refresh();
|
||||
@ -231,232 +231,257 @@ class _ParentItemSection extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: topPadding,
|
||||
),
|
||||
if (!splitViewEnabled)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||
child: OfflineBanner(),
|
||||
return Semantics(
|
||||
label:
|
||||
'''Posted by ${state.item.by} ${state.item.timeAgo}, ${state.item.title}. ${state.item.text}''',
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: topPadding,
|
||||
),
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (!splitViewEnabled)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||
child: OfflineBanner(),
|
||||
),
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (state.item.id !=
|
||||
context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
context.read<EditCubit>().onReplyTapped(state.item);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(state.item, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
if (state.item.id !=
|
||||
context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
context.read<EditCubit>().onReplyTapped(state.item);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(state.item, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
),
|
||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.fontSize != current.fontSize,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return Column(
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story)
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launch(
|
||||
state.item.url,
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.readerEnabled,
|
||||
offlineReading: context
|
||||
.read<StoriesBloc>()
|
||||
.state
|
||||
.offlineReading,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
top: Dimens.pt12,
|
||||
Text(
|
||||
state.item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.timeAgo,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.fontSize != current.fontSize,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (state.item is Story)
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launch(
|
||||
state.item.url,
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.readerEnabled,
|
||||
offlineReading: context
|
||||
.read<StoriesBloc>()
|
||||
.state
|
||||
.isOfflineReading,
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: state.item.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: state.item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
top: Dimens.pt12,
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color,
|
||||
),
|
||||
if (state.item.url.isNotEmpty)
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text:
|
||||
''' (${(state.item as Story).readableUrl})''',
|
||||
semanticsLabel: state.item.title,
|
||||
text: state.item.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize:
|
||||
prefState.fontSize.fontSize - 4,
|
||||
color: Palette.orange,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: state.item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (state.item.url.isNotEmpty)
|
||||
TextSpan(
|
||||
text:
|
||||
''' (${(state.item as Story).readableUrl})''',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize:
|
||||
prefState.fontSize.fontSize - 4,
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: ItemText(
|
||||
item: state.item,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: ItemText(
|
||||
item: state.item,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (state.item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)..init(),
|
||||
child: const PollView(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
if (state.onlyShowTargetComment) ...<Widget>[
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () =>
|
||||
context.read<CommentsCubit>().loadAll(state.item as Story),
|
||||
child: const Text('View all comments'),
|
||||
if (state.item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)..init(),
|
||||
child: const PollView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text(
|
||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
if (state.onlyShowTargetComment) ...<Widget>[
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () =>
|
||||
context.read<CommentsCubit>().loadAll(state.item as Story),
|
||||
child: const Text('View all comments'),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: context.read<CommentsCubit>().loadParentThread,
|
||||
child: state.fetchParentStatus == CommentsStatus.loading
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
Text(
|
||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: context.read<CommentsCubit>().loadParentThread,
|
||||
child: state.fetchParentStatus == CommentsStatus.loading
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View parent thread',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (!state.isOfflineReading)
|
||||
DropdownButton<FetchMode>(
|
||||
value: state.fetchMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: FetchMode.values
|
||||
.map(
|
||||
(FetchMode val) => DropdownMenuItem<FetchMode>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View parent thread',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onFetchModeChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt6,
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (!state.offlineReading)
|
||||
DropdownButton<FetchMode>(
|
||||
value: state.fetchMode,
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: state.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: FetchMode.values
|
||||
items: CommentsOrder.values
|
||||
.map(
|
||||
(FetchMode val) => DropdownMenuItem<FetchMode>(
|
||||
(CommentsOrder val) => DropdownMenuItem<CommentsOrder>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
@ -467,51 +492,31 @@ class _ParentItemSection extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onFetchModeChanged,
|
||||
onChanged: context.read<CommentsCubit>().onOrderChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt6,
|
||||
),
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: state.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: CommentsOrder.values
|
||||
.map(
|
||||
(CommentsOrder val) => DropdownMenuItem<CommentsOrder>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onOrderChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
if (state.comments.isEmpty &&
|
||||
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nothing yet',
|
||||
style: TextStyle(color: Palette.grey),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
if (state.comments.isEmpty &&
|
||||
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nothing yet',
|
||||
style: TextStyle(color: Palette.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,8 @@ class MorePopupMenu extends StatelessWidget {
|
||||
final bool isBlocked;
|
||||
final VoidCallback onLoginTapped;
|
||||
|
||||
static const double _storySheetHeight = 500;
|
||||
static const double _commentSheetHeight = 480;
|
||||
static const double _storySheetHeight = 485;
|
||||
static const double _commentSheetHeight = 470;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -81,63 +81,71 @@ class MorePopupMenu extends StatelessWidget {
|
||||
UserCubit()..init(userId: item.by),
|
||||
child: BlocBuilder<UserCubit, UserState>(
|
||||
builder: (BuildContext context, UserState state) {
|
||||
return ListTile(
|
||||
leading: const Icon(
|
||||
Icons.account_circle,
|
||||
),
|
||||
title: Text(item.by),
|
||||
subtitle: Text(
|
||||
state.user.description,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text('About ${state.user.id}'),
|
||||
content: state.user.about.isEmpty
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
Text(
|
||||
'empty',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
return Semantics(
|
||||
excludeSemantics: state.status == UserStatus.loading,
|
||||
child: ListTile(
|
||||
leading: const Icon(
|
||||
Icons.account_circle,
|
||||
),
|
||||
title: Text(item.by),
|
||||
subtitle: Text(
|
||||
state.user.description,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
semanticLabel:
|
||||
'''About ${state.user.id}. ${state.user.about}''',
|
||||
title: Text(
|
||||
'About ${state.user.id}',
|
||||
),
|
||||
content: state.user.about.isEmpty
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
Text(
|
||||
'empty',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: SelectableLinkify(
|
||||
text: HtmlUtil.parseHtml(
|
||||
state.user.about,
|
||||
),
|
||||
],
|
||||
)
|
||||
: SelectableLinkify(
|
||||
text: HtmlUtil.parseHtml(
|
||||
state.user.about,
|
||||
linkStyle: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) =>
|
||||
LinkUtil.launch(link.url),
|
||||
semanticsLabel: state.user.about,
|
||||
),
|
||||
linkStyle: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) =>
|
||||
LinkUtil.launch(link.url),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onSearchUserTapped(context);
|
||||
},
|
||||
child: const Text(
|
||||
'Search',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onSearchUserTapped(context);
|
||||
},
|
||||
child: const Text(
|
||||
'Search',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'Okay',
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'Okay',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -34,6 +34,7 @@ class _ScrollUpIconButtonState extends State<ScrollUpIconButton> {
|
||||
return Opacity(
|
||||
opacity: opacity.clamp(0, 1),
|
||||
child: IconButton(
|
||||
tooltip: 'Scroll to top',
|
||||
icon: const Icon(
|
||||
FeatherIcons.chevronsUp,
|
||||
color: Palette.orange,
|
||||
|
@ -118,7 +118,7 @@ class InboxView extends StatelessWidget {
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
e.postedDate,
|
||||
e.timeAgo,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
|
@ -152,7 +152,7 @@ class CommentTile extends StatelessWidget {
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
comment.postedDate,
|
||||
comment.timeAgo,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
@ -188,16 +188,19 @@ class CommentTile extends StatelessWidget {
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ItemText(
|
||||
key: ValueKey<int>(comment.id),
|
||||
item: comment,
|
||||
onTap: () {
|
||||
if (onTap == null) {
|
||||
_onTextTapped(context);
|
||||
} else {
|
||||
onTap!.call();
|
||||
}
|
||||
},
|
||||
child: Semantics(
|
||||
label: '''At level ${comment.level}.''',
|
||||
child: ItemText(
|
||||
key: ValueKey<int>(comment.id),
|
||||
item: comment,
|
||||
onTap: () {
|
||||
if (onTap == null) {
|
||||
_onTextTapped(context);
|
||||
} else {
|
||||
onTap!.call();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -153,6 +153,7 @@ class SelectableLinkify extends StatelessWidget {
|
||||
const SelectableLinkify({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.semanticsLabel,
|
||||
this.linkifiers = defaultLinkifiers,
|
||||
this.onOpen,
|
||||
this.options = LinkifierUtil.linkifyOptions,
|
||||
@ -188,6 +189,8 @@ class SelectableLinkify extends StatelessWidget {
|
||||
/// Text to be linkified
|
||||
final String text;
|
||||
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// The number of font pixels for each logical pixel
|
||||
final double textScaleFactor;
|
||||
|
||||
@ -317,6 +320,7 @@ class SelectableLinkify extends StatelessWidget {
|
||||
selectionControls: selectionControls,
|
||||
onSelectionChanged: onSelectionChanged,
|
||||
contextMenuBuilder: contextMenuBuilder,
|
||||
semanticsLabel: semanticsLabel,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,7 @@ class ItemText extends StatelessWidget {
|
||||
editableTextState,
|
||||
item: item,
|
||||
),
|
||||
semanticsLabel: item.text,
|
||||
);
|
||||
} else {
|
||||
return SelectableLinkify(
|
||||
@ -65,6 +66,7 @@ class ItemText extends StatelessWidget {
|
||||
editableTextState,
|
||||
item: item,
|
||||
),
|
||||
semanticsLabel: item.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -16,10 +16,9 @@ class LinkPreview extends StatefulWidget {
|
||||
required this.story,
|
||||
required this.showMetadata,
|
||||
required this.showUrl,
|
||||
required this.offlineReading,
|
||||
required this.isOfflineReading,
|
||||
required this.titleStyle,
|
||||
this.cache = const Duration(days: 30),
|
||||
this.titleStyle,
|
||||
this.bodyStyle,
|
||||
this.showMultimedia = true,
|
||||
this.backgroundColor = const Color.fromRGBO(235, 235, 235, 1),
|
||||
this.bodyMaxLines = 3,
|
||||
@ -84,10 +83,7 @@ class LinkPreview extends StatefulWidget {
|
||||
final Duration cache;
|
||||
|
||||
/// Customize body `TextStyle`
|
||||
final TextStyle? titleStyle;
|
||||
|
||||
/// Customize body `TextStyle`
|
||||
final TextStyle? bodyStyle;
|
||||
final TextStyle titleStyle;
|
||||
|
||||
/// Show or Hide image if available defaults to `true`
|
||||
final bool showMultimedia;
|
||||
@ -105,7 +101,7 @@ class LinkPreview extends StatefulWidget {
|
||||
|
||||
final bool showMetadata;
|
||||
final bool showUrl;
|
||||
final bool offlineReading;
|
||||
final bool isOfflineReading;
|
||||
|
||||
@override
|
||||
_LinkPreviewState createState() => _LinkPreviewState();
|
||||
@ -135,7 +131,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
_info = await WebAnalyzer.getInfo(
|
||||
story: widget.story,
|
||||
cache: widget.cache,
|
||||
offlineReading: widget.offlineReading,
|
||||
offlineReading: widget.isOfflineReading,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
@ -190,7 +186,6 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
imagePath: Constants.hackerNewsLogoPath,
|
||||
onTap: _launchURL,
|
||||
titleTextStyle: widget.titleStyle,
|
||||
bodyTextStyle: widget.bodyStyle,
|
||||
bodyTextOverflow: widget.bodyTextOverflow,
|
||||
bodyMaxLines: widget.bodyMaxLines,
|
||||
showMultiMedia: widget.showMultimedia,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/link_preview/models/models.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:memoize/memoize.dart';
|
||||
|
||||
class LinkView extends StatelessWidget {
|
||||
LinkView({
|
||||
@ -17,10 +19,9 @@ class LinkView extends StatelessWidget {
|
||||
required this.showMetadata,
|
||||
required bool showUrl,
|
||||
required this.bodyMaxLines,
|
||||
required this.titleTextStyle,
|
||||
this.imageUri,
|
||||
this.imagePath,
|
||||
this.titleTextStyle,
|
||||
this.bodyTextStyle,
|
||||
this.showMultiMedia = true,
|
||||
this.bodyTextOverflow,
|
||||
this.isIcon = false,
|
||||
@ -41,8 +42,7 @@ class LinkView extends StatelessWidget {
|
||||
final String? imageUri;
|
||||
final String? imagePath;
|
||||
final void Function(String) onTap;
|
||||
final TextStyle? titleTextStyle;
|
||||
final TextStyle? bodyTextStyle;
|
||||
final TextStyle titleTextStyle;
|
||||
final bool showMultiMedia;
|
||||
final TextOverflow? bodyTextOverflow;
|
||||
final int bodyMaxLines;
|
||||
@ -52,36 +52,90 @@ class LinkView extends StatelessWidget {
|
||||
final bool showMetadata;
|
||||
final bool showUrl;
|
||||
|
||||
static final double Function(double) _getTitleFontSize =
|
||||
memo1(_computeTitleFontSize);
|
||||
static const double _bottomPadding = 6;
|
||||
static late TextStyle _urlStyle;
|
||||
static late TextStyle _metadataStyle;
|
||||
static late TextStyle _descriptionStyle;
|
||||
|
||||
static double _computeTitleFontSize(double width) {
|
||||
double size = width * 0.13;
|
||||
if (size > 15) {
|
||||
size = 15;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
static final Map<MaxLineComputationParams, int> _computationCache =
|
||||
<MaxLineComputationParams, int>{};
|
||||
|
||||
static final int Function(double) _getTitleLines = memo1(_computeTitleLines);
|
||||
|
||||
static int _computeTitleLines(double layoutHeight) {
|
||||
return layoutHeight >= 100 ? 2 : 1;
|
||||
}
|
||||
|
||||
static final int Function(int, bool, bool, String?) _getBodyLines =
|
||||
memo4(_computeBodyLines);
|
||||
|
||||
static int _computeBodyLines(
|
||||
int bodyMaxLines,
|
||||
bool showMetadata,
|
||||
bool showUrl,
|
||||
String? fontFamily,
|
||||
static int getDescriptionMaxLines(
|
||||
MaxLineComputationParams params,
|
||||
TextStyle titleStyle,
|
||||
) {
|
||||
final int maxLines = bodyMaxLines -
|
||||
(showMetadata ? 1 : 0) -
|
||||
(showUrl ? 1 : 0) +
|
||||
(fontFamily == Font.ubuntuMono.name ? 1 : 0);
|
||||
if (_computationCache.containsKey(params)) {
|
||||
return _computationCache[params]!;
|
||||
}
|
||||
|
||||
_urlStyle = titleStyle.copyWith(
|
||||
color: Palette.grey,
|
||||
fontSize: TextDimens.pt12,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontFamily: params.fontFamily,
|
||||
);
|
||||
_descriptionStyle = TextStyle(
|
||||
color: Palette.grey,
|
||||
fontFamily: params.fontFamily,
|
||||
fontSize: TextDimens.pt14,
|
||||
);
|
||||
_metadataStyle = _descriptionStyle.copyWith(
|
||||
fontSize: TextDimens.pt12,
|
||||
fontFamily: params.fontFamily,
|
||||
);
|
||||
|
||||
final double urlHeight = (TextPainter(
|
||||
text: TextSpan(
|
||||
text: '(url)',
|
||||
style: _urlStyle,
|
||||
),
|
||||
maxLines: 1,
|
||||
textScaleFactor: params.textScaleFactor,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout())
|
||||
.size
|
||||
.height;
|
||||
final double metadataHeight = (TextPainter(
|
||||
text: TextSpan(
|
||||
text: '123metadata',
|
||||
style: _metadataStyle,
|
||||
),
|
||||
maxLines: 1,
|
||||
textScaleFactor: params.textScaleFactor,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout())
|
||||
.size
|
||||
.height;
|
||||
final double descriptionHeight = (TextPainter(
|
||||
text: TextSpan(
|
||||
text: 'DESCRIPTION',
|
||||
style: _descriptionStyle,
|
||||
),
|
||||
maxLines: 1,
|
||||
textScaleFactor: params.textScaleFactor,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout())
|
||||
.size
|
||||
.height;
|
||||
|
||||
final double allPaddings =
|
||||
params.fontFamily == Font.robotoSlab.name ? Dimens.pt2 : Dimens.pt4;
|
||||
|
||||
final double height = <double>[
|
||||
params.titleHeight,
|
||||
if (params.showUrl) urlHeight,
|
||||
if (params.showMetadata) metadataHeight,
|
||||
allPaddings,
|
||||
_bottomPadding,
|
||||
].reduce((double a, double b) => a + b);
|
||||
|
||||
final double descriptionAllowedHeight = params.layoutHeight - height;
|
||||
|
||||
final int maxLines =
|
||||
max(1, (descriptionAllowedHeight / descriptionHeight).floor());
|
||||
|
||||
_computationCache[params] = maxLines;
|
||||
|
||||
return maxLines;
|
||||
}
|
||||
|
||||
@ -91,19 +145,36 @@ class LinkView extends StatelessWidget {
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
final double layoutWidth = constraints.biggest.width;
|
||||
final double layoutHeight = constraints.biggest.height;
|
||||
final double bodyWidth = layoutWidth - layoutHeight - 8;
|
||||
final String? fontFamily =
|
||||
Theme.of(context).primaryTextTheme.bodyMedium?.fontFamily;
|
||||
final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
|
||||
|
||||
final TextStyle titleFontStyle = titleTextStyle ??
|
||||
TextStyle(
|
||||
fontSize: _getTitleFontSize(layoutWidth),
|
||||
color: Palette.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
final TextStyle bodyFontStyle = bodyTextStyle ??
|
||||
TextStyle(
|
||||
fontSize: _getTitleFontSize(layoutWidth) - 1,
|
||||
color: Palette.grey,
|
||||
fontWeight: FontWeight.w400,
|
||||
);
|
||||
final TextStyle titleStyle = titleTextStyle;
|
||||
final double titleHeight = (TextPainter(
|
||||
text: TextSpan(
|
||||
text: title,
|
||||
style: titleStyle,
|
||||
),
|
||||
maxLines: 2,
|
||||
textScaleFactor: textScaleFactor,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout(maxWidth: bodyWidth))
|
||||
.size
|
||||
.height;
|
||||
|
||||
final int descriptionMaxLines = getDescriptionMaxLines(
|
||||
MaxLineComputationParams(
|
||||
fontFamily ?? Font.roboto.name,
|
||||
bodyWidth,
|
||||
layoutHeight,
|
||||
titleHeight,
|
||||
textScaleFactor,
|
||||
showUrl,
|
||||
showMetadata,
|
||||
),
|
||||
titleStyle,
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => onTap(url),
|
||||
@ -138,85 +209,51 @@ class LinkView extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 5),
|
||||
Expanded(
|
||||
const SizedBox(width: Dimens.pt5),
|
||||
SizedBox(
|
||||
height: layoutHeight,
|
||||
width: layoutWidth - layoutHeight - 8,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.fontFamily ==
|
||||
Font.robotoSlab.name
|
||||
? 2
|
||||
: 4,
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
title,
|
||||
style: titleFontStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: _getTitleLines(layoutHeight),
|
||||
),
|
||||
),
|
||||
if (showUrl && url.isNotEmpty)
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
'($readableUrl)',
|
||||
textAlign: TextAlign.left,
|
||||
style: titleFontStyle.copyWith(
|
||||
color: Palette.grey,
|
||||
fontSize: titleFontStyle.fontSize == null
|
||||
? 12
|
||||
: titleFontStyle.fontSize! - 4,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
overflow:
|
||||
bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height:
|
||||
Theme.of(context).textTheme.bodyMedium?.fontFamily ==
|
||||
Font.robotoSlab.name
|
||||
? Dimens.pt2
|
||||
: Dimens.pt4,
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: titleStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
if (showUrl)
|
||||
Text(
|
||||
'($readableUrl)',
|
||||
textAlign: TextAlign.left,
|
||||
style: _urlStyle,
|
||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
if (showMetadata)
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
metadata,
|
||||
textAlign: TextAlign.left,
|
||||
style: bodyFontStyle.copyWith(
|
||||
fontSize: bodyFontStyle.fontSize == null
|
||||
? 12
|
||||
: bodyFontStyle.fontSize! - 2,
|
||||
),
|
||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
description,
|
||||
textAlign: TextAlign.left,
|
||||
style: bodyFontStyle,
|
||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||
maxLines: _getBodyLines(
|
||||
bodyMaxLines,
|
||||
showMetadata,
|
||||
showUrl,
|
||||
Theme.of(context).textTheme.bodyMedium?.fontFamily,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
metadata,
|
||||
textAlign: TextAlign.left,
|
||||
style: _metadataStyle,
|
||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
Text(
|
||||
description,
|
||||
textAlign: TextAlign.left,
|
||||
style: _descriptionStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: descriptionMaxLines,
|
||||
),
|
||||
const SizedBox(
|
||||
height: _bottomPadding,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -0,0 +1,33 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class MaxLineComputationParams extends Equatable {
|
||||
const MaxLineComputationParams(
|
||||
this.fontFamily,
|
||||
this.layoutWidth,
|
||||
this.layoutHeight,
|
||||
this.titleHeight,
|
||||
this.textScaleFactor,
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
this.showUrl,
|
||||
this.showMetadata,
|
||||
);
|
||||
|
||||
final String fontFamily;
|
||||
final double layoutWidth;
|
||||
final double layoutHeight;
|
||||
final double titleHeight;
|
||||
final double textScaleFactor;
|
||||
final bool showUrl;
|
||||
final bool showMetadata;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object>[
|
||||
fontFamily,
|
||||
layoutWidth,
|
||||
layoutHeight,
|
||||
titleHeight,
|
||||
textScaleFactor,
|
||||
showUrl,
|
||||
showMetadata,
|
||||
];
|
||||
}
|
1
lib/screens/widgets/link_preview/models/models.dart
Normal file
1
lib/screens/widgets/link_preview/models/models.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'max_line_computation_params.dart';
|
@ -17,9 +17,9 @@ class OfflineBanner extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<StoriesBloc, StoriesState>(
|
||||
buildWhen: (StoriesState previous, StoriesState current) =>
|
||||
previous.offlineReading != current.offlineReading,
|
||||
previous.isOfflineReading != current.isOfflineReading,
|
||||
builder: (BuildContext context, StoriesState state) {
|
||||
if (state.offlineReading) {
|
||||
if (state.isOfflineReading) {
|
||||
return MaterialBanner(
|
||||
content: Text(
|
||||
'You are currently in offline mode. '
|
||||
|
@ -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.''',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -86,7 +86,7 @@ class _StoriesListViewState extends State<StoriesListView> {
|
||||
},
|
||||
onTap: onStoryTapped,
|
||||
onPinned: context.read<PinCubit>().pinStory,
|
||||
header: state.offlineReading ? null : header,
|
||||
header: state.isOfflineReading ? null : header,
|
||||
onMoreTapped: onMoreTapped,
|
||||
);
|
||||
},
|
||||
|
@ -33,101 +33,110 @@ 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,
|
||||
excludeSemantics: true,
|
||||
child: TapDownWrapper(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: LinkPreview(
|
||||
story: story,
|
||||
link: story.url,
|
||||
isOfflineReading:
|
||||
context.read<StoriesBloc>().state.isOfflineReading,
|
||||
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,
|
||||
excludeSemantics: true,
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
30
pubspec.lock
30
pubspec.lock
@ -13,10 +13,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: adaptive_theme
|
||||
sha256: "84af26cfc68220df3cd35d9d94cb8953e7182ef560e13d8efb87f32bf1e588fc"
|
||||
sha256: "61bde10390e937d11d05c6cf0d5cf378a73d49f9a442262e43613dae60ed0b3f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.2.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -201,6 +201,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.8"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
diff_match_patch:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -213,10 +229,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
|
||||
sha256: "3e5c4a94d112540d0c9a6b7f3969832e1604eb8cde0f88d0808382f9f632100b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.6"
|
||||
version: "5.0.3"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -572,10 +588,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: "5076f09225f91dc49289a4ccb92df2eeea9ea01cf7c26d49b3a1f04c6a49eec1"
|
||||
sha256: db2ff852ed77090ba9f62d3611e4208a3d11dfa35991a81ae724c113fcb3e3f7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.3.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1359,4 +1375,4 @@ packages:
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=2.19.0 <3.0.0"
|
||||
flutter: ">=3.7.5"
|
||||
flutter: ">=3.7.8"
|
||||
|
11
pubspec.yaml
11
pubspec.yaml
@ -1,21 +1,22 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 1.3.1+100
|
||||
version: 1.3.4+103
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
flutter: "3.7.5"
|
||||
flutter: "3.7.8"
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.0.0
|
||||
adaptive_theme: ^3.2.0
|
||||
badges: ^3.0.2
|
||||
bloc: ^8.1.1
|
||||
cached_network_image: ^3.2.3
|
||||
clipboard: ^0.1.3
|
||||
collection: ^1.17.0
|
||||
connectivity_plus: ^3.0.2
|
||||
dio: ^4.0.6
|
||||
device_info_plus: ^8.1.0
|
||||
dio: ^5.0.3
|
||||
equatable: ^2.0.5
|
||||
fast_gbk: ^1.0.0
|
||||
feature_discovery:
|
||||
@ -44,7 +45,7 @@ dependencies:
|
||||
hydrated_bloc: ^9.1.0
|
||||
intl: ^0.18.0
|
||||
linkify: ^4.1.0
|
||||
logger: ^1.1.0
|
||||
logger: ^1.3.0
|
||||
memoize: ^3.0.0
|
||||
package_info_plus: ^3.0.3
|
||||
path: ^1.8.2
|
||||
|
Submodule submodules/flutter updated: c07f788888...90c64ed42b
Reference in New Issue
Block a user