Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
19f2107d95 | |||
c9b2d82dfc |
Before Width: | Height: | Size: 935 KiB After Width: | Height: | Size: 820 KiB |
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 406 KiB |
3
fastlane/metadata/android/en-US/changelogs/69.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Lazy loading.
|
||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/70.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Lazy loading.
|
||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
Before Width: | Height: | Size: 935 KiB After Width: | Height: | Size: 820 KiB |
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 406 KiB |
@ -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 = 2;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
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.26;
|
MARKETING_VERSION = 0.2.28;
|
||||||
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 = 2;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
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.26;
|
MARKETING_VERSION = 0.2.28;
|
||||||
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 = 2;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
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.26;
|
MARKETING_VERSION = 0.2.28;
|
||||||
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 = "";
|
||||||
|
@ -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,8 +22,11 @@ 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 CommentsOrder defaultCommentsOrder,
|
||||||
}) : _collapseCache = collapseCache,
|
}) : _collapseCache = collapseCache,
|
||||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||||
_offlineRepository =
|
_offlineRepository =
|
||||||
@ -31,15 +35,32 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
_sembastRepository =
|
_sembastRepository =
|
||||||
sembastRepository ?? locator.get<SembastRepository>(),
|
sembastRepository ?? locator.get<SembastRepository>(),
|
||||||
super(CommentsState.init(offlineReading: offlineReading, item: item));
|
_logger = logger ?? locator.get<Logger>(),
|
||||||
|
super(
|
||||||
|
CommentsState.init(
|
||||||
|
offlineReading: offlineReading,
|
||||||
|
item: item,
|
||||||
|
fetchMode: defaultFetchMode,
|
||||||
|
order: defaultCommentsOrder,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final CollapseCache _collapseCache;
|
final CollapseCache _collapseCache;
|
||||||
final CommentCache _commentCache;
|
final CommentCache _commentCache;
|
||||||
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
|
||||||
@ -64,7 +85,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
_streamSubscription = _storiesRepository
|
_streamSubscription = _storiesRepository
|
||||||
.fetchCommentsStream(
|
.fetchAllCommentsRecursivelyStream(
|
||||||
ids: targetParents!.last.kids,
|
ids: targetParents!.last.kids,
|
||||||
level: targetParents.last.level + 1,
|
level: targetParents.last.level + 1,
|
||||||
)
|
)
|
||||||
@ -74,7 +95,13 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(state.copyWith(status: CommentsStatus.loading));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: CommentsStatus.loading,
|
||||||
|
comments: <Comment>[],
|
||||||
|
currentPage: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final Item item = state.item;
|
final Item item = state.item;
|
||||||
final Item updatedItem = state.offlineReading
|
final Item updatedItem = state.offlineReading
|
||||||
@ -90,13 +117,26 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
.listen(_onCommentFetched)
|
.listen(_onCommentFetched)
|
||||||
..onDone(_onDone);
|
..onDone(_onDone);
|
||||||
} else {
|
} else {
|
||||||
_streamSubscription = _storiesRepository
|
switch (state.fetchMode) {
|
||||||
.fetchCommentsStream(
|
case FetchMode.lazy:
|
||||||
ids: kids,
|
_streamSubscription = _storiesRepository
|
||||||
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
.fetchCommentsStream(
|
||||||
)
|
ids: kids,
|
||||||
.listen(_onCommentFetched)
|
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||||
..onDone(_onDone);
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
break;
|
||||||
|
case FetchMode.eager:
|
||||||
|
_streamSubscription = _storiesRepository
|
||||||
|
.fetchAllCommentsRecursivelyStream(
|
||||||
|
ids: kids,
|
||||||
|
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||||
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,26 +150,47 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_collapseCache.resetCollapsedComments();
|
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CommentsStatus.loading,
|
status: CommentsStatus.loading,
|
||||||
comments: <Comment>[],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_collapseCache.resetCollapsedComments();
|
||||||
|
|
||||||
await _streamSubscription?.cancel();
|
await _streamSubscription?.cancel();
|
||||||
|
for (final int id in _streamSubscriptions.keys) {
|
||||||
|
await _streamSubscriptions[id]?.cancel();
|
||||||
|
}
|
||||||
|
_streamSubscriptions.clear();
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
comments: <Comment>[],
|
||||||
|
currentPage: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
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;
|
||||||
final List<int> kids = sortKids(updatedItem.kids);
|
final List<int> kids = sortKids(updatedItem.kids);
|
||||||
|
|
||||||
_streamSubscription = _storiesRepository
|
if (state.fetchMode == FetchMode.lazy) {
|
||||||
.fetchCommentsStream(ids: kids)
|
_streamSubscription = _storiesRepository
|
||||||
.listen(_onCommentFetched)
|
.fetchCommentsStream(
|
||||||
..onDone(_onDone);
|
ids: kids,
|
||||||
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
} else {
|
||||||
|
_streamSubscription = _storiesRepository
|
||||||
|
.fetchAllCommentsRecursivelyStream(
|
||||||
|
ids: kids,
|
||||||
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -144,17 +205,67 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
onlyShowTargetComment: false,
|
onlyShowTargetComment: false,
|
||||||
comments: <Comment>[],
|
|
||||||
item: story,
|
item: story,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadMore() {
|
/// [comment] is only used for lazy fetching.
|
||||||
if (_streamSubscription != null) {
|
void loadMore({Comment? comment}) {
|
||||||
emit(state.copyWith(status: CommentsStatus.loading));
|
switch (state.fetchMode) {
|
||||||
_streamSubscription?.resume();
|
case FetchMode.lazy:
|
||||||
|
if (comment == null) return;
|
||||||
|
if (_streamSubscriptions.containsKey(comment.id)) return;
|
||||||
|
|
||||||
|
final int level = comment.level + 1;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
/// Ignoring because the subscription will be cancelled in close()
|
||||||
|
// ignore: cancel_subscriptions
|
||||||
|
final StreamSubscription<Comment> streamSubscription =
|
||||||
|
_storiesRepository
|
||||||
|
.fetchCommentsStream(ids: comment.kids)
|
||||||
|
.listen((Comment cmt) {
|
||||||
|
_collapseCache.addKid(cmt.id, to: cmt.parent);
|
||||||
|
_commentCache.cacheComment(cmt);
|
||||||
|
_sembastRepository.cacheComment(cmt);
|
||||||
|
|
||||||
|
final List<LinkifyElement> elements = _linkify(
|
||||||
|
cmt.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
final BuildableComment buildableComment =
|
||||||
|
BuildableComment.fromComment(cmt, elements: elements);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
comments: <Comment>[...state.comments]..insert(
|
||||||
|
state.comments.indexOf(comment) + offset + 1,
|
||||||
|
buildableComment.copyWith(level: level),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,10 +292,29 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onOrderChanged(CommentsOrder? order) {
|
void onOrderChanged(CommentsOrder? order) {
|
||||||
HapticFeedback.selectionClick();
|
|
||||||
if (order == null) return;
|
if (order == null) return;
|
||||||
|
if (state.order == order) return;
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
_streamSubscription?.cancel();
|
_streamSubscription?.cancel();
|
||||||
emit(state.copyWith(order: order, comments: <Comment>[]));
|
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||||
|
s.cancel();
|
||||||
|
}
|
||||||
|
_streamSubscriptions.clear();
|
||||||
|
emit(state.copyWith(order: order));
|
||||||
|
init(useCommentCache: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFetchModeChanged(FetchMode? fetchMode) {
|
||||||
|
if (fetchMode == null) return;
|
||||||
|
if (state.fetchMode == fetchMode) return;
|
||||||
|
_collapseCache.resetCollapsedComments();
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
_streamSubscription?.cancel();
|
||||||
|
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||||
|
s.cancel();
|
||||||
|
}
|
||||||
|
_streamSubscriptions.clear();
|
||||||
|
emit(state.copyWith(fetchMode: fetchMode));
|
||||||
init(useCommentCache: true);
|
init(useCommentCache: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,21 +359,24 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
|
|
||||||
emit(state.copyWith(comments: updatedComments));
|
emit(state.copyWith(comments: updatedComments));
|
||||||
|
|
||||||
if (updatedComments.length >= _pageSize + _pageSize * state.currentPage &&
|
if (state.fetchMode == FetchMode.eager) {
|
||||||
updatedComments.length <=
|
if (updatedComments.length >=
|
||||||
_pageSize * 2 + _pageSize * state.currentPage) {
|
_pageSize + _pageSize * state.currentPage &&
|
||||||
final bool isHidden = _collapseCache.isHidden(comment.id);
|
updatedComments.length <=
|
||||||
|
_pageSize * 2 + _pageSize * state.currentPage) {
|
||||||
|
final bool isHidden = _collapseCache.isHidden(comment.id);
|
||||||
|
|
||||||
if (!isHidden) {
|
if (!isHidden) {
|
||||||
_streamSubscription?.pause();
|
_streamSubscription?.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
currentPage: state.currentPage + 1,
|
||||||
|
status: CommentsStatus.loaded,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
currentPage: state.currentPage + 1,
|
|
||||||
status: CommentsStatus.loaded,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -276,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,11 @@ enum CommentsOrder {
|
|||||||
oldestFirst,
|
oldestFirst,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FetchMode {
|
||||||
|
lazy,
|
||||||
|
eager,
|
||||||
|
}
|
||||||
|
|
||||||
class CommentsState extends Equatable {
|
class CommentsState extends Equatable {
|
||||||
const CommentsState({
|
const CommentsState({
|
||||||
required this.item,
|
required this.item,
|
||||||
@ -21,6 +26,7 @@ class CommentsState extends Equatable {
|
|||||||
required this.status,
|
required this.status,
|
||||||
required this.fetchParentStatus,
|
required this.fetchParentStatus,
|
||||||
required this.order,
|
required this.order,
|
||||||
|
required this.fetchMode,
|
||||||
required this.onlyShowTargetComment,
|
required this.onlyShowTargetComment,
|
||||||
required this.offlineReading,
|
required this.offlineReading,
|
||||||
required this.currentPage,
|
required this.currentPage,
|
||||||
@ -29,10 +35,11 @@ class CommentsState extends Equatable {
|
|||||||
CommentsState.init({
|
CommentsState.init({
|
||||||
required this.offlineReading,
|
required this.offlineReading,
|
||||||
required this.item,
|
required this.item,
|
||||||
|
required this.fetchMode,
|
||||||
|
required this.order,
|
||||||
}) : comments = <Comment>[],
|
}) : comments = <Comment>[],
|
||||||
status = CommentsStatus.init,
|
status = CommentsStatus.init,
|
||||||
fetchParentStatus = CommentsStatus.init,
|
fetchParentStatus = CommentsStatus.init,
|
||||||
order = CommentsOrder.natural,
|
|
||||||
onlyShowTargetComment = false,
|
onlyShowTargetComment = false,
|
||||||
currentPage = 0;
|
currentPage = 0;
|
||||||
|
|
||||||
@ -41,6 +48,7 @@ class CommentsState extends Equatable {
|
|||||||
final CommentsStatus status;
|
final CommentsStatus status;
|
||||||
final CommentsStatus fetchParentStatus;
|
final CommentsStatus fetchParentStatus;
|
||||||
final CommentsOrder order;
|
final CommentsOrder order;
|
||||||
|
final FetchMode fetchMode;
|
||||||
final bool onlyShowTargetComment;
|
final bool onlyShowTargetComment;
|
||||||
final bool offlineReading;
|
final bool offlineReading;
|
||||||
final int currentPage;
|
final int currentPage;
|
||||||
@ -51,6 +59,7 @@ class CommentsState extends Equatable {
|
|||||||
CommentsStatus? status,
|
CommentsStatus? status,
|
||||||
CommentsStatus? fetchParentStatus,
|
CommentsStatus? fetchParentStatus,
|
||||||
CommentsOrder? order,
|
CommentsOrder? order,
|
||||||
|
FetchMode? fetchMode,
|
||||||
bool? onlyShowTargetComment,
|
bool? onlyShowTargetComment,
|
||||||
bool? offlineReading,
|
bool? offlineReading,
|
||||||
int? currentPage,
|
int? currentPage,
|
||||||
@ -61,6 +70,7 @@ class CommentsState extends Equatable {
|
|||||||
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
|
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
order: order ?? this.order,
|
order: order ?? this.order,
|
||||||
|
fetchMode: fetchMode ?? this.fetchMode,
|
||||||
onlyShowTargetComment:
|
onlyShowTargetComment:
|
||||||
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
||||||
offlineReading: offlineReading ?? this.offlineReading,
|
offlineReading: offlineReading ?? this.offlineReading,
|
||||||
@ -68,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,
|
||||||
@ -75,6 +87,7 @@ class CommentsState extends Equatable {
|
|||||||
status,
|
status,
|
||||||
fetchParentStatus,
|
fetchParentStatus,
|
||||||
order,
|
order,
|
||||||
|
fetchMode,
|
||||||
onlyShowTargetComment,
|
onlyShowTargetComment,
|
||||||
offlineReading,
|
offlineReading,
|
||||||
currentPage,
|
currentPage,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
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/cubits/comments/comments_cubit.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
|
|
||||||
part 'preference_state.dart';
|
part 'preference_state.dart';
|
||||||
@ -33,6 +34,10 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
|||||||
.then((bool value) => emit(state.copyWith(markReadStories: value)));
|
.then((bool value) => emit(state.copyWith(markReadStories: value)));
|
||||||
_preferenceRepository.shouldShowMetadata
|
_preferenceRepository.shouldShowMetadata
|
||||||
.then((bool value) => emit(state.copyWith(showMetadata: value)));
|
.then((bool value) => emit(state.copyWith(showMetadata: value)));
|
||||||
|
_preferenceRepository.fetchMode
|
||||||
|
.then((FetchMode value) => emit(state.copyWith(fetchMode: value)));
|
||||||
|
_preferenceRepository.commentsOrder
|
||||||
|
.then((CommentsOrder value) => emit(state.copyWith(order: value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleNotificationMode() {
|
void toggleNotificationMode() {
|
||||||
@ -74,4 +79,16 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
|||||||
emit(state.copyWith(showMetadata: !state.showMetadata));
|
emit(state.copyWith(showMetadata: !state.showMetadata));
|
||||||
_preferenceRepository.toggleMetadataMode();
|
_preferenceRepository.toggleMetadataMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void selectFetchMode(FetchMode? fetchMode) {
|
||||||
|
if (fetchMode == null || state.fetchMode == fetchMode) return;
|
||||||
|
emit(state.copyWith(fetchMode: fetchMode));
|
||||||
|
_preferenceRepository.selectFetchMode(fetchMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectCommentsOrder(CommentsOrder? order) {
|
||||||
|
if (order == null || state.order == order) return;
|
||||||
|
emit(state.copyWith(order: order));
|
||||||
|
_preferenceRepository.selectCommentsOrder(order);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ class PreferenceState extends Equatable {
|
|||||||
required this.useReader,
|
required this.useReader,
|
||||||
required this.markReadStories,
|
required this.markReadStories,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
|
required this.fetchMode,
|
||||||
|
required this.order,
|
||||||
});
|
});
|
||||||
|
|
||||||
const PreferenceState.init()
|
const PreferenceState.init()
|
||||||
@ -20,7 +22,9 @@ class PreferenceState extends Equatable {
|
|||||||
useTrueDark = false,
|
useTrueDark = false,
|
||||||
useReader = false,
|
useReader = false,
|
||||||
markReadStories = false,
|
markReadStories = false,
|
||||||
showMetadata = false;
|
showMetadata = false,
|
||||||
|
fetchMode = FetchMode.eager,
|
||||||
|
order = CommentsOrder.natural;
|
||||||
|
|
||||||
final bool showNotification;
|
final bool showNotification;
|
||||||
final bool showComplexStoryTile;
|
final bool showComplexStoryTile;
|
||||||
@ -30,6 +34,8 @@ class PreferenceState extends Equatable {
|
|||||||
final bool useReader;
|
final bool useReader;
|
||||||
final bool markReadStories;
|
final bool markReadStories;
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
|
final FetchMode fetchMode;
|
||||||
|
final CommentsOrder order;
|
||||||
|
|
||||||
PreferenceState copyWith({
|
PreferenceState copyWith({
|
||||||
bool? showNotification,
|
bool? showNotification,
|
||||||
@ -40,6 +46,8 @@ class PreferenceState extends Equatable {
|
|||||||
bool? useReader,
|
bool? useReader,
|
||||||
bool? markReadStories,
|
bool? markReadStories,
|
||||||
bool? showMetadata,
|
bool? showMetadata,
|
||||||
|
FetchMode? fetchMode,
|
||||||
|
CommentsOrder? order,
|
||||||
}) {
|
}) {
|
||||||
return PreferenceState(
|
return PreferenceState(
|
||||||
showNotification: showNotification ?? this.showNotification,
|
showNotification: showNotification ?? this.showNotification,
|
||||||
@ -50,6 +58,8 @@ class PreferenceState extends Equatable {
|
|||||||
useReader: useReader ?? this.useReader,
|
useReader: useReader ?? this.useReader,
|
||||||
markReadStories: markReadStories ?? this.markReadStories,
|
markReadStories: markReadStories ?? this.markReadStories,
|
||||||
showMetadata: showMetadata ?? this.showMetadata,
|
showMetadata: showMetadata ?? this.showMetadata,
|
||||||
|
fetchMode: fetchMode ?? this.fetchMode,
|
||||||
|
order: order ?? this.order,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,5 +73,7 @@ class PreferenceState extends Equatable {
|
|||||||
useReader,
|
useReader,
|
||||||
markReadStories,
|
markReadStories,
|
||||||
showMetadata,
|
showMetadata,
|
||||||
|
fetchMode,
|
||||||
|
order,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
@ -9,4 +11,48 @@ extension TryReadContext on BuildContext {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rect? get rect {
|
||||||
|
final RenderBox? box = findRenderObject() as RenderBox?;
|
||||||
|
final Rect? rect =
|
||||||
|
box == null ? null : box.localToGlobal(Offset.zero) & box.size;
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/models/item.dart';
|
||||||
|
|
||||||
enum StoryType {
|
enum StoryType {
|
||||||
@ -113,9 +111,10 @@ class Story extends Item {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
final String prettyString =
|
// final String prettyString =
|
||||||
const JsonEncoder.withIndent(' ').convert(this);
|
// const JsonEncoder.withIndent(' ').convert(this);
|
||||||
return 'Story $prettyString';
|
// return 'Story $prettyString';
|
||||||
|
return 'Story $id';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
|
import 'package:hacki/cubits/comments/comments_cubit.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:synced_shared_preferences/synced_shared_preferences.dart';
|
import 'package:synced_shared_preferences/synced_shared_preferences.dart';
|
||||||
@ -42,6 +43,8 @@ class PreferenceRepository {
|
|||||||
static const String _navigationModeKey = 'navigationMode';
|
static const String _navigationModeKey = 'navigationMode';
|
||||||
static const String _eyeCandyModeKey = 'eyeCandyMode';
|
static const String _eyeCandyModeKey = 'eyeCandyMode';
|
||||||
static const String _markReadStoriesModeKey = 'markReadStoriesMode';
|
static const String _markReadStoriesModeKey = 'markReadStoriesMode';
|
||||||
|
static const String _fetchModeKey = 'fetchMode';
|
||||||
|
static const String _commentsOrderKey = 'commentsOrder';
|
||||||
|
|
||||||
static const bool _notificationModeDefaultValue = true;
|
static const bool _notificationModeDefaultValue = true;
|
||||||
static const bool _displayModeDefaultValue = true;
|
static const bool _displayModeDefaultValue = true;
|
||||||
@ -53,6 +56,8 @@ class PreferenceRepository {
|
|||||||
static const bool _markReadStoriesModeDefaultValue = true;
|
static const bool _markReadStoriesModeDefaultValue = true;
|
||||||
static const bool _isFirstLaunchKeyDefaultValue = true;
|
static const bool _isFirstLaunchKeyDefaultValue = true;
|
||||||
static const bool _metadataModeDefaultValue = true;
|
static const bool _metadataModeDefaultValue = true;
|
||||||
|
static final int _fetchModeDefaultValue = FetchMode.eager.index;
|
||||||
|
static final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
|
||||||
|
|
||||||
final SyncedSharedPreferences _syncedPrefs;
|
final SyncedSharedPreferences _syncedPrefs;
|
||||||
final Future<SharedPreferences> _prefs;
|
final Future<SharedPreferences> _prefs;
|
||||||
@ -120,6 +125,17 @@ class PreferenceRepository {
|
|||||||
_markReadStoriesModeDefaultValue,
|
_markReadStoriesModeDefaultValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<FetchMode> get fetchMode async => _prefs.then(
|
||||||
|
(SharedPreferences prefs) => FetchMode.values
|
||||||
|
.elementAt(prefs.getInt(_fetchModeKey) ?? _fetchModeDefaultValue),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<CommentsOrder> get commentsOrder async => _prefs.then(
|
||||||
|
(SharedPreferences prefs) => CommentsOrder.values.elementAt(
|
||||||
|
prefs.getInt(_commentsOrderKey) ?? _commentsOrderDefaultValue,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Future<bool> hasPushed(int commentId) async =>
|
Future<bool> hasPushed(int commentId) async =>
|
||||||
_prefs.then((SharedPreferences prefs) {
|
_prefs.then((SharedPreferences prefs) {
|
||||||
final bool? val = prefs.getBool(_getPushNotificationKey(commentId));
|
final bool? val = prefs.getBool(_getPushNotificationKey(commentId));
|
||||||
@ -237,6 +253,18 @@ class PreferenceRepository {
|
|||||||
await prefs.setBool(_metadataModeKey, !currentMode);
|
await prefs.setBool(_metadataModeKey, !currentMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> selectFetchMode(FetchMode fetchMode) async {
|
||||||
|
final SharedPreferences prefs = await _prefs;
|
||||||
|
final int index = fetchMode.index;
|
||||||
|
await prefs.setInt(_fetchModeKey, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> selectCommentsOrder(CommentsOrder order) async {
|
||||||
|
final SharedPreferences prefs = await _prefs;
|
||||||
|
final int index = order.index;
|
||||||
|
await prefs.setInt(_commentsOrderKey, index);
|
||||||
|
}
|
||||||
|
|
||||||
//#region fav
|
//#region fav
|
||||||
|
|
||||||
Future<List<int>> favList({required String of}) async {
|
Future<List<int>> favList({required String of}) async {
|
||||||
|
@ -68,8 +68,33 @@ class StoriesRepository {
|
|||||||
|
|
||||||
if (comment != null) {
|
if (comment != null) {
|
||||||
yield comment;
|
yield comment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
yield* fetchCommentsStream(
|
Stream<Comment> fetchAllCommentsRecursivelyStream({
|
||||||
|
required List<int> ids,
|
||||||
|
int level = 0,
|
||||||
|
Comment? Function(int)? getFromCache,
|
||||||
|
}) async* {
|
||||||
|
for (final int id in ids) {
|
||||||
|
Comment? comment = getFromCache?.call(id)?.copyWith(level: level);
|
||||||
|
|
||||||
|
comment ??= await _firebaseClient
|
||||||
|
.get('${_baseUrl}item/$id.json')
|
||||||
|
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
||||||
|
.then((Map<String, dynamic>? json) async {
|
||||||
|
if (json == null) return null;
|
||||||
|
|
||||||
|
final Comment comment = Comment.fromJson(json, level: level);
|
||||||
|
return comment;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (comment != null) {
|
||||||
|
yield comment;
|
||||||
|
|
||||||
|
yield* fetchAllCommentsRecursivelyStream(
|
||||||
ids: comment.kids,
|
ids: comment.kids,
|
||||||
level: level + 1,
|
level: level + 1,
|
||||||
getFromCache: getFromCache,
|
getFromCache: getFromCache,
|
||||||
|
@ -452,7 +452,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) {
|
locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
goToItemScreen(args: ItemScreenArgs(item: item));
|
goToItemScreen(
|
||||||
|
args: ItemScreenArgs(item: item),
|
||||||
|
forceNewScreen: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -86,6 +86,10 @@ class ItemScreen extends StatefulWidget {
|
|||||||
context.read<StoriesBloc>().state.offlineReading,
|
context.read<StoriesBloc>().state.offlineReading,
|
||||||
item: args.item,
|
item: args.item,
|
||||||
collapseCache: context.read<CollapseCache>(),
|
collapseCache: context.read<CollapseCache>(),
|
||||||
|
defaultFetchMode:
|
||||||
|
context.read<PreferenceCubit>().state.fetchMode,
|
||||||
|
defaultCommentsOrder:
|
||||||
|
context.read<PreferenceCubit>().state.order,
|
||||||
)..init(
|
)..init(
|
||||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||||
targetParents: args.targetComments,
|
targetParents: args.targetComments,
|
||||||
@ -128,6 +132,10 @@ class ItemScreen extends StatefulWidget {
|
|||||||
context.read<StoriesBloc>().state.offlineReading,
|
context.read<StoriesBloc>().state.offlineReading,
|
||||||
item: args.item,
|
item: args.item,
|
||||||
collapseCache: context.read<CollapseCache>(),
|
collapseCache: context.read<CollapseCache>(),
|
||||||
|
defaultFetchMode:
|
||||||
|
context.read<PreferenceCubit>().state.fetchMode,
|
||||||
|
defaultCommentsOrder:
|
||||||
|
context.read<PreferenceCubit>().state.order,
|
||||||
)..init(
|
)..init(
|
||||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||||
targetParents: args.targetComments,
|
targetParents: args.targetComments,
|
||||||
@ -307,7 +315,13 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoading: context.read<CommentsCubit>().loadMore,
|
onLoading: () {
|
||||||
|
if (state.fetchMode == FetchMode.eager) {
|
||||||
|
context.read<CommentsCubit>().loadMore();
|
||||||
|
} else {
|
||||||
|
refreshController.loadComplete();
|
||||||
|
}
|
||||||
|
},
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
primary: false,
|
primary: false,
|
||||||
itemCount: state.comments.length + 2,
|
itemCount: state.comments.length + 2,
|
||||||
@ -349,7 +363,8 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
icon: Icons.message,
|
icon: Icons.message,
|
||||||
),
|
),
|
||||||
SlidableAction(
|
SlidableAction(
|
||||||
onPressed: (_) => onMoreTapped(state.item),
|
onPressed: (BuildContext context) =>
|
||||||
|
onMoreTapped(state.item, context.rect),
|
||||||
backgroundColor: Palette.orange,
|
backgroundColor: Palette.orange,
|
||||||
foregroundColor: Palette.white,
|
foregroundColor: Palette.white,
|
||||||
icon: Icons.more_horiz,
|
icon: Icons.more_horiz,
|
||||||
@ -486,6 +501,9 @@ 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(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
] else ...<Widget>[
|
] else ...<Widget>[
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@ -505,10 +523,46 @@ 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.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
if (!state.offlineReading)
|
||||||
|
DropdownButton<FetchMode>(
|
||||||
|
value: state.fetchMode,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
items: const <DropdownMenuItem<FetchMode>>[
|
||||||
|
DropdownMenuItem<FetchMode>(
|
||||||
|
value: FetchMode.lazy,
|
||||||
|
child: Text(
|
||||||
|
'Lazy',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<FetchMode>(
|
||||||
|
value: FetchMode.eager,
|
||||||
|
child: Text(
|
||||||
|
'Eager',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: context
|
||||||
|
.read<CommentsCubit>()
|
||||||
|
.onFetchModeChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt6,
|
||||||
|
),
|
||||||
DropdownButton<CommentsOrder>(
|
DropdownButton<CommentsOrder>(
|
||||||
value: state.order,
|
value: state.order,
|
||||||
underline: const SizedBox.shrink(),
|
underline: const SizedBox.shrink(),
|
||||||
@ -519,7 +573,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'Natural',
|
'Natural',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: TextDimens.pt14,
|
fontSize: TextDimens.pt12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -528,7 +582,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'Newest first',
|
'Newest first',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: TextDimens.pt14,
|
fontSize: TextDimens.pt12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -537,7 +591,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'Oldest first',
|
'Oldest first',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: TextDimens.pt14,
|
fontSize: TextDimens.pt12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -595,6 +649,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
myUsername:
|
myUsername:
|
||||||
authState.isLoggedIn ? authState.username : null,
|
authState.isLoggedIn ? authState.username : null,
|
||||||
opUsername: state.item.by,
|
opUsername: state.item.by,
|
||||||
|
fetchMode: state.fetchMode,
|
||||||
onReplyTapped: (Comment cmt) {
|
onReplyTapped: (Comment cmt) {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
if (cmt.deleted || cmt.dead) {
|
if (cmt.deleted || cmt.dead) {
|
||||||
@ -854,6 +909,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
context.read<AuthBloc>().state.username,
|
context.read<AuthBloc>().state.username,
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
onStoryLinkTapped: onStoryLinkTapped,
|
||||||
actionable: false,
|
actionable: false,
|
||||||
|
fetchMode: FetchMode.eager,
|
||||||
),
|
),
|
||||||
const Divider(
|
const Divider(
|
||||||
height: Dimens.zero,
|
height: Dimens.zero,
|
||||||
@ -895,7 +951,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onMoreTapped(Item item) {
|
void onMoreTapped(Item item, Rect? rect) {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
|
|
||||||
if (item.dead || item.deleted) {
|
if (item.dead || item.deleted) {
|
||||||
@ -1104,7 +1160,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
case _MenuAction.downvote:
|
case _MenuAction.downvote:
|
||||||
break;
|
break;
|
||||||
case _MenuAction.share:
|
case _MenuAction.share:
|
||||||
onShareTapped(item);
|
onShareTapped(item, rect);
|
||||||
break;
|
break;
|
||||||
case _MenuAction.flag:
|
case _MenuAction.flag:
|
||||||
onFlagTapped(item);
|
onFlagTapped(item);
|
||||||
@ -1119,8 +1175,12 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onShareTapped(Item item) =>
|
void onShareTapped(Item item, Rect? rect) {
|
||||||
Share.share('https://news.ycombinator.com/item?id=${item.id}');
|
Share.share(
|
||||||
|
'https://news.ycombinator.com/item?id=${item.id}',
|
||||||
|
sharePositionOrigin: rect,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void onFlagTapped(Item item) {
|
void onFlagTapped(Item item) {
|
||||||
showDialog<bool>(
|
showDialog<bool>(
|
||||||
|
@ -283,6 +283,122 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
},
|
},
|
||||||
activeColor: Palette.orange,
|
activeColor: Palette.orange,
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt8,
|
||||||
|
),
|
||||||
|
Flex(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Flexible(
|
||||||
|
child: Row(
|
||||||
|
children: const <Widget>[
|
||||||
|
SizedBox(
|
||||||
|
width: Dimens.pt16,
|
||||||
|
),
|
||||||
|
Text('Default fetch mode'),
|
||||||
|
Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Row(
|
||||||
|
children: const <Widget>[
|
||||||
|
Text('Default comments order'),
|
||||||
|
Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Flex(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Flexible(
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt16,
|
||||||
|
),
|
||||||
|
DropdownButton<FetchMode>(
|
||||||
|
value: preferenceState.fetchMode,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
items: const <
|
||||||
|
DropdownMenuItem<FetchMode>>[
|
||||||
|
DropdownMenuItem<FetchMode>(
|
||||||
|
value: FetchMode.lazy,
|
||||||
|
child: Text(
|
||||||
|
'Lazy',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<FetchMode>(
|
||||||
|
value: FetchMode.eager,
|
||||||
|
child: Text(
|
||||||
|
'Eager',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: context
|
||||||
|
.read<PreferenceCubit>()
|
||||||
|
.selectFetchMode,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
DropdownButton<CommentsOrder>(
|
||||||
|
value: preferenceState.order,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
items: const <
|
||||||
|
DropdownMenuItem<CommentsOrder>>[
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.natural,
|
||||||
|
child: Text(
|
||||||
|
'Natural',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.newestFirst,
|
||||||
|
child: Text(
|
||||||
|
'Newest first',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.oldestFirst,
|
||||||
|
child: Text(
|
||||||
|
'Oldest first',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: context
|
||||||
|
.read<PreferenceCubit>()
|
||||||
|
.selectCommentsOrder,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: const Text('Complex Story Tile'),
|
title: const Text('Complex Story Tile'),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
@ -410,7 +526,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: 'Hacki',
|
applicationName: 'Hacki',
|
||||||
applicationVersion: 'v0.2.26',
|
applicationVersion: 'v0.2.28',
|
||||||
applicationIcon: ClipRRect(
|
applicationIcon: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(
|
Radius.circular(
|
||||||
|
@ -17,6 +17,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
required this.myUsername,
|
required this.myUsername,
|
||||||
required this.comment,
|
required this.comment,
|
||||||
required this.onStoryLinkTapped,
|
required this.onStoryLinkTapped,
|
||||||
|
required this.fetchMode,
|
||||||
this.onReplyTapped,
|
this.onReplyTapped,
|
||||||
this.onMoreTapped,
|
this.onMoreTapped,
|
||||||
this.onEditTapped,
|
this.onEditTapped,
|
||||||
@ -32,10 +33,11 @@ class CommentTile extends StatelessWidget {
|
|||||||
final int level;
|
final int level;
|
||||||
final bool actionable;
|
final bool actionable;
|
||||||
final Function(Comment)? onReplyTapped;
|
final Function(Comment)? onReplyTapped;
|
||||||
final Function(Comment)? onMoreTapped;
|
final Function(Comment, Rect?)? onMoreTapped;
|
||||||
final Function(Comment)? onEditTapped;
|
final Function(Comment)? onEditTapped;
|
||||||
final Function(Comment)? onRightMoreTapped;
|
final Function(Comment)? onRightMoreTapped;
|
||||||
final Function(String) onStoryLinkTapped;
|
final Function(String) onStoryLinkTapped;
|
||||||
|
final FetchMode fetchMode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -88,8 +90,11 @@ class CommentTile extends StatelessWidget {
|
|||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
),
|
),
|
||||||
SlidableAction(
|
SlidableAction(
|
||||||
onPressed: (_) =>
|
onPressed: (BuildContext context) =>
|
||||||
onMoreTapped?.call(comment),
|
onMoreTapped?.call(
|
||||||
|
comment,
|
||||||
|
context.rect,
|
||||||
|
),
|
||||||
backgroundColor: Palette.orange,
|
backgroundColor: Palette.orange,
|
||||||
foregroundColor: Palette.white,
|
foregroundColor: Palette.white,
|
||||||
icon: Icons.more_horiz,
|
icon: Icons.more_horiz,
|
||||||
@ -277,6 +282,30 @@ class CommentTile extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (!state.collapsed &&
|
||||||
|
fetchMode == FetchMode.lazy &&
|
||||||
|
comment.kids.isNotEmpty &&
|
||||||
|
!context
|
||||||
|
.read<CommentsCubit>()
|
||||||
|
.state
|
||||||
|
.commentIds
|
||||||
|
.contains(comment.kids.first))
|
||||||
|
Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
context
|
||||||
|
.read<CommentsCubit>()
|
||||||
|
.loadMore(comment: comment);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'''Load ${comment.kids.length} ${comment.kids.length > 1 ? 'replies' : 'reply'}''',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const Divider(
|
const Divider(
|
||||||
height: Dimens.zero,
|
height: Dimens.zero,
|
||||||
),
|
),
|
||||||
@ -298,7 +327,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
: Palette.transparent;
|
: Palette.transparent;
|
||||||
final bool isMyComment = myUsername == comment.by;
|
final bool isMyComment = myUsername == comment.by;
|
||||||
|
|
||||||
Widget? wrapper = child;
|
Widget wrapper = child;
|
||||||
|
|
||||||
if (isMyComment && level == 0) {
|
if (isMyComment && level == 0) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -330,7 +359,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrapper!;
|
return wrapper;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 0.2.26+68
|
version: 0.2.28+70
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|