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. - Mark stories as favorite.
- Browse comments and stories you have posted in the past. - Browse comments and stories you have posted in the past.
- Search for stories on Hacker News. - Search for stories on Hacker News.
- Double tap to collapse a comment. - Collapse comments.
- Long press to vote on a comment or story. - Vote on comments or stories.
- Swipe to right to pin a story to top. - Pin stories to the top of home page.
- Get in-app notification when there is new reply to your stories or comments. - Get in-app notification when there is new reply to your stories or comments.
- And more... - 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/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/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/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/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/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"> <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"> <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> </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)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.1.6; MARKETING_VERSION = 0.1.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 = "";
@ -500,7 +500,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.1.6; MARKETING_VERSION = 0.1.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 = "";
@ -529,7 +529,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.1.6; MARKETING_VERSION = 0.1.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 = "";

View File

@ -11,4 +11,5 @@ class Constants {
static const String featureAddStoryToFavList = 'add_story_to_fav_list'; static const String featureAddStoryToFavList = 'add_story_to_fav_list';
static const String featureOpenStoryInWebView = 'open_story_in_web_view'; static const String featureOpenStoryInWebView = 'open_story_in_web_view';
static const String featureLogIn = 'log_in'; 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))); .then((value) => emit(state.copyWith(showComplexStoryTile: value)));
_storageRepository.shouldShowWebFirst _storageRepository.shouldShowWebFirst
.then((value) => emit(state.copyWith(showWebFirst: value))); .then((value) => emit(state.copyWith(showWebFirst: value)));
_storageRepository.shouldCommentBorder
.then((value) => emit(state.copyWith(showCommentBorder: value)));
_storageRepository.shouldShowEyeCandy _storageRepository.shouldShowEyeCandy
.then((value) => emit(state.copyWith(showEyeCandy: value))); .then((value) => emit(state.copyWith(showEyeCandy: value)));
_storageRepository.trueDarkMode _storageRepository.trueDarkMode
@ -47,11 +45,6 @@ class PreferenceCubit extends Cubit<PreferenceState> {
_storageRepository.toggleNavigationMode(); _storageRepository.toggleNavigationMode();
} }
void toggleCommentBorderMode() {
emit(state.copyWith(showCommentBorder: !state.showCommentBorder));
_storageRepository.toggleCommentBorderMode();
}
void toggleEyeCandyMode() { void toggleEyeCandyMode() {
emit(state.copyWith(showEyeCandy: !state.showEyeCandy)); emit(state.copyWith(showEyeCandy: !state.showEyeCandy));
_storageRepository.toggleEyeCandyMode(); _storageRepository.toggleEyeCandyMode();

View File

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

View File

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

View File

@ -21,7 +21,7 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key); const HomeScreen({Key? key}) : super(key: key);
static const String routeName = '/home'; static const String routeName = '/';
static Route route() { static Route route() {
return MaterialPageRoute<HomeScreen>( return MaterialPageRoute<HomeScreen>(
@ -54,6 +54,7 @@ class _HomeScreenState extends State<HomeScreen>
// Constants.featureLogIn, // Constants.featureLogIn,
// Constants.featureAddStoryToFavList, // Constants.featureAddStoryToFavList,
// Constants.featureOpenStoryInWebView, // Constants.featureOpenStoryInWebView,
// Constants.featurePinToTop,
// ]); // ]);
SchedulerBinding.instance?.addPostFrameCallback((_) { SchedulerBinding.instance?.addPostFrameCallback((_) {
@ -88,8 +89,10 @@ class _HomeScreenState extends State<HomeScreen>
motion: const BehindMotion(), motion: const BehindMotion(),
children: [ children: [
SlidableAction( SlidableAction(
onPressed: (_) => onPressed: (_) {
context.read<PinCubit>().unpinStory(story), HapticFeedback.lightImpact();
context.read<PinCubit>().unpinStory(story);
},
backgroundColor: Colors.red, backgroundColor: Colors.red,
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: preferenceState.showComplexStoryTile icon: preferenceState.showComplexStoryTile
@ -151,247 +154,245 @@ class _HomeScreenState extends State<HomeScreen>
} }
}, },
builder: (context, state) { builder: (context, state) {
return WillPopScope( return DefaultTabController(
onWillPop: () => Future.value(false), length: 6,
child: DefaultTabController( child: Scaffold(
length: 6, resizeToAvoidBottomInset: false,
child: Scaffold( appBar: PreferredSize(
resizeToAvoidBottomInset: false, preferredSize: const Size(0, 48),
appBar: PreferredSize( child: Column(
preferredSize: const Size(0, 48), children: [
child: Column( SizedBox(
children: [ height: MediaQuery.of(context).padding.top,
SizedBox( ),
height: MediaQuery.of(context).padding.top, TabBar(
), isScrollable: true,
TabBar( controller: tabController,
isScrollable: true, indicatorColor: Colors.orange,
controller: tabController, tabs: [
indicatorColor: Colors.orange, Tab(
tabs: [ child: Text(
Tab( 'TOP',
child: Text( style: TextStyle(
'TOP', fontSize: 14,
style: TextStyle( color: currentIndex == 0
fontSize: 14, ? Colors.orange
color: currentIndex == 0 : Colors.grey,
? Colors.orange
: Colors.grey,
),
), ),
), ),
Tab( ),
child: Text( Tab(
'NEW', child: Text(
style: TextStyle( 'NEW',
fontSize: 14, style: TextStyle(
color: currentIndex == 1 fontSize: 14,
? Colors.orange color: currentIndex == 1
: Colors.grey, ? Colors.orange
), : Colors.grey,
), ),
), ),
Tab( ),
child: Text( Tab(
'ASK', child: Text(
style: TextStyle( 'ASK',
fontSize: 14, style: TextStyle(
color: currentIndex == 2 fontSize: 14,
? Colors.orange color: currentIndex == 2
: Colors.grey, ? Colors.orange
), : Colors.grey,
), ),
), ),
Tab( ),
child: Text( Tab(
'SHOW', child: Text(
style: TextStyle( 'SHOW',
fontSize: 13, style: TextStyle(
color: currentIndex == 3 fontSize: 13,
? Colors.orange color: currentIndex == 3
: Colors.grey, ? Colors.orange
), : Colors.grey,
), ),
), ),
Tab( ),
child: Text( Tab(
'JOBS', child: Text(
style: TextStyle( 'JOBS',
fontSize: 14, style: TextStyle(
color: currentIndex == 4 fontSize: 14,
? Colors.orange color: currentIndex == 4
: Colors.grey, ? Colors.orange
), : Colors.grey,
), ),
), ),
Tab( ),
child: DescribedFeatureOverlay( Tab(
targetColor: Theme.of(context).primaryColor, child: DescribedFeatureOverlay(
tapTarget: const Icon( barrierDismissible: false,
Icons.person, overflowMode: OverflowMode.extendBackground,
size: 16, targetColor: Theme.of(context).primaryColor,
color: Colors.white, tapTarget: const Icon(
), Icons.person,
featureId: Constants.featureLogIn, size: 16,
title: const Text(''), color: Colors.white,
description: const Text( ),
'Log in using your Hacker News account ' featureId: Constants.featureLogIn,
'to check out stories and comments you have ' title: const Text(''),
'posted in the past, and get in-app ' description: const Text(
'notification when there is new reply to ' 'Log in using your Hacker News account '
'your comments or stories.\n\nAlso, you can ' 'to check out stories and comments you have '
'long press here to submit a new link to ' 'posted in the past, and get in-app '
'Hacker News.', 'notification when there is new reply to '
style: TextStyle(fontSize: 16), 'your comments or stories.\n\nAlso, you can '
), 'long press here to submit a new link to '
child: BlocBuilder<NotificationCubit, 'Hacker News.',
NotificationState>( style: TextStyle(fontSize: 16),
builder: (context, state) { ),
if (state.unreadCommentsIds.isEmpty) { child: BlocBuilder<NotificationCubit,
return Icon( 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, Icons.person,
size: 16, size: 16,
color: currentIndex == 5 color: currentIndex == 5
? Colors.orange ? Colors.orange
: Colors.grey, : 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:in_app_review/in_app_review.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
enum PageType { enum _PageType {
fav, fav,
history, history,
settings, settings,
@ -41,7 +41,7 @@ class _ProfileScreenState extends State<ProfileScreen>
final refreshControllerNotification = RefreshController(); final refreshControllerNotification = RefreshController();
final scrollController = ScrollController(); final scrollController = ScrollController();
PageType pageType = PageType.notification; _PageType pageType = _PageType.notification;
final magicWords = <String>[ final magicWords = <String>[
'to be a lord.', 'to be a lord.',
@ -71,7 +71,7 @@ class _ProfileScreenState extends State<ProfileScreen>
builder: (context, notificationState) { builder: (context, notificationState) {
return Stack( return Stack(
children: [ children: [
if (!authState.isLoggedIn && pageType == PageType.history) if (!authState.isLoggedIn && pageType == _PageType.history)
Positioned.fill( Positioned.fill(
child: Column( child: Column(
children: [ children: [
@ -94,7 +94,7 @@ class _ProfileScreenState extends State<ProfileScreen>
top: 50, top: 50,
child: Offstage( child: Offstage(
offstage: !authState.isLoggedIn || offstage: !authState.isLoggedIn ||
pageType != PageType.history, pageType != _PageType.history,
child: BlocConsumer<HistoryCubit, HistoryState>( child: BlocConsumer<HistoryCubit, HistoryState>(
listener: (context, historyState) { listener: (context, historyState) {
if (historyState.status == HistoryStatus.loaded) { if (historyState.status == HistoryStatus.loaded) {
@ -144,7 +144,7 @@ class _ProfileScreenState extends State<ProfileScreen>
Positioned.fill( Positioned.fill(
top: 50, top: 50,
child: Offstage( child: Offstage(
offstage: pageType != PageType.fav, offstage: pageType != _PageType.fav,
child: BlocConsumer<FavCubit, FavState>( child: BlocConsumer<FavCubit, FavState>(
listener: (context, favState) { listener: (context, favState) {
if (favState.status == FavStatus.loaded) { if (favState.status == FavStatus.loaded) {
@ -179,14 +179,14 @@ class _ProfileScreenState extends State<ProfileScreen>
Positioned.fill( Positioned.fill(
top: 50, top: 50,
child: Offstage( child: Offstage(
offstage: pageType != PageType.search, offstage: pageType != _PageType.search,
child: const SearchScreen(), child: const SearchScreen(),
), ),
), ),
Positioned.fill( Positioned.fill(
top: 50, top: 50,
child: Offstage( child: Offstage(
offstage: pageType != PageType.notification, offstage: pageType != _PageType.notification,
child: InboxView( child: InboxView(
refreshController: refreshControllerNotification, refreshController: refreshControllerNotification,
unreadCommentsIds: unreadCommentsIds:
@ -226,7 +226,7 @@ class _ProfileScreenState extends State<ProfileScreen>
Positioned.fill( Positioned.fill(
top: 50, top: 50,
child: Offstage( child: Offstage(
offstage: pageType != PageType.settings, offstage: pageType != _PageType.settings,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
@ -306,18 +306,6 @@ class _ProfileScreenState extends State<ProfileScreen>
}, },
activeColor: Colors.orange, 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( SwitchListTile(
title: const Text('Eye Candy'), title: const Text('Eye Candy'),
subtitle: const Text('some sort of magic.'), subtitle: const Text('some sort of magic.'),
@ -377,7 +365,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationName: 'Hacki', applicationName: 'Hacki',
applicationVersion: 'v0.1.6', applicationVersion: 'v0.1.7',
applicationIcon: Image.asset( applicationIcon: Image.asset(
Constants.hackiIconPath, Constants.hackiIconPath,
height: 50, height: 50,
@ -467,11 +455,11 @@ class _ProfileScreenState extends State<ProfileScreen>
label: 'Inbox : ' label: 'Inbox : '
//ignore: lines_longer_than_80_chars //ignore: lines_longer_than_80_chars
'${notificationState.unreadCommentsIds.length}', '${notificationState.unreadCommentsIds.length}',
selected: pageType == PageType.notification, selected: pageType == _PageType.notification,
onSelected: (val) { onSelected: (val) {
if (val) { if (val) {
setState(() { setState(() {
pageType = PageType.notification; pageType = _PageType.notification;
}); });
} }
}, },
@ -481,11 +469,11 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
CustomChip( CustomChip(
label: 'Favorite', label: 'Favorite',
selected: pageType == PageType.fav, selected: pageType == _PageType.fav,
onSelected: (val) { onSelected: (val) {
if (val) { if (val) {
setState(() { setState(() {
pageType = PageType.fav; pageType = _PageType.fav;
}); });
} }
}, },
@ -495,11 +483,11 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
CustomChip( CustomChip(
label: 'Submitted', label: 'Submitted',
selected: pageType == PageType.history, selected: pageType == _PageType.history,
onSelected: (val) { onSelected: (val) {
if (val) { if (val) {
setState(() { setState(() {
pageType = PageType.history; pageType = _PageType.history;
}); });
} }
}, },
@ -509,11 +497,11 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
CustomChip( CustomChip(
label: 'Search', label: 'Search',
selected: pageType == PageType.search, selected: pageType == _PageType.search,
onSelected: (val) { onSelected: (val) {
if (val) { if (val) {
setState(() { setState(() {
pageType = PageType.search; pageType = _PageType.search;
}); });
} }
}, },
@ -523,11 +511,11 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
CustomChip( CustomChip(
label: 'Settings', label: 'Settings',
selected: pageType == PageType.settings, selected: pageType == _PageType.settings,
onSelected: (val) { onSelected: (val) {
if (val) { if (val) {
setState(() { 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:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -105,6 +107,7 @@ class _StoryScreenState extends State<StoryScreen> {
FeatureDiscovery.discoverFeatures( FeatureDiscovery.discoverFeatures(
context, context,
const <String>{ const <String>{
Constants.featurePinToTop,
Constants.featureAddStoryToFavList, Constants.featureAddStoryToFavList,
Constants.featureOpenStoryInWebView, Constants.featureOpenStoryInWebView,
}, },
@ -205,12 +208,69 @@ class _StoryScreenState extends State<StoryScreen> {
ScrollUpIconButton( ScrollUpIconButton(
scrollController: scrollController, 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>( BlocBuilder<FavCubit, FavState>(
builder: (context, favState) { builder: (context, favState) {
final isFav = final isFav =
favState.favIds.contains(widget.story.id); favState.favIds.contains(widget.story.id);
return IconButton( return IconButton(
icon: DescribedFeatureOverlay( icon: DescribedFeatureOverlay(
barrierDismissible: false,
overflowMode: OverflowMode.extendBackground,
targetColor: Theme.of(context).primaryColor, targetColor: Theme.of(context).primaryColor,
tapTarget: Icon( tapTarget: Icon(
isFav isFav
@ -221,7 +281,7 @@ class _StoryScreenState extends State<StoryScreen> {
featureId: Constants.featureAddStoryToFavList, featureId: Constants.featureAddStoryToFavList,
title: const Text('Fav a Story'), title: const Text('Fav a Story'),
description: const Text( description: const Text(
'Save this article for later.', 'Add it to your favorites.',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
), ),
child: Icon( child: Icon(
@ -250,6 +310,8 @@ class _StoryScreenState extends State<StoryScreen> {
), ),
IconButton( IconButton(
icon: DescribedFeatureOverlay( icon: DescribedFeatureOverlay(
barrierDismissible: false,
overflowMode: OverflowMode.extendBackground,
targetColor: Theme.of(context).primaryColor, targetColor: Theme.of(context).primaryColor,
tapTarget: const Icon( tapTarget: const Icon(
Icons.stream, Icons.stream,
@ -327,7 +389,7 @@ class _StoryScreenState extends State<StoryScreen> {
focusNode.requestFocus(); focusNode.requestFocus();
}); });
}, },
onLongPress: () => onLongPressed(widget.story), onLongPress: () => onMorePressed(widget.story),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -407,7 +469,8 @@ class _StoryScreenState extends State<StoryScreen> {
myUsername: authState.isLoggedIn myUsername: authState.isLoggedIn
? authState.username ? authState.username
: null, : null,
onTap: (cmt) { onReplyTapped: (cmt) {
HapticFeedback.lightImpact();
if (cmt.deleted || cmt.dead) { if (cmt.deleted || cmt.dead) {
return; return;
} }
@ -419,7 +482,7 @@ class _StoryScreenState extends State<StoryScreen> {
editCubit.onItemTapped(cmt); editCubit.onItemTapped(cmt);
focusNode.requestFocus(); focusNode.requestFocus();
}, },
onLongPress: onLongPressed, onMoreTapped: onMorePressed,
onStoryLinkTapped: (link) { onStoryLinkTapped: (link) {
final regex = RegExp(r'\d+$'); final regex = RegExp(r'\d+$');
final match = regex.stringMatch(link) ?? ''; 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) { if (item.dead || item.deleted) {
return; return;
} }

View File

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

View File

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

View File

@ -6,10 +6,21 @@ class LinkUtil {
static final _browser = ChromeSafariBrowser(); static final _browser = ChromeSafariBrowser();
static void launchUrl(String link, {bool useReader = false}) { 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) { canLaunch(link).then((val) {
if (val) { if (val) {
final rinsedLink = rinseLink(link);
_browser.open( _browser.open(
url: Uri.parse(link), url: Uri.parse(rinsedLink),
options: ChromeSafariBrowserClassOptions( options: ChromeSafariBrowserClassOptions(
ios: IOSSafariOptions( ios: IOSSafariOptions(
entersReaderIfAvailable: useReader, entersReaderIfAvailable: useReader,

View File

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