Skip to content

Commit 44ec61b

Browse files
authored
plans: make finished plans look differently (#132)
1 parent e3ce8eb commit 44ec61b

File tree

6 files changed

+161
-100
lines changed

6 files changed

+161
-100
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
run: "xcrun xctrace list devices"
3434
- name: Start simulator
3535
run: |
36-
UDID=$(xcrun xctrace list devices | grep "^iPhone 15 Plus Simulator (18.1)" | awk '{gsub(/[()]/,""); print $NF}')
36+
UDID=$(xcrun xctrace list devices | grep "^iPhone 15 Simulator (18.1)" | awk '{gsub(/[()]/,""); print $NF}')
3737
echo $UDID
3838
xcrun simctl boot "${UDID:?No Simulator with this name found}"
3939
- uses: actions/checkout@v3

integration_test/app_test.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,11 @@ void main() async {
436436
.color,
437437
Colors.red);
438438

439+
await tester.scrollUntilVisible(
440+
find.byKey(const Key('day-82')),
441+
50.0,
442+
);
443+
await tester.pumpAndSettle();
439444
await tester.tap(find
440445
.descendant(
441446
of: find.byKey(const Key('day-83')),
@@ -550,6 +555,45 @@ void main() async {
550555
expect(find.byKey(const Key('target-status')), findsOneWidget);
551556
});
552557

558+
testWidgets('Plans showing if finished', (tester) async {
559+
final providerContainer =
560+
await SettledTester(tester, sharedPreferences: testPlansPreferences)
561+
.providerContainer;
562+
await tester.pumpAndSettle();
563+
564+
expect(find.byIcon(Icons.verified), findsOneWidget);
565+
566+
await tester.tap(find.byType(PlanCard).first);
567+
await tester.pumpAndSettle();
568+
await tester.scrollUntilVisible(
569+
find.byKey(const Key('day-365')),
570+
500.0,
571+
maxScrolls: 200,
572+
);
573+
await tester.pumpAndSettle();
574+
await Future<void>.delayed(Duration(seconds: 2));
575+
await tester.tap(find
576+
.descendant(
577+
of: find.byKey(const Key('day-365')),
578+
matching: find.byType(IconButton))
579+
.last);
580+
await tester.pumpAndSettle();
581+
await tester.tap(find.byKey(const Key('confirm-toggle-read')));
582+
await tester.pumpAndSettle();
583+
584+
expect(providerContainer.read(plansProvider).plans.first.bookmark.dayIndex,
585+
366);
586+
expect(providerContainer.read(plansProvider).plans.first.lastDate,
587+
DateUtils.dateOnly(DateTime.now()));
588+
expect(find.byIcon(Icons.check_circle), findsWidgets);
589+
expect(find.byIcon(Icons.check_circle_outline), findsNothing);
590+
591+
await tester.pageBack();
592+
await tester.pumpAndSettle();
593+
594+
expect(find.byIcon(Icons.verified), findsExactly(2));
595+
});
596+
553597
testWidgets('Bookmark button resets schedule view', (tester) async {
554598
await SettledTester(tester, sharedPreferences: testPlansPreferences)
555599
.providerContainer;

lib/src/plans/entities/plan.dart

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'package:nwt_reading/src/schedules/entities/schedule.dart';
77
final planProviderFamily =
88
AutoDisposeNotifierProviderFamily<PlanNotifier, Plan, String>(
99
PlanNotifier.new,
10-
dependencies: [plansProvider],
1110
name: 'planProviderFamily');
1211

1312
class PlanNotifier extends AutoDisposeFamilyNotifier<Plan, String> {
@@ -19,13 +18,14 @@ class PlanNotifier extends AutoDisposeFamilyNotifier<Plan, String> {
1918
final planId = arg;
2019

2120
ref.watch(plansProvider);
22-
plansNotifier = ref.read(plansProvider.notifier);
21+
plansNotifier = ref.watch(plansProvider.notifier);
2322

24-
return plansNotifier!.getPlan(planId) ?? plansNotifier!.getNewPlan(planId);
25-
}
23+
final plan =
24+
(plansNotifier!.getPlan(planId) ?? plansNotifier!.getNewPlan(planId));
25+
schedule = ref.watch(scheduleProviderFamily(plan.scheduleKey)).valueOrNull;
2626

27-
Schedule? getSchedule() =>
28-
ref.read(scheduleProviderFamily(state.scheduleKey)).valueOrNull;
27+
return plan.copyWith();
28+
}
2929

3030
void delete() {
3131
plansNotifier?.removePlan(state.id);
@@ -48,7 +48,7 @@ class PlanNotifier extends AutoDisposeFamilyNotifier<Plan, String> {
4848

4949
void setRead(
5050
{required int dayIndex, required int sectionIndex, bool force = false}) {
51-
final sections = getSchedule()?.days[dayIndex].sections.length;
51+
final sections = schedule?.days[dayIndex].sections.length;
5252
final newBookmark = sections != null && sectionIndex >= sections - 1
5353
? Bookmark(dayIndex: dayIndex + 1, sectionIndex: -1)
5454
: Bookmark(dayIndex: dayIndex, sectionIndex: sectionIndex);
@@ -59,7 +59,7 @@ class PlanNotifier extends AutoDisposeFamilyNotifier<Plan, String> {
5959

6060
plansNotifier?.updatePlan(state.copyWith(
6161
bookmark: newBookmark,
62-
startDate: _getStartDate(newBookmark),
62+
startDate: getStartDate(newBookmark),
6363
lastDate: DateUtils.dateOnly(DateTime.now()),
6464
targetDate: state.targetDate ?? calcTargetDate(newBookmark),
6565
));
@@ -78,14 +78,17 @@ class PlanNotifier extends AutoDisposeFamilyNotifier<Plan, String> {
7878
}
7979
}
8080

81-
double getProgress() => getSchedule() == null
82-
? 0
83-
: state.bookmark.dayIndex / getSchedule()!.length;
81+
double getProgress() =>
82+
schedule == null ? 0 : state.bookmark.dayIndex / schedule!.length;
8483

8584
int getDeviationDays() {
86-
if (state.withTargetDate && getTargetDate() != null) {
87-
final deviationDays =
88-
getTargetDate()!.difference(calcTargetDate()!).inDays;
85+
final targetDate = getTargetDate();
86+
final calculatedTargetDate = calcTargetDate();
87+
88+
if (state.withTargetDate &&
89+
targetDate != null &&
90+
calculatedTargetDate != null) {
91+
final deviationDays = targetDate.difference(calculatedTargetDate).inDays;
8992

9093
// If ahead, don't count the current day.
9194
return deviationDays <= 0 ? deviationDays : deviationDays - 1;
@@ -94,33 +97,34 @@ class PlanNotifier extends AutoDisposeFamilyNotifier<Plan, String> {
9497
}
9598
}
9699

97-
int getRemainingDays() => _getRemainingDays();
98-
99-
DateTime? getStartDate() => _getStartDate();
100+
bool isFinished() => getRemainingDays() == 0;
100101

101102
DateTime? getTargetDate() => state.targetDate ?? calcTargetDate();
102103

103-
DateTime? calcTargetDate([Bookmark? bookmark]) => state.withTargetDate
104-
? DateUtils.dateOnly(DateTime.now())
105-
.add(Duration(days: _getRemainingDays(bookmark)))
106-
: null;
104+
DateTime? calcTargetDate([Bookmark? bookmark]) {
105+
final remainingDays = getRemainingDays(bookmark);
106+
107+
return state.withTargetDate && remainingDays != null
108+
? DateUtils.dateOnly(DateTime.now()).add(Duration(days: remainingDays))
109+
: null;
110+
}
107111

108112
int? todayTargetIndex() => state.withTargetDate &&
109113
getTargetDate() != null &&
110-
getSchedule() != null
111-
? getSchedule()!.length -
114+
schedule != null
115+
? schedule!.length -
112116
getTargetDate()!.difference(DateUtils.dateOnly(DateTime.now())).inDays
113117
: null;
114118

115-
DateTime? _getStartDate([Bookmark? bookmark]) => state.withTargetDate
119+
DateTime? getStartDate([Bookmark? bookmark]) => state.withTargetDate
116120
? state.startDate ??
117121
DateUtils.dateOnly(DateTime.now())
118122
.add(Duration(days: -(bookmark ?? state.bookmark).dayIndex))
119123
: null;
120124

121-
int _getRemainingDays([Bookmark? bookmark]) => getSchedule() == null
122-
? 0
123-
: getSchedule()!.length - (bookmark ?? state.bookmark).dayIndex;
125+
int? getRemainingDays([Bookmark? bookmark]) => schedule == null
126+
? null
127+
: schedule!.length - (bookmark ?? state.bookmark).dayIndex;
124128
}
125129

126130
class TogglingTooManyDaysException implements Exception {}

lib/src/plans/presentations/plan_card.dart

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,63 +16,81 @@ class PlanCard extends ConsumerWidget {
1616
final planNotifier = ref.read(planProviderFamily(planId).notifier);
1717
final deviationDays = planNotifier.getDeviationDays();
1818
final remainingDays = planNotifier.getRemainingDays();
19+
final isFinished = planNotifier.isFinished();
1920
final progress = planNotifier.getProgress();
2021
const planTypeIcons = {
2122
ScheduleType.chronological: Icons.hourglass_empty,
2223
ScheduleType.sequential: Icons.menu_book,
2324
ScheduleType.written: Icons.edit_note,
2425
};
2526

27+
buildNameTitle() => Row(children: [
28+
Icon(
29+
planTypeIcons[plan.scheduleKey.type],
30+
color: Theme.of(context).colorScheme.surface,
31+
size: 56,
32+
shadows: [
33+
Shadow(
34+
offset: Offset(-1, -1),
35+
color: Theme.of(context).colorScheme.primary,
36+
blurRadius: 2),
37+
Shadow(
38+
offset: Offset(1, 1),
39+
color: Theme.of(context).colorScheme.onPrimary,
40+
blurRadius: 2)
41+
],
42+
),
43+
Text(
44+
plan.name,
45+
style: Theme.of(context)
46+
.textTheme
47+
.headlineMedium
48+
?.copyWith(color: Theme.of(context).colorScheme.secondary),
49+
)
50+
]);
51+
52+
buildYearText() => Text(
53+
plan.lastDate == null
54+
? ''
55+
: MaterialLocalizations.of(context).formatYear(plan.lastDate!),
56+
style: TextStyle(
57+
fontSize: 80, color: Theme.of(context).colorScheme.primary),
58+
);
59+
60+
buildRemainingDaysStatus() => isFinished
61+
? Icon(Icons.verified, color: Colors.green, size: 72)
62+
: Stack(alignment: Alignment.center, children: [
63+
Text(
64+
style:
65+
TextStyle(color: Theme.of(context).colorScheme.secondary),
66+
remainingDays == null
67+
? ''
68+
: AppLocalizations.of(context)
69+
.plansPageCardRemainingDays(remainingDays)),
70+
SizedBox(
71+
width: 60,
72+
height: 60,
73+
child:
74+
CircularProgressIndicator(strokeWidth: 6, value: progress)),
75+
]);
76+
2677
final card = Card(
27-
key: Key('plan-$planId'),
28-
child: Stack(
29-
children: [
30-
Positioned(
31-
left: 10,
32-
top: 10,
33-
child: Row(children: [
34-
Icon(
35-
planTypeIcons[plan.scheduleKey.type],
36-
color: Colors.black54,
37-
size: 56,
38-
shadows: const [
39-
Shadow(
40-
offset: Offset(-1, -1),
41-
color: Colors.black26,
42-
blurRadius: 2),
43-
Shadow(
44-
offset: Offset(1, 1),
45-
color: Colors.white,
46-
blurRadius: 2)
47-
],
48-
),
49-
Text(
50-
plan.name,
51-
style: Theme.of(context).textTheme.headlineMedium,
52-
)
53-
])),
54-
Positioned(
55-
right: 15,
56-
bottom: 15,
57-
child: Stack(alignment: Alignment.center, children: [
58-
Text(AppLocalizations.of(context)
59-
.plansPageCardRemainingDays(remainingDays)),
60-
SizedBox(
61-
width: 60,
62-
height: 60,
63-
child: CircularProgressIndicator(
64-
strokeWidth: 6,
65-
value: progress,
66-
))
67-
])),
68-
],
69-
));
78+
key: Key('plan-$planId'),
79+
child: Stack(
80+
children: [
81+
Positioned(left: 10, top: 10, child: buildNameTitle()),
82+
Positioned(right: 15, bottom: 15, child: buildRemainingDaysStatus()),
83+
Positioned(left: 20, bottom: 0, child: buildYearText()),
84+
],
85+
),
86+
);
87+
7088
return GestureDetector(
7189
onTap: () {
7290
Navigator.restorablePushNamed(context, SchedulePage.routeName,
7391
arguments: planId);
7492
},
75-
child: deviationDays == 0
93+
child: deviationDays == 0 || isFinished
7694
? card
7795
: Badge(
7896
key: Key('badge-$planId'),

lib/src/theme.dart

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
11
import 'package:flutter/material.dart';
22

3+
final seedColor = const Color(0xff007bff);
4+
5+
final ColorScheme lightColorScheme = ColorScheme.fromSeed(seedColor: seedColor);
6+
final ColorScheme darkColorScheme =
7+
ColorScheme.fromSeed(seedColor: seedColor, brightness: Brightness.dark);
8+
final CardTheme cardTheme = const CardTheme(
9+
clipBehavior: Clip.antiAliasWithSaveLayer,
10+
elevation: 10,
11+
shape: RoundedRectangleBorder(
12+
borderRadius: BorderRadius.all(Radius.circular(20.0))),
13+
);
14+
315
final lightTheme = ThemeData(
4-
colorSchemeSeed: const Color(0xff007bff),
5-
brightness: Brightness.light,
6-
appBarTheme: const AppBarTheme(
7-
actionsIconTheme: IconThemeData(color: Color(0xff007bff)),
8-
),
9-
cardTheme: const CardTheme(
10-
clipBehavior: Clip.antiAliasWithSaveLayer,
11-
elevation: 10,
12-
shape: RoundedRectangleBorder(
13-
borderRadius: BorderRadius.all(Radius.circular(20.0))),
14-
),
15-
progressIndicatorTheme: const ProgressIndicatorThemeData(
16-
circularTrackColor: Colors.black12,
16+
colorScheme: lightColorScheme,
17+
cardTheme: cardTheme,
18+
progressIndicatorTheme: ProgressIndicatorThemeData(
19+
circularTrackColor: lightColorScheme.surfaceDim,
1720
),
1821
);
1922

2023
final darkTheme = ThemeData(
21-
colorSchemeSeed: const Color(0xff007bff),
22-
brightness: Brightness.dark,
23-
appBarTheme: const AppBarTheme(
24-
actionsIconTheme: IconThemeData(color: Color(0xff007bff)),
25-
),
26-
cardTheme: const CardTheme(
27-
clipBehavior: Clip.antiAliasWithSaveLayer,
28-
elevation: 10,
29-
shape: RoundedRectangleBorder(
30-
borderRadius: BorderRadius.all(Radius.circular(20.0))),
31-
),
32-
progressIndicatorTheme: const ProgressIndicatorThemeData(
33-
circularTrackColor: Colors.black12,
24+
colorScheme: darkColorScheme,
25+
cardTheme: cardTheme,
26+
progressIndicatorTheme: ProgressIndicatorThemeData(
27+
circularTrackColor: darkColorScheme.surfaceDim,
3428
),
3529
);

test/test_plans.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import 'package:nwt_reading/src/schedules/entities/schedule.dart';
55
const legacyExportPreferenceKey = 'legacyExport';
66
const plansPreferenceKey = 'plans';
77

8-
final Plans testPlans = Plans(const [
8+
final Plans testPlans = Plans([
99
Plan(
1010
id: '5aa4de9e-036b-42cd-8bcb-a92cae46db27',
1111
name: 'Chronological y1',
@@ -39,6 +39,7 @@ final Plans testPlans = Plans(const [
3939
version: '1.0'),
4040
language: 'ro',
4141
bookmark: Bookmark(dayIndex: 364, sectionIndex: 1),
42+
lastDate: DateTime(2024, 11, 21),
4243
withTargetDate: true,
4344
showEvents: true,
4445
showLocations: true),
@@ -59,7 +60,7 @@ final Plans testPlans = Plans(const [
5960
const List<String> testPlansSerialized = [
6061
'{"id":"5aa4de9e-036b-42cd-8bcb-a92cae46db27","name":"Chronological y1","scheduleKey":{"type":0,"duration":2,"version":"1.0"},"language":"en","bookmark":{"dayIndex":75,"sectionIndex":0},"withTargetDate":true,"showEvents":true,"showLocations":true}',
6162
'{"id":"0da6b8a7-ccd4-4270-8058-9e30a3f55ceb","name":"Written","scheduleKey":{"type":2,"duration":2,"version":"1.0"},"language":"de","bookmark":{"dayIndex":0,"sectionIndex":-1},"withTargetDate":true,"showEvents":true,"showLocations":true}',
62-
'{"id":"2dab49f3-aecf-4aba-9e91-d75c297d4b7e","name":"Sequential","scheduleKey":{"type":1,"duration":2,"version":"1.0"},"language":"ro","bookmark":{"dayIndex":364,"sectionIndex":1},"withTargetDate":true,"showEvents":true,"showLocations":true}',
63+
'{"id":"2dab49f3-aecf-4aba-9e91-d75c297d4b7e","name":"Sequential","scheduleKey":{"type":1,"duration":2,"version":"1.0"},"language":"ro","bookmark":{"dayIndex":364,"sectionIndex":1},"lastDate":"2024-11-21T00:00:00.000","withTargetDate":true,"showEvents":true,"showLocations":true}',
6364
'{"id":"e37bf9df-077a-49db-adcb-d56384906103","name":"Chronological","scheduleKey":{"type":0,"duration":1,"version":"1.0"},"language":"en","bookmark":{"dayIndex":182,"sectionIndex":1},"withTargetDate":true,"showEvents":true,"showLocations":true}'
6465
];
6566

0 commit comments

Comments
 (0)