Compare commits

..

15 Commits

Author SHA1 Message Date
a77eb889f1 updated fastlane. 2022-02-23 19:15:21 -08:00
b35ffa2921 added haptic feedback. 2022-02-23 19:13:08 -08:00
d0d031600c updated README.md 2022-02-23 18:59:52 -08:00
a8d3002f31 Merge pull request #3 from Livinglist/v0.1.7
v0.1.7
2022-02-23 18:58:18 -08:00
a35aa6ea3b fixed order of feature discovery. 2022-02-23 18:18:02 -08:00
b2d4369b57 fixed routing. 2022-02-23 18:05:08 -08:00
fa3b28d050 updated version. 2022-02-23 17:34:15 -08:00
746dd61f48 updated fastlane. 2022-02-23 17:31:31 -08:00
29165bdb09 added slidable to comment tile. 2022-02-23 17:20:42 -08:00
4b9de44297 removed comment border option. 2022-02-23 15:51:44 -08:00
9e48be158b added pin button to story screen. 2022-02-23 15:49:25 -08:00
e64ea5e99a disabled feature discovery resetting. 2022-02-23 15:32:16 -08:00
0fce662954 fixed discovery overlay behavior. 2022-02-23 15:31:38 -08:00
b9b9d5f99f fixed link parsing. 2022-02-23 15:26:54 -08:00
1583525b48 added fastlane. 2022-02-20 01:24:49 -08:00
30 changed files with 418 additions and 336 deletions

View File

@ -22,9 +22,9 @@ Features:
- Mark stories as favorite.
- Browse comments and stories you have posted in the past.
- Search for stories on Hacker News.
- Double tap to collapse a comment.
- Long press to vote on a comment or story.
- Swipe to right to pin a story to top.
- Collapse comments.
- Vote on comments or stories.
- Pin stories to the top of home page.
- Get in-app notification when there is new reply to your stories or comments.
- And more...
@ -34,7 +34,7 @@ Features:
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859627-48290a22-9679-442b-bae4-97f21546b3ae.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859630-93f7e372-f2e7-4357-86c0-250a3f69c10f.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859632-b52a89ca-b8d7-464c-a508-faa86bcc87f8.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148904175-8313d30a-ef84-4f3a-9ac2-f9e06021615d.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/155449312-4208a961-44ac-42b3-968b-9526d4a07787.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/150713047-2710add8-0493-4c42-a710-f96dc77cfde1.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/150918515-0fc4869f-efa3-473f-90af-381daf5e4915.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/152305175-94fa3696-f40f-4f40-b040-f17fc59ff260.png">
@ -44,4 +44,3 @@ Features:
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/153973715-a33018d2-d3b1-4bfa-be39-56f5e3c4830b.png">
</p>

View File

@ -0,0 +1 @@
- Bugfixes.

View File

@ -0,0 +1 @@
- Updates to UI.

View File

@ -0,0 +1 @@
- Updates to UI.

View File

@ -0,0 +1,13 @@
Features:
- Log in using your Hacker News account.
- Browse stories from various categories.
- Submit links.
- Leave comments on stories.
- Mark stories as favorite.
- Browse comments and stories you have posted in the past.
- Search for stories on Hacker News.
- Collapse comments.
- Vote on comments or stories.
- Pin stories to the top of home page.
- Get in-app notification when there is new reply to your stories or comments.
- And more...

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

View File

@ -0,0 +1 @@
Hacki is a simple noiseless Hacker News reader.

View File

@ -0,0 +1 @@
Hacki for Hacker News

View File

@ -365,7 +365,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1.6;
MARKETING_VERSION = 0.1.7;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -500,7 +500,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1.6;
MARKETING_VERSION = 0.1.7;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -529,7 +529,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1.6;
MARKETING_VERSION = 0.1.7;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -11,4 +11,5 @@ class Constants {
static const String featureAddStoryToFavList = 'add_story_to_fav_list';
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
static const String featureLogIn = 'log_in';
static const String featurePinToTop = 'pin_to_top';
}

View File

@ -22,8 +22,6 @@ class PreferenceCubit extends Cubit<PreferenceState> {
.then((value) => emit(state.copyWith(showComplexStoryTile: value)));
_storageRepository.shouldShowWebFirst
.then((value) => emit(state.copyWith(showWebFirst: value)));
_storageRepository.shouldCommentBorder
.then((value) => emit(state.copyWith(showCommentBorder: value)));
_storageRepository.shouldShowEyeCandy
.then((value) => emit(state.copyWith(showEyeCandy: value)));
_storageRepository.trueDarkMode
@ -47,11 +45,6 @@ class PreferenceCubit extends Cubit<PreferenceState> {
_storageRepository.toggleNavigationMode();
}
void toggleCommentBorderMode() {
emit(state.copyWith(showCommentBorder: !state.showCommentBorder));
_storageRepository.toggleCommentBorderMode();
}
void toggleEyeCandyMode() {
emit(state.copyWith(showEyeCandy: !state.showEyeCandy));
_storageRepository.toggleEyeCandyMode();

View File

@ -5,7 +5,6 @@ class PreferenceState extends Equatable {
required this.showNotification,
required this.showComplexStoryTile,
required this.showWebFirst,
required this.showCommentBorder,
required this.showEyeCandy,
required this.useTrueDark,
required this.useReader,
@ -15,7 +14,6 @@ class PreferenceState extends Equatable {
: showNotification = false,
showComplexStoryTile = false,
showWebFirst = false,
showCommentBorder = false,
showEyeCandy = false,
useTrueDark = false,
useReader = false;
@ -23,7 +21,6 @@ class PreferenceState extends Equatable {
final bool showNotification;
final bool showComplexStoryTile;
final bool showWebFirst;
final bool showCommentBorder;
final bool showEyeCandy;
final bool useTrueDark;
final bool useReader;
@ -32,7 +29,6 @@ class PreferenceState extends Equatable {
bool? showNotification,
bool? showComplexStoryTile,
bool? showWebFirst,
bool? showCommentBorder,
bool? showEyeCandy,
bool? useTrueDark,
bool? useReader,
@ -41,7 +37,6 @@ class PreferenceState extends Equatable {
showNotification: showNotification ?? this.showNotification,
showComplexStoryTile: showComplexStoryTile ?? this.showComplexStoryTile,
showWebFirst: showWebFirst ?? this.showWebFirst,
showCommentBorder: showCommentBorder ?? this.showCommentBorder,
showEyeCandy: showEyeCandy ?? this.showEyeCandy,
useTrueDark: useTrueDark ?? this.useTrueDark,
useReader: useReader ?? this.useReader,
@ -53,7 +48,6 @@ class PreferenceState extends Equatable {
showNotification,
showComplexStoryTile,
showWebFirst,
showCommentBorder,
showEyeCandy,
useTrueDark,
useReader,

View File

@ -12,6 +12,8 @@ class StorageRepository {
static const String _passwordKey = 'password';
static const String _blocklistKey = 'blocklist';
static const String _pinnedStoriesIdsKey = 'pinnedStoriesIds';
static const String _unreadCommentsIdsKey = 'unreadCommentsIds';
static const String _notificationModeKey = 'notificationMode';
static const String _trueDarkModeKey = 'trueDarkMode';
static const String _readerModeKey = 'readerMode';
@ -24,17 +26,14 @@ class StorageRepository {
/// navigated to web view first. Defaults to false.
static const String _navigationModeKey = 'navigationMode';
static const String _commentBorderModeKey = 'commentBorderMode';
static const String _eyeCandyModeKey = 'eyeCandyMode';
static const String _unreadCommentsIdsKey = 'unreadCommentsIds';
static const bool _notificationModeDefaultValue = true;
static const bool _displayModeDefaultValue = true;
static const bool _navigationModeDefaultValue = true;
static const bool _commentBorderModeDefaultValue = true;
static const bool _eyeCandyModeDefaultValue = false;
static const bool _trueDarkModeDefaultValue = false;
static const bool _readerModeKeyDefaultValue = true;
static const bool _readerModeDefaultValue = true;
final Future<SharedPreferences> _prefs;
final FlutterSecureStorage _secureStorage;
@ -60,9 +59,6 @@ class StorageRepository {
Future<bool> get shouldShowWebFirst async => _prefs.then((prefs) =>
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue);
Future<bool> get shouldCommentBorder async => _prefs.then((prefs) =>
prefs.getBool(_commentBorderModeKey) ?? _commentBorderModeDefaultValue);
Future<bool> get shouldShowEyeCandy async => _prefs.then(
(prefs) => prefs.getBool(_eyeCandyModeKey) ?? _eyeCandyModeDefaultValue);
@ -70,7 +66,7 @@ class StorageRepository {
(prefs) => prefs.getBool(_trueDarkModeKey) ?? _trueDarkModeDefaultValue);
Future<bool> get readerMode async => _prefs.then(
(prefs) => prefs.getBool(_readerModeKey) ?? _readerModeKeyDefaultValue);
(prefs) => prefs.getBool(_readerModeKey) ?? _readerModeDefaultValue);
Future<List<int>> get unreadCommentsIds async => _prefs.then((prefs) =>
prefs.getStringList(_unreadCommentsIdsKey)?.map(int.parse).toList() ??
@ -124,13 +120,6 @@ class StorageRepository {
await prefs.setBool(_navigationModeKey, !currentMode);
}
Future<void> toggleCommentBorderMode() async {
final prefs = await _prefs;
final currentMode =
prefs.getBool(_commentBorderModeKey) ?? _commentBorderModeDefaultValue;
await prefs.setBool(_commentBorderModeKey, !currentMode);
}
Future<void> toggleEyeCandyMode() async {
final prefs = await _prefs;
final currentMode =
@ -148,7 +137,7 @@ class StorageRepository {
Future<void> toggleReaderMode() async {
final prefs = await _prefs;
final currentMode =
prefs.getBool(_readerModeKey) ?? _readerModeKeyDefaultValue;
prefs.getBool(_readerModeKey) ?? _readerModeDefaultValue;
await prefs.setBool(_readerModeKey, !currentMode);
}

View File

@ -21,7 +21,7 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
static const String routeName = '/home';
static const String routeName = '/';
static Route route() {
return MaterialPageRoute<HomeScreen>(
@ -54,6 +54,7 @@ class _HomeScreenState extends State<HomeScreen>
// Constants.featureLogIn,
// Constants.featureAddStoryToFavList,
// Constants.featureOpenStoryInWebView,
// Constants.featurePinToTop,
// ]);
SchedulerBinding.instance?.addPostFrameCallback((_) {
@ -88,8 +89,10 @@ class _HomeScreenState extends State<HomeScreen>
motion: const BehindMotion(),
children: [
SlidableAction(
onPressed: (_) =>
context.read<PinCubit>().unpinStory(story),
onPressed: (_) {
HapticFeedback.lightImpact();
context.read<PinCubit>().unpinStory(story);
},
backgroundColor: Colors.red,
foregroundColor: Colors.white,
icon: preferenceState.showComplexStoryTile
@ -151,247 +154,245 @@ class _HomeScreenState extends State<HomeScreen>
}
},
builder: (context, state) {
return WillPopScope(
onWillPop: () => Future.value(false),
child: DefaultTabController(
length: 6,
child: Scaffold(
resizeToAvoidBottomInset: false,
appBar: PreferredSize(
preferredSize: const Size(0, 48),
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).padding.top,
),
TabBar(
isScrollable: true,
controller: tabController,
indicatorColor: Colors.orange,
tabs: [
Tab(
child: Text(
'TOP',
style: TextStyle(
fontSize: 14,
color: currentIndex == 0
? Colors.orange
: Colors.grey,
),
return DefaultTabController(
length: 6,
child: Scaffold(
resizeToAvoidBottomInset: false,
appBar: PreferredSize(
preferredSize: const Size(0, 48),
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).padding.top,
),
TabBar(
isScrollable: true,
controller: tabController,
indicatorColor: Colors.orange,
tabs: [
Tab(
child: Text(
'TOP',
style: TextStyle(
fontSize: 14,
color: currentIndex == 0
? Colors.orange
: Colors.grey,
),
),
Tab(
child: Text(
'NEW',
style: TextStyle(
fontSize: 14,
color: currentIndex == 1
? Colors.orange
: Colors.grey,
),
),
Tab(
child: Text(
'NEW',
style: TextStyle(
fontSize: 14,
color: currentIndex == 1
? Colors.orange
: Colors.grey,
),
),
Tab(
child: Text(
'ASK',
style: TextStyle(
fontSize: 14,
color: currentIndex == 2
? Colors.orange
: Colors.grey,
),
),
Tab(
child: Text(
'ASK',
style: TextStyle(
fontSize: 14,
color: currentIndex == 2
? Colors.orange
: Colors.grey,
),
),
Tab(
child: Text(
'SHOW',
style: TextStyle(
fontSize: 13,
color: currentIndex == 3
? Colors.orange
: Colors.grey,
),
),
Tab(
child: Text(
'SHOW',
style: TextStyle(
fontSize: 13,
color: currentIndex == 3
? Colors.orange
: Colors.grey,
),
),
Tab(
child: Text(
'JOBS',
style: TextStyle(
fontSize: 14,
color: currentIndex == 4
? Colors.orange
: Colors.grey,
),
),
Tab(
child: Text(
'JOBS',
style: TextStyle(
fontSize: 14,
color: currentIndex == 4
? Colors.orange
: Colors.grey,
),
),
Tab(
child: DescribedFeatureOverlay(
targetColor: Theme.of(context).primaryColor,
tapTarget: const Icon(
Icons.person,
size: 16,
color: Colors.white,
),
featureId: Constants.featureLogIn,
title: const Text(''),
description: const Text(
'Log in using your Hacker News account '
'to check out stories and comments you have '
'posted in the past, and get in-app '
'notification when there is new reply to '
'your comments or stories.\n\nAlso, you can '
'long press here to submit a new link to '
'Hacker News.',
style: TextStyle(fontSize: 16),
),
child: BlocBuilder<NotificationCubit,
NotificationState>(
builder: (context, state) {
if (state.unreadCommentsIds.isEmpty) {
return Icon(
),
Tab(
child: DescribedFeatureOverlay(
barrierDismissible: false,
overflowMode: OverflowMode.extendBackground,
targetColor: Theme.of(context).primaryColor,
tapTarget: const Icon(
Icons.person,
size: 16,
color: Colors.white,
),
featureId: Constants.featureLogIn,
title: const Text(''),
description: const Text(
'Log in using your Hacker News account '
'to check out stories and comments you have '
'posted in the past, and get in-app '
'notification when there is new reply to '
'your comments or stories.\n\nAlso, you can '
'long press here to submit a new link to '
'Hacker News.',
style: TextStyle(fontSize: 16),
),
child: BlocBuilder<NotificationCubit,
NotificationState>(
builder: (context, state) {
if (state.unreadCommentsIds.isEmpty) {
return Icon(
Icons.person,
size: 16,
color: currentIndex == 5
? Colors.orange
: Colors.grey,
);
} else {
return Badge(
borderRadius: BorderRadius.circular(100),
badgeContent: Container(
height: 3,
width: 3,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white),
),
child: Icon(
Icons.person,
size: 16,
color: currentIndex == 5
? Colors.orange
: Colors.grey,
);
} else {
return Badge(
borderRadius:
BorderRadius.circular(100),
badgeContent: Container(
height: 3,
width: 3,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white),
),
child: Icon(
Icons.person,
size: 16,
color: currentIndex == 5
? Colors.orange
: Colors.grey,
),
);
}
},
),
),
);
}
},
),
),
],
),
],
),
),
body: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: tabController,
children: [
ItemsListView<Story>(
pinnable: true,
showWebPreview: preferenceState.showComplexStoryTile,
refreshController: refreshControllerTop,
items: state.storiesByType[StoryType.top]!,
onRefresh: () {
HapticFeedback.lightImpact();
context
.read<StoriesBloc>()
.add(StoriesRefresh(type: StoryType.top));
},
onLoadMore: () {
context
.read<StoriesBloc>()
.add(StoriesLoadMore(type: StoryType.top));
},
onTap: onStoryTapped,
onPinned: context.read<PinCubit>().pinStory,
header: pinnedStories,
),
],
),
ItemsListView<Story>(
pinnable: true,
showWebPreview: preferenceState.showComplexStoryTile,
refreshController: refreshControllerNew,
items: state.storiesByType[StoryType.latest]!,
onRefresh: () {
HapticFeedback.lightImpact();
context
.read<StoriesBloc>()
.add(StoriesRefresh(type: StoryType.latest));
},
onLoadMore: () {
context
.read<StoriesBloc>()
.add(StoriesLoadMore(type: StoryType.latest));
},
onTap: onStoryTapped,
onPinned: context.read<PinCubit>().pinStory,
header: pinnedStories,
),
ItemsListView<Story>(
pinnable: true,
showWebPreview: preferenceState.showComplexStoryTile,
refreshController: refreshControllerAsk,
items: state.storiesByType[StoryType.ask]!,
onRefresh: () {
HapticFeedback.lightImpact();
context
.read<StoriesBloc>()
.add(StoriesRefresh(type: StoryType.ask));
},
onLoadMore: () {
context
.read<StoriesBloc>()
.add(StoriesLoadMore(type: StoryType.ask));
},
onTap: onStoryTapped,
onPinned: context.read<PinCubit>().pinStory,
header: pinnedStories,
),
ItemsListView<Story>(
pinnable: true,
showWebPreview: preferenceState.showComplexStoryTile,
refreshController: refreshControllerShow,
items: state.storiesByType[StoryType.show]!,
onRefresh: () {
HapticFeedback.lightImpact();
context
.read<StoriesBloc>()
.add(StoriesRefresh(type: StoryType.show));
},
onLoadMore: () {
context
.read<StoriesBloc>()
.add(StoriesLoadMore(type: StoryType.show));
},
onTap: onStoryTapped,
onPinned: context.read<PinCubit>().pinStory,
header: pinnedStories,
),
ItemsListView<Story>(
pinnable: true,
showWebPreview: preferenceState.showComplexStoryTile,
refreshController: refreshControllerJobs,
items: state.storiesByType[StoryType.jobs]!,
onRefresh: () {
HapticFeedback.lightImpact();
context
.read<StoriesBloc>()
.add(StoriesRefresh(type: StoryType.jobs));
},
onLoadMore: () {
context
.read<StoriesBloc>()
.add(StoriesLoadMore(type: StoryType.jobs));
},
onTap: onStoryTapped,
onPinned: context.read<PinCubit>().pinStory,
header: pinnedStories,
),
const ProfileScreen(),
],
),
),
body: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: tabController,
children: [
ItemsListView<Story>(
pinnable: true,
showWebPreview: preferenceState.showComplexStoryTile,
refreshController: refreshControllerTop,
items: state.storiesByType[StoryType.top]!,
onRefresh: () {
HapticFeedback.lightImpact();
context
.read<StoriesBloc>()
.add(StoriesRefresh(type: StoryType.top));
},
onLoadMore: () {
context
.read<StoriesBloc>()
.add(StoriesLoadMore(type: StoryType.top));
},
onTap: onStoryTapped,
onPinned: context.read<PinCubit>().pinStory,
header: pinnedStories,
),
ItemsListView<Story>(
pinnable: true,
showWebPreview: preferenceState.showComplexStoryTile,
refreshController: refreshControllerNew,
items: state.storiesByType[StoryType.latest]!,
onRefresh: () {
HapticFeedback.lightImpact();
context
.read<StoriesBloc>()
.add(StoriesRefresh(type: StoryType.latest));
},
onLoadMore: () {
context
.read<StoriesBloc>()
.add(StoriesLoadMore(type: StoryType.latest));
},
onTap: onStoryTapped,
onPinned: context.read<PinCubit>().pinStory,
header: pinnedStories,
),
ItemsListView<Story>(
pinnable: true,
showWebPreview: preferenceState.showComplexStoryTile,
refreshController: refreshControllerAsk,
items: state.storiesByType[StoryType.ask]!,
onRefresh: () {
HapticFeedback.lightImpact();
context
.read<StoriesBloc>()
.add(StoriesRefresh(type: StoryType.ask));
},
onLoadMore: () {
context
.read<StoriesBloc>()
.add(StoriesLoadMore(type: StoryType.ask));
},
onTap: onStoryTapped,
onPinned: context.read<PinCubit>().pinStory,
header: pinnedStories,
),
ItemsListView<Story>(
pinnable: true,
showWebPreview: preferenceState.showComplexStoryTile,
refreshController: refreshControllerShow,
items: state.storiesByType[StoryType.show]!,
onRefresh: () {
HapticFeedback.lightImpact();
context
.read<StoriesBloc>()
.add(StoriesRefresh(type: StoryType.show));
},
onLoadMore: () {
context
.read<StoriesBloc>()
.add(StoriesLoadMore(type: StoryType.show));
},
onTap: onStoryTapped,
onPinned: context.read<PinCubit>().pinStory,
header: pinnedStories,
),
ItemsListView<Story>(
pinnable: true,
showWebPreview: preferenceState.showComplexStoryTile,
refreshController: refreshControllerJobs,
items: state.storiesByType[StoryType.jobs]!,
onRefresh: () {
HapticFeedback.lightImpact();
context
.read<StoriesBloc>()
.add(StoriesRefresh(type: StoryType.jobs));
},
onLoadMore: () {
context
.read<StoriesBloc>()
.add(StoriesLoadMore(type: StoryType.jobs));
},
onTap: onStoryTapped,
onPinned: context.read<PinCubit>().pinStory,
header: pinnedStories,
),
const ProfileScreen(),
],
),
),
);
},

View File

@ -19,7 +19,7 @@ import 'package:hacki/utils/utils.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
enum PageType {
enum _PageType {
fav,
history,
settings,
@ -41,7 +41,7 @@ class _ProfileScreenState extends State<ProfileScreen>
final refreshControllerNotification = RefreshController();
final scrollController = ScrollController();
PageType pageType = PageType.notification;
_PageType pageType = _PageType.notification;
final magicWords = <String>[
'to be a lord.',
@ -71,7 +71,7 @@ class _ProfileScreenState extends State<ProfileScreen>
builder: (context, notificationState) {
return Stack(
children: [
if (!authState.isLoggedIn && pageType == PageType.history)
if (!authState.isLoggedIn && pageType == _PageType.history)
Positioned.fill(
child: Column(
children: [
@ -94,7 +94,7 @@ class _ProfileScreenState extends State<ProfileScreen>
top: 50,
child: Offstage(
offstage: !authState.isLoggedIn ||
pageType != PageType.history,
pageType != _PageType.history,
child: BlocConsumer<HistoryCubit, HistoryState>(
listener: (context, historyState) {
if (historyState.status == HistoryStatus.loaded) {
@ -144,7 +144,7 @@ class _ProfileScreenState extends State<ProfileScreen>
Positioned.fill(
top: 50,
child: Offstage(
offstage: pageType != PageType.fav,
offstage: pageType != _PageType.fav,
child: BlocConsumer<FavCubit, FavState>(
listener: (context, favState) {
if (favState.status == FavStatus.loaded) {
@ -179,14 +179,14 @@ class _ProfileScreenState extends State<ProfileScreen>
Positioned.fill(
top: 50,
child: Offstage(
offstage: pageType != PageType.search,
offstage: pageType != _PageType.search,
child: const SearchScreen(),
),
),
Positioned.fill(
top: 50,
child: Offstage(
offstage: pageType != PageType.notification,
offstage: pageType != _PageType.notification,
child: InboxView(
refreshController: refreshControllerNotification,
unreadCommentsIds:
@ -226,7 +226,7 @@ class _ProfileScreenState extends State<ProfileScreen>
Positioned.fill(
top: 50,
child: Offstage(
offstage: pageType != PageType.settings,
offstage: pageType != _PageType.settings,
child: SingleChildScrollView(
child: Column(
children: [
@ -306,18 +306,6 @@ class _ProfileScreenState extends State<ProfileScreen>
},
activeColor: Colors.orange,
),
SwitchListTile(
title: const Text('Show Comment Outlines'),
subtitle: const Text('be nice to your eyes.'),
value: preferenceState.showCommentBorder,
onChanged: (val) {
HapticFeedback.lightImpact();
context
.read<PreferenceCubit>()
.toggleCommentBorderMode();
},
activeColor: Colors.orange,
),
SwitchListTile(
title: const Text('Eye Candy'),
subtitle: const Text('some sort of magic.'),
@ -377,7 +365,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog(
context: context,
applicationName: 'Hacki',
applicationVersion: 'v0.1.6',
applicationVersion: 'v0.1.7',
applicationIcon: Image.asset(
Constants.hackiIconPath,
height: 50,
@ -467,11 +455,11 @@ class _ProfileScreenState extends State<ProfileScreen>
label: 'Inbox : '
//ignore: lines_longer_than_80_chars
'${notificationState.unreadCommentsIds.length}',
selected: pageType == PageType.notification,
selected: pageType == _PageType.notification,
onSelected: (val) {
if (val) {
setState(() {
pageType = PageType.notification;
pageType = _PageType.notification;
});
}
},
@ -481,11 +469,11 @@ class _ProfileScreenState extends State<ProfileScreen>
),
CustomChip(
label: 'Favorite',
selected: pageType == PageType.fav,
selected: pageType == _PageType.fav,
onSelected: (val) {
if (val) {
setState(() {
pageType = PageType.fav;
pageType = _PageType.fav;
});
}
},
@ -495,11 +483,11 @@ class _ProfileScreenState extends State<ProfileScreen>
),
CustomChip(
label: 'Submitted',
selected: pageType == PageType.history,
selected: pageType == _PageType.history,
onSelected: (val) {
if (val) {
setState(() {
pageType = PageType.history;
pageType = _PageType.history;
});
}
},
@ -509,11 +497,11 @@ class _ProfileScreenState extends State<ProfileScreen>
),
CustomChip(
label: 'Search',
selected: pageType == PageType.search,
selected: pageType == _PageType.search,
onSelected: (val) {
if (val) {
setState(() {
pageType = PageType.search;
pageType = _PageType.search;
});
}
},
@ -523,11 +511,11 @@ class _ProfileScreenState extends State<ProfileScreen>
),
CustomChip(
label: 'Settings',
selected: pageType == PageType.settings,
selected: pageType == _PageType.settings,
onSelected: (val) {
if (val) {
setState(() {
pageType = PageType.settings;
pageType = _PageType.settings;
});
}
},

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@ -105,6 +107,7 @@ class _StoryScreenState extends State<StoryScreen> {
FeatureDiscovery.discoverFeatures(
context,
const <String>{
Constants.featurePinToTop,
Constants.featureAddStoryToFavList,
Constants.featureOpenStoryInWebView,
},
@ -205,12 +208,69 @@ class _StoryScreenState extends State<StoryScreen> {
ScrollUpIconButton(
scrollController: scrollController,
),
BlocBuilder<PinCubit, PinState>(
builder: (context, pinState) {
final pinned = pinState.pinnedStoriesIds
.contains(widget.story.id);
return Transform.rotate(
angle: pi / 4,
child: Transform.translate(
offset: const Offset(2, 0),
child: IconButton(
icon: DescribedFeatureOverlay(
barrierDismissible: false,
overflowMode:
OverflowMode.extendBackground,
targetColor:
Theme.of(context).primaryColor,
tapTarget: Icon(
pinned
? Icons.push_pin
: Icons.push_pin_outlined,
color: Colors.white,
),
featureId: Constants.featurePinToTop,
title: const Text('Pin a Story'),
description: const Text(
'Pin this story to the top of your '
'home screen so that you can come'
' back later.',
style: TextStyle(fontSize: 16),
),
child: Icon(
pinned
? Icons.push_pin
: Icons.push_pin_outlined,
color: pinned
? Colors.orange
: Theme.of(context).iconTheme.color,
),
),
onPressed: () {
HapticFeedback.lightImpact();
if (pinned) {
context
.read<PinCubit>()
.unpinStory(widget.story);
} else {
context
.read<PinCubit>()
.pinStory(widget.story);
}
},
),
),
);
},
),
BlocBuilder<FavCubit, FavState>(
builder: (context, favState) {
final isFav =
favState.favIds.contains(widget.story.id);
return IconButton(
icon: DescribedFeatureOverlay(
barrierDismissible: false,
overflowMode: OverflowMode.extendBackground,
targetColor: Theme.of(context).primaryColor,
tapTarget: Icon(
isFav
@ -221,7 +281,7 @@ class _StoryScreenState extends State<StoryScreen> {
featureId: Constants.featureAddStoryToFavList,
title: const Text('Fav a Story'),
description: const Text(
'Save this article for later.',
'Add it to your favorites.',
style: TextStyle(fontSize: 16),
),
child: Icon(
@ -250,6 +310,8 @@ class _StoryScreenState extends State<StoryScreen> {
),
IconButton(
icon: DescribedFeatureOverlay(
barrierDismissible: false,
overflowMode: OverflowMode.extendBackground,
targetColor: Theme.of(context).primaryColor,
tapTarget: const Icon(
Icons.stream,
@ -327,7 +389,7 @@ class _StoryScreenState extends State<StoryScreen> {
focusNode.requestFocus();
});
},
onLongPress: () => onLongPressed(widget.story),
onLongPress: () => onMorePressed(widget.story),
child: Column(
children: [
Padding(
@ -407,7 +469,8 @@ class _StoryScreenState extends State<StoryScreen> {
myUsername: authState.isLoggedIn
? authState.username
: null,
onTap: (cmt) {
onReplyTapped: (cmt) {
HapticFeedback.lightImpact();
if (cmt.deleted || cmt.dead) {
return;
}
@ -419,7 +482,7 @@ class _StoryScreenState extends State<StoryScreen> {
editCubit.onItemTapped(cmt);
focusNode.requestFocus();
},
onLongPress: onLongPressed,
onMoreTapped: onMorePressed,
onStoryLinkTapped: (link) {
final regex = RegExp(r'\d+$');
final match = regex.stringMatch(link) ?? '';
@ -483,7 +546,9 @@ class _StoryScreenState extends State<StoryScreen> {
);
}
void onLongPressed(Item item) {
void onMorePressed(Item item) {
HapticFeedback.lightImpact();
if (item.dead || item.deleted) {
return;
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/utils/utils.dart';
@ -11,8 +12,8 @@ class CommentTile extends StatelessWidget {
Key? key,
required this.myUsername,
required this.comment,
required this.onTap,
required this.onLongPress,
required this.onReplyTapped,
required this.onMoreTapped,
required this.onStoryLinkTapped,
this.loadKids = true,
this.level = 0,
@ -22,8 +23,8 @@ class CommentTile extends StatelessWidget {
final Comment comment;
final int level;
final bool loadKids;
final Function(Comment) onTap;
final Function(Comment) onLongPress;
final Function(Comment) onReplyTapped;
final Function(Comment) onMoreTapped;
final Function(String) onStoryLinkTapped;
@override
@ -58,37 +59,56 @@ class CommentTile extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () => onTap(comment),
onLongPress: () => onLongPress(comment),
onDoubleTap: () {
context.read<CommentsCubit>().collapse();
},
Slidable(
startActionPane: ActionPane(
motion: const BehindMotion(),
children: [
SlidableAction(
onPressed: (_) => onReplyTapped(comment),
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
icon: Icons.message,
label: 'Reply',
),
SlidableAction(
onPressed: (_) => onMoreTapped(comment),
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
icon: Icons.more_horiz,
label: 'More',
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
left: 6, right: 6, top: 6),
child: Row(
children: [
Text(
comment.by,
style: TextStyle(
//255, 152, 0
color: prefState.showEyeCandy
? orange
: color,
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
context.read<CommentsCubit>().collapse(),
child: Padding(
padding: const EdgeInsets.only(
left: 6, right: 6, top: 6),
child: Row(
children: [
Text(
comment.by,
style: TextStyle(
//255, 152, 0
color: prefState.showEyeCandy
? orange
: color,
),
),
),
const Spacer(),
Text(
comment.postedDate,
style: const TextStyle(
color: Colors.grey,
const Spacer(),
Text(
comment.postedDate,
style: const TextStyle(
color: Colors.grey,
),
),
),
],
],
),
),
),
if (comment.deleted)
@ -141,7 +161,7 @@ class CommentTile extends StatelessWidget {
top: 6,
bottom: 12,
),
child: Linkify(
child: SelectableLinkify(
key: ObjectKey(comment),
text: comment.text,
onOpen: (link) {
@ -170,8 +190,8 @@ class CommentTile extends StatelessWidget {
child: CommentTile(
comment: e,
myUsername: myUsername,
onTap: onTap,
onLongPress: onLongPress,
onReplyTapped: onReplyTapped,
onMoreTapped: onMoreTapped,
level: level + 1,
onStoryLinkTapped: onStoryLinkTapped,
),
@ -195,9 +215,8 @@ class CommentTile extends StatelessWidget {
Theme.of(context).brightness == Brightness.dark
? 0.03
: 0.15;
final borderColor = prefState.showCommentBorder && level != 0
? color.withOpacity(0.5)
: Colors.transparent;
final borderColor =
level != 0 ? color.withOpacity(0.5) : Colors.transparent;
final commentColor = prefState.showEyeCandy
? color.withOpacity(commentBackgroundColorOpacity)
: Colors.transparent;

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
@ -52,11 +53,14 @@ class ItemsListView<T extends Item> extends StatelessWidget {
motion: const BehindMotion(),
children: [
SlidableAction(
onPressed: (_) => onPinned?.call(e),
onPressed: (_) {
HapticFeedback.lightImpact();
onPinned?.call(e);
},
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
icon: showWebPreview
? Icons.vertical_align_top
? Icons.push_pin_outlined
: null,
label: 'Pin to top',
),

View File

@ -6,10 +6,21 @@ class LinkUtil {
static final _browser = ChromeSafariBrowser();
static void launchUrl(String link, {bool useReader = false}) {
String rinseLink(String link) {
if (link.contains(')')) {
final regex = RegExp(r'\).*$');
final match = regex.stringMatch(link) ?? '';
return link.replaceAll(match, '');
}
return link;
}
canLaunch(link).then((val) {
if (val) {
final rinsedLink = rinseLink(link);
_browser.open(
url: Uri.parse(link),
url: Uri.parse(rinsedLink),
options: ChromeSafariBrowserClassOptions(
ios: IOSSafariOptions(
entersReaderIfAvailable: useReader,

View File

@ -1,6 +1,6 @@
name: hacki
description: A Hacker News reader.
version: 0.1.6+21
version: 0.1.7+24
publish_to: none
environment: