mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
9cefffa518 | |||
fe630ea7a9 |
@ -10,8 +10,6 @@ A simple noiseless [Hacker News](https://news.ycombinator.com/) client made with
|
|||||||
[](https://img.shields.io/github/stars/livinglist/Hacki?style=social)
|
[](https://img.shields.io/github/stars/livinglist/Hacki?style=social)
|
||||||
[](https://pub.dev/packages/effective_dart)
|
[](https://pub.dev/packages/effective_dart)
|
||||||
|
|
||||||
<noscript><a href="https://liberapay.com/jfeng_for_open_source/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a></noscript>
|
|
||||||
|
|
||||||
[<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/)
|
||||||
|
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 698 KiB After Width: | Height: | Size: 935 KiB |
1
fastlane/metadata/android/en-US/changelogs/61.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/61.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Offline mode now includes web pages.
|
2
fastlane/metadata/android/en-US/changelogs/62.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/62.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
Binary file not shown.
Before Width: | Height: | Size: 698 KiB After Width: | Height: | Size: 935 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 = 5;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
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.19;
|
MARKETING_VERSION = 0.2.20;
|
||||||
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 = 5;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
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.19;
|
MARKETING_VERSION = 0.2.20;
|
||||||
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 = 5;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
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.19;
|
MARKETING_VERSION = 0.2.20;
|
||||||
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 = "";
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
@ -71,28 +72,34 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
final Story updatedStory = state.offlineReading
|
final Story updatedStory = state.offlineReading
|
||||||
? story
|
? story
|
||||||
: await _storiesRepository.fetchStoryBy(story.id) ?? story;
|
: await _storiesRepository.fetchStoryBy(story.id) ?? story;
|
||||||
|
final List<int> kids = () {
|
||||||
|
switch (state.order) {
|
||||||
|
case CommentsOrder.natural:
|
||||||
|
return updatedStory.kids;
|
||||||
|
case CommentsOrder.newestFirst:
|
||||||
|
return updatedStory.kids.sorted((int a, int b) => b.compareTo(a));
|
||||||
|
case CommentsOrder.oldestFirst:
|
||||||
|
return updatedStory.kids.sorted((int a, int b) => a.compareTo(b));
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
emit(state.copyWith(story: updatedStory));
|
emit(state.copyWith(story: updatedStory));
|
||||||
|
|
||||||
if (state.offlineReading) {
|
if (state.offlineReading) {
|
||||||
_streamSubscription = _cacheRepository
|
_streamSubscription = _cacheRepository
|
||||||
.getCachedCommentsStream(ids: updatedStory.kids)
|
.getCachedCommentsStream(ids: kids)
|
||||||
.listen(_onCommentFetched)
|
.listen(_onCommentFetched)
|
||||||
..onDone(_onDone);
|
..onDone(_onDone);
|
||||||
} else {
|
} else {
|
||||||
_streamSubscription = _storiesRepository
|
_streamSubscription = _storiesRepository
|
||||||
.fetchCommentsStream(ids: updatedStory.kids)
|
.fetchCommentsStream(ids: kids)
|
||||||
.listen(_onCommentFetched)
|
.listen(_onCommentFetched)
|
||||||
..onDone(_onDone);
|
..onDone(_onDone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
final bool offlineReading = await _cacheRepository.hasCachedStories;
|
if (state.offlineReading) {
|
||||||
|
|
||||||
_cacheService.resetCollapsedComments();
|
|
||||||
|
|
||||||
if (offlineReading) {
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CommentsStatus.loaded,
|
status: CommentsStatus.loaded,
|
||||||
@ -101,6 +108,10 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cacheService
|
||||||
|
..resetComments()
|
||||||
|
..resetCollapsedComments();
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CommentsStatus.loading,
|
status: CommentsStatus.loading,
|
||||||
@ -113,8 +124,19 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
final Story story = state.story;
|
final Story story = state.story;
|
||||||
final Story updatedStory =
|
final Story updatedStory =
|
||||||
await _storiesRepository.fetchStoryBy(story.id) ?? story;
|
await _storiesRepository.fetchStoryBy(story.id) ?? story;
|
||||||
|
final List<int> kids = () {
|
||||||
|
switch (state.order) {
|
||||||
|
case CommentsOrder.natural:
|
||||||
|
return updatedStory.kids;
|
||||||
|
case CommentsOrder.newestFirst:
|
||||||
|
return updatedStory.kids.sorted((int a, int b) => b.compareTo(a));
|
||||||
|
case CommentsOrder.oldestFirst:
|
||||||
|
return updatedStory.kids.sorted((int a, int b) => a.compareTo(b));
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
_streamSubscription = _storiesRepository
|
_streamSubscription = _storiesRepository
|
||||||
.fetchCommentsStream(ids: updatedStory.kids)
|
.fetchCommentsStream(ids: kids)
|
||||||
.listen(_onCommentFetched)
|
.listen(_onCommentFetched)
|
||||||
..onDone(_onDone);
|
..onDone(_onDone);
|
||||||
|
|
||||||
@ -161,7 +183,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
..cacheComment(comment);
|
..cacheComment(comment);
|
||||||
_sembastRepository.cacheComment(comment);
|
_sembastRepository.cacheComment(comment);
|
||||||
|
|
||||||
final List<LinkifyElement> elements = linkify(
|
final List<LinkifyElement> elements = _linkify(
|
||||||
comment.text,
|
comment.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -194,7 +216,14 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<LinkifyElement> linkify(
|
void onOrderChanged(CommentsOrder? order) {
|
||||||
|
if (order == null) return;
|
||||||
|
_streamSubscription?.cancel();
|
||||||
|
emit(state.copyWith(order: order, comments: <Comment>[]));
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<LinkifyElement> _linkify(
|
||||||
String text, {
|
String text, {
|
||||||
LinkifyOptions options = const LinkifyOptions(),
|
LinkifyOptions options = const LinkifyOptions(),
|
||||||
List<Linkifier> linkifiers = const <Linkifier>[
|
List<Linkifier> linkifiers = const <Linkifier>[
|
||||||
|
@ -8,12 +8,18 @@ enum CommentsStatus {
|
|||||||
failure,
|
failure,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CommentsOrder {
|
||||||
|
natural,
|
||||||
|
newestFirst,
|
||||||
|
oldestFirst,
|
||||||
|
}
|
||||||
|
|
||||||
class CommentsState extends Equatable {
|
class CommentsState extends Equatable {
|
||||||
const CommentsState({
|
const CommentsState({
|
||||||
required this.story,
|
required this.story,
|
||||||
required this.comments,
|
required this.comments,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.collapsed,
|
required this.order,
|
||||||
required this.onlyShowTargetComment,
|
required this.onlyShowTargetComment,
|
||||||
required this.offlineReading,
|
required this.offlineReading,
|
||||||
required this.currentPage,
|
required this.currentPage,
|
||||||
@ -24,14 +30,14 @@ class CommentsState extends Equatable {
|
|||||||
required this.story,
|
required this.story,
|
||||||
}) : comments = <Comment>[],
|
}) : comments = <Comment>[],
|
||||||
status = CommentsStatus.init,
|
status = CommentsStatus.init,
|
||||||
collapsed = false,
|
order = CommentsOrder.natural,
|
||||||
onlyShowTargetComment = false,
|
onlyShowTargetComment = false,
|
||||||
currentPage = 0;
|
currentPage = 0;
|
||||||
|
|
||||||
final Story story;
|
final Story story;
|
||||||
final List<Comment> comments;
|
final List<Comment> comments;
|
||||||
final CommentsStatus status;
|
final CommentsStatus status;
|
||||||
final bool collapsed;
|
final CommentsOrder order;
|
||||||
final bool onlyShowTargetComment;
|
final bool onlyShowTargetComment;
|
||||||
final bool offlineReading;
|
final bool offlineReading;
|
||||||
final int currentPage;
|
final int currentPage;
|
||||||
@ -40,7 +46,7 @@ class CommentsState extends Equatable {
|
|||||||
Story? story,
|
Story? story,
|
||||||
List<Comment>? comments,
|
List<Comment>? comments,
|
||||||
CommentsStatus? status,
|
CommentsStatus? status,
|
||||||
bool? collapsed,
|
CommentsOrder? order,
|
||||||
bool? onlyShowTargetComment,
|
bool? onlyShowTargetComment,
|
||||||
bool? offlineReading,
|
bool? offlineReading,
|
||||||
int? currentPage,
|
int? currentPage,
|
||||||
@ -49,7 +55,7 @@ class CommentsState extends Equatable {
|
|||||||
story: story ?? this.story,
|
story: story ?? this.story,
|
||||||
comments: comments ?? this.comments,
|
comments: comments ?? this.comments,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
collapsed: collapsed ?? this.collapsed,
|
order: order ?? this.order,
|
||||||
onlyShowTargetComment:
|
onlyShowTargetComment:
|
||||||
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
||||||
offlineReading: offlineReading ?? this.offlineReading,
|
offlineReading: offlineReading ?? this.offlineReading,
|
||||||
@ -62,7 +68,7 @@ class CommentsState extends Equatable {
|
|||||||
story,
|
story,
|
||||||
comments,
|
comments,
|
||||||
status,
|
status,
|
||||||
collapsed,
|
order,
|
||||||
onlyShowTargetComment,
|
onlyShowTargetComment,
|
||||||
offlineReading,
|
offlineReading,
|
||||||
currentPage,
|
currentPage,
|
||||||
|
@ -179,10 +179,6 @@ 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(
|
||||||
|
@ -11,6 +11,7 @@ class BuildableComment extends Comment {
|
|||||||
required super.by,
|
required super.by,
|
||||||
required super.text,
|
required super.text,
|
||||||
required super.kids,
|
required super.kids,
|
||||||
|
required super.dead,
|
||||||
required super.deleted,
|
required super.deleted,
|
||||||
required super.level,
|
required super.level,
|
||||||
required this.elements,
|
required this.elements,
|
||||||
@ -25,6 +26,7 @@ class BuildableComment extends Comment {
|
|||||||
by: comment.by,
|
by: comment.by,
|
||||||
text: comment.text,
|
text: comment.text,
|
||||||
kids: comment.kids,
|
kids: comment.kids,
|
||||||
|
dead: comment.dead,
|
||||||
deleted: comment.deleted,
|
deleted: comment.deleted,
|
||||||
level: comment.level,
|
level: comment.level,
|
||||||
);
|
);
|
||||||
|
@ -12,11 +12,11 @@ class Comment extends Item {
|
|||||||
required super.by,
|
required super.by,
|
||||||
required super.text,
|
required super.text,
|
||||||
required super.kids,
|
required super.kids,
|
||||||
|
required super.dead,
|
||||||
required super.deleted,
|
required super.deleted,
|
||||||
required this.level,
|
required this.level,
|
||||||
}) : super(
|
}) : super(
|
||||||
descendants: 0,
|
descendants: 0,
|
||||||
dead: false,
|
|
||||||
parts: <int>[],
|
parts: <int>[],
|
||||||
title: '',
|
title: '',
|
||||||
url: '',
|
url: '',
|
||||||
@ -55,6 +55,7 @@ class Comment extends Item {
|
|||||||
by: by,
|
by: by,
|
||||||
text: text,
|
text: text,
|
||||||
kids: kids,
|
kids: kids,
|
||||||
|
dead: dead,
|
||||||
deleted: deleted,
|
deleted: deleted,
|
||||||
level: level ?? this.level,
|
level: level ?? this.level,
|
||||||
);
|
);
|
||||||
|
@ -408,7 +408,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: 'Hacki',
|
applicationName: 'Hacki',
|
||||||
applicationVersion: 'v0.2.19',
|
applicationVersion: 'v0.2.20',
|
||||||
applicationIcon: ClipRRect(
|
applicationIcon: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(12),
|
Radius.circular(12),
|
||||||
@ -676,7 +676,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
.get<SembastRepository>()
|
.get<SembastRepository>()
|
||||||
.deleteAllCachedComments()
|
.deleteAllCachedComments()
|
||||||
.whenComplete(
|
.whenComplete(
|
||||||
locator.get<SembastRepository>().deleteAllCachedComments,
|
locator.get<CacheRepository>().deleteAll,
|
||||||
)
|
)
|
||||||
.whenComplete(
|
.whenComplete(
|
||||||
locator.get<PreferenceRepository>().clearAllReadStories,
|
locator.get<PreferenceRepository>().clearAllReadStories,
|
||||||
|
@ -76,6 +76,10 @@ class StoryScreen extends StatefulWidget {
|
|||||||
targetParents: args.targetComments,
|
targetParents: args.targetComments,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
BlocProvider<EditCubit>(
|
||||||
|
lazy: false,
|
||||||
|
create: (BuildContext context) => EditCubit(),
|
||||||
|
),
|
||||||
if (args.story.isPoll)
|
if (args.story.isPoll)
|
||||||
BlocProvider<PollCubit>(
|
BlocProvider<PollCubit>(
|
||||||
create: (BuildContext context) =>
|
create: (BuildContext context) =>
|
||||||
@ -112,6 +116,10 @@ class StoryScreen extends StatefulWidget {
|
|||||||
targetParents: args.targetComments,
|
targetParents: args.targetComments,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
BlocProvider<EditCubit>(
|
||||||
|
lazy: false,
|
||||||
|
create: (BuildContext context) => EditCubit(),
|
||||||
|
),
|
||||||
if (args.story.isPoll)
|
if (args.story.isPoll)
|
||||||
BlocProvider<PollCubit>(
|
BlocProvider<PollCubit>(
|
||||||
create: (BuildContext context) =>
|
create: (BuildContext context) =>
|
||||||
@ -187,6 +195,7 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
scrollController.dispose();
|
scrollController.dispose();
|
||||||
storyLinkTapThrottle.dispose();
|
storyLinkTapThrottle.dispose();
|
||||||
featureDiscoveryDismissThrottle.dispose();
|
featureDiscoveryDismissThrottle.dispose();
|
||||||
|
focusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,16 +280,18 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
controller: refreshController,
|
controller: refreshController,
|
||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
locator.get<CacheService>().resetComments();
|
|
||||||
|
if (context.read<StoriesBloc>().state.offlineReading) {
|
||||||
|
refreshController.refreshCompleted();
|
||||||
|
} else {
|
||||||
context.read<CommentsCubit>().refresh();
|
context.read<CommentsCubit>().refresh();
|
||||||
|
|
||||||
if (widget.story.isPoll) {
|
if (widget.story.isPoll) {
|
||||||
context.read<PollCubit>().refresh();
|
context.read<PollCubit>().refresh();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLoading: () {
|
onLoading: context.read<CommentsCubit>().loadMore,
|
||||||
context.read<CommentsCubit>().loadMore();
|
|
||||||
},
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
primary: false,
|
primary: false,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -432,6 +443,59 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
const Divider(
|
const Divider(
|
||||||
height: 0,
|
height: 0,
|
||||||
),
|
),
|
||||||
|
] else ...<Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'''${state.story.score} karma, ${state.story.descendants} comment${state.story.descendants > 1 ? 's' : ''}''',
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
DropdownButton<CommentsOrder>(
|
||||||
|
value: state.order,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
items: const <DropdownMenuItem<CommentsOrder>>[
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.natural,
|
||||||
|
child: Text(
|
||||||
|
'Natural',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.newestFirst,
|
||||||
|
child: Text(
|
||||||
|
'Newest first',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.oldestFirst,
|
||||||
|
child: Text(
|
||||||
|
'Oldest first',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged:
|
||||||
|
context.read<CommentsCubit>().onOrderChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
if (state.comments.isEmpty &&
|
if (state.comments.isEmpty &&
|
||||||
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
||||||
@ -530,8 +594,10 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
SplitViewState current,
|
SplitViewState current,
|
||||||
) =>
|
) =>
|
||||||
previous.expanded != current.expanded,
|
previous.expanded != current.expanded,
|
||||||
builder:
|
builder: (
|
||||||
(BuildContext context, SplitViewState state) {
|
BuildContext context,
|
||||||
|
SplitViewState state,
|
||||||
|
) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@ -109,7 +109,7 @@ class LinkPreview extends StatefulWidget {
|
|||||||
|
|
||||||
class _LinkPreviewState extends State<LinkPreview> {
|
class _LinkPreviewState extends State<LinkPreview> {
|
||||||
InfoBase? _info;
|
InfoBase? _info;
|
||||||
String? _errorTitle, _errorBody, _url;
|
String? _errorTitle, _errorBody;
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -119,37 +119,19 @@ class _LinkPreviewState extends State<LinkPreview> {
|
|||||||
'Oops! Unable to parse the url. We have '
|
'Oops! Unable to parse the url. We have '
|
||||||
'sent feedback to our developers & '
|
'sent feedback to our developers & '
|
||||||
'we will try to fix this in our next release. Thanks!';
|
'we will try to fix this in our next release. Thanks!';
|
||||||
_url = widget.link.trim();
|
|
||||||
|
|
||||||
if (_url?.isNotEmpty ?? false) {
|
|
||||||
_info = WebAnalyzer.getInfoFromCache(_url);
|
|
||||||
} else {
|
|
||||||
_info = WebAnalyzer.getInfoFromCache(widget.story.id.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_info == null) {
|
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_getInfo();
|
_getInfo();
|
||||||
}
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getInfo() async {
|
Future<void> _getInfo() async {
|
||||||
if (_url!.startsWith('http') || _url!.startsWith('https')) {
|
|
||||||
_info = await WebAnalyzer.getInfo(
|
_info = await WebAnalyzer.getInfo(
|
||||||
_url,
|
|
||||||
story: widget.story,
|
story: widget.story,
|
||||||
cache: widget.cache,
|
cache: widget.cache,
|
||||||
offlineReading: widget.offlineReading,
|
offlineReading: widget.offlineReading,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
_info = await WebAnalyzer.getInfo(
|
|
||||||
null,
|
|
||||||
story: widget.story,
|
|
||||||
cache: widget.cache,
|
|
||||||
offlineReading: widget.offlineReading,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -193,7 +175,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
|||||||
metadata: widget.story.simpleMetadata,
|
metadata: widget.story.simpleMetadata,
|
||||||
url: widget.link,
|
url: widget.link,
|
||||||
title: widget.story.title,
|
title: widget.story.title,
|
||||||
description: desc ?? title ?? 'no comments yet.',
|
description: desc ?? title ?? 'no comment yet.',
|
||||||
imageUri: imageUri,
|
imageUri: imageUri,
|
||||||
imagePath: Constants.hackerNewsLogoPath,
|
imagePath: Constants.hackerNewsLogoPath,
|
||||||
onTap: _launchURL,
|
onTap: _launchURL,
|
||||||
|
@ -95,12 +95,12 @@ class WebAnalyzer {
|
|||||||
|
|
||||||
/// Get web information
|
/// Get web information
|
||||||
/// return [InfoBase]
|
/// return [InfoBase]
|
||||||
static InfoBase? getInfoFromCache(String? url) {
|
static InfoBase? getInfoFromCache(String? cacheKey) {
|
||||||
final InfoBase? info = cacheMap[url];
|
final InfoBase? info = cacheMap[cacheKey];
|
||||||
|
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
if (!info._timeout.isAfter(DateTime.now())) {
|
if (!info._timeout.isAfter(DateTime.now())) {
|
||||||
cacheMap.remove(url);
|
cacheMap.remove(cacheKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return info;
|
return info;
|
||||||
@ -108,14 +108,16 @@ class WebAnalyzer {
|
|||||||
|
|
||||||
/// Get web information
|
/// Get web information
|
||||||
/// return [InfoBase]
|
/// return [InfoBase]
|
||||||
static Future<InfoBase?> getInfo(
|
static Future<InfoBase?> getInfo({
|
||||||
String? url, {
|
|
||||||
required Story story,
|
required Story story,
|
||||||
Duration cache = const Duration(hours: 24),
|
Duration cache = const Duration(hours: 24),
|
||||||
bool multimedia = true,
|
bool multimedia = true,
|
||||||
required bool offlineReading,
|
required bool offlineReading,
|
||||||
}) async {
|
}) async {
|
||||||
InfoBase? info = getInfoFromCache(url);
|
final String key = getKey(story);
|
||||||
|
final String url = story.url;
|
||||||
|
|
||||||
|
InfoBase? info = getInfoFromCache(key);
|
||||||
|
|
||||||
if (info != null) return info;
|
if (info != null) return info;
|
||||||
|
|
||||||
@ -126,7 +128,8 @@ class WebAnalyzer {
|
|||||||
)
|
)
|
||||||
.._timeout = DateTime.now().add(cache)
|
.._timeout = DateTime.now().add(cache)
|
||||||
.._shouldRetry = false;
|
.._shouldRetry = false;
|
||||||
cacheMap[story.id.toString()] = info;
|
|
||||||
|
cacheMap[key] = info;
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
@ -148,7 +151,9 @@ class WebAnalyzer {
|
|||||||
)
|
)
|
||||||
.._shouldRetry = false
|
.._shouldRetry = false
|
||||||
.._timeout = DateTime.now();
|
.._timeout = DateTime.now();
|
||||||
cacheMap[url] = info;
|
|
||||||
|
cacheMap[key] = info;
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +166,7 @@ class WebAnalyzer {
|
|||||||
|
|
||||||
if (info != null && !info._shouldRetry) {
|
if (info != null && !info._shouldRetry) {
|
||||||
info._timeout = DateTime.now().add(cache);
|
info._timeout = DateTime.now().add(cache);
|
||||||
cacheMap[url] = info;
|
cacheMap[key] = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
@ -214,12 +219,12 @@ class WebAnalyzer {
|
|||||||
|
|
||||||
if (res == null || isEmpty(res[2] as String?)) {
|
if (res == null || isEmpty(res[2] as String?)) {
|
||||||
final String? commentText = await compute(
|
final String? commentText = await compute(
|
||||||
_fetchInfoFromStoryId,
|
_fetchInfoFromStory,
|
||||||
story.kids,
|
<int>[story.id, ...story.kids],
|
||||||
);
|
);
|
||||||
|
|
||||||
shouldRetry = commentText == null;
|
shouldRetry = commentText == null;
|
||||||
fallbackDescription = commentText ?? 'no comments yet';
|
fallbackDescription = commentText ?? 'no comment yet';
|
||||||
} else {
|
} else {
|
||||||
shouldRetry = false;
|
shouldRetry = false;
|
||||||
}
|
}
|
||||||
@ -281,11 +286,25 @@ class WebAnalyzer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> _fetchInfoFromStoryId(List<int> kids) async {
|
static Future<String?> _fetchInfoFromStory(List<int> meta) async {
|
||||||
|
final StoriesRepository storiesRepository = StoriesRepository();
|
||||||
|
final int storyId = meta.first;
|
||||||
|
List<int> kids = meta.sublist(1, meta.length);
|
||||||
|
|
||||||
|
// Kids of stories from search results are always empty, so here we try
|
||||||
|
// to fetch the story itself first and see if the kids are still empty.
|
||||||
|
if (kids.isEmpty) {
|
||||||
|
final Story? story = await storiesRepository.fetchStoryBy(storyId);
|
||||||
|
|
||||||
|
if (story == null) return null;
|
||||||
|
|
||||||
|
kids = story.kids;
|
||||||
|
|
||||||
if (kids.isEmpty) return null;
|
if (kids.isEmpty) return null;
|
||||||
|
}
|
||||||
|
|
||||||
final Comment? comment =
|
final Comment? comment =
|
||||||
await StoriesRepository().fetchCommentBy(id: kids.first);
|
await storiesRepository.fetchCommentBy(id: kids.first);
|
||||||
|
|
||||||
return comment != null ? '${comment.by}: ${comment.text}' : null;
|
return comment != null ? '${comment.by}: ${comment.text}' : null;
|
||||||
}
|
}
|
||||||
@ -540,4 +559,7 @@ class WebAnalyzer {
|
|||||||
}
|
}
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String getKey(Story story) =>
|
||||||
|
story.url.isNotEmpty ? story.url : story.id.toString();
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 0.2.19+60
|
version: 0.2.20+62
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
Reference in New Issue
Block a user