Compare commits

...

9 Commits

Author SHA1 Message Date
c2b66d29c3 add sharing option. (#131) 2023-02-04 18:46:04 -08:00
e0a53e44b2 bump flutter to 3.7.1 (#129) 2023-02-01 15:19:06 -08:00
4cf8379db0 fix Story model. (#128) 2023-01-31 22:02:17 -08:00
c1c26bf0e0 fix preference model. (#127) 2023-01-31 18:19:34 -08:00
29e2f4163d fix offline mode. (#126) 2023-01-31 16:54:28 -08:00
c3de80015d fix PinnedStories (#125) 2023-01-31 16:36:58 -08:00
436cd9ce8b fix Item model. (#123) 2023-01-31 15:56:29 -08:00
efb326be68 refactor models. (#122) 2023-01-30 23:43:12 -08:00
047903fe24 refactor. (#121) 2023-01-30 22:46:29 -08:00
29 changed files with 347 additions and 348 deletions

View File

@ -12,12 +12,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
env: env:
FLUTTER_VERSION: "3.7.0" FLUTTER_VERSION: "3.7.1"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: '3.7.0' flutter-version: '3.7.1'
channel: 'stable' channel: 'stable'
- run: flutter pub get - run: flutter pub get
- run: flutter format --set-exit-if-changed . - run: flutter format --set-exit-if-changed .

View File

@ -31,7 +31,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
cache: true cache: true
flutter-version: 3.7.0 flutter-version: 3.7.1
- run: flutter pub get - run: flutter pub get
- run: flutter format --set-exit-if-changed . - run: flutter format --set-exit-if-changed .
- run: flutter analyze - run: flutter analyze

View File

@ -0,0 +1,3 @@
- Customization of tab bar.
- Option to enable swipe gesture for switching between tabs.
- Access to action menu from home screen.

View File

@ -83,7 +83,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final int pageSize = _getPageSize(isComplexTile: isComplexTile); final int pageSize = _getPageSize(isComplexTile: isComplexTile);
emit( emit(
const StoriesState.init().copyWith( const StoriesState.init().copyWith(
offlineReading: hasCachedStories, offlineReading: hasCachedStories &&
// Only go into offline mode in the next session.
state.downloadStatus == StoriesDownloadStatus.initial,
currentPageSize: pageSize, currentPageSize: pageSize,
downloadStatus: state.downloadStatus, downloadStatus: state.downloadStatus,
storiesDownloaded: state.storiesDownloaded, storiesDownloaded: state.storiesDownloaded,

View File

@ -1,3 +1,5 @@
import 'package:hacki/extensions/extensions.dart';
abstract class Constants { abstract class Constants {
static const String endUserAgreementLink = static const String endUserAgreementLink =
'https://www.termsfeed.com/live/c1417f5c-a48b-4bd7-93b2-9cd4577bfc45'; 'https://www.termsfeed.com/live/c1417f5c-a48b-4bd7-93b2-9cd4577bfc45';
@ -34,16 +36,16 @@ abstract class Constants {
static const String featureLogIn = 'log_in'; static const String featureLogIn = 'log_in';
static const String featurePinToTop = 'pin_to_top'; static const String featurePinToTop = 'pin_to_top';
static const List<String> happyFaces = <String>[ static final String happyFace = <String>[
'(๑•̀ㅂ•́)و✧', '(๑•̀ㅂ•́)و✧',
'( ͡• ͜ʖ ͡•)', '( ͡• ͜ʖ ͡•)',
'( ͡~ ͜ʖ ͡°)', '( ͡~ ͜ʖ ͡°)',
'٩(˘◡˘)۶', '٩(˘◡˘)۶',
'(─‿‿─)', '(─‿‿─)',
'(¬‿¬)', '(¬‿¬)',
]; ].pickRandomly()!;
static const List<String> sadFaces = <String>[ static final String sadFace = <String>[
'ಥ_ಥ', 'ಥ_ಥ',
'(╯°□°)╯︵ ┻━┻', '(╯°□°)╯︵ ┻━┻',
r'¯\_(ツ)_/¯', r'¯\_(ツ)_/¯',
@ -53,7 +55,7 @@ abstract class Constants {
'(ㆆ_ㆆ)', '(ㆆ_ㆆ)',
'ʕ•́ᴥ•̀ʔっ', 'ʕ•́ᴥ•̀ʔっ',
'(ㆆ_ㆆ)', '(ㆆ_ㆆ)',
]; ].pickRandomly()!;
} }
abstract class RegExpConstants { abstract class RegExpConstants {

View File

@ -56,13 +56,6 @@ class PreferenceCubit extends Cubit<PreferenceState> {
} }
} }
void toggle(BooleanPreference preference) {
final BooleanPreference updatedPreference =
preference.copyWith(val: !preference.val) as BooleanPreference;
emit(state.copyWithPreference(updatedPreference));
_preferenceRepository.setBool(preference.key, !preference.val);
}
void update<T>(Preference<T> preference, {required T to}) { void update<T>(Preference<T> preference, {required T to}) {
final T value = to; final T value = to;
final Preference<T> updatedPreference = preference.copyWith(val: value); final Preference<T> updatedPreference = preference.copyWith(val: value);

View File

@ -119,11 +119,45 @@ extension StateExtension on State {
} }
} }
void onShareTapped(Item item, Rect? rect) { Future<void> onShareTapped(Item item, Rect? rect) async {
Share.share( late final String? linkToShare;
'https://news.ycombinator.com/item?id=${item.id}', if (item.url.isNotEmpty) {
sharePositionOrigin: rect, linkToShare = await showModalBottomSheet<String>(
); context: context,
builder: (BuildContext context) {
return Container(
height: 140,
color: Theme.of(context).canvasColor,
child: Material(
child: Column(
children: <Widget>[
ListTile(
onTap: () => Navigator.pop(context, item.url),
title: const Text('Link to article'),
),
ListTile(
onTap: () => Navigator.pop(
context,
'https://news.ycombinator.com/item?id=${item.id}',
),
title: const Text('Link to HN'),
),
],
),
),
);
},
);
} else {
linkToShare = 'https://news.ycombinator.com/item?id=${item.id}';
}
if (linkToShare != null) {
await Share.share(
linkToShare,
sharePositionOrigin: rect,
);
}
} }
void onFlagTapped(Item item) { void onFlagTapped(Item item) {

View File

@ -20,23 +20,7 @@ class Comment extends Item {
type: '', type: '',
); );
Comment.fromJson(Map<String, dynamic> json, {this.level = 0}) Comment.fromJson(super.json, {this.level = 0}) : super.fromJson();
: super(
id: json['id'] as int? ?? 0,
time: json['time'] as int? ?? 0,
by: json['by'] as String? ?? '',
text: json['text'] as String? ?? '',
kids: (json['kids'] as List<dynamic>?)?.cast<int>() ?? <int>[],
parent: json['parent'] as int? ?? 0,
deleted: json['deleted'] as bool? ?? false,
score: json['score'] as int? ?? 0,
descendants: 0,
dead: json['dead'] as bool? ?? false,
parts: <int>[],
title: '',
url: '',
type: '',
);
final int level; final int level;

View File

@ -44,11 +44,11 @@ class Item extends Equatable {
title = json['title'] as String? ?? '', title = json['title'] as String? ?? '',
text = json['text'] as String? ?? '', text = json['text'] as String? ?? '',
url = json['url'] as String? ?? '', url = json['url'] as String? ?? '',
kids = <int>[], kids = (json['kids'] as List<dynamic>?)?.cast<int>() ?? <int>[],
dead = json['dead'] as bool? ?? false, dead = json['dead'] as bool? ?? false,
deleted = json['deleted'] as bool? ?? false, deleted = json['deleted'] as bool? ?? false,
parent = json['parent'] as int? ?? 0, parent = json['parent'] as int? ?? 0,
parts = <int>[], parts = (json['parts'] as List<dynamic>?)?.cast<int>() ?? <int>[],
type = json['type'] as String? ?? ''; type = json['type'] as String? ?? '';
final int id; final int id;

View File

@ -9,4 +9,5 @@ export 'post_data.dart';
export 'preference.dart'; export 'preference.dart';
export 'search_params.dart'; export 'search_params.dart';
export 'story.dart'; export 'story.dart';
export 'story_type.dart';
export 'user.dart'; export 'user.dart';

View File

@ -24,41 +24,11 @@ class PollOption extends Item {
PollOption.empty() PollOption.empty()
: ratio = 0, : ratio = 0,
super( super.empty();
id: 0,
score: 0,
descendants: 0,
time: 0,
by: '',
title: '',
url: '',
kids: <int>[],
dead: false,
parts: <int>[],
deleted: false,
parent: 0,
text: '',
type: '',
);
PollOption.fromJson(Map<String, dynamic> json) PollOption.fromJson(super.json)
: ratio = 0, : ratio = 0,
super( super.fromJson();
descendants: 0,
id: json['id'] as int? ?? 0,
score: json['score'] as int? ?? 0,
time: json['time'] as int? ?? 0,
by: json['by'] as String? ?? '',
title: json['title'] as String? ?? '',
url: json['url'] as String? ?? '',
kids: <int>[],
text: json['text'] as String? ?? '',
dead: json['dead'] as bool? ?? false,
deleted: json['deleted'] as bool? ?? false,
type: json['type'] as String? ?? '',
parts: <int>[],
parent: 0,
);
final double ratio; final double ratio;

View File

@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:io'; import 'dart:io';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
@ -13,26 +14,29 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
Preference<T> copyWith({required T? val}); Preference<T> copyWith({required T? val});
static List<Preference<dynamic>> allPreferences = <Preference<dynamic>>[ static final List<Preference<dynamic>> allPreferences =
// Order of these first three preferences does not matter. UnmodifiableListView<Preference<dynamic>>(
FetchModePreference(), <Preference<dynamic>>[
CommentsOrderPreference(), // Order of these first four preferences does not matter.
FontSizePreference(), FetchModePreference(),
TabOrderPreference(), CommentsOrderPreference(),
// Order of items below matters and FontSizePreference(),
// reflects the order on settings screen. TabOrderPreference(),
const DisplayModePreference(), // Order of items below matters and
const MetadataModePreference(), // reflects the order on settings screen.
const StoryUrlModePreference(), const DisplayModePreference(),
const NotificationModePreference(), const MetadataModePreference(),
const SwipeGesturePreference(), const StoryUrlModePreference(),
const CollapseModePreference(), const NotificationModePreference(),
NavigationModePreference(), const SwipeGesturePreference(),
const ReaderModePreference(), const CollapseModePreference(),
const MarkReadStoriesModePreference(), NavigationModePreference(),
const EyeCandyModePreference(), const ReaderModePreference(),
const TrueDarkModePreference(), const MarkReadStoriesModePreference(),
]; const EyeCandyModePreference(),
const TrueDarkModePreference(),
],
);
@override @override
List<Object?> get props => <Object?>[key]; List<Object?> get props => <Object?>[key];
@ -81,7 +85,7 @@ class SwipeGesturePreference extends BooleanPreference {
@override @override
String get subtitle => String get subtitle =>
'''Enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu.'''; '''enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu.''';
} }
class NotificationModePreference extends BooleanPreference { class NotificationModePreference extends BooleanPreference {
@ -118,6 +122,10 @@ class CollapseModePreference extends BooleanPreference {
@override @override
String get title => 'Tap Anywhere to Collapse'; String get title => 'Tap Anywhere to Collapse';
@override
String get subtitle =>
'''if disabled, tap on the top of comment tile to collapse.''';
} }
/// The value deciding whether or not the story /// The value deciding whether or not the story

View File

@ -1,41 +1,6 @@
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/models/item.dart'; import 'package:hacki/models/item.dart';
enum StoryType {
top('topstories'),
best('beststories'),
latest('newstories'),
ask('askstories'),
show('showstories');
const StoryType(this.path);
final String path;
String get label {
switch (this) {
case StoryType.top:
return 'TOP';
case StoryType.best:
return 'BEST';
case StoryType.latest:
return 'NEW';
case StoryType.ask:
return 'ASK';
case StoryType.show:
return 'SHOW';
}
}
static int convertToSettingsValue(List<StoryType> tabs) {
return int.parse(
tabs
.map((StoryType e) => e.index.toString())
.reduce((String value, String element) => '$value$element'),
);
}
}
class Story extends Item { class Story extends Item {
const Story({ const Story({
required super.descendants, required super.descendants,
@ -55,23 +20,7 @@ class Story extends Item {
parent: 0, parent: 0,
); );
Story.empty() Story.empty() : super.empty();
: super(
id: 0,
score: 0,
descendants: 0,
time: 0,
by: '',
title: '',
url: '',
kids: <int>[],
dead: false,
parts: <int>[],
deleted: false,
parent: 0,
text: '',
type: '',
);
Story.placeholder() Story.placeholder()
: super( : super(
@ -91,23 +40,7 @@ class Story extends Item {
type: '', type: '',
); );
Story.fromJson(Map<String, dynamic> json) Story.fromJson(super.json) : super.fromJson();
: super(
descendants: json['descendants'] as int? ?? 0,
id: json['id'] as int? ?? 0,
score: json['score'] as int? ?? 0,
time: json['time'] as int? ?? 0,
by: json['by'] as String? ?? '',
title: json['title'] as String? ?? '',
url: json['url'] as String? ?? '',
kids: (json['kids'] as List<dynamic>?)?.cast<int>() ?? <int>[],
text: json['text'] as String? ?? '',
dead: json['dead'] as bool? ?? false,
deleted: json['deleted'] as bool? ?? false,
type: json['type'] as String? ?? '',
parts: (json['parts'] as List<dynamic>?)?.cast<int>() ?? <int>[],
parent: 0,
);
String get metadata => String get metadata =>
'''$score point${score > 1 ? 's' : ''} by $by $postedDate | $descendants comment${descendants > 1 ? 's' : ''}'''; '''$score point${score > 1 ? 's' : ''} by $by $postedDate | $descendants comment${descendants > 1 ? 's' : ''}''';

View File

@ -0,0 +1,34 @@
enum StoryType {
top('topstories'),
best('beststories'),
latest('newstories'),
ask('askstories'),
show('showstories');
const StoryType(this.path);
final String path;
String get label {
switch (this) {
case StoryType.top:
return 'TOP';
case StoryType.best:
return 'BEST';
case StoryType.latest:
return 'NEW';
case StoryType.ask:
return 'ASK';
case StoryType.show:
return 'SHOW';
}
}
static int convertToSettingsValue(List<StoryType> tabs) {
return int.parse(
tabs
.map((StoryType e) => e.index.toString())
.reduce((String value, String element) => '$value$element'),
);
}
}

View File

@ -5,11 +5,8 @@ import 'dart:io';
import 'package:feature_discovery/feature_discovery.dart'; import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart' hide Badge; import 'package:flutter/material.dart' hide Badge;
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.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_siri_suggestions/flutter_siri_suggestions.dart'; import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
@ -18,6 +15,7 @@ import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart'; import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/home/widgets/widgets.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
@ -145,57 +143,6 @@ class _HomeScreenState extends State<HomeScreen>
previous.metadataEnabled != current.metadataEnabled || previous.metadataEnabled != current.metadataEnabled ||
previous.swipeGestureEnabled != current.swipeGestureEnabled, previous.swipeGestureEnabled != current.swipeGestureEnabled,
builder: (BuildContext context, PreferenceState preferenceState) { builder: (BuildContext context, PreferenceState preferenceState) {
final BlocBuilder<PinCubit, PinState> pinnedStories =
BlocBuilder<PinCubit, PinState>(
builder: (BuildContext context, PinState state) {
return Column(
children: <Widget>[
for (final Story story in state.pinnedStories)
FadeIn(
child: Slidable(
startActionPane: ActionPane(
motion: const BehindMotion(),
children: <Widget>[
SlidableAction(
onPressed: (_) {
HapticFeedback.lightImpact();
context.read<PinCubit>().unpinStory(story);
},
backgroundColor: Palette.red,
foregroundColor: Palette.white,
icon: preferenceState.complexStoryTileEnabled
? Icons.close
: null,
label: 'Unpin',
),
],
),
child: ColoredBox(
color: Palette.orangeAccent.withOpacity(0.2),
child: StoryTile(
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
story: story,
onTap: () => onStoryTapped(story, isPin: true),
showWebPreview:
preferenceState.complexStoryTileEnabled,
showMetadata: preferenceState.metadataEnabled,
showUrl: preferenceState.urlEnabled,
),
),
),
),
if (state.pinnedStories.isNotEmpty)
const Padding(
padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
child: Divider(
color: Palette.orangeAccent,
),
),
],
);
},
);
return DefaultTabController( return DefaultTabController(
length: tabLength, length: tabLength,
child: Scaffold( child: Scaffold(
@ -235,7 +182,10 @@ class _HomeScreenState extends State<HomeScreen>
StoriesListView( StoriesListView(
key: ValueKey<StoryType>(type), key: ValueKey<StoryType>(type),
storyType: type, storyType: type,
header: pinnedStories, header: PinnedStories(
preferenceState: preferenceState,
onStoryTapped: onStoryTapped,
),
onStoryTapped: onStoryTapped, onStoryTapped: onStoryTapped,
), ),
const ProfileScreen(), const ProfileScreen(),
@ -251,11 +201,11 @@ class _HomeScreenState extends State<HomeScreen>
return ScreenTypeLayout.builder( return ScreenTypeLayout.builder(
mobile: (BuildContext context) { mobile: (BuildContext context) {
context.read<SplitViewCubit>().disableSplitView(); context.read<SplitViewCubit>().disableSplitView();
return _MobileHomeScreen( return MobileHomeScreen(
homeScreen: homeScreen, homeScreen: homeScreen,
); );
}, },
tablet: (BuildContext context) => _TabletHomeScreen( tablet: (BuildContext context) => TabletHomeScreen(
homeScreen: homeScreen, homeScreen: homeScreen,
), ),
); );
@ -385,112 +335,3 @@ class _HomeScreenState extends State<HomeScreen>
} }
} }
} }
class _MobileHomeScreen extends StatelessWidget {
const _MobileHomeScreen({
required this.homeScreen,
});
final Widget homeScreen;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned.fill(child: homeScreen),
if (!context.read<ReminderCubit>().state.hasShown)
const Positioned(
left: Dimens.pt24,
right: Dimens.pt24,
bottom: Dimens.pt36,
height: Dimens.pt40,
child: CountdownReminder(),
),
],
);
}
}
class _TabletHomeScreen extends StatelessWidget {
const _TabletHomeScreen({
required this.homeScreen,
});
final Widget homeScreen;
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (BuildContext context, SizingInformation sizeInfo) {
context.read<SplitViewCubit>().enableSplitView();
double homeScreenWidth = 428;
if (sizeInfo.screenSize.width < homeScreenWidth * 2) {
homeScreenWidth = 345;
}
return BlocBuilder<SplitViewCubit, SplitViewState>(
buildWhen: (SplitViewState previous, SplitViewState current) =>
previous.expanded != current.expanded,
builder: (BuildContext context, SplitViewState state) {
return Stack(
children: <Widget>[
AnimatedPositioned(
left: Dimens.zero,
top: Dimens.zero,
bottom: Dimens.zero,
width: homeScreenWidth,
duration: const Duration(milliseconds: 300),
curve: Curves.elasticOut,
child: homeScreen,
),
Positioned(
left: Dimens.pt24,
bottom: Dimens.pt36,
height: Dimens.pt40,
width: homeScreenWidth - Dimens.pt24,
child: const CountdownReminder(),
),
AnimatedPositioned(
right: Dimens.zero,
top: Dimens.zero,
bottom: Dimens.zero,
left: state.expanded ? Dimens.zero : homeScreenWidth,
duration: const Duration(milliseconds: 300),
curve: Curves.elasticOut,
child: const _TabletStoryView(),
),
],
);
},
);
},
);
}
}
class _TabletStoryView extends StatelessWidget {
const _TabletStoryView();
@override
Widget build(BuildContext context) {
return BlocBuilder<SplitViewCubit, SplitViewState>(
buildWhen: (SplitViewState previous, SplitViewState current) =>
previous.itemScreenArgs != current.itemScreenArgs,
builder: (BuildContext context, SplitViewState state) {
if (state.itemScreenArgs != null) {
return ItemScreen.build(context, state.itemScreenArgs!);
}
return Material(
child: ColoredBox(
color: Theme.of(context).canvasColor,
child: const Center(
child: Text('Tap on story tile to view comments.'),
),
),
);
},
);
}
}

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart' hide Badge;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({
super.key,
required this.homeScreen,
});
final Widget homeScreen;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned.fill(child: homeScreen),
if (!context.read<ReminderCubit>().state.hasShown)
const Positioned(
left: Dimens.pt24,
right: Dimens.pt24,
bottom: Dimens.pt36,
height: Dimens.pt40,
child: CountdownReminder(),
),
],
);
}
}

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart' hide Badge;
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
class PinnedStories extends StatelessWidget {
const PinnedStories({
super.key,
required this.preferenceState,
required this.onStoryTapped,
});
final PreferenceState preferenceState;
final void Function(Story story, {bool isPin}) onStoryTapped;
@override
Widget build(BuildContext context) {
return BlocBuilder<PinCubit, PinState>(
builder: (BuildContext context, PinState state) {
return Column(
children: <Widget>[
for (final Story story in state.pinnedStories)
FadeIn(
child: Slidable(
startActionPane: ActionPane(
motion: const BehindMotion(),
children: <Widget>[
SlidableAction(
onPressed: (_) {
HapticFeedback.lightImpact();
context.read<PinCubit>().unpinStory(story);
},
backgroundColor: Palette.red,
foregroundColor: Palette.white,
icon: preferenceState.complexStoryTileEnabled
? Icons.close
: null,
label: 'Unpin',
),
],
),
child: ColoredBox(
color: Palette.orangeAccent.withOpacity(0.2),
child: StoryTile(
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
story: story,
onTap: () => onStoryTapped(story, isPin: true),
showWebPreview: preferenceState.complexStoryTileEnabled,
showMetadata: preferenceState.metadataEnabled,
showUrl: preferenceState.urlEnabled,
),
),
),
),
if (state.pinnedStories.isNotEmpty)
const Padding(
padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
child: Divider(
color: Palette.orangeAccent,
),
),
],
);
},
);
}
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart' hide Badge;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:responsive_builder/responsive_builder.dart';
class TabletHomeScreen extends StatelessWidget {
const TabletHomeScreen({
super.key,
required this.homeScreen,
});
final Widget homeScreen;
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (BuildContext context, SizingInformation sizeInfo) {
context.read<SplitViewCubit>().enableSplitView();
double homeScreenWidth = 428;
if (sizeInfo.screenSize.width < homeScreenWidth * 2) {
homeScreenWidth = 345;
}
return BlocBuilder<SplitViewCubit, SplitViewState>(
buildWhen: (SplitViewState previous, SplitViewState current) =>
previous.expanded != current.expanded,
builder: (BuildContext context, SplitViewState state) {
return Stack(
children: <Widget>[
AnimatedPositioned(
left: Dimens.zero,
top: Dimens.zero,
bottom: Dimens.zero,
width: homeScreenWidth,
duration: const Duration(milliseconds: 300),
curve: Curves.elasticOut,
child: homeScreen,
),
Positioned(
left: Dimens.pt24,
bottom: Dimens.pt36,
height: Dimens.pt40,
width: homeScreenWidth - Dimens.pt24,
child: const CountdownReminder(),
),
AnimatedPositioned(
right: Dimens.zero,
top: Dimens.zero,
bottom: Dimens.zero,
left: state.expanded ? Dimens.zero : homeScreenWidth,
duration: const Duration(milliseconds: 300),
curve: Curves.elasticOut,
child: const _TabletStoryView(),
),
],
);
},
);
},
);
}
}
class _TabletStoryView extends StatelessWidget {
const _TabletStoryView();
@override
Widget build(BuildContext context) {
return BlocBuilder<SplitViewCubit, SplitViewState>(
buildWhen: (SplitViewState previous, SplitViewState current) =>
previous.itemScreenArgs != current.itemScreenArgs,
builder: (BuildContext context, SplitViewState state) {
if (state.itemScreenArgs != null) {
return ItemScreen.build(context, state.itemScreenArgs!);
}
return Material(
child: ColoredBox(
color: Theme.of(context).canvasColor,
child: const Center(
child: Text('Tap on story tile to view comments.'),
),
),
);
},
);
}
}

View File

@ -0,0 +1,3 @@
export 'mobile_home_screen.dart';
export 'pinned_stories.dart';
export 'tablet_home_screen.dart';

View File

@ -149,7 +149,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
initialRefreshStatus: RefreshStatus.refreshing, initialRefreshStatus: RefreshStatus.refreshing,
); );
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
final String happyFace = Constants.happyFaces.pickRandomly()!;
final Throttle storyLinkTapThrottle = Throttle( final Throttle storyLinkTapThrottle = Throttle(
delay: _storyLinkTapThrottleDelay, delay: _storyLinkTapThrottleDelay,
); );
@ -233,8 +232,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
context.read<EditCubit>().state.replyingTo == null context.read<EditCubit>().state.replyingTo == null
? 'updated' ? 'updated'
: 'submitted'; : 'submitted';
final String msg = final String msg = 'Comment $verb! ${Constants.happyFace}';
'Comment $verb! ${Constants.happyFaces.pickRandomly()}';
focusNode.unfocus(); focusNode.unfocus();
HapticFeedback.lightImpact(); HapticFeedback.lightImpact();
showSnackBar(content: msg); showSnackBar(content: msg);
@ -243,7 +241,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
} else if (postState.status == PostStatus.failure) { } else if (postState.status == PostStatus.failure) {
showSnackBar( showSnackBar(
content: 'Something went wrong...' content: 'Something went wrong...'
'${Constants.sadFaces.pickRandomly()}', '${Constants.sadFace}',
label: 'Okay', label: 'Okay',
action: ScaffoldMessenger.of(context).hideCurrentSnackBar, action: ScaffoldMessenger.of(context).hideCurrentSnackBar,
); );

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
@ -28,9 +27,10 @@ class LoginDialog extends StatelessWidget {
return BlocConsumer<AuthBloc, AuthState>( return BlocConsumer<AuthBloc, AuthState>(
listener: (BuildContext context, AuthState state) { listener: (BuildContext context, AuthState state) {
if (state.isLoggedIn) { if (state.isLoggedIn) {
final String happyFace = Constants.happyFaces.pickRandomly()!;
Navigator.pop(context); Navigator.pop(context);
showSnackBar(content: 'Logged in successfully! $happyFace'); showSnackBar(
content: 'Logged in successfully! ${Constants.happyFace}',
);
} }
}, },
builder: (BuildContext context, AuthState state) { builder: (BuildContext context, AuthState state) {

View File

@ -135,7 +135,7 @@ class MainView extends StatelessWidget {
return SizedBox( return SizedBox(
height: _trailingBoxHeight, height: _trailingBoxHeight,
child: Center( child: Center(
child: Text(Constants.happyFaces.pickRandomly()!), child: Text(Constants.happyFace),
), ),
); );
} else { } else {

View File

@ -131,6 +131,7 @@ class _SettingsState extends State<Settings> {
.toList(), .toList(),
onChanged: (FetchMode? fetchMode) { onChanged: (FetchMode? fetchMode) {
if (fetchMode != null) { if (fetchMode != null) {
HapticFeedback.selectionClick();
context.read<PreferenceCubit>().update( context.read<PreferenceCubit>().update(
FetchModePreference(), FetchModePreference(),
to: fetchMode.index, to: fetchMode.index,
@ -164,6 +165,7 @@ class _SettingsState extends State<Settings> {
.toList(), .toList(),
onChanged: (CommentsOrder? order) { onChanged: (CommentsOrder? order) {
if (order != null) { if (order != null) {
HapticFeedback.selectionClick();
context.read<PreferenceCubit>().update( context.read<PreferenceCubit>().update(
CommentsOrderPreference(), CommentsOrderPreference(),
to: order.index, to: order.index,

View File

@ -1,4 +1,4 @@
export 'home_screen.dart'; export 'home/home_screen.dart';
export 'item/item_screen.dart'; export 'item/item_screen.dart';
export 'profile/profile_screen.dart'; export 'profile/profile_screen.dart';
export 'search/search_screen.dart'; export 'search/search_screen.dart';

View File

@ -4,7 +4,6 @@ import 'dart:math';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/utils/html_util.dart'; import 'package:hacki/utils/html_util.dart';
@ -43,7 +42,6 @@ abstract class Fetcher {
final SembastRepository sembastRepository = SembastRepository(); final SembastRepository sembastRepository = SembastRepository();
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
final String happyFace = Constants.happyFaces.pickRandomly()!;
final String? username = await authRepository.username; final String? username = await authRepository.username;
final List<int> unreadIds = await preferenceRepository.unreadCommentsIds; final List<int> unreadIds = await preferenceRepository.unreadCommentsIds;
@ -123,7 +121,7 @@ abstract class Fetcher {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
newReply?.id ?? 0, newReply?.id ?? 0,
'You have a new reply! $happyFace', 'You have a new reply! ${Constants.happyFace}',
'${newReply?.by}: $text', '${newReply?.by}: $text',
const NotificationDetails( const NotificationDetails(
iOS: DarwinNotificationDetails( iOS: DarwinNotificationDetails(

View File

@ -2,14 +2,12 @@ import 'dart:convert';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
class LocalNotification { class LocalNotification {
Future<void> pushForNewReply(Comment newReply, int storyId) async { Future<void> pushForNewReply(Comment newReply, int storyId) async {
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
final String happyFace = Constants.happyFaces.pickRandomly()!;
final Map<String, int> payloadJson = <String, int>{ final Map<String, int> payloadJson = <String, int>{
'commentId': newReply.id, 'commentId': newReply.id,
@ -19,7 +17,7 @@ class LocalNotification {
return flutterLocalNotificationsPlugin.show( return flutterLocalNotificationsPlugin.show(
newReply.id, newReply.id,
'You have a new reply! $happyFace', 'You have a new reply! ${Constants.happyFace}',
'${newReply.by}: ${newReply.text}', '${newReply.by}: ${newReply.text}',
const NotificationDetails( const NotificationDetails(
iOS: DarwinNotificationDetails( iOS: DarwinNotificationDetails(

View File

@ -1358,5 +1358,5 @@ packages:
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
sdks: sdks:
dart: ">=2.18.0 <4.0.0" dart: ">=2.18.0 <3.0.0"
flutter: ">=3.7.0" flutter: ">=3.7.1"

View File

@ -1,11 +1,11 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 1.0.4+82 version: 1.0.7+85
publish_to: none publish_to: none
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
flutter: "3.7.0" flutter: "3.7.1"
dependencies: dependencies:
adaptive_theme: ^3.0.0 adaptive_theme: ^3.0.0