added rounded expansion tile (fixed absence bug)

This commit is contained in:
Kima 2023-08-26 13:50:03 +02:00
parent 568d4cb769
commit 2d11c45972
6 changed files with 180 additions and 65 deletions

View File

@ -12,7 +12,8 @@ import 'package:filcnaplo_mobile_ui/common/widgets/lesson/changed_lesson_tile.da
import 'package:filcnaplo/utils/format.dart';
// difference.inDays is not reliable
bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day);
bool _sameDate(DateTime a, DateTime b) =>
(a.year == b.year && a.month == b.month && a.day == b.day);
List<Widget> sortDateWidgets(
BuildContext context, {
@ -35,13 +36,16 @@ List<Widget> sortDateWidgets(
if (message.conversationId != null) {
convMessages.add(w);
Conversation conv = conversations.firstWhere((e) => e.id == message.conversationId, orElse: () => Conversation(id: message.conversationId!));
Conversation conv = conversations.firstWhere(
(e) => e.id == message.conversationId,
orElse: () => Conversation(id: message.conversationId!));
conv.add(message);
if (conv.messages.length == 1) conversations.add(conv);
}
if (conversations.any((c) => c.id == message.messageId)) {
Conversation conv = conversations.firstWhere((e) => e.id == message.messageId);
Conversation conv =
conversations.firstWhere((e) => e.id == message.messageId);
convMessages.add(w);
conv.add(message);
}
@ -87,26 +91,41 @@ List<Widget> sortDateWidgets(
// Group Absence Tiles
List<DateWidget> absenceTileWidgets = elements.where((element) {
return element.widget is AbsenceViewable && (element.widget as AbsenceViewable).absence.delay == 0;
return element.widget is AbsenceViewable &&
(element.widget as AbsenceViewable).absence.delay == 0;
}).toList();
List<AbsenceViewable> absenceTiles = absenceTileWidgets.map((e) => e.widget as AbsenceViewable).toList();
List<AbsenceViewable> absenceTiles =
absenceTileWidgets.map((e) => e.widget as AbsenceViewable).toList();
if (absenceTiles.length > 1) {
elements.removeWhere((element) => element.widget.runtimeType == AbsenceViewable && (element.widget as AbsenceViewable).absence.delay == 0);
elements.removeWhere((element) =>
element.widget.runtimeType == AbsenceViewable &&
(element.widget as AbsenceViewable).absence.delay == 0);
if (elements.isEmpty) {
cst = false;
}
elements.add(DateWidget(
widget: AbsenceGroupTile(absenceTiles, showDate: !cst),
date: absenceTileWidgets.first.date,
key: "${absenceTileWidgets.first.date.millisecondsSinceEpoch}-absence-group"));
elements.add(
DateWidget(
widget: AbsenceGroupTile(
absenceTiles,
showDate: !cst,
padding: const EdgeInsets.symmetric(horizontal: 6.0),
),
date: absenceTileWidgets.first.date,
key:
"${absenceTileWidgets.first.date.millisecondsSinceEpoch}-absence-group"),
);
}
// Bring Lesson Tiles to front & sort by index asc
List<DateWidget> lessonTiles = elements.where((element) {
return element.widget.runtimeType == ChangedLessonTile;
}).toList();
lessonTiles.sort((a, b) => (a.widget as ChangedLessonTile).lesson.lessonIndex.compareTo((b.widget as ChangedLessonTile).lesson.lessonIndex));
elements.removeWhere((element) => element.widget.runtimeType == ChangedLessonTile);
lessonTiles.sort((a, b) => (a.widget as ChangedLessonTile)
.lesson
.lessonIndex
.compareTo((b.widget as ChangedLessonTile).lesson.lessonIndex));
elements.removeWhere(
(element) => element.widget.runtimeType == ChangedLessonTile);
elements.insertAll(0, lessonTiles);
final date = (elements + absenceTileWidgets).first.date;
@ -122,7 +141,8 @@ List<Widget> sortDateWidgets(
spawnIsolate: false,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, animation, item, index) => filterItemBuilder(context, animation, item.widget, index),
itemBuilder: (context, animation, item, index) =>
filterItemBuilder(context, animation, item.widget, index),
items: elements,
),
),
@ -131,9 +151,12 @@ List<Widget> sortDateWidgets(
}
final nh = DateTime.now();
final now = DateTime(nh.year, nh.month, nh.day).subtract(const Duration(seconds: 1));
final now =
DateTime(nh.year, nh.month, nh.day).subtract(const Duration(seconds: 1));
if (showDivider && items.any((i) => i.date.isBefore(now)) && items.any((i) => i.date.isAfter(now))) {
if (showDivider &&
items.any((i) => i.date.isBefore(now)) &&
items.any((i) => i.date.isAfter(now))) {
items.add(
DateWidget(
date: now,
@ -153,7 +176,9 @@ List<Widget> sortDateWidgets(
}
// Sort future dates asc, past dates desc
items.sort((a, b) => (a.date.isAfter(now) && b.date.isAfter(now) ? 1 : -1) * a.date.compareTo(b.date));
items.sort((a, b) =>
(a.date.isAfter(now) && b.date.isAfter(now) ? 1 : -1) *
a.date.compareTo(b.date));
return items.map((e) => e.widget).toList();
}

View File

@ -35,7 +35,8 @@ class SubjectAbsence {
List<Absence> absences;
double percentage;
SubjectAbsence({required this.subject, this.absences = const [], this.percentage = 0.0});
SubjectAbsence(
{required this.subject, this.absences = const [], this.percentage = 0.0});
}
class AbsencesPage extends StatefulWidget {
@ -45,7 +46,8 @@ class AbsencesPage extends StatefulWidget {
_AbsencesPageState createState() => _AbsencesPageState();
}
class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMixin {
class _AbsencesPageState extends State<AbsencesPage>
with TickerProviderStateMixin {
late UserProvider user;
late AbsenceProvider absenceProvider;
late TimetableProvider timetableProvider;
@ -65,7 +67,9 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
for (final lesson in timetableProvider.getWeek(Week.current()) ?? []) {
if (!lesson.isEmpty && lesson.subject.id != '' && lesson.lessonYearIndex != null) {
if (!lesson.isEmpty &&
lesson.subject.id != '' &&
lesson.lessonYearIndex != null) {
_lessonCount.update(
lesson.subject,
(value) {
@ -89,25 +93,30 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
if (absence.delay != 0) continue;
if (!_absences.containsKey(absence.subject)) {
_absences[absence.subject] = SubjectAbsence(subject: absence.subject, absences: [absence]);
_absences[absence.subject] =
SubjectAbsence(subject: absence.subject, absences: [absence]);
} else {
_absences[absence.subject]?.absences.add(absence);
}
}
_absences.forEach((subject, absence) {
final absentLessonsOfSubject = absenceProvider.absences.where((e) => e.subject == subject && e.delay == 0).length;
final absentLessonsOfSubject = absenceProvider.absences
.where((e) => e.subject == subject && e.delay == 0)
.length;
final totalLessonsOfSubject = _lessonCount[subject]?.lessonYearIndex ?? 0;
double absentLessonsOfSubjectPercentage;
if (absentLessonsOfSubject <= totalLessonsOfSubject) {
absentLessonsOfSubjectPercentage = absentLessonsOfSubject / totalLessonsOfSubject * 100;
absentLessonsOfSubjectPercentage =
absentLessonsOfSubject / totalLessonsOfSubject * 100;
} else {
absentLessonsOfSubjectPercentage = -1;
}
_absences[subject]?.percentage = absentLessonsOfSubjectPercentage.clamp(-1, 100.0);
_absences[subject]?.percentage =
absentLessonsOfSubjectPercentage.clamp(-1, 100.0);
});
absences = _absences.values.toList();
@ -131,7 +140,8 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
body: Padding(
padding: const EdgeInsets.only(top: 12.0),
child: NestedScrollView(
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
headerSliverBuilder: (context, _) => [
SliverAppBar(
pinned: true,
@ -145,7 +155,10 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Absences".i18n,
style: TextStyle(color: AppColors.of(context).text, fontSize: 32.0, fontWeight: FontWeight.bold),
style: TextStyle(
color: AppColors.of(context).text,
fontSize: 32.0,
fontWeight: FontWeight.bold),
),
),
bottom: FilterBar(items: [
@ -158,7 +171,8 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
body: TabBarView(
physics: const BouncingScrollPhysics(),
controller: _tabController,
children: List.generate(3, (index) => filterViewBuilder(context, index))),
children: List.generate(
3, (index) => filterViewBuilder(context, index))),
),
),
);
@ -174,10 +188,17 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
widget: AbsenceSubjectTile(
a.subject,
percentage: a.percentage,
excused: a.absences.where((a) => a.state == Justification.excused).length,
unexcused: a.absences.where((a) => a.state == Justification.unexcused).length,
pending: a.absences.where((a) => a.state == Justification.pending).length,
onTap: () => AbsenceSubjectView.show(a.subject, a.absences, context: context),
excused: a.absences
.where((a) => a.state == Justification.excused)
.length,
unexcused: a.absences
.where((a) => a.state == Justification.unexcused)
.length,
pending: a.absences
.where((a) => a.state == Justification.pending)
.length,
onTap: () => AbsenceSubjectView.show(a.subject, a.absences,
context: context),
),
));
}
@ -186,15 +207,18 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
for (var absence in absenceProvider.absences) {
if (absence.delay != 0) {
items.add(DateWidget(
date: absence.date,
widget: AbsenceViewable(absence, padding: EdgeInsets.zero),
));
date: absence.date,
widget: AbsenceViewable(
absence,
padding: EdgeInsets.zero,
)));
}
}
break;
case AbsenceFilter.misses:
for (var note in noteProvider.notes) {
if (note.type?.name == "HaziFeladatHiany" || note.type?.name == "Felszereleshiany") {
if (note.type?.name == "HaziFeladatHiany" ||
note.type?.name == "Felszereleshiany") {
items.add(DateWidget(
date: note.date,
widget: MissTile(note),
@ -232,10 +256,15 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
title: Text("attention".i18n),
content: Text("attention_body".i18n),
actions: [ActionButton(label: "Ok", onTap: () => Navigator.of(context).pop())],
actions: [
ActionButton(
label: "Ok",
onTap: () => Navigator.of(context).pop())
],
),
);
},
@ -262,7 +291,10 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
);
},
child: Column(
children: getFilterWidgets(AbsenceFilter.values[activeData]).map((e) => e.widget).cast<Widget>().toList(),
children: getFilterWidgets(AbsenceFilter.values[activeData])
.map((e) => e.widget)
.cast<Widget>()
.toList(),
),
),
),
@ -284,7 +316,8 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
itemCount: max(filterWidgets.length + (activeData <= 1 ? 1 : 0), 1),
itemBuilder: (context, index) {
if (filterWidgets.isNotEmpty) {
if ((index == 0 && activeData == 1) || (index == 0 && activeData == 0)) {
if ((index == 0 && activeData == 1) ||
(index == 0 && activeData == 0)) {
int value1 = 0;
int value2 = 0;
String title1 = "";
@ -292,18 +325,26 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
String suffix = "";
if (activeData == AbsenceFilter.absences.index) {
value1 = absenceProvider.absences.where((e) => e.delay == 0 && e.state == Justification.excused).length;
value2 = absenceProvider.absences.where((e) => e.delay == 0 && e.state == Justification.unexcused).length;
value1 = absenceProvider.absences
.where((e) =>
e.delay == 0 && e.state == Justification.excused)
.length;
value2 = absenceProvider.absences
.where((e) =>
e.delay == 0 && e.state == Justification.unexcused)
.length;
title1 = "stat_1".i18n;
title2 = "stat_2".i18n;
suffix = " " + "hr".i18n;
} else if (activeData == AbsenceFilter.delays.index) {
value1 = absenceProvider.absences
.where((e) => e.delay != 0 && e.state == Justification.excused)
.where((e) =>
e.delay != 0 && e.state == Justification.excused)
.map((e) => e.delay)
.fold(0, (a, b) => a + b);
value2 = absenceProvider.absences
.where((e) => e.delay != 0 && e.state == Justification.unexcused)
.where((e) =>
e.delay != 0 && e.state == Justification.unexcused)
.map((e) => e.delay)
.fold(0, (a, b) => a + b);
title1 = "stat_3".i18n;
@ -312,7 +353,8 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
}
return Padding(
padding: const EdgeInsets.only(bottom: 24.0, left: 24.0, right: 24.0),
padding: const EdgeInsets.only(
bottom: 24.0, left: 24.0, right: 24.0),
child: Row(children: [
Expanded(
child: StatisticsTile(
@ -348,7 +390,8 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
padding:
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
child: filterWidgets[index - (activeData <= 1 ? 1 : 0)],
);
} else {

View File

@ -9,7 +9,9 @@ import 'package:provider/provider.dart';
import 'absence_tile.i18n.dart';
class AbsenceTile extends StatelessWidget {
const AbsenceTile(this.absence, {Key? key, this.onTap, this.elevation = 0.0, this.padding}) : super(key: key);
const AbsenceTile(this.absence,
{Key? key, this.onTap, this.elevation = 0.0, this.padding})
: super(key: key);
final Absence absence;
final void Function()? onTap;
@ -37,39 +39,61 @@ class AbsenceTile extends StatelessWidget {
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? (group ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 8.0)),
padding: padding ??
(group
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 8.0,
)),
child: ListTile(
onTap: onTap,
visualDensity: VisualDensity.compact,
dense: group,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(!group ? 14.0 : 12.0)),
contentPadding: const EdgeInsets.only(
left: 14.0, right: 12.0, top: 2.0, bottom: 2.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(!group ? 14.0 : 12.0)),
leading: Container(
width: 44.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: !group ? color.withOpacity(.25) : null,
),
child: Center(child: Icon(justificationIcon(absence.state), color: color)),
child: Center(
child: Icon(justificationIcon(absence.state), color: color)),
),
title: !group
? Text.rich(TextSpan(
text: "${absence.delay == 0 ? "" : absence.delay}",
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15.5),
style: const TextStyle(
fontWeight: FontWeight.w700, fontSize: 15.5),
children: [
TextSpan(
text: absence.delay == 0
? justificationName(absence.state).fill(["absence".i18n]).capital()
: 'minute'.plural(absence.delay) + justificationName(absence.state).fill(["delay".i18n]),
? justificationName(absence.state)
.fill(["absence".i18n]).capital()
: 'minute'.plural(absence.delay) +
justificationName(absence.state)
.fill(["delay".i18n]),
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
))
: Text(
(absence.lessonIndex != null ? "${absence.lessonIndex}. " : "") + (absence.subject.renamedTo ?? absence.subject.name.capital()),
(absence.lessonIndex != null
? "${absence.lessonIndex}. "
: "") +
(absence.subject.renamedTo ??
absence.subject.name.capital()),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14.0, fontStyle: absence.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.0,
fontStyle: absence.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
),
subtitle: !group
? Text(
@ -77,7 +101,12 @@ class AbsenceTile extends StatelessWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
// DateFormat("MM. dd. (EEEEE)", I18n.of(context).locale.toString()).format(absence.date),
style: TextStyle(fontWeight: FontWeight.w500, fontStyle: absence.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null),
style: TextStyle(
fontWeight: FontWeight.w500,
fontStyle: absence.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
)
: null,
),
@ -97,7 +126,8 @@ class AbsenceTile extends StatelessWidget {
}
}
static Color justificationColor(Justification state, {required BuildContext context}) {
static Color justificationColor(Justification state,
{required BuildContext context}) {
switch (state) {
case Justification.excused:
return AppColors.of(context).green;

View File

@ -5,6 +5,8 @@ import 'package:filcnaplo_mobile_ui/common/widgets/absence_group/absence_group_c
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_tile.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:rounded_expansion_tile/rounded_expansion_tile.dart';
import 'absence_group_tile.i18n.dart';
class AbsenceGroupTile extends StatelessWidget {
@ -30,13 +32,17 @@ class AbsenceGroupTile extends StatelessWidget {
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
padding: padding ??
const EdgeInsets.symmetric(horizontal: 0.0, vertical: 0.0),
child: AbsenceGroupContainer(
child: ExpansionTile(
child: RoundedExpansionTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
tilePadding: const EdgeInsets.symmetric(horizontal: 8.0),
backgroundColor: Colors.transparent,
childrenPadding: const EdgeInsets.symmetric(horizontal: 8.0),
tileColor: Colors.transparent,
duration: const Duration(milliseconds: 250),
trailingDuration: 0.5,
trailing: const Icon(FeatherIcons.chevronDown),
leading: Container(
width: 44.0,
height: 44.0,

View File

@ -19,14 +19,18 @@ import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_view.i18n.dar
import 'package:provider/provider.dart';
class AbsenceSubjectView extends StatelessWidget {
const AbsenceSubjectView(this.subject, {Key? key, this.absences = const []}) : super(key: key);
const AbsenceSubjectView(this.subject, {Key? key, this.absences = const []})
: super(key: key);
final Subject subject;
final List<Absence> absences;
static void show(Subject subject, List<Absence> absences, {required BuildContext context}) {
static void show(Subject subject, List<Absence> absences,
{required BuildContext context}) {
Navigator.of(context, rootNavigator: true)
.push<Absence>(CupertinoPageRoute(builder: (context) => AbsenceSubjectView(subject, absences: absences)))
.push<Absence>(CupertinoPageRoute(
builder: (context) =>
AbsenceSubjectView(subject, absences: absences)))
.then((value) {
if (value == null) return;
@ -36,7 +40,8 @@ class AbsenceSubjectView extends StatelessWidget {
TimetablePage.jump(context, lesson: lesson);
} else {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
content: Text("Cannot find lesson".i18n, style: const TextStyle(color: Colors.white)),
content: Text("Cannot find lesson".i18n,
style: const TextStyle(color: Colors.white)),
backgroundColor: AppColors.of(context).red,
context: context,
));
@ -54,7 +59,10 @@ class AbsenceSubjectView extends StatelessWidget {
date: a.date,
))
.toList();
List<Widget> absenceTiles = sortDateWidgets(context, dateWidgets: dateWidgets, padding: EdgeInsets.zero, hasShadow: true);
List<Widget> absenceTiles = sortDateWidgets(context,
dateWidgets: dateWidgets,
padding: const EdgeInsets.symmetric(vertical: 6.0),
hasShadow: true);
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);

View File

@ -43,6 +43,9 @@ dependencies:
dotted_border: ^2.0.0+3
screenshot: ^2.1.0
image_gallery_saver: ^2.0.2
rounded_expansion_tile:
git:
url: https://github.com/kimaah/rounded_expansion_tile.git
dev_dependencies:
flutter_lints: ^1.0.0