Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
a77eb889f1 | |||
b35ffa2921 | |||
d0d031600c | |||
a8d3002f31 | |||
a35aa6ea3b | |||
b2d4369b57 | |||
fa3b28d050 | |||
746dd61f48 | |||
29165bdb09 | |||
4b9de44297 | |||
9e48be158b | |||
e64ea5e99a | |||
0fce662954 | |||
b9b9d5f99f | |||
1583525b48 |
@ -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>
|
||||
|
||||
|
||||
|
1
fastlane/metadata/android/en-US/changelogs/22.txt
Normal file
@ -0,0 +1 @@
|
||||
- Bugfixes.
|
1
fastlane/metadata/android/en-US/changelogs/23.txt
Normal file
@ -0,0 +1 @@
|
||||
- Updates to UI.
|
1
fastlane/metadata/android/en-US/changelogs/24.txt
Normal file
@ -0,0 +1 @@
|
||||
- Updates to UI.
|
13
fastlane/metadata/android/en-US/full_description.txt
Normal 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...
|
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 393 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/10.png
Normal file
After Width: | Height: | Size: 420 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 696 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 250 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 231 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 903 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 931 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
Normal file
After Width: | Height: | Size: 298 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/8.png
Normal file
After Width: | Height: | Size: 247 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/9.png
Normal file
After Width: | Height: | Size: 496 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@ -0,0 +1 @@
|
||||
Hacki is a simple noiseless Hacker News reader.
|
1
fastlane/metadata/android/en-US/title.txt
Normal file
@ -0,0 +1 @@
|
||||
Hacki for Hacker News
|
@ -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 = "";
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,9 +154,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return WillPopScope(
|
||||
onWillPop: () => Future.value(false),
|
||||
child: DefaultTabController(
|
||||
return DefaultTabController(
|
||||
length: 6,
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
@ -226,6 +227,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
Tab(
|
||||
child: DescribedFeatureOverlay(
|
||||
barrierDismissible: false,
|
||||
overflowMode: OverflowMode.extendBackground,
|
||||
targetColor: Theme.of(context).primaryColor,
|
||||
tapTarget: const Icon(
|
||||
Icons.person,
|
||||
@ -257,8 +260,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
} else {
|
||||
return Badge(
|
||||
borderRadius:
|
||||
BorderRadius.circular(100),
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
badgeContent: Container(
|
||||
height: 3,
|
||||
width: 3,
|
||||
@ -392,7 +394,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,16 +59,34 @@ 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(
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () =>
|
||||
context.read<CommentsCubit>().collapse(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6, right: 6, top: 6),
|
||||
child: Row(
|
||||
@ -91,6 +110,7 @@ class CommentTile extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (comment.deleted)
|
||||
const Center(
|
||||
child: Padding(
|
||||
@ -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;
|
||||
|
@ -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',
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|