Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/cubits/cubits.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export 'post/post_cubit.dart';
export 'preference/preference_cubit.dart';
export 'reminder/reminder_cubit.dart';
export 'remote_config/remote_config_cubit.dart';
export 'review_request/review_request_cubit.dart';
export 'search/search_cubit.dart';
export 'split_view/split_view_cubit.dart';
export 'submit/submit_cubit.dart';
Expand Down
42 changes: 42 additions & 0 deletions lib/cubits/review_request/review_request_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:equatable/equatable.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

part 'review_request_state.dart';

class ReviewRequestCubit extends HydratedCubit<ReviewRequestState> {
ReviewRequestCubit() : super(ReviewRequestState.init()) {
_incrementLaunchCounter();
}

void _incrementLaunchCounter() =>
emit(state.copyWith(launchCounter: state.launchCounter + 1));

static const int _askWhen = 100;
static const String _launchCounterKey = 'launchCounterKey';
static const String _firstRecordedLaunchDateKey =
'firstRecordedLaunchDateKey';

bool get feelingLucky => _askWhen == state.launchCounter;

void markAsShown() => emit(state.copyWith(hasShown: true));

@override
ReviewRequestState? fromJson(Map<String, dynamic> json) {
return state.copyWith(
launchCounter: json[_launchCounterKey] as int? ?? 0,
firstRecordedLaunchDate: DateTime.fromMillisecondsSinceEpoch(
json[_firstRecordedLaunchDateKey] as int? ??
DateTime.now().millisecondsSinceEpoch,
),
);
}

@override
Map<String, dynamic>? toJson(ReviewRequestState state) {
return <String, dynamic>{
_launchCounterKey: state.launchCounter,
_firstRecordedLaunchDateKey:
state.firstRecordedLaunchDate.millisecondsSinceEpoch,
};
}
}
38 changes: 38 additions & 0 deletions lib/cubits/review_request/review_request_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
part of 'review_request_cubit.dart';

class ReviewRequestState extends Equatable {
const ReviewRequestState({
required this.launchCounter,
required this.firstRecordedLaunchDate,
required this.hasShown,
});

ReviewRequestState.init()
: launchCounter = 0,
firstRecordedLaunchDate = .now(),
hasShown = false;

final int launchCounter;
final DateTime firstRecordedLaunchDate;
final bool hasShown;

ReviewRequestState copyWith({
int? launchCounter,
DateTime? firstRecordedLaunchDate,
bool? hasShown,
}) {
return ReviewRequestState(
launchCounter: launchCounter ?? this.launchCounter,
firstRecordedLaunchDate:
firstRecordedLaunchDate ?? this.firstRecordedLaunchDate,
hasShown: hasShown ?? this.hasShown,
);
}

@override
List<Object?> get props => <Object?>[
launchCounter,
firstRecordedLaunchDate,
hasShown,
];
}
4 changes: 4 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ class HackiApp extends StatelessWidget {
preferenceCubit: context.read<PreferenceCubit>(),
),
),
BlocProvider<ReviewRequestCubit>(
lazy: false,
create: (BuildContext context) => ReviewRequestCubit(),
),
],
child: BlocConsumer<PreferenceCubit, PreferenceState>(
listenWhen: (PreferenceState previous, PreferenceState current) =>
Expand Down
16 changes: 16 additions & 0 deletions lib/screens/home/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ class _HomeScreenState extends State<HomeScreen>
});

tabController = TabController(length: tabLength, vsync: this);

WidgetsBinding.instance.addPostFrameCallback((_) {
final ReviewRequestCubit reviewRequestCubit = context
.read<ReviewRequestCubit>();
if (!reviewRequestCubit.state.hasShown &&
reviewRequestCubit.feelingLucky) {
reviewRequestCubit.markAsShown();
showModalBottomSheet<bool>(
context: context,
isDismissible: false,
builder: (BuildContext context) {
return const ReviewRequestBottomSheet();
},
);
}
});
}

@override
Expand Down
220 changes: 220 additions & 0 deletions lib/screens/home/widgets/review_request_bottom_sheet.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import 'dart:io';

import 'package:go_router/go_router.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
import 'package:material_ui/material_ui.dart';

class ReviewRequestBottomSheet extends StatefulWidget {
const ReviewRequestBottomSheet({super.key});

@override
State<ReviewRequestBottomSheet> createState() =>
_ReviewRequestBottomSheetState();
}

enum Stage { stars, reviewRequest, issueSubmission }

class _ReviewRequestBottomSheetState extends State<ReviewRequestBottomSheet> {
final PageController pageController = PageController();
Stage currentStage = .stars;
int stars = 1;
static const int maxStars = 5;

@override
Widget build(BuildContext context) {
final Color highlightColor = Theme.of(context).colorScheme.primary;
return Container(
height: 360,
color: Theme.of(context).scaffoldBackgroundColor,
child: PageView(
controller: pageController,
physics: const NeverScrollableScrollPhysics(),
children: <Widget>[
Column(
children: <Widget>[
SizedBoxes.pt24,
const Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.pt16,
horizontal: Dimens.pt12,
),
child: Text(
'How would you rate Hacki so far?',
style: TextStyle(fontSize: TextDimens.pt20),
textAlign: .center,
),
),
SizedBoxes.pt24,
Row(
mainAxisAlignment: .center,
children: <Widget>[
for (final int i in List<int>.generate(
maxStars,
(int i) => i + 1,
)) ...<Widget>[
InkWell(
onTap: () {
HapticFeedbackUtils.selection();
setState(() {
stars = i;
});
},
customBorder: StarBorder(
side: BorderSide(
color: highlightColor,
width: Dimens.pt2,
),
),
child: Container(
height: Dimens.pt50,
width: Dimens.pt50,
decoration: ShapeDecoration(
color: i <= stars
? highlightColor
: Palette.transparent,
shape: StarBorder(
side: i <= stars
? .none
: BorderSide(
color: highlightColor,
width: Dimens.pt2,
),
),
),
),
),
SizedBoxes.pt12,
],
],
),
const Spacer(),
ElevatedButton.icon(
onPressed: () {
HapticFeedbackUtils.selection();
if (stars == maxStars) {
pageController.animateToPage(
Stage.reviewRequest.index,
duration: AppDurations.ms300,
curve: Curves.bounceInOut,
);
} else {
pageController.animateToPage(
Stage.issueSubmission.index,
duration: AppDurations.ms300,
curve: Curves.bounceInOut,
);
}
},
label: const Icon(Icons.check, size: TextDimens.pt64),
),
SizedBoxes.pt36,
],
),
Column(
children: <Widget>[
SizedBoxes.pt24,
const Padding(
padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
child: Text(
'Would you like to leave a review?',
style: TextStyle(fontSize: TextDimens.pt20),
textAlign: .center,
),
),
SizedBoxes.pt12,
const Padding(
padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
child: Text(
'''
(๑•̀ㅂ•́)و✧

Hacki is completely free and open source. Your feedback keeps me motivated to make it even better. : )''',
style: TextStyle(fontSize: TextDimens.pt16),
textAlign: .center,
),
),
const Spacer(),
Row(
mainAxisAlignment: .center,
children: <Widget>[
ElevatedButton.icon(
onPressed: () {
HapticFeedbackUtils.selection();
context.pop();
},
label: const Icon(Icons.close, size: TextDimens.pt64),
),
SizedBoxes.pt48,
ElevatedButton.icon(
onPressed: () {
HapticFeedbackUtils.selection();
LinkUtils.launch(
Platform.isIOS
? Constants.appStoreLink
: Constants.googlePlayLink,
context,
);
context.pop();
},
label: const Icon(Icons.check, size: TextDimens.pt64),
),
],
),
SizedBoxes.pt48,
],
),
Column(
children: <Widget>[
SizedBoxes.pt24,
const Padding(
padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
child: Text(
'Would you like to share some feedback?',
style: TextStyle(fontSize: TextDimens.pt20),
textAlign: .center,
),
),
SizedBoxes.pt12,
const Padding(
padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
child: Text(
'''
Hacki is completely free and open source. Your feedback keeps me motivated to make it even better.

Tapping YES will take you to Hacki's GitHub page.''',
style: TextStyle(fontSize: TextDimens.pt16),
textAlign: .center,
),
),
const Spacer(),
Row(
mainAxisAlignment: .center,
children: <Widget>[
ElevatedButton.icon(
onPressed: () {
HapticFeedbackUtils.selection();
context.pop();
},
label: const Icon(Icons.close, size: TextDimens.pt64),
),
SizedBoxes.pt48,
ElevatedButton.icon(
onPressed: () {
HapticFeedbackUtils.selection();
LinkUtils.launch(Constants.githubLink, context);
context.pop();
},
label: const Icon(Icons.check, size: TextDimens.pt64),
),
],
),
SizedBoxes.pt48,
],
),
],
),
);
}
}
1 change: 1 addition & 0 deletions lib/screens/home/widgets/widgets.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'mobile_home_screen.dart';
export 'pinned_stories.dart';
export 'review_request_bottom_sheet.dart';
export 'tablet_home_screen.dart';
2 changes: 2 additions & 0 deletions lib/styles/dimens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ abstract class TextDimens {
static const double pt24 = 24;
static const double pt26 = 26;
static const double pt36 = 36;
static const double pt48 = 48;
static const double pt64 = 64;
}

abstract class NumSwitch {
Expand Down