Compare commits

...

5 Commits

Author SHA1 Message Date
d83381a7fd v0.2.30 (#68)
* bumped version.

* cache  in case app gets killed in the background.

* bumped flutter version.

* updated

* improved 'ReplyBox'.

* fixed lint.

* fixed EditCubit.

* updated EditCubit.

* fixed navigation in tablet mode.

* clear cache after submission.
2022-08-05 22:13:30 -07:00
764ff09345 v0.2.29-hotfix (#67)
* fixed datetime extension.

* updated README.md
2022-07-06 20:37:18 -07:00
ab449adce2 v0.2.29 (#66)
* fixed stuff.

* fixed UI.

* bumped version.
2022-07-06 17:04:14 -07:00
2ec41b26f2 updated README.md 2022-07-06 16:23:05 -07:00
19f2107d95 v0.2.28 (#65)
* bumped version.

* fixed comments cubit and story tile.

* cancel subscription on error.
2022-07-02 01:14:40 -07:00
34 changed files with 460 additions and 157 deletions

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
JAVA_VERSION: "11.0" JAVA_VERSION: "11.0"
FLUTTER_VERSION: "3.0.3" FLUTTER_VERSION: "3.0.5"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
@ -20,7 +20,7 @@ jobs:
java-version: '17' java-version: '17'
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: '3.0.3' flutter-version: '3.0.5'
channel: 'stable' channel: 'stable'
- run: flutter pub get - run: flutter pub get
- run: flutter analyze - run: flutter analyze

View File

@ -1,14 +1,13 @@
# <img width="64" src="https://user-images.githubusercontent.com/7277662/167775086-0b234f28-dee4-44f6-aae4-14a28ed4bbb6.png"> Hacki for Hacker News # <img width="64" src="https://user-images.githubusercontent.com/7277662/167775086-0b234f28-dee4-44f6-aae4-14a28ed4bbb6.png"> Hacki for Hacker News
A simple noiseless [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough. A [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough.
[![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone) [![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
[![Fdroid version](https://img.shields.io/f-droid/v/com.jiaqifeng.hacki)](https://f-droid.org/en/packages/com.jiaqifeng.hacki/) [![Fdroid version](https://img.shields.io/f-droid/v/com.jiaqifeng.hacki)](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
[![GH version](https://img.shields.io/github/release/livinglist/hacki.svg?logo=github)](https://github.com/Livinglist/Hacki/releases/latest) [![GH version](https://img.shields.io/github/release/livinglist/hacki.svg?logo=github)](https://github.com/Livinglist/Hacki/releases/latest)
[![Visits Badge](https://badges.pufler.dev/visits/livinglist/Hacki)](https://badges.pufler.dev)
[![GitHub](https://img.shields.io/github/stars/livinglist/Hacki?style=social)](https://img.shields.io/github/stars/livinglist/Hacki?style=social)
[![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://pub.dev/packages/effective_dart) [![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://pub.dev/packages/effective_dart)
[![GitHub](https://img.shields.io/github/stars/livinglist/Hacki?style=social)](https://img.shields.io/github/stars/livinglist/Hacki?style=social)
[<img src="assets/images/app_store_badge.png" height="50">](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone) [<img src="assets/images/google_play_badge.png" height="50">](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US) [<img src="assets/images/f_droid_badge.png" height="50">](https://f-droid.org/en/packages/com.jiaqifeng.hacki/) [<img src="assets/images/app_store_badge.png" height="50">](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone) [<img src="assets/images/google_play_badge.png" height="50">](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US) [<img src="assets/images/f_droid_badge.png" height="50">](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)

View File

@ -0,0 +1,3 @@
- Lazy loading.
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,3 @@
- Lazy loading.
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,3 @@
- Lazy loading.
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,3 @@
- Lazy loading.
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -568,7 +568,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -577,7 +577,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.27; MARKETING_VERSION = 0.2.30;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -705,7 +705,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -714,7 +714,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.27; MARKETING_VERSION = 0.2.30;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -736,7 +736,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -745,7 +745,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.27; MARKETING_VERSION = 0.2.30;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -11,6 +11,7 @@ import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:logger/logger.dart';
part 'comments_state.dart'; part 'comments_state.dart';
@ -21,6 +22,7 @@ class CommentsCubit extends Cubit<CommentsState> {
OfflineRepository? offlineRepository, OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository, StoriesRepository? storiesRepository,
SembastRepository? sembastRepository, SembastRepository? sembastRepository,
Logger? logger,
required bool offlineReading, required bool offlineReading,
required Item item, required Item item,
required FetchMode defaultFetchMode, required FetchMode defaultFetchMode,
@ -33,6 +35,7 @@ class CommentsCubit extends Cubit<CommentsState> {
storiesRepository ?? locator.get<StoriesRepository>(), storiesRepository ?? locator.get<StoriesRepository>(),
_sembastRepository = _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(), sembastRepository ?? locator.get<SembastRepository>(),
_logger = logger ?? locator.get<Logger>(),
super( super(
CommentsState.init( CommentsState.init(
offlineReading: offlineReading, offlineReading: offlineReading,
@ -47,8 +50,17 @@ class CommentsCubit extends Cubit<CommentsState> {
final OfflineRepository _offlineRepository; final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository; final StoriesRepository _storiesRepository;
final SembastRepository _sembastRepository; final SembastRepository _sembastRepository;
final Logger _logger;
/// The [StreamSubscription] for stream (both lazy or eager)
/// fetching comments posted directly to the story.
StreamSubscription<Comment>? _streamSubscription; StreamSubscription<Comment>? _streamSubscription;
/// The map of [StreamSubscription] for streams
/// fetching comments lazily. [int] is the id of parent comment.
final Map<int, StreamSubscription<Comment>> _streamSubscriptions =
<int, StreamSubscription<Comment>>{};
static const int _pageSize = 20; static const int _pageSize = 20;
@override @override
@ -73,7 +85,7 @@ class CommentsCubit extends Cubit<CommentsState> {
); );
_streamSubscription = _storiesRepository _streamSubscription = _storiesRepository
.fetchAllCommentsStream( .fetchAllCommentsRecursivelyStream(
ids: targetParents!.last.kids, ids: targetParents!.last.kids,
level: targetParents.last.level + 1, level: targetParents.last.level + 1,
) )
@ -105,7 +117,8 @@ class CommentsCubit extends Cubit<CommentsState> {
.listen(_onCommentFetched) .listen(_onCommentFetched)
..onDone(_onDone); ..onDone(_onDone);
} else { } else {
if (state.fetchMode == FetchMode.lazy) { switch (state.fetchMode) {
case FetchMode.lazy:
_streamSubscription = _storiesRepository _streamSubscription = _storiesRepository
.fetchCommentsStream( .fetchCommentsStream(
ids: kids, ids: kids,
@ -113,14 +126,16 @@ class CommentsCubit extends Cubit<CommentsState> {
) )
.listen(_onCommentFetched) .listen(_onCommentFetched)
..onDone(_onDone); ..onDone(_onDone);
} else { break;
case FetchMode.eager:
_streamSubscription = _storiesRepository _streamSubscription = _storiesRepository
.fetchAllCommentsStream( .fetchAllCommentsRecursivelyStream(
ids: kids, ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null, getFromCache: useCommentCache ? _commentCache.getComment : null,
) )
.listen(_onCommentFetched) .listen(_onCommentFetched)
..onDone(_onDone); ..onDone(_onDone);
break;
} }
} }
} }
@ -135,18 +150,27 @@ class CommentsCubit extends Cubit<CommentsState> {
return; return;
} }
_collapseCache.resetCollapsedComments();
emit( emit(
state.copyWith( state.copyWith(
status: CommentsStatus.loading, status: CommentsStatus.loading,
),
);
_collapseCache.resetCollapsedComments();
await _streamSubscription?.cancel();
for (final int id in _streamSubscriptions.keys) {
await _streamSubscriptions[id]?.cancel();
}
_streamSubscriptions.clear();
emit(
state.copyWith(
comments: <Comment>[], comments: <Comment>[],
currentPage: 0, currentPage: 0,
), ),
); );
await _streamSubscription?.cancel();
final Item item = state.item; final Item item = state.item;
final Item updatedItem = final Item updatedItem =
await _storiesRepository.fetchItemBy(id: item.id) ?? item; await _storiesRepository.fetchItemBy(id: item.id) ?? item;
@ -161,7 +185,7 @@ class CommentsCubit extends Cubit<CommentsState> {
..onDone(_onDone); ..onDone(_onDone);
} else { } else {
_streamSubscription = _storiesRepository _streamSubscription = _storiesRepository
.fetchAllCommentsStream( .fetchAllCommentsRecursivelyStream(
ids: kids, ids: kids,
) )
.listen(_onCommentFetched) .listen(_onCommentFetched)
@ -189,20 +213,20 @@ class CommentsCubit extends Cubit<CommentsState> {
/// [comment] is only used for lazy fetching. /// [comment] is only used for lazy fetching.
void loadMore({Comment? comment}) { void loadMore({Comment? comment}) {
if (state.fetchMode == FetchMode.eager) { switch (state.fetchMode) {
if (_streamSubscription != null) { case FetchMode.lazy:
emit(state.copyWith(status: CommentsStatus.loading));
_streamSubscription?.resume();
}
} else {
if (comment == null) return; if (comment == null) return;
if (_streamSubscriptions.containsKey(comment.id)) return;
final int level = comment.level + 1; final int level = comment.level + 1;
int offset = 0; int offset = 0;
_streamSubscription = _streamSubscription = /// Ignoring because the subscription will be cancelled in close()
_storiesRepository.fetchCommentsStream(ids: comment.kids).listen( // ignore: cancel_subscriptions
(Comment cmt) { final StreamSubscription<Comment> streamSubscription =
_storiesRepository
.fetchCommentsStream(ids: comment.kids)
.listen((Comment cmt) {
_collapseCache.addKid(cmt.id, to: cmt.parent); _collapseCache.addKid(cmt.id, to: cmt.parent);
_commentCache.cacheComment(cmt); _commentCache.cacheComment(cmt);
_sembastRepository.cacheComment(cmt); _sembastRepository.cacheComment(cmt);
@ -223,8 +247,25 @@ class CommentsCubit extends Cubit<CommentsState> {
), ),
); );
offset++; offset++;
}, })
); ..onDone(() {
_streamSubscriptions[comment.id]?.cancel();
_streamSubscriptions.remove(comment.id);
})
..onError((dynamic error) {
_logger.e(error);
_streamSubscriptions[comment.id]?.cancel();
_streamSubscriptions.remove(comment.id);
});
_streamSubscriptions[comment.id] = streamSubscription;
break;
case FetchMode.eager:
if (_streamSubscription != null) {
emit(state.copyWith(status: CommentsStatus.loading));
_streamSubscription?.resume();
}
break;
} }
} }
@ -255,6 +296,10 @@ class CommentsCubit extends Cubit<CommentsState> {
if (state.order == order) return; if (state.order == order) return;
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
_streamSubscription?.cancel(); _streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
s.cancel();
}
_streamSubscriptions.clear();
emit(state.copyWith(order: order)); emit(state.copyWith(order: order));
init(useCommentCache: true); init(useCommentCache: true);
} }
@ -265,6 +310,10 @@ class CommentsCubit extends Cubit<CommentsState> {
_collapseCache.resetCollapsedComments(); _collapseCache.resetCollapsedComments();
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
_streamSubscription?.cancel(); _streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
s.cancel();
}
_streamSubscriptions.clear();
emit(state.copyWith(fetchMode: fetchMode)); emit(state.copyWith(fetchMode: fetchMode));
init(useCommentCache: true); init(useCommentCache: true);
} }
@ -360,6 +409,9 @@ class CommentsCubit extends Cubit<CommentsState> {
@override @override
Future<void> close() async { Future<void> close() async {
await _streamSubscription?.cancel(); await _streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
await s.cancel();
}
await super.close(); await super.close();
} }
} }

View File

@ -78,6 +78,8 @@ class CommentsState extends Equatable {
); );
} }
Set<int> get commentIds => comments.map((Comment e) => e.id).toSet();
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
item, item,

View File

@ -1,13 +1,14 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:hacki/utils/debouncer.dart'; import 'package:hacki/utils/debouncer.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
part 'edit_state.dart'; part 'edit_state.dart';
class EditCubit extends Cubit<EditState> { class EditCubit extends HydratedCubit<EditState> {
EditCubit({DraftCache? draftCache}) EditCubit({DraftCache? draftCache})
: _draftCache = draftCache ?? locator.get<DraftCache>(), : _draftCache = draftCache ?? locator.get<DraftCache>(),
_debouncer = Debouncer(delay: const Duration(seconds: 1)), _debouncer = Debouncer(delay: const Duration(seconds: 1)),
@ -47,6 +48,7 @@ class EditCubit extends Cubit<EditState> {
_draftCache.removeDraft(replyingTo: state.replyingTo!.id); _draftCache.removeDraft(replyingTo: state.replyingTo!.id);
} }
emit(const EditState.init()); emit(const EditState.init());
clear();
} }
void onTextChanged(String text) { void onTextChanged(String text) {
@ -61,4 +63,47 @@ class EditCubit extends Cubit<EditState> {
}); });
} }
} }
void deleteDraft() => clear();
@override
EditState? fromJson(Map<String, dynamic> json) {
final String text = json['text'] as String? ?? '';
final Map<String, dynamic>? itemJson =
json['item'] as Map<String, dynamic>?;
final Item? replyingTo = itemJson == null ? null : Item.fromJson(itemJson);
if (replyingTo != null && text.isNotEmpty) {
_draftCache.cacheDraft(text: text, replyingTo: replyingTo.id);
final EditState state = EditState(
text: text,
replyingTo: replyingTo,
);
cachedState = state;
return state;
}
return state;
}
@override
Map<String, dynamic>? toJson(EditState state) {
EditState selected = state;
if (state.replyingTo == null ||
(state.replyingTo?.id != cachedState.replyingTo?.id &&
state.text.isNullOrEmpty)) {
selected = cachedState;
}
return <String, dynamic>{
'text': selected.text,
'item': selected.replyingTo?.toJson(),
};
}
static EditState cachedState = const EditState.init();
} }

View File

@ -33,7 +33,8 @@ class NotificationCubit extends Cubit<NotificationState> {
_preferenceRepository.shouldShowNotification _preferenceRepository.shouldShowNotification
.then((bool showNotification) { .then((bool showNotification) {
if (showNotification) { if (showNotification) {
init(); // Delaying the initialization to prevent janks in home screen.
Future<void>.delayed(const Duration(seconds: 2), init);
} }
}); });

View File

@ -1,5 +1,6 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/services.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/comments/comments_cubit.dart'; import 'package:hacki/cubits/comments/comments_cubit.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
@ -82,12 +83,14 @@ class PreferenceCubit extends Cubit<PreferenceState> {
void selectFetchMode(FetchMode? fetchMode) { void selectFetchMode(FetchMode? fetchMode) {
if (fetchMode == null || state.fetchMode == fetchMode) return; if (fetchMode == null || state.fetchMode == fetchMode) return;
HapticFeedback.lightImpact();
emit(state.copyWith(fetchMode: fetchMode)); emit(state.copyWith(fetchMode: fetchMode));
_preferenceRepository.selectFetchMode(fetchMode); _preferenceRepository.selectFetchMode(fetchMode);
} }
void selectCommentsOrder(CommentsOrder? order) { void selectCommentsOrder(CommentsOrder? order) {
if (order == null || state.order == order) return; if (order == null || state.order == order) return;
HapticFeedback.lightImpact();
emit(state.copyWith(order: order)); emit(state.copyWith(order: order));
_preferenceRepository.selectCommentsOrder(order); _preferenceRepository.selectCommentsOrder(order);
} }

View File

@ -1,7 +1,9 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
extension TryReadContext on BuildContext { extension ContextExtension on BuildContext {
T? tryRead<T>() { T? tryRead<T>() {
try { try {
return read<T>(); return read<T>();
@ -16,4 +18,41 @@ extension TryReadContext on BuildContext {
box == null ? null : box.localToGlobal(Offset.zero) & box.size; box == null ? null : box.localToGlobal(Offset.zero) & box.size;
return rect; return rect;
} }
static double _screenWidth = 0;
static double _storyTileHeight = 0;
static int _storyTileMaxLines = 4;
static const double _screenWidthLowerBound = 428,
_screenWidthUpperBound = 850,
_picHeightLowerBound = 110,
_picHeightUpperBound = 128,
_smallPicHeight = 100,
_picHeightFactor = 0.3;
double get storyTileHeight {
final double screenWidth =
min(MediaQuery.of(this).size.height, MediaQuery.of(this).size.width);
if (screenWidth == _screenWidth) {
return _storyTileHeight;
} else {
_screenWidth = screenWidth;
}
final bool showSmallerPreviewPic = screenWidth > _screenWidthLowerBound &&
screenWidth < _screenWidthUpperBound;
final double height = showSmallerPreviewPic
? _smallPicHeight
: (screenWidth * _picHeightFactor)
.clamp(_picHeightLowerBound, _picHeightUpperBound);
final int maxLines = height == _smallPicHeight ? 3 : 4;
_storyTileMaxLines = maxLines;
_storyTileHeight = height;
return height;
}
int get storyTileMaxLines {
return _storyTileMaxLines;
}
} }

View File

@ -7,10 +7,8 @@ extension DateTimeExtension on DateTime {
return '$gap year${gap == 1 ? '' : 's'} ago'; return '$gap year${gap == 1 ? '' : 's'} ago';
} else if (diff.inDays > 30) { } else if (diff.inDays > 30) {
int gap = now.month - month; int gap = now.month - month;
if (gap == 0) { if (gap <= 0) {
gap = 1; gap = now.month + 12 - month;
} else if (gap < 0) {
gap = now.month + (12 - month);
} }
return '$gap month${gap == 1 ? '' : 's'} ago'; return '$gap month${gap == 1 ? '' : 's'} ago';
} else if (diff.inDays >= 1) { } else if (diff.inDays >= 1) {

View File

@ -13,3 +13,12 @@ extension StringExtension on String {
return replaceAllMapped(regex, (_) => ''); return replaceAllMapped(regex, (_) => '');
} }
} }
extension OptionalStringExtension on String? {
bool get isNullOrEmpty {
if (this == null) return true;
return this!.trim().isEmpty;
}
bool get isNotNullOrEmpty => !isNullOrEmpty;
}

View File

@ -19,6 +19,7 @@ import 'package:hacki/services/custom_bloc_observer.dart';
import 'package:hacki/services/fetcher.dart'; import 'package:hacki/services/fetcher.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject; import 'package:rxdart/rxdart.dart' show BehaviorSubject;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -39,6 +40,12 @@ Future<void> main({bool testing = false}) async {
isTesting = testing; isTesting = testing;
final HydratedStorage storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
);
if (Platform.isIOS) { if (Platform.isIOS) {
unawaited( unawaited(
Workmanager().initialize( Workmanager().initialize(
@ -93,20 +100,26 @@ Future<void> main({bool testing = false}) async {
prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false; prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false;
if (kReleaseMode) { if (kReleaseMode) {
runApp( HydratedBlocOverrides.runZoned(
() => runApp(
HackiApp( HackiApp(
savedThemeMode: savedThemeMode, savedThemeMode: savedThemeMode,
trueDarkMode: trueDarkMode, trueDarkMode: trueDarkMode,
), ),
),
storage: storage,
); );
} else { } else {
BlocOverrides.runZoned( BlocOverrides.runZoned(
() { () {
runApp( HydratedBlocOverrides.runZoned(
() => runApp(
HackiApp( HackiApp(
savedThemeMode: savedThemeMode, savedThemeMode: savedThemeMode,
trueDarkMode: trueDarkMode, trueDarkMode: trueDarkMode,
), ),
),
storage: storage,
); );
}, },
blocObserver: CustomBlocObserver(), blocObserver: CustomBlocObserver(),
@ -187,6 +200,10 @@ class HackiApp extends StatelessWidget {
lazy: false, lazy: false,
create: (BuildContext context) => PostCubit(), create: (BuildContext context) => PostCubit(),
), ),
BlocProvider<EditCubit>(
lazy: false,
create: (BuildContext context) => EditCubit(),
),
], ],
child: AdaptiveTheme( child: AdaptiveTheme(
light: ThemeData( light: ThemeData(

View File

@ -59,6 +59,7 @@ class Comment extends Item {
); );
} }
@override
Map<String, dynamic> toJson() => <String, dynamic>{ Map<String, dynamic> toJson() => <String, dynamic>{
'id': id, 'id': id,
'time': time, 'time': time,

View File

@ -1,7 +1,7 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/extensions/date_time_extension.dart'; import 'package:hacki/extensions/date_time_extension.dart';
abstract class Item extends Equatable { class Item extends Equatable {
const Item({ const Item({
required this.id, required this.id,
required this.deleted, required this.deleted,
@ -35,6 +35,22 @@ abstract class Item extends Equatable {
text = '', text = '',
type = ''; type = '';
Item.fromJson(Map<String, dynamic> json)
: id = json['id'] as int? ?? 0,
score = json['score'] as int? ?? 0,
descendants = json['descendants'] as int? ?? 0,
time = json['time'] as int? ?? 0,
by = json['by'] as String? ?? '',
title = json['title'] as String? ?? '',
text = json['text'] as String? ?? '',
url = json['url'] as String? ?? '',
kids = <int>[],
dead = json['dead'] as bool? ?? false,
deleted = json['deleted'] as bool? ?? false,
parent = json['parent'] as int? ?? 0,
parts = <int>[],
type = json['type'] as String? ?? '';
final int id; final int id;
final int time; final int time;
final int score; final int score;
@ -65,4 +81,40 @@ abstract class Item extends Equatable {
bool get isJob => type == 'job'; bool get isJob => type == 'job';
bool get isComment => type == 'comment'; bool get isComment => type == 'comment';
Map<String, dynamic> toJson() {
return <String, dynamic>{
'descendants': descendants,
'id': id,
'score': score,
'time': time,
'by': by,
'title': title,
'url': url,
'kids': kids,
'text': text,
'dead': dead,
'deleted': deleted,
'type': type,
'parts': parts,
};
}
@override
List<Object?> get props => <Object?>[
id,
deleted,
by,
time,
text,
dead,
parent,
kids,
url,
score,
title,
type,
parts,
descendants,
];
} }

View File

@ -79,6 +79,7 @@ class PollOption extends Item {
); );
} }
@override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return <String, dynamic>{ return <String, dynamic>{
'descendants': descendants, 'descendants': descendants,

View File

@ -91,6 +91,7 @@ class Story extends Item {
String get simpleMetadata => String get simpleMetadata =>
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate'''; '''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate''';
@override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return <String, dynamic>{ return <String, dynamic>{
'descendants': descendants, 'descendants': descendants,

View File

@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class User { class User {
@ -39,8 +37,6 @@ class User {
@override @override
String toString() { String toString() {
final String prettyString = return 'User $about, $created, $delay, $id, $karma';
const JsonEncoder.withIndent(' ').convert(this);
return 'User $prettyString';
} }
} }

View File

@ -73,7 +73,7 @@ class StoriesRepository {
return; return;
} }
Stream<Comment> fetchAllCommentsStream({ Stream<Comment> fetchAllCommentsRecursivelyStream({
required List<int> ids, required List<int> ids,
int level = 0, int level = 0,
Comment? Function(int)? getFromCache, Comment? Function(int)? getFromCache,
@ -94,7 +94,7 @@ class StoriesRepository {
if (comment != null) { if (comment != null) {
yield comment; yield comment;
yield* fetchAllCommentsStream( yield* fetchAllCommentsRecursivelyStream(
ids: comment.kids, ids: comment.kids,
level: level + 1, level: level + 1,
getFromCache: getFromCache, getFromCache: getFromCache,

View File

@ -96,10 +96,6 @@ class ItemScreen extends StatefulWidget {
useCommentCache: args.useCommentCache, useCommentCache: args.useCommentCache,
), ),
), ),
BlocProvider<EditCubit>(
lazy: false,
create: (BuildContext context) => EditCubit(),
),
], ],
child: ItemScreen( child: ItemScreen(
item: args.item, item: args.item,
@ -141,10 +137,6 @@ class ItemScreen extends StatefulWidget {
targetParents: args.targetComments, targetParents: args.targetComments,
), ),
), ),
BlocProvider<EditCubit>(
lazy: false,
create: (BuildContext context) => EditCubit(),
),
], ],
child: ItemScreen( child: ItemScreen(
item: args.item, item: args.item,
@ -164,7 +156,7 @@ class ItemScreen extends StatefulWidget {
_ItemScreenState createState() => _ItemScreenState(); _ItemScreenState createState() => _ItemScreenState();
} }
class _ItemScreenState extends State<ItemScreen> { class _ItemScreenState extends State<ItemScreen> with RouteAware {
final TextEditingController commentEditingController = final TextEditingController commentEditingController =
TextEditingController(); TextEditingController();
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
@ -185,11 +177,20 @@ class _ItemScreenState extends State<ItemScreen> {
static const Duration _featureDiscoveryDismissThrottleDelay = static const Duration _featureDiscoveryDismissThrottleDelay =
Duration(seconds: 1); Duration(seconds: 1);
@override
void didPop() {
super.didPop();
if (context.read<EditCubit>().state.text.isNullOrEmpty) {
context.read<EditCubit>().onReplyBoxClosed();
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) { SchedulerBinding.instance
..addPostFrameCallback((_) {
if (!isTesting) { if (!isTesting) {
FeatureDiscovery.discoverFeatures( FeatureDiscovery.discoverFeatures(
context, context,
@ -200,6 +201,15 @@ class _ItemScreenState extends State<ItemScreen> {
}, },
); );
} }
})
..addPostFrameCallback((_) {
final ModalRoute<dynamic>? route = ModalRoute.of(context);
if (route == null) return;
locator
.get<RouteObserver<ModalRoute<dynamic>>>()
.subscribe(this, route);
}); });
scrollController.addListener(() { scrollController.addListener(() {
@ -208,6 +218,8 @@ class _ItemScreenState extends State<ItemScreen> {
context.read<EditCubit>().onScrolled(); context.read<EditCubit>().onScrolled();
} }
}); });
commentEditingController.text = context.read<EditCubit>().state.text ?? '';
} }
@override @override
@ -502,7 +514,7 @@ class _ItemScreenState extends State<ItemScreen> {
Text( Text(
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''', '''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
style: const TextStyle( style: const TextStyle(
fontSize: TextDimens.pt12, fontSize: TextDimens.pt13,
), ),
), ),
] else ...<Widget>[ ] else ...<Widget>[
@ -523,7 +535,12 @@ class _ItemScreenState extends State<ItemScreen> {
strokeWidth: Dimens.pt2, strokeWidth: Dimens.pt2,
), ),
) )
: const Text('View parent thread'), : const Text(
'View parent thread',
style: TextStyle(
fontSize: TextDimens.pt13,
),
),
), ),
], ],
const Spacer(), const Spacer(),
@ -537,7 +554,7 @@ class _ItemScreenState extends State<ItemScreen> {
child: Text( child: Text(
'Lazy', 'Lazy',
style: TextStyle( style: TextStyle(
fontSize: TextDimens.pt12, fontSize: TextDimens.pt13,
), ),
), ),
), ),
@ -546,7 +563,7 @@ class _ItemScreenState extends State<ItemScreen> {
child: Text( child: Text(
'Eager', 'Eager',
style: TextStyle( style: TextStyle(
fontSize: TextDimens.pt12, fontSize: TextDimens.pt13,
), ),
), ),
), ),
@ -568,7 +585,7 @@ class _ItemScreenState extends State<ItemScreen> {
child: Text( child: Text(
'Natural', 'Natural',
style: TextStyle( style: TextStyle(
fontSize: TextDimens.pt12, fontSize: TextDimens.pt13,
), ),
), ),
), ),
@ -577,7 +594,7 @@ class _ItemScreenState extends State<ItemScreen> {
child: Text( child: Text(
'Newest first', 'Newest first',
style: TextStyle( style: TextStyle(
fontSize: TextDimens.pt12, fontSize: TextDimens.pt13,
), ),
), ),
), ),
@ -586,7 +603,7 @@ class _ItemScreenState extends State<ItemScreen> {
child: Text( child: Text(
'Oldest first', 'Oldest first',
style: TextStyle( style: TextStyle(
fontSize: TextDimens.pt12, fontSize: TextDimens.pt13,
), ),
), ),
), ),
@ -680,7 +697,8 @@ class _ItemScreenState extends State<ItemScreen> {
return BlocListener<EditCubit, EditState>( return BlocListener<EditCubit, EditState>(
listenWhen: (EditState previous, EditState current) { listenWhen: (EditState previous, EditState current) {
return previous.replyingTo != current.replyingTo || return previous.replyingTo != current.replyingTo ||
previous.itemBeingEdited != current.itemBeingEdited; previous.itemBeingEdited != current.itemBeingEdited ||
commentEditingController.text != current.text;
}, },
listener: (BuildContext context, EditState editState) { listener: (BuildContext context, EditState editState) {
if (editState.replyingTo != null || if (editState.replyingTo != null ||

View File

@ -5,7 +5,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/item.dart'; import 'package:hacki/models/item.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/link_util.dart'; import 'package:hacki/utils/link_util.dart';
@ -145,6 +147,39 @@ class _ReplyBoxState extends State<ReplyBox> {
color: Palette.orange, color: Palette.orange,
), ),
onPressed: () { onPressed: () {
final EditState state =
context.read<EditCubit>().state;
if (state.replyingTo != null &&
state.text.isNotNullOrEmpty) {
showDialog<void>(
context: context,
builder: (BuildContext context) =>
AlertDialog(
title: const Text('Save draft?'),
actions: <Widget>[
TextButton(
onPressed: () {
context
.read<EditCubit>()
.deleteDraft();
Navigator.pop(context);
},
child: const Text(
'No',
style: TextStyle(
color: Palette.red,
),
),
),
TextButton(
onPressed: () =>
Navigator.pop(context),
child: const Text('Yes'),
),
],
),
);
}
widget.onCloseTapped(); widget.onCloseTapped();
expanded = false; expanded = false;
}, },
@ -249,8 +284,31 @@ class _ReplyBoxState extends State<ReplyBox> {
style: const TextStyle(color: Palette.grey), style: const TextStyle(color: Palette.grey),
), ),
const Spacer(), const Spacer(),
if (replyingTo != null)
TextButton( TextButton(
child: const Text('Copy All'), child: const Text('View thread'),
onPressed: () {
HapticFeedback.lightImpact();
setState(() {
expanded = false;
});
Navigator.popUntil(
context,
(Route<dynamic> route) =>
route.settings.name == ItemScreen.routeName ||
route.isFirst,
);
goToItemScreen(
args: ItemScreenArgs(
item: replyingTo,
useCommentCache: true,
),
forceNewScreen: true,
);
},
),
TextButton(
child: const Text('Copy all'),
onPressed: () => FlutterClipboard.copy( onPressed: () => FlutterClipboard.copy(
replyingTo?.text ?? '', replyingTo?.text ?? '',
).then((_) => HapticFeedback.selectionClick()), ).then((_) => HapticFeedback.selectionClick()),

View File

@ -20,7 +20,7 @@ class _ScrollUpIconButtonState extends State<ScrollUpIconButton> {
super.initState(); super.initState();
widget.scrollController.addListener(() { widget.scrollController.addListener(() {
if (widget.scrollController.offset <= 1000) { if (widget.scrollController.offset <= 1000 && mounted) {
setState(() {}); setState(() {});
} }
}); });

View File

@ -526,7 +526,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationName: 'Hacki', applicationName: 'Hacki',
applicationVersion: 'v0.2.27', applicationVersion: 'v0.2.30',
applicationIcon: ClipRRect( applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular( Radius.circular(

View File

@ -288,11 +288,22 @@ class CommentTile extends StatelessWidget {
!context !context
.read<CommentsCubit>() .read<CommentsCubit>()
.state .state
.comments .commentIds
.map((Comment e) => e.id) .contains(comment.kids.first) &&
.toSet() !context
.contains(comment.kids.first)) .read<CommentsCubit>()
.state
.onlyShowTargetComment)
Center( Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt12,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextButton( child: TextButton(
onPressed: () { onPressed: () {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
@ -308,6 +319,10 @@ class CommentTile extends StatelessWidget {
), ),
), ),
), ),
],
),
),
),
const Divider( const Divider(
height: Dimens.zero, height: Dimens.zero,
), ),

View File

@ -33,7 +33,7 @@ class _CountDownReminderState extends State<CountdownReminder>
bool isVisible = false; bool isVisible = false;
static const Duration countdownDuration = Duration(seconds: 8); static const Duration countdownDuration = Duration(seconds: 8);
static const Duration visibilityCountdownDuration = Duration(seconds: 3); static const Duration visibilityCountdownDuration = Duration.zero;
@override @override
void initState() { void initState() {

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/link_preview/link_view.dart'; import 'package:hacki/screens/widgets/link_preview/link_view.dart';
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart'; import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart';
@ -199,23 +200,9 @@ class _LinkPreviewState extends State<LinkPreview> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const double screenWidthLowerBound = 428,
screenWidthUpperBound = 850,
picHeightLowerBound = 118,
picHeightUpperBound = 140,
smallPicHeight = 100,
picHeightFactor = 0.14;
final double screenWidth = MediaQuery.of(context).size.width;
final bool showSmallerPreviewPic = screenWidth > screenWidthLowerBound &&
screenWidth < screenWidthUpperBound;
final double height = showSmallerPreviewPic
? smallPicHeight
: (MediaQuery.of(context).size.height * picHeightFactor)
.clamp(picHeightLowerBound, picHeightUpperBound);
final Widget loadingWidget = widget.placeholderWidget ?? final Widget loadingWidget = widget.placeholderWidget ??
Container( Container(
height: height, height: context.storyTileHeight,
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
@ -232,13 +219,13 @@ class _LinkPreviewState extends State<LinkPreview> {
final WebInfo? info = _info as WebInfo?; final WebInfo? info = _info as WebInfo?;
loadedWidget = _info == null loadedWidget = _info == null
? _buildLinkContainer( ? _buildLinkContainer(
height, context.storyTileHeight,
title: _errorTitle, title: _errorTitle,
desc: _errorBody, desc: _errorBody,
imageUri: null, imageUri: null,
) )
: _buildLinkContainer( : _buildLinkContainer(
height, context.storyTileHeight,
title: _errorTitle, title: _errorTitle,
desc: WebAnalyzer.isNotEmpty(info!.description) desc: WebAnalyzer.isNotEmpty(info!.description)
? info.description ? info.description

View File

@ -147,7 +147,7 @@ class LinkView extends StatelessWidget {
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) { Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(4, 2, 3, 1), padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
Container( Container(
@ -168,7 +168,7 @@ class LinkView extends StatelessWidget {
return Expanded( return Expanded(
flex: 2, flex: 2,
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(5, 3, 5, 0), padding: const EdgeInsets.fromLTRB(5, 2, 5, 0),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
if (showMetadata) if (showMetadata)

View File

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart'; import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
@ -29,20 +30,7 @@ class StoryTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (showWebPreview) { if (showWebPreview) {
const double screenWidthLowerBound = 428, final double height = context.storyTileHeight;
screenWidthUpperBound = 850,
picHeightLowerBound = 118,
picHeightUpperBound = 140,
smallPicHeight = 100,
picHeightFactor = 0.14;
final double screenWidth = MediaQuery.of(context).size.width;
final bool showSmallerPreviewPic = screenWidth > screenWidthLowerBound &&
screenWidth < screenWidthUpperBound;
final double height = showSmallerPreviewPic
? smallPicHeight
: (MediaQuery.of(context).size.height * picHeightFactor)
.clamp(picHeightLowerBound, picHeightUpperBound);
return TapDownWrapper( return TapDownWrapper(
onTap: onTap, onTap: onTap,
child: Padding( child: Padding(
@ -143,7 +131,7 @@ class StoryTile extends StatelessWidget {
backgroundColor: Palette.transparent, backgroundColor: Palette.transparent,
borderRadius: Dimens.zero, borderRadius: Dimens.zero,
removeElevation: true, removeElevation: true,
bodyMaxLines: height == smallPicHeight ? 3 : 4, bodyMaxLines: context.storyTileMaxLines,
errorTitle: story.title, errorTitle: story.title,
titleStyle: TextStyle( titleStyle: TextStyle(
color: hasRead color: hasRead

View File

@ -27,6 +27,7 @@ abstract class TextDimens {
static const double pt8 = 8; static const double pt8 = 8;
static const double pt10 = 10; static const double pt10 = 10;
static const double pt12 = 12; static const double pt12 = 12;
static const double pt13 = 13;
static const double pt14 = 14; static const double pt14 = 14;
static const double pt15 = 15; static const double pt15 = 15;
static const double pt16 = 16; static const double pt16 = 16;

View File

@ -490,6 +490,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.1" version: "4.0.1"
hydrated_bloc:
dependency: "direct main"
description:
name: hydrated_bloc
url: "https://pub.dartlang.org"
source: hosted
version: "8.1.0"
integration_test: integration_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -1246,4 +1253,4 @@ packages:
version: "3.1.1" version: "3.1.1"
sdks: sdks:
dart: ">=2.17.0 <3.0.0" dart: ">=2.17.0 <3.0.0"
flutter: ">=3.0.3" flutter: ">=3.0.5"

View File

@ -1,11 +1,11 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 0.2.27+69 version: 0.2.30+73
publish_to: none publish_to: none
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
flutter: "3.0.3" flutter: "3.0.5"
dependencies: dependencies:
adaptive_theme: ^3.0.0 adaptive_theme: ^3.0.0
@ -42,6 +42,7 @@ dependencies:
html: ^0.15.0 html: ^0.15.0
html_unescape: ^2.0.0 html_unescape: ^2.0.0
http: ^0.13.3 http: ^0.13.3
hydrated_bloc: ^8.1.0
intl: ^0.17.0 intl: ^0.17.0
logger: ^1.1.0 logger: ^1.1.0
path: ^1.8.0 path: ^1.8.0