Skip to content

Commit

Permalink
Add skeletons (#1015)
Browse files Browse the repository at this point in the history
  • Loading branch information
LouisPhilippeHeon authored Jul 11, 2024
1 parent 85078ec commit a693804
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 112 deletions.
2 changes: 1 addition & 1 deletion l10n/intl_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@
"percentage": {}
}
},
"progress_bar_message_remaining_days": "{remainingDays} jours restant",
"progress_bar_message_remaining_days": "{remainingDays} jours restants",
"@progress_bar_message_remaining_days": {
"placeholders": {
"remainingDays": {}
Expand Down
1 change: 1 addition & 0 deletions lib/features/app/widgets/base_scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ class _BaseScaffoldState extends State<BaseScaffold> {
Widget bodyPortraitMode() {
return SafeArea(
top: false,
bottom: widget._safeArea,
child: Stack(
children: [
widget.body!,
Expand Down
224 changes: 138 additions & 86 deletions lib/features/dashboard/dashboard_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'package:auto_size_text/auto_size_text.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:notredame/features/app/signets-api/models/course.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:stacked/stacked.dart';

// Project imports:
Expand Down Expand Up @@ -80,7 +82,7 @@ class _DashboardViewState extends State<DashboardView>
child: ReorderableListView(
onReorder: (oldIndex, newIndex) =>
onReorder(model, oldIndex, newIndex),
padding: const EdgeInsets.fromLTRB(0, 4, 0, 8),
padding: const EdgeInsets.fromLTRB(0, 4, 0, 24),
children: _buildCards(model),
proxyDecorator: (child, _, __) {
return HapticsContainer(child: child);
Expand Down Expand Up @@ -213,7 +215,6 @@ class _DashboardViewState extends State<DashboardView>
Widget _buildProgressBarCard(
DashboardViewModel model, PreferencesFlag flag) =>
DismissibleCard(
isBusy: model.busy(model.progress),
key: UniqueKey(),
onDismissed: (DismissDirection direction) {
dismissCard(model, flag);
Expand All @@ -226,47 +227,51 @@ class _DashboardViewState extends State<DashboardView>
child: Text(AppIntl.of(context)!.progress_bar_title,
style: Theme.of(context).textTheme.titleLarge),
)),
if (model.progress >= 0.0)
Stack(children: [
Container(
padding: const EdgeInsets.fromLTRB(17, 10, 15, 20),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(10)),
child: GestureDetector(
onTap: () => setState(
() => setState(() {
model.changeProgressBarText();
setText(model);
}),
),
child: LinearProgressIndicator(
value: model.progress,
minHeight: 30,
valueColor: const AlwaysStoppedAnimation<Color>(
AppTheme.gradeGoodMax),
backgroundColor: AppTheme.etsDarkGrey,
if (model.busy(model.progress) || model.progress >= 0.0)
Skeletonizer(
enabled: model.busy(model.progress),
ignoreContainers: true,
child: Stack(children: [
Container(
padding: const EdgeInsets.fromLTRB(17, 10, 15, 20),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(10)),
child: GestureDetector(
onTap: () => setState(
() => setState(() {
model.changeProgressBarText();
setText(model);
}),
),
child: LinearProgressIndicator(
value: model.progress,
minHeight: 30,
valueColor: const AlwaysStoppedAnimation<Color>(
AppTheme.gradeGoodMax),
backgroundColor: AppTheme.etsDarkGrey,
),
),
),
),
),
GestureDetector(
onTap: () => setState(() {
model.changeProgressBarText();
setText(model);
}),
child: Container(
padding: const EdgeInsets.only(top: 16),
child: Center(
child: progressBarText ??
Text(
AppIntl.of(context)!.progress_bar_message(
model.sessionDays[0], model.sessionDays[1]),
style: const TextStyle(color: Colors.white),
),
GestureDetector(
onTap: () => setState(() {
model.changeProgressBarText();
setText(model);
}),
child: Container(
padding: const EdgeInsets.only(top: 16),
child: Center(
child: progressBarText ??
Text(
AppIntl.of(context)!.progress_bar_message(
model.sessionDays[0], model.sessionDays[1]),
style: const TextStyle(color: Colors.white),
),
),
),
),
),
])
]),
)
else
Container(
padding: const EdgeInsets.all(16),
Expand Down Expand Up @@ -307,11 +312,36 @@ class _DashboardViewState extends State<DashboardView>
Widget _buildScheduleCard(DashboardViewModel model, PreferencesFlag flag) {
var title = AppIntl.of(context)!.title_schedule;
if (model.todayDateEvents.isEmpty && model.tomorrowDateEvents.isNotEmpty) {
title = title + AppIntl.of(context)!.card_schedule_tomorrow;
title += AppIntl.of(context)!.card_schedule_tomorrow;
}
final bool isLoading = model.busy(model.todayDateEvents) ||
model.busy(model.tomorrowDateEvents);

late List<CourseActivity>? courseActivities;
if (isLoading) {
// User will not see this.
// It serves the purpuse of creating text in the skeleton and make it look closer to the real schedule.
courseActivities = [
CourseActivity(
courseGroup: "APP375-99",
courseName: "Développement mobile (ÉTSMobile)",
activityName: '',
activityDescription: '5 à 7',
activityLocation: '100 Génies',
startDateTime: DateTime.now(),
endDateTime: DateTime.now())
];
} else if (model.todayDateEvents.isEmpty) {
if (model.tomorrowDateEvents.isEmpty) {
courseActivities = null;
} else {
courseActivities = model.tomorrowDateEvents;
}
} else {
courseActivities = model.todayDateEvents;
}

return DismissibleCard(
isBusy: model.busy(model.todayDateEvents) ||
model.busy(model.tomorrowDateEvents),
onDismissed: (DismissDirection direction) {
dismissCard(model, flag);
},
Expand All @@ -330,16 +360,14 @@ class _DashboardViewState extends State<DashboardView>
style: Theme.of(context).textTheme.titleLarge),
),
)),
if (model.todayDateEvents.isEmpty)
if (model.tomorrowDateEvents.isEmpty)
SizedBox(
height: 100,
child: Center(
child: Text(AppIntl.of(context)!.schedule_no_event)))
else
_buildEventList(model.tomorrowDateEvents)
if (courseActivities != null)
Skeletonizer(
enabled: isLoading, child: _buildEventList(courseActivities))
else
_buildEventList(model.todayDateEvents)
SizedBox(
height: 100,
child:
Center(child: Text(AppIntl.of(context)!.schedule_no_event)))
]),
),
);
Expand All @@ -359,53 +387,77 @@ class _DashboardViewState extends State<DashboardView>
itemCount: events.length);
}

Widget _buildGradesCards(DashboardViewModel model, PreferencesFlag flag) =>
DismissibleCard(
key: UniqueKey(),
onDismissed: (DismissDirection direction) {
dismissCard(model, flag);
},
isBusy: model.busy(model.courses),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.centerLeft,
child: Container(
padding: const EdgeInsets.fromLTRB(17, 15, 0, 0),
child: GestureDetector(
onTap: () => _navigationService
.pushNamedAndRemoveUntil(RouterPaths.student),
child: Text(AppIntl.of(context)!.grades_title,
style: Theme.of(context).textTheme.titleLarge),
),
Widget _buildGradesCards(DashboardViewModel model, PreferencesFlag flag) {
final bool loaded = !model.busy(model.courses);
late List<Course> courses = model.courses;

// When loading courses, there are 2 stages. First, the courses of user are fetched, then, grades are fetched.
// During that first stage, putting empty courses with no title allows for a smoother transition.
if (courses.isEmpty && !loaded) {
final Course skeletonCourse = Course(
acronym: " ",
title: "",
group: "",
session: "",
programCode: "",
numberOfCredits: 0);
courses = [
skeletonCourse,
skeletonCourse,
skeletonCourse,
skeletonCourse
];
}

return DismissibleCard(
key: UniqueKey(),
onDismissed: (DismissDirection direction) {
dismissCard(model, flag);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.centerLeft,
child: Container(
padding: const EdgeInsets.fromLTRB(17, 15, 0, 0),
child: GestureDetector(
onTap: () => _navigationService
.pushNamedAndRemoveUntil(RouterPaths.student),
child: Text(AppIntl.of(context)!.grades_title,
style: Theme.of(context).textTheme.titleLarge),
),
),
if (model.courses.isEmpty)
SizedBox(
height: 100,
child: Center(
child: Text(AppIntl.of(context)!
.grades_msg_no_grades
.split("\n")
.first)),
)
else
Container(
),
if (model.courses.isEmpty && loaded)
SizedBox(
height: 100,
child: Center(
child: Text(AppIntl.of(context)!
.grades_msg_no_grades
.split("\n")
.first)),
)
else
Skeletonizer(
enabled: !loaded,
child: Container(
padding: const EdgeInsets.fromLTRB(17, 10, 15, 10),
child: Wrap(
children: model.courses
children: courses
.map((course) => GradeButton(course,
color:
Theme.of(context).brightness == Brightness.light
? AppTheme.lightThemeBackground
: AppTheme.darkThemeBackground))
.toList(),
),
)
]),
);
),
)
]),
);
}

Widget _buildMessageBroadcastCard(
DashboardViewModel model, PreferencesFlag flag) {
Expand Down
4 changes: 2 additions & 2 deletions lib/features/dashboard/dashboard_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,11 @@ class DashboardViewModel extends FutureViewModel<Map<PreferencesFlag, int>> {
}

Future<List<CourseActivity>> futureToRunSchedule() async {
setBusyForObject(_todayDateEvents, true);
setBusyForObject(_tomorrowDateEvents, true);
try {
var courseActivities =
await _courseRepository.getCoursesActivities(fromCacheOnly: true);
setBusyForObject(_todayDateEvents, true);
setBusyForObject(_tomorrowDateEvents, true);
_todayDateEvents.clear();
_tomorrowDateEvents.clear();
final todayDate = _settingsManager.dateTimeNow;
Expand Down
5 changes: 4 additions & 1 deletion lib/features/dashboard/widgets/course_activity_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:intl/intl.dart';

// Project imports:
import 'package:notredame/features/app/signets-api/models/course_activity.dart';
import 'package:skeletonizer/skeletonizer.dart';

class CourseActivityTile extends StatelessWidget {
/// Course to display
Expand Down Expand Up @@ -51,7 +52,9 @@ class CourseActivityTile extends StatelessWidget {
style: Theme.of(context).textTheme.bodySmall),
],
),
VerticalDivider(color: colorFor(activity.courseName), thickness: 2)
Skeleton.shade(
child: VerticalDivider(
color: colorFor(activity.courseName), thickness: 2))
],
),
);
Expand Down
Loading

0 comments on commit a693804

Please sign in to comment.