Compare commits

..

10 Commits

Author SHA1 Message Date
c7824eaef3 bump flutter to 3.7.2 (#134) 2023-02-08 17:43:23 -08:00
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.2"
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.2'
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.2
- 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,12 +119,46 @@ extension StateExtension on State {
} }
} }
void onShareTapped(Item item, Rect? rect) { Future<void> onShareTapped(Item item, Rect? rect) async {
Share.share( late final String? linkToShare;
if (item.url.isNotEmpty) {
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}', '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, sharePositionOrigin: rect,
); );
} }
}
void onFlagTapped(Item item) { void onFlagTapped(Item item) {
showDialog<bool>( showDialog<bool>(

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,8 +14,10 @@ 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>>(
<Preference<dynamic>>[
// Order of these first four preferences does not matter.
FetchModePreference(), FetchModePreference(),
CommentsOrderPreference(), CommentsOrderPreference(),
FontSizePreference(), FontSizePreference(),
@ -32,7 +35,8 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
const MarkReadStoriesModePreference(), const MarkReadStoriesModePreference(),
const EyeCandyModePreference(), const EyeCandyModePreference(),
const TrueDarkModePreference(), 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.8+86
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.2"
dependencies: dependencies:
adaptive_theme: ^3.0.0 adaptive_theme: ^3.0.0