Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add skeletons #1015

Merged
merged 14 commits into from
Jul 11, 2024
Merged
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",
LouisPhilippeHeon marked this conversation as resolved.
Show resolved Hide resolved
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: "",
LouisPhilippeHeon marked this conversation as resolved.
Show resolved Hide resolved
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