Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
553335d8dc | |||
747491944f |
@ -11,7 +11,7 @@ A simple noiseless [Hacker News](https://news.ycombinator.com/) reader made with
|
|||||||
|
|
||||||
<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>
|
<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="images/app_store_badge.png" height="50">](https://apps.apple.com/us/app/hacki/id1602043763) [<img src="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/app_store_badge.png" height="50">](https://apps.apple.com/us/app/hacki/id1602043763) [<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)
|
||||||
|
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
assets/images/comment_tile_left_slide.png
Normal file
After Width: | Height: | Size: 548 KiB |
BIN
assets/images/comment_tile_right_slide.png
Normal file
After Width: | Height: | Size: 571 KiB |
BIN
assets/images/comment_tile_top_tap.png
Normal file
After Width: | Height: | Size: 592 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
4
fastlane/metadata/android/en-US/changelogs/41.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
- You can now participate in polls.
|
||||||
|
- Pick up where you left off.
|
||||||
|
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
||||||
|
- Huge performance boost.
|
4
fastlane/metadata/android/en-US/changelogs/42.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
- You can now participate in polls.
|
||||||
|
- Pick up where you left off.
|
||||||
|
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
||||||
|
- Huge performance boost.
|
@ -367,7 +367,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 = 1;
|
CURRENT_PROJECT_VERSION = 6;
|
||||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -376,7 +376,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.6;
|
MARKETING_VERSION = 0.2.7;
|
||||||
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 = "";
|
||||||
@ -503,7 +503,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 = 1;
|
CURRENT_PROJECT_VERSION = 6;
|
||||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -512,7 +512,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.6;
|
MARKETING_VERSION = 0.2.7;
|
||||||
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 = "";
|
||||||
@ -533,7 +533,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 = 1;
|
CURRENT_PROJECT_VERSION = 6;
|
||||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -542,7 +542,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.6;
|
MARKETING_VERSION = 0.2.7;
|
||||||
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 = "";
|
||||||
|
@ -10,8 +10,15 @@ abstract class Constants {
|
|||||||
static const String googlePlayLink =
|
static const String googlePlayLink =
|
||||||
'https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US';
|
'https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US';
|
||||||
|
|
||||||
static const String hackerNewsLogoPath = 'images/hacker_news_logo.png';
|
static const String _imagePath = 'assets/images';
|
||||||
static const String hackiIconPath = 'images/hacki_icon.png';
|
static const String hackerNewsLogoPath = '$_imagePath/hacker_news_logo.png';
|
||||||
|
static const String hackiIconPath = '$_imagePath/hacki_icon.png';
|
||||||
|
static const String commentTileLeftSlidePath =
|
||||||
|
'$_imagePath/comment_tile_left_slide.png';
|
||||||
|
static const String commentTileRightSlidePath =
|
||||||
|
'$_imagePath/comment_tile_right_slide.png';
|
||||||
|
static const String commentTileTopTapPath =
|
||||||
|
'$_imagePath/comment_tile_top_tap.png';
|
||||||
|
|
||||||
/// Feature ids for feature discovery.
|
/// Feature ids for feature discovery.
|
||||||
static const String featureAddStoryToFavList = 'add_story_to_fav_list';
|
static const String featureAddStoryToFavList = 'add_story_to_fav_list';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
@ -18,5 +19,8 @@ Future<void> setUpLocator() async {
|
|||||||
..registerSingleton<CacheRepository>(CacheRepository())
|
..registerSingleton<CacheRepository>(CacheRepository())
|
||||||
..registerSingleton<CacheService>(CacheService())
|
..registerSingleton<CacheService>(CacheService())
|
||||||
..registerSingleton<LocalNotification>(LocalNotification())
|
..registerSingleton<LocalNotification>(LocalNotification())
|
||||||
..registerSingleton<Logger>(Logger());
|
..registerSingleton<Logger>(Logger())
|
||||||
|
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
|
||||||
|
RouteObserver<ModalRoute<dynamic>>(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
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';
|
||||||
@ -15,21 +17,74 @@ class CollapseCubit extends Cubit<CollapseState> {
|
|||||||
|
|
||||||
final int _commentId;
|
final int _commentId;
|
||||||
final CacheService _cacheService;
|
final CacheService _cacheService;
|
||||||
|
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
|
_streamSubscription =
|
||||||
|
_cacheService.hiddenComments.listen(hiddenCommentsStreamListener);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
|
collapsedCount: _cacheService.totalHidden(_commentId),
|
||||||
collapsed: _cacheService.isCollapsed(_commentId),
|
collapsed: _cacheService.isCollapsed(_commentId),
|
||||||
|
hidden: _cacheService.isHidden(_commentId),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void collapse() {
|
void collapse() {
|
||||||
_cacheService.updateCollapsedComments(_commentId);
|
if (state.collapsed) {
|
||||||
|
_cacheService.uncollapse(_commentId);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
collapsed: !state.collapsed,
|
collapsed: false,
|
||||||
|
collapsedCount: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final int count = _cacheService.collapse(_commentId);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
collapsed: true,
|
||||||
|
collapsedCount: state.collapsed ? 0 : count,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hiddenCommentsStreamListener(Map<int, Set<int>> event) {
|
||||||
|
for (final int key in event.keys) {
|
||||||
|
if (key == _commentId && !isClosed) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
collapsedCount: event[key]?.length ?? 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Set<int> val in event.values) {
|
||||||
|
if (val.contains(_commentId) && !isClosed) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(hidden: true),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isClosed) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(hidden: false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _streamSubscription.cancel();
|
||||||
|
await super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,37 @@
|
|||||||
part of 'collapse_cubit.dart';
|
part of 'collapse_cubit.dart';
|
||||||
|
|
||||||
class CollapseState extends Equatable {
|
class CollapseState extends Equatable {
|
||||||
const CollapseState({required this.collapsed});
|
const CollapseState({
|
||||||
|
required this.collapsed,
|
||||||
|
required this.hidden,
|
||||||
|
required this.collapsedCount,
|
||||||
|
});
|
||||||
|
|
||||||
const CollapseState.init() : collapsed = false;
|
const CollapseState.init()
|
||||||
|
: collapsed = false,
|
||||||
|
hidden = false,
|
||||||
|
collapsedCount = 0;
|
||||||
|
|
||||||
final bool collapsed;
|
final bool collapsed;
|
||||||
|
final bool hidden;
|
||||||
|
final int collapsedCount;
|
||||||
|
|
||||||
CollapseState copyWith({bool? collapsed}) {
|
CollapseState copyWith({
|
||||||
|
bool? collapsed,
|
||||||
|
bool? hidden,
|
||||||
|
int? collapsedCount,
|
||||||
|
}) {
|
||||||
return CollapseState(
|
return CollapseState(
|
||||||
collapsed: collapsed ?? this.collapsed,
|
collapsed: collapsed ?? this.collapsed,
|
||||||
|
hidden: hidden ?? this.hidden,
|
||||||
|
collapsedCount: collapsedCount ?? this.collapsedCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[collapsed];
|
List<Object?> get props => <Object?>[
|
||||||
|
collapsed,
|
||||||
|
hidden,
|
||||||
|
collapsedCount,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@ -153,8 +153,10 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
|
|
||||||
void _onCommentFetched(Comment? comment) {
|
void _onCommentFetched(Comment? comment) {
|
||||||
if (comment != null) {
|
if (comment != null) {
|
||||||
_cacheService.cacheComment(comment);
|
_cacheService
|
||||||
_sembastRepository.saveComment(comment);
|
..addKid(comment.id, to: comment.parent)
|
||||||
|
..cacheComment(comment);
|
||||||
|
_sembastRepository.cacheComment(comment);
|
||||||
final List<Comment> updatedComments = <Comment>[
|
final List<Comment> updatedComments = <Comment>[
|
||||||
...state.comments,
|
...state.comments,
|
||||||
comment
|
comment
|
||||||
|
@ -97,31 +97,35 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _fetchReplies().whenComplete(() {
|
await _fetchReplies().whenComplete(_initializeTimer);
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: NotificationStatus.loaded,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_initializeTimer();
|
|
||||||
}).onError((Object? error, StackTrace stackTrace) {
|
|
||||||
_initializeTimer();
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void markAsRead(int id) {
|
void markAsRead(int id) {
|
||||||
|
Future.doWhile(() {
|
||||||
|
if (state.status != NotificationStatus.loading) {
|
||||||
if (state.unreadCommentsIds.contains(id)) {
|
if (state.unreadCommentsIds.contains(id)) {
|
||||||
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
|
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
|
||||||
..remove(id);
|
..remove(id);
|
||||||
_preferenceRepository.updateUnreadCommentsIds(updatedUnreadIds);
|
_preferenceRepository.updateUnreadCommentsIds(updatedUnreadIds);
|
||||||
emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
|
emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void markAllAsRead() {
|
void markAllAsRead() {
|
||||||
|
Future.doWhile(() {
|
||||||
|
if (state.status != NotificationStatus.loading) {
|
||||||
emit(state.copyWith(unreadCommentsIds: <int>[]));
|
emit(state.copyWith(unreadCommentsIds: <int>[]));
|
||||||
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
@ -134,22 +138,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
|
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
|
||||||
await _fetchReplies().whenComplete(() {
|
await _fetchReplies().whenComplete(_initializeTimer);
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: NotificationStatus.loaded,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_initializeTimer();
|
|
||||||
}).onError((Object? error, StackTrace stackTrace) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: NotificationStatus.loaded,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_initializeTimer();
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -247,6 +236,10 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}).whenComplete(
|
||||||
|
() => emit(
|
||||||
|
state.copyWith(status: NotificationStatus.loaded),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
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/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
|
import 'package:hacki/services/services.dart';
|
||||||
|
|
||||||
part 'split_view_state.dart';
|
part 'split_view_state.dart';
|
||||||
|
|
||||||
class SplitViewCubit extends Cubit<SplitViewState> {
|
class SplitViewCubit extends Cubit<SplitViewState> {
|
||||||
SplitViewCubit() : super(const SplitViewState.init());
|
SplitViewCubit({CacheService? cacheService})
|
||||||
|
: _cacheService = cacheService ?? locator.get<CacheService>(),
|
||||||
|
super(const SplitViewState.init());
|
||||||
|
|
||||||
|
final CacheService _cacheService;
|
||||||
|
|
||||||
void updateStoryScreenArgs(StoryScreenArgs args) {
|
void updateStoryScreenArgs(StoryScreenArgs args) {
|
||||||
|
_cacheService.resetCollapsedComments();
|
||||||
emit(state.copyWith(storyScreenArgs: args));
|
emit(state.copyWith(storyScreenArgs: args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,14 +24,14 @@ class TimeMachineCubit extends Cubit<TimeMachineState> {
|
|||||||
|
|
||||||
final List<Comment> parents = <Comment>[];
|
final List<Comment> parents = <Comment>[];
|
||||||
Comment? parent = _cacheService.getComment(comment.parent);
|
Comment? parent = _cacheService.getComment(comment.parent);
|
||||||
parent ??= await _sembastRepository.getComment(id: comment.parent);
|
parent ??= await _sembastRepository.getCachedComment(id: comment.parent);
|
||||||
|
|
||||||
while (parent != null) {
|
while (parent != null) {
|
||||||
parents.insert(0, parent);
|
parents.insert(0, parent);
|
||||||
|
|
||||||
final int parentId = parent.parent;
|
final int parentId = parent.parent;
|
||||||
parent = _cacheService.getComment(parentId);
|
parent = _cacheService.getComment(parentId);
|
||||||
parent ??= await _sembastRepository.getComment(id: parentId);
|
parent ??= await _sembastRepository.getCachedComment(id: parentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(state.copyWith(parents: parents));
|
emit(state.copyWith(parents: parents));
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export 'date_time_extension.dart';
|
export 'date_time_extension.dart';
|
||||||
|
export 'int_extension.dart';
|
||||||
export 'list_extension.dart';
|
export 'list_extension.dart';
|
||||||
export 'object_extension.dart';
|
export 'object_extension.dart';
|
||||||
export 'state_extension.dart';
|
export 'state_extension.dart';
|
||||||
|
5
lib/extensions/int_extension.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
extension IntExtension on int {
|
||||||
|
Iterable<int> to(int other, {bool inclusive = true}) => other > this
|
||||||
|
? <int>[for (int i = this; i < other; i++) i, if (inclusive) other]
|
||||||
|
: <int>[for (int i = this; i > other; i--) i, if (inclusive) other];
|
||||||
|
}
|
@ -208,6 +208,9 @@ class HackiApp extends StatelessWidget {
|
|||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: useTrueDark ? trueDarkTheme : theme,
|
theme: useTrueDark ? trueDarkTheme : theme,
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
|
navigatorObservers: <NavigatorObserver>[
|
||||||
|
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
|
||||||
|
],
|
||||||
onGenerateRoute: CustomRouter.onGenerateRoute,
|
onGenerateRoute: CustomRouter.onGenerateRoute,
|
||||||
initialRoute: HomeScreen.routeName,
|
initialRoute: HomeScreen.routeName,
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
|
/// [CacheRepository] is for storing stories and comments for offline reading.
|
||||||
|
/// It's using [Hive] as its database which is being stored in temp directory.
|
||||||
class CacheRepository {
|
class CacheRepository {
|
||||||
CacheRepository({
|
CacheRepository({
|
||||||
Future<Box<List<int>>>? storyIdBox,
|
Future<Box<List<int>>>? storyIdBox,
|
||||||
@ -116,4 +118,10 @@ class CacheRepository {
|
|||||||
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
|
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
|
||||||
return box.clear();
|
return box.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> deleteAll() async {
|
||||||
|
return deleteAllStoryIds()
|
||||||
|
.whenComplete(deleteAllStories)
|
||||||
|
.whenComplete(deleteAllComments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ class PreferenceRepository {
|
|||||||
static const String _pinnedStoriesIdsKey = 'pinnedStoriesIds';
|
static const String _pinnedStoriesIdsKey = 'pinnedStoriesIds';
|
||||||
static const String _unreadCommentsIdsKey = 'unreadCommentsIds';
|
static const String _unreadCommentsIdsKey = 'unreadCommentsIds';
|
||||||
static const String _lastReadStoryIdKey = 'lastReadStoryId';
|
static const String _lastReadStoryIdKey = 'lastReadStoryId';
|
||||||
|
static const String _isFirstLaunchKey = 'isFirstLaunch';
|
||||||
|
|
||||||
static const String _notificationModeKey = 'notificationMode';
|
static const String _notificationModeKey = 'notificationMode';
|
||||||
static const String _readerModeKey = 'readerMode';
|
static const String _readerModeKey = 'readerMode';
|
||||||
@ -44,6 +45,7 @@ class PreferenceRepository {
|
|||||||
static const bool _trueDarkModeDefaultValue = false;
|
static const bool _trueDarkModeDefaultValue = false;
|
||||||
static const bool _readerModeDefaultValue = true;
|
static const bool _readerModeDefaultValue = true;
|
||||||
static const bool _markReadStoriesModeDefaultValue = true;
|
static const bool _markReadStoriesModeDefaultValue = true;
|
||||||
|
static const bool _isFirstLaunchKeyDefaultValue = true;
|
||||||
|
|
||||||
final SyncedSharedPreferences _syncedPrefs;
|
final SyncedSharedPreferences _syncedPrefs;
|
||||||
final Future<SharedPreferences> _prefs;
|
final Future<SharedPreferences> _prefs;
|
||||||
@ -55,6 +57,16 @@ class PreferenceRepository {
|
|||||||
|
|
||||||
Future<String?> get password async => _secureStorage.read(key: _passwordKey);
|
Future<String?> get password async => _secureStorage.read(key: _passwordKey);
|
||||||
|
|
||||||
|
Future<bool> get isFirstLaunch async {
|
||||||
|
final SharedPreferences prefs = await _prefs;
|
||||||
|
final bool val =
|
||||||
|
prefs.getBool(_isFirstLaunchKey) ?? _isFirstLaunchKeyDefaultValue;
|
||||||
|
|
||||||
|
await prefs.setBool(_isFirstLaunchKey, false);
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<String>> get blocklist async => _prefs.then(
|
Future<List<String>> get blocklist async => _prefs.then(
|
||||||
(SharedPreferences prefs) =>
|
(SharedPreferences prefs) =>
|
||||||
prefs.getStringList(_blocklistKey) ?? <String>[],
|
prefs.getStringList(_blocklistKey) ?? <String>[],
|
||||||
|
@ -6,6 +6,8 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import 'package:sembast/sembast.dart';
|
import 'package:sembast/sembast.dart';
|
||||||
import 'package:sembast/sembast_io.dart';
|
import 'package:sembast/sembast_io.dart';
|
||||||
|
|
||||||
|
/// [SembastRepository] is for storing stories and comments for faster loading.
|
||||||
|
/// It's using Sembast as its database which is being stored in doc directory.
|
||||||
class SembastRepository {
|
class SembastRepository {
|
||||||
SembastRepository({Database? database}) {
|
SembastRepository({Database? database}) {
|
||||||
if (database == null) {
|
if (database == null) {
|
||||||
@ -18,6 +20,7 @@ class SembastRepository {
|
|||||||
Database? _database;
|
Database? _database;
|
||||||
List<int>? _idsOfCommentsRepliedToMe;
|
List<int>? _idsOfCommentsRepliedToMe;
|
||||||
|
|
||||||
|
static const String _cachedCommentsKey = 'cachedComments';
|
||||||
static const String _commentsKey = 'comments';
|
static const String _commentsKey = 'comments';
|
||||||
static const String _idsOfCommentsRepliedToMeKey = 'idsOfCommentsRepliedToMe';
|
static const String _idsOfCommentsRepliedToMeKey = 'idsOfCommentsRepliedToMe';
|
||||||
|
|
||||||
@ -31,6 +34,38 @@ class SembastRepository {
|
|||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#region Cached comments for time machine feature.
|
||||||
|
Future<Map<String, Object?>> cacheComment(Comment comment) async {
|
||||||
|
final Database db = _database ?? await initializeDatabase();
|
||||||
|
final StoreRef<int, Map<String, Object?>> store =
|
||||||
|
intMapStoreFactory.store(_cachedCommentsKey);
|
||||||
|
return store.record(comment.id).put(db, comment.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Comment?> getCachedComment({required int id}) async {
|
||||||
|
final Database db = _database ?? await initializeDatabase();
|
||||||
|
final StoreRef<int, Map<String, Object?>> store =
|
||||||
|
intMapStoreFactory.store(_cachedCommentsKey);
|
||||||
|
final RecordSnapshot<int, Map<String, Object?>>? snapshot =
|
||||||
|
await store.record(id).getSnapshot(db);
|
||||||
|
if (snapshot != null) {
|
||||||
|
final Comment comment = Comment.fromJson(snapshot.value);
|
||||||
|
return comment;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteAllCachedComments() async {
|
||||||
|
final Database db = _database ?? await initializeDatabase();
|
||||||
|
final StoreRef<int, Map<String, Object?>> store =
|
||||||
|
intMapStoreFactory.store(_cachedCommentsKey);
|
||||||
|
return store.delete(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Saved comments for notification feature.
|
||||||
Future<Map<String, Object?>> saveComment(Comment comment) async {
|
Future<Map<String, Object?>> saveComment(Comment comment) async {
|
||||||
final Database db = _database ?? await initializeDatabase();
|
final Database db = _database ?? await initializeDatabase();
|
||||||
final StoreRef<int, Map<String, Object?>> store =
|
final StoreRef<int, Map<String, Object?>> store =
|
||||||
@ -137,6 +172,8 @@ class SembastRepository {
|
|||||||
return kids;
|
return kids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
Future<FileSystemEntity> deleteAll() async {
|
Future<FileSystemEntity> deleteAll() async {
|
||||||
final Directory dir = await getApplicationDocumentsDirectory();
|
final Directory dir = await getApplicationDocumentsDirectory();
|
||||||
await dir.create(recursive: true);
|
await dir.create(recursive: true);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// ignore_for_file: lines_longer_than_80_chars
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:badges/badges.dart';
|
import 'package:badges/badges.dart';
|
||||||
@ -41,11 +42,26 @@ class HomeScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen>
|
class _HomeScreenState extends State<HomeScreen>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin, RouteAware {
|
||||||
final CacheService cacheService = locator.get<CacheService>();
|
final CacheService cacheService = locator.get<CacheService>();
|
||||||
|
final Throttle featureDiscoveryDismissThrottle = Throttle(
|
||||||
|
delay: _throttleDelay,
|
||||||
|
);
|
||||||
|
|
||||||
late final TabController tabController;
|
late final TabController tabController;
|
||||||
int currentIndex = 0;
|
int currentIndex = 0;
|
||||||
|
|
||||||
|
static const Duration _throttleDelay = Duration(seconds: 1);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPopNext() {
|
||||||
|
if (context.read<StoriesBloc>().deviceScreenType ==
|
||||||
|
DeviceScreenType.mobile) {
|
||||||
|
cacheService.resetCollapsedComments();
|
||||||
|
}
|
||||||
|
super.didPopNext();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -86,13 +102,23 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance
|
||||||
|
..addPostFrameCallback((_) {
|
||||||
FeatureDiscovery.discoverFeatures(
|
FeatureDiscovery.discoverFeatures(
|
||||||
context,
|
context,
|
||||||
const <String>{
|
const <String>{
|
||||||
Constants.featureLogIn,
|
Constants.featureLogIn,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
..addPostFrameCallback((_) {
|
||||||
|
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
||||||
|
|
||||||
|
if (route == null) return;
|
||||||
|
|
||||||
|
locator
|
||||||
|
.get<RouteObserver<ModalRoute<dynamic>>>()
|
||||||
|
.subscribe(this, route);
|
||||||
});
|
});
|
||||||
|
|
||||||
tabController = TabController(vsync: this, length: 6)
|
tabController = TabController(vsync: this, length: 6)
|
||||||
@ -258,6 +284,12 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
child: DescribedFeatureOverlay(
|
child: DescribedFeatureOverlay(
|
||||||
onBackgroundTap: onFeatureDiscoveryDismissed,
|
onBackgroundTap: onFeatureDiscoveryDismissed,
|
||||||
onDismiss: onFeatureDiscoveryDismissed,
|
onDismiss: onFeatureDiscoveryDismissed,
|
||||||
|
onComplete: () async {
|
||||||
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
|
unawaited(HapticFeedback.lightImpact());
|
||||||
|
showOnboarding();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
overflowMode: OverflowMode.extendBackground,
|
overflowMode: OverflowMode.extendBackground,
|
||||||
targetColor: Theme.of(context).primaryColor,
|
targetColor: Theme.of(context).primaryColor,
|
||||||
tapTarget: const Icon(
|
tapTarget: const Icon(
|
||||||
@ -372,9 +404,12 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> onFeatureDiscoveryDismissed() {
|
Future<bool> onFeatureDiscoveryDismissed() {
|
||||||
|
featureDiscoveryDismissThrottle.run(() {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
showSnackBar(content: 'Tap on icon to continue');
|
showSnackBar(content: 'Tap on icon to continue');
|
||||||
|
});
|
||||||
|
|
||||||
return Future<bool>.value(false);
|
return Future<bool>.value(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,7 +449,6 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
|
|
||||||
if (!offlineReading && (isJobWithLink || (showWebFirst && !hasRead))) {
|
if (!offlineReading && (isJobWithLink || (showWebFirst && !hasRead))) {
|
||||||
LinkUtil.launchUrl(story.url, useReader: useReader);
|
LinkUtil.launchUrl(story.url, useReader: useReader);
|
||||||
cacheService.store(story.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.read<StoriesBloc>().add(
|
context.read<StoriesBloc>().add(
|
||||||
@ -423,6 +457,16 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showOnboarding() {
|
||||||
|
Navigator.push<dynamic>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute<dynamic>(
|
||||||
|
builder: (BuildContext context) => const OnboardingView(),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MobileHomeScreenBuilder extends StatelessWidget {
|
class _MobileHomeScreenBuilder extends StatelessWidget {
|
||||||
|
@ -4,6 +4,7 @@ import 'package:adaptive_theme/adaptive_theme.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.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';
|
||||||
@ -375,6 +376,12 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
),
|
),
|
||||||
onTap: showThemeSettingDialog,
|
onTap: showThemeSettingDialog,
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Clear Data',
|
||||||
|
),
|
||||||
|
onTap: showClearDataDialog,
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('About'),
|
title: const Text('About'),
|
||||||
subtitle:
|
subtitle:
|
||||||
@ -383,7 +390,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: 'Hacki',
|
applicationName: 'Hacki',
|
||||||
applicationVersion: 'v0.2.6',
|
applicationVersion: 'v0.2.7',
|
||||||
applicationIcon: ClipRRect(
|
applicationIcon: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(12),
|
Radius.circular(12),
|
||||||
@ -609,6 +616,61 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showClearDataDialog() {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Clear Data?'),
|
||||||
|
content: const Text(
|
||||||
|
'Clear all cached images, stories and comments.',
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text(
|
||||||
|
'Cancel',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
locator
|
||||||
|
.get<SembastRepository>()
|
||||||
|
.deleteAllCachedComments()
|
||||||
|
.whenComplete(
|
||||||
|
locator.get<SembastRepository>().deleteAllCachedComments,
|
||||||
|
)
|
||||||
|
.whenComplete(
|
||||||
|
locator.get<PreferenceRepository>().clearAllReadStories,
|
||||||
|
)
|
||||||
|
.whenComplete(
|
||||||
|
DefaultCacheManager().emptyCache,
|
||||||
|
)
|
||||||
|
.whenComplete(() {
|
||||||
|
showSnackBar(content: 'Data cleared!');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStateProperty.all(Colors.deepOrange),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Yes',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void onCommentTapped(Comment comment, {VoidCallback? then}) {
|
void onCommentTapped(Comment comment, {VoidCallback? then}) {
|
||||||
throttle.run(() {
|
throttle.run(() {
|
||||||
locator
|
locator
|
||||||
|
@ -132,8 +132,17 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
initialRefreshStatus: RefreshStatus.refreshing,
|
initialRefreshStatus: RefreshStatus.refreshing,
|
||||||
);
|
);
|
||||||
final FocusNode focusNode = FocusNode();
|
final FocusNode focusNode = FocusNode();
|
||||||
final Throttle throttle = Throttle(delay: const Duration(seconds: 2));
|
|
||||||
final String happyFace = Constants.happyFaces.pickRandomly()!;
|
final String happyFace = Constants.happyFaces.pickRandomly()!;
|
||||||
|
final Throttle storyLinkTapThrottle = Throttle(
|
||||||
|
delay: _storyLinkTapThrottleDelay,
|
||||||
|
);
|
||||||
|
final Throttle featureDiscoveryDismissThrottle = Throttle(
|
||||||
|
delay: _featureDiscoveryDismissThrottleDelay,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const Duration _storyLinkTapThrottleDelay = Duration(seconds: 2);
|
||||||
|
static const Duration _featureDiscoveryDismissThrottleDelay =
|
||||||
|
Duration(seconds: 1);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -164,7 +173,8 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
refreshController.dispose();
|
refreshController.dispose();
|
||||||
commentEditingController.dispose();
|
commentEditingController.dispose();
|
||||||
scrollController.dispose();
|
scrollController.dispose();
|
||||||
throttle.dispose();
|
storyLinkTapThrottle.dispose();
|
||||||
|
featureDiscoveryDismissThrottle.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,9 +569,11 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> onFeatureDiscoveryDismissed() {
|
Future<bool> onFeatureDiscoveryDismissed() {
|
||||||
|
featureDiscoveryDismissThrottle.run(() {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
showSnackBar(content: 'Tap on icon to continue');
|
showSnackBar(content: 'Tap on icon to continue');
|
||||||
|
});
|
||||||
return Future<bool>.value(false);
|
return Future<bool>.value(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,10 +627,10 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
in state.parents) ...<Widget>[
|
in state.parents) ...<Widget>[
|
||||||
CommentTile(
|
CommentTile(
|
||||||
comment: c,
|
comment: c,
|
||||||
loadKids: false,
|
|
||||||
myUsername:
|
myUsername:
|
||||||
context.read<AuthBloc>().state.username,
|
context.read<AuthBloc>().state.username,
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
onStoryLinkTapped: onStoryLinkTapped,
|
||||||
|
actionable: false,
|
||||||
),
|
),
|
||||||
const Divider(
|
const Divider(
|
||||||
height: 0,
|
height: 0,
|
||||||
@ -645,7 +657,7 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
final String match = regex.stringMatch(link) ?? '';
|
final String match = regex.stringMatch(link) ?? '';
|
||||||
final int? id = int.tryParse(match);
|
final int? id = int.tryParse(match);
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
throttle.run(() {
|
storyLinkTapThrottle.run(() {
|
||||||
locator
|
locator
|
||||||
.get<StoriesRepository>()
|
.get<StoriesRepository>()
|
||||||
.fetchParentStory(id: id)
|
.fetchParentStory(id: id)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:feature_discovery/feature_discovery.dart';
|
import 'package:feature_discovery/feature_discovery.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -26,6 +28,10 @@ class FavIconButton extends StatelessWidget {
|
|||||||
icon: DescribedFeatureOverlay(
|
icon: DescribedFeatureOverlay(
|
||||||
onBackgroundTap: onBackgroundTap,
|
onBackgroundTap: onBackgroundTap,
|
||||||
onDismiss: onDismiss,
|
onDismiss: onDismiss,
|
||||||
|
onComplete: () async {
|
||||||
|
unawaited(HapticFeedback.lightImpact());
|
||||||
|
return true;
|
||||||
|
},
|
||||||
overflowMode: OverflowMode.extendBackground,
|
overflowMode: OverflowMode.extendBackground,
|
||||||
targetColor: Theme.of(context).primaryColor,
|
targetColor: Theme.of(context).primaryColor,
|
||||||
tapTarget: Icon(
|
tapTarget: Icon(
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:feature_discovery/feature_discovery.dart';
|
import 'package:feature_discovery/feature_discovery.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
|
||||||
@ -21,6 +24,10 @@ class LinkIconButton extends StatelessWidget {
|
|||||||
icon: DescribedFeatureOverlay(
|
icon: DescribedFeatureOverlay(
|
||||||
onBackgroundTap: onBackgroundTap,
|
onBackgroundTap: onBackgroundTap,
|
||||||
onDismiss: onDismiss,
|
onDismiss: onDismiss,
|
||||||
|
onComplete: () async {
|
||||||
|
unawaited(HapticFeedback.lightImpact());
|
||||||
|
return true;
|
||||||
|
},
|
||||||
overflowMode: OverflowMode.extendBackground,
|
overflowMode: OverflowMode.extendBackground,
|
||||||
targetColor: Theme.of(context).primaryColor,
|
targetColor: Theme.of(context).primaryColor,
|
||||||
tapTarget: const Icon(
|
tapTarget: const Icon(
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:feature_discovery/feature_discovery.dart';
|
import 'package:feature_discovery/feature_discovery.dart';
|
||||||
@ -33,6 +34,10 @@ class PinIconButton extends StatelessWidget {
|
|||||||
icon: DescribedFeatureOverlay(
|
icon: DescribedFeatureOverlay(
|
||||||
onBackgroundTap: onBackgroundTap,
|
onBackgroundTap: onBackgroundTap,
|
||||||
onDismiss: onDismiss,
|
onDismiss: onDismiss,
|
||||||
|
onComplete: () async {
|
||||||
|
unawaited(HapticFeedback.lightImpact());
|
||||||
|
return true;
|
||||||
|
},
|
||||||
overflowMode: OverflowMode.extendBackground,
|
overflowMode: OverflowMode.extendBackground,
|
||||||
targetColor: Theme.of(context).primaryColor,
|
targetColor: Theme.of(context).primaryColor,
|
||||||
tapTarget: Icon(
|
tapTarget: Icon(
|
||||||
|
@ -5,6 +5,7 @@ import 'package:flutter_linkify/flutter_linkify.dart';
|
|||||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
import 'package:hacki/blocs/blocs.dart';
|
import 'package:hacki/blocs/blocs.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
this.onEditTapped,
|
this.onEditTapped,
|
||||||
this.onTimeMachineActivated,
|
this.onTimeMachineActivated,
|
||||||
this.opUsername,
|
this.opUsername,
|
||||||
this.loadKids = true,
|
this.actionable = true,
|
||||||
this.level = 0,
|
this.level = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
final String? opUsername;
|
final String? opUsername;
|
||||||
final Comment comment;
|
final Comment comment;
|
||||||
final int level;
|
final int level;
|
||||||
final bool loadKids;
|
final bool actionable;
|
||||||
final Function(Comment)? onReplyTapped;
|
final Function(Comment)? onReplyTapped;
|
||||||
final Function(Comment)? onMoreTapped;
|
final Function(Comment)? onMoreTapped;
|
||||||
final Function(Comment)? onEditTapped;
|
final Function(Comment)? onEditTapped;
|
||||||
@ -43,6 +44,8 @@ class CommentTile extends StatelessWidget {
|
|||||||
)..init(),
|
)..init(),
|
||||||
child: BlocBuilder<CollapseCubit, CollapseState>(
|
child: BlocBuilder<CollapseCubit, CollapseState>(
|
||||||
builder: (BuildContext context, CollapseState state) {
|
builder: (BuildContext context, CollapseState state) {
|
||||||
|
if (actionable && state.hidden) return const SizedBox.shrink();
|
||||||
|
|
||||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||||
builder: (BuildContext context, PreferenceState prefState) {
|
builder: (BuildContext context, PreferenceState prefState) {
|
||||||
return BlocBuilder<BlocklistCubit, BlocklistState>(
|
return BlocBuilder<BlocklistCubit, BlocklistState>(
|
||||||
@ -56,7 +59,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Slidable(
|
Slidable(
|
||||||
startActionPane: loadKids
|
startActionPane: actionable
|
||||||
? ActionPane(
|
? ActionPane(
|
||||||
motion: const StretchMotion(),
|
motion: const StretchMotion(),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -90,7 +93,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
endActionPane: loadKids && level != 0
|
endActionPane: actionable && level != 0
|
||||||
? ActionPane(
|
? ActionPane(
|
||||||
motion: const StretchMotion(),
|
motion: const StretchMotion(),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -104,16 +107,17 @@ class CommentTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (actionable) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
context.read<CollapseCubit>().collapse();
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
GestureDetector(
|
Padding(
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onTap: () {
|
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
context.read<CollapseCubit>().collapse();
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: 6,
|
left: 6,
|
||||||
right: 6,
|
right: 6,
|
||||||
@ -146,24 +150,27 @@ class CommentTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
if (comment.deleted)
|
if (comment.deleted)
|
||||||
const Center(
|
const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 12),
|
padding: EdgeInsets.only(bottom: 12),
|
||||||
child: Text(
|
child: Text(
|
||||||
'deleted',
|
'deleted',
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (comment.dead)
|
else if (comment.dead)
|
||||||
const Center(
|
const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 12),
|
padding: EdgeInsets.only(bottom: 12),
|
||||||
child: Text(
|
child: Text(
|
||||||
'dead',
|
'dead',
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -171,21 +178,26 @@ class CommentTile extends StatelessWidget {
|
|||||||
.contains(comment.by))
|
.contains(comment.by))
|
||||||
const Center(
|
const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 12),
|
padding: EdgeInsets.only(bottom: 12),
|
||||||
child: Text(
|
child: Text(
|
||||||
'blocked',
|
'blocked',
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (state.collapsed)
|
else if (actionable && state.collapsed)
|
||||||
const Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(bottom: 8),
|
padding:
|
||||||
|
const EdgeInsets.only(bottom: 12),
|
||||||
child: Text(
|
child: Text(
|
||||||
'collapsed',
|
'collapsed '
|
||||||
style:
|
'(${state.collapsedCount + 1})',
|
||||||
TextStyle(color: Colors.orangeAccent),
|
style: const TextStyle(
|
||||||
|
color: Colors.orangeAccent,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -228,6 +240,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -251,10 +264,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final int i in List<int>.generate(
|
for (final int i in level.to(0, inclusive: false)) {
|
||||||
level,
|
|
||||||
(int index) => level - index,
|
|
||||||
)) {
|
|
||||||
final Color wrapperBorderColor = _getColor(i);
|
final Color wrapperBorderColor = _getColor(i);
|
||||||
final bool shouldHighlight = isMyComment && i == level;
|
final bool shouldHighlight = isMyComment && i == level;
|
||||||
wrapper = Container(
|
wrapper = Container(
|
||||||
|
138
lib/screens/widgets/onboarding_view.dart
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hacki/config/constants.dart';
|
||||||
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
|
||||||
|
class OnboardingView extends StatefulWidget {
|
||||||
|
const OnboardingView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OnboardingView> createState() => _OnboardingViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OnboardingViewState extends State<OnboardingView> {
|
||||||
|
final PageController pageController = PageController();
|
||||||
|
final Throttle throttle = Throttle(delay: _throttleDelay);
|
||||||
|
|
||||||
|
static const Duration _throttleDelay = Duration(milliseconds: 100);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Theme.of(context).brightness == Brightness.light
|
||||||
|
? Colors.orange
|
||||||
|
: Theme.of(context).canvasColor,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).brightness == Brightness.light
|
||||||
|
? Colors.orange
|
||||||
|
: null,
|
||||||
|
body: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Positioned(
|
||||||
|
top: 40,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 550,
|
||||||
|
child: PageView(
|
||||||
|
controller: pageController,
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
children: const <Widget>[
|
||||||
|
_PageViewChild(
|
||||||
|
path: Constants.commentTileRightSlidePath,
|
||||||
|
description: 'Swipe right to leave a comment or vote.',
|
||||||
|
),
|
||||||
|
_PageViewChild(
|
||||||
|
path: Constants.commentTileLeftSlidePath,
|
||||||
|
description: 'Swipe left to view all the parent comments.',
|
||||||
|
),
|
||||||
|
_PageViewChild(
|
||||||
|
path: Constants.commentTileTopTapPath,
|
||||||
|
description: 'Tap on the top of comment tile to collapse.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 40,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
if (pageController.page! >= 2) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
} else {
|
||||||
|
throttle.run(() {
|
||||||
|
pageController.nextPage(
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
curve: SpringCurve.underDamped,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
primary: Colors.orange,
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.arrow_drop_down_circle_outlined,
|
||||||
|
size: 24,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PageViewChild extends StatelessWidget {
|
||||||
|
const _PageViewChild({
|
||||||
|
Key? key,
|
||||||
|
required this.path,
|
||||||
|
required this.description,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
SizedBox(
|
||||||
|
height: 400,
|
||||||
|
child: Image.asset(path),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 24,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
description,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ export 'items_list_view.dart';
|
|||||||
export 'link_preview/link_preview.dart';
|
export 'link_preview/link_preview.dart';
|
||||||
export 'link_preview/link_preview.dart';
|
export 'link_preview/link_preview.dart';
|
||||||
export 'offline_banner.dart';
|
export 'offline_banner.dart';
|
||||||
|
export 'onboarding_view.dart';
|
||||||
export 'spring_curve.dart';
|
export 'spring_curve.dart';
|
||||||
export 'stories_list_view.dart';
|
export 'stories_list_view.dart';
|
||||||
export 'story_tile.dart';
|
export 'story_tile.dart';
|
||||||
|
@ -1,24 +1,81 @@
|
|||||||
import 'package:hacki/models/models.dart' show Comment;
|
import 'package:hacki/models/models.dart' show Comment;
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
class CacheService {
|
class CacheService {
|
||||||
static final Set<int> _tappedStories = <int>{};
|
|
||||||
static final Map<int, Comment> _comments = <int, Comment>{};
|
static final Map<int, Comment> _comments = <int, Comment>{};
|
||||||
static final Set<int> _commentsCollapsed = <int>{};
|
|
||||||
static final Map<int, String> _drafts = <int, String>{};
|
static final Map<int, String> _drafts = <int, String>{};
|
||||||
|
static final Map<int, Set<int>> _kids = <int, Set<int>>{};
|
||||||
|
static final Set<int> _collapsed = <int>{};
|
||||||
|
static final Map<int, Set<int>> _hidden = <int, Set<int>>{};
|
||||||
|
final PublishSubject<Map<int, Set<int>>> _hiddenCommentsSubject =
|
||||||
|
PublishSubject<Map<int, Set<int>>>();
|
||||||
|
|
||||||
bool isFirstTimeReading(int storyId) => !_tappedStories.contains(storyId);
|
Stream<Map<int, Set<int>>> get hiddenComments =>
|
||||||
|
_hiddenCommentsSubject.stream;
|
||||||
|
|
||||||
bool isCollapsed(int commentId) => _commentsCollapsed.contains(commentId);
|
void addKid(int commentId, {required int to}) {
|
||||||
|
_kids[to] = <int>{...?_kids[to], commentId};
|
||||||
|
addIfParentIsHiddenOrCollapsed(commentId, to);
|
||||||
|
}
|
||||||
|
|
||||||
void store(int storyId) => _tappedStories.add(storyId);
|
int collapse(int commentId) {
|
||||||
|
_collapsed.add(commentId);
|
||||||
|
|
||||||
void updateCollapsedComments(int commentId) {
|
Set<int> findHiddenComments(int commentId) {
|
||||||
if (_commentsCollapsed.contains(commentId)) {
|
final Set<int> directKids = <int>{...?_kids[commentId]};
|
||||||
_commentsCollapsed.remove(commentId);
|
final Set<int> temp = <int>{...directKids};
|
||||||
} else {
|
|
||||||
_commentsCollapsed.add(commentId);
|
for (final int i in temp) {
|
||||||
|
directKids.addAll(findHiddenComments(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return directKids;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<int> hiddenComments = findHiddenComments(commentId);
|
||||||
|
|
||||||
|
_hidden[commentId] = hiddenComments;
|
||||||
|
|
||||||
|
_hiddenCommentsSubject.add(_hidden);
|
||||||
|
|
||||||
|
return hiddenComments.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uncollapse(int commentId) {
|
||||||
|
_collapsed.remove(commentId);
|
||||||
|
|
||||||
|
_hidden.remove(commentId);
|
||||||
|
|
||||||
|
_hiddenCommentsSubject.add(_hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isHidden(int commentId) {
|
||||||
|
for (final Set<int> val in _hidden.values) {
|
||||||
|
if (val.contains(commentId)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addIfParentIsHiddenOrCollapsed(int commentId, int parentId) {
|
||||||
|
for (final int key in _hidden.keys) {
|
||||||
|
if (key == parentId || (_hidden[key]?.contains(parentId) ?? false)) {
|
||||||
|
_hidden[key]?.add(commentId);
|
||||||
|
_hiddenCommentsSubject.add(_hidden);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetCollapsedComments() {
|
||||||
|
_kids.clear();
|
||||||
|
_collapsed.clear();
|
||||||
|
_hidden.clear();
|
||||||
|
_hiddenCommentsSubject.add(_hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCollapsed(int commentId) => _collapsed.contains(commentId);
|
||||||
|
|
||||||
|
int totalHidden(int commentId) => _hidden[commentId]?.length ?? 0;
|
||||||
|
|
||||||
void cacheComment(Comment comment) => _comments[comment.id] = comment;
|
void cacheComment(Comment comment) => _comments[comment.id] = comment;
|
||||||
|
|
||||||
|
28
pubspec.lock
@ -14,7 +14,7 @@ packages:
|
|||||||
name: adaptive_theme
|
name: adaptive_theme
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "3.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -70,7 +70,7 @@ packages:
|
|||||||
name: cached_network_image
|
name: cached_network_image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.1"
|
||||||
cached_network_image_platform_interface:
|
cached_network_image_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -105,7 +105,7 @@ packages:
|
|||||||
name: chewie
|
name: chewie
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.3"
|
||||||
chewie_audio:
|
chewie_audio:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -256,9 +256,11 @@ packages:
|
|||||||
feature_discovery:
|
feature_discovery:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: feature_discovery
|
path: "."
|
||||||
url: "https://pub.dartlang.org"
|
ref: flutter3_compatibility
|
||||||
source: hosted
|
resolved-ref: "896306d04130f870c7ce99ce832fd977283b2392"
|
||||||
|
url: "https://github.com/livinglist/feature_discovery"
|
||||||
|
source: git
|
||||||
version: "0.14.1"
|
version: "0.14.1"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@ -294,7 +296,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.8"
|
version: "0.6.8"
|
||||||
flutter_cache_manager:
|
flutter_cache_manager:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_cache_manager
|
name: flutter_cache_manager
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
@ -418,7 +420,7 @@ packages:
|
|||||||
name: flutter_slidable
|
name: flutter_slidable
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.1"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -761,9 +763,11 @@ packages:
|
|||||||
pull_to_refresh:
|
pull_to_refresh:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: pull_to_refresh
|
path: "."
|
||||||
url: "https://pub.dartlang.org"
|
ref: master
|
||||||
source: hosted
|
resolved-ref: "6d7fbdd5b3cde242a474d66da312aac4aa892b9b"
|
||||||
|
url: "https://github.com/livinglist/flutter_pulltorefresh"
|
||||||
|
source: git
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
quiver:
|
quiver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@ -1255,4 +1259,4 @@ packages:
|
|||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.17.0 <3.0.0"
|
dart: ">=2.17.0 <3.0.0"
|
||||||
flutter: ">=2.10.0"
|
flutter: ">=3.0.0"
|
||||||
|
24
pubspec.yaml
@ -1,26 +1,31 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 0.2.6+40
|
version: 0.2.7+42
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
adaptive_theme: ^2.3.0
|
adaptive_theme: ^3.0.0
|
||||||
badges: ^2.0.2
|
badges: ^2.0.2
|
||||||
bloc: ^8.0.3
|
bloc: ^8.0.3
|
||||||
cached_network_image: ^3.2.0
|
cached_network_image: ^3.2.1
|
||||||
clipboard: ^0.1.3
|
clipboard: ^0.1.3
|
||||||
collection:
|
collection:
|
||||||
connectivity_plus: ^2.2.1
|
connectivity_plus: ^2.2.1
|
||||||
dio: ^4.0.4
|
dio: ^4.0.4
|
||||||
equatable: 2.0.3
|
equatable: 2.0.3
|
||||||
fast_gbk: ^1.0.0
|
fast_gbk: ^1.0.0
|
||||||
feature_discovery: ^0.14.0
|
# feature_discovery: ^0.14.0
|
||||||
|
feature_discovery:
|
||||||
|
git:
|
||||||
|
url: https://github.com/livinglist/feature_discovery
|
||||||
|
ref: flutter3_compatibility
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_bloc: ^8.0.1
|
flutter_bloc: ^8.0.1
|
||||||
|
flutter_cache_manager: ^3.3.0
|
||||||
flutter_fadein: ^2.0.0
|
flutter_fadein: ^2.0.0
|
||||||
flutter_feather_icons: 2.0.0+1
|
flutter_feather_icons: 2.0.0+1
|
||||||
flutter_html: ^2.2.1
|
flutter_html: ^2.2.1
|
||||||
@ -28,7 +33,7 @@ dependencies:
|
|||||||
flutter_linkify: ^5.0.2
|
flutter_linkify: ^5.0.2
|
||||||
flutter_local_notifications: ^9.5.0
|
flutter_local_notifications: ^9.5.0
|
||||||
flutter_secure_storage: ^5.0.2
|
flutter_secure_storage: ^5.0.2
|
||||||
flutter_slidable: ^1.2.0
|
flutter_slidable: ^1.2.1
|
||||||
font_awesome_flutter: ^9.2.0
|
font_awesome_flutter: ^9.2.0
|
||||||
gbk_codec: ^0.4.0
|
gbk_codec: ^0.4.0
|
||||||
get_it: 7.2.0
|
get_it: 7.2.0
|
||||||
@ -42,7 +47,11 @@ dependencies:
|
|||||||
path_provider: ^2.0.8
|
path_provider: ^2.0.8
|
||||||
path_provider_android: ^2.0.8
|
path_provider_android: ^2.0.8
|
||||||
path_provider_ios: ^2.0.8
|
path_provider_ios: ^2.0.8
|
||||||
pull_to_refresh: ^2.0.0
|
# pull_to_refresh: ^2.0.0
|
||||||
|
pull_to_refresh:
|
||||||
|
git:
|
||||||
|
url: https://github.com/livinglist/flutter_pulltorefresh
|
||||||
|
ref: master
|
||||||
responsive_builder: ^0.4.2
|
responsive_builder: ^0.4.2
|
||||||
rxdart: ^0.27.3
|
rxdart: ^0.27.3
|
||||||
sembast: ^3.1.1+1
|
sembast: ^3.1.1+1
|
||||||
@ -70,7 +79,6 @@ flutter:
|
|||||||
generate: true
|
generate: true
|
||||||
|
|
||||||
assets:
|
assets:
|
||||||
- images/hacki_icon.png
|
- assets/images/
|
||||||
- images/hacker_news_logo.png
|
|
||||||
|
|
||||||
|
|
||||||
|