2024-03-11 23:29:20 +01:00

379 lines
16 KiB
Dart

import 'dart:math';
import 'package:animations/animations.dart';
import 'package:refilc/api/providers/update_provider.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc_kreta_api/client/client.dart';
import 'package:refilc_kreta_api/models/week.dart';
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc_kreta_api/models/lesson.dart';
import 'package:refilc_mobile_ui/common/empty.dart';
import 'package:refilc_mobile_ui/common/panel/panel.dart';
import 'package:refilc_kreta_api/controllers/timetable_controller.dart';
import 'package:refilc_desktop_ui/common/widgets/lesson/lesson_viewable.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'package:refilc/utils/format.dart';
import 'package:intl/intl.dart';
import 'package:i18n_extension/i18n_extension.dart';
import 'timetable_page.i18n.dart';
// todo: "fix" overflow (priority: -1)
class TimetablePage extends StatefulWidget {
const TimetablePage({super.key, this.initialDay, this.initialWeek});
final DateTime? initialDay;
final Week? initialWeek;
static void jump(BuildContext context,
{Week? week, DateTime? day, Lesson? lesson}) {
// Go to timetable page with arguments
// NavigationScreen.of(context)?.customRoute(navigationPageRoute((context) => TimetablePage(
// initialDay: lesson?.date ?? day,
// initialWeek: lesson?.date != null
// ? Week.fromDate(lesson!.date)
// : day != null
// ? Week.fromDate(day)
// : week,
// )));
// NavigationScreen.of(context)?.setPage("timetable");
// Show initial Lesson
// if (lesson != null) LessonView.show(lesson, context: context);
}
@override
TimetablePageState createState() => TimetablePageState();
}
class TimetablePageState extends State<TimetablePage>
with TickerProviderStateMixin {
late UserProvider user;
late TimetableProvider timetableProvider;
late UpdateProvider updateProvider;
late String firstName;
late TimetableController _controller;
late TabController _tabController;
late Widget empty;
int _getDayIndex(DateTime date) {
int index = 0;
if (_controller.days == null || (_controller.days?.isEmpty ?? true)) {
return index;
}
// find the first day with upcoming lessons
index = _controller.days!.indexWhere((day) => day.last.end.isAfter(date));
if (index == -1) index = 0; // fallback
return index;
}
// Update timetable on user change
Future<void> _userListener() async {
await Provider.of<KretaClient>(context, listen: false).refreshLogin();
if (mounted) _controller.jump(_controller.currentWeek, context: context);
}
@override
void initState() {
super.initState();
// Initalize controllers
_controller = TimetableController();
_tabController = TabController(length: 0, vsync: this, initialIndex: 0);
empty = Empty(subtitle: "empty".i18n);
bool initial = true;
// Only update the TabController on week changes
_controller.addListener(() {
if (_controller.days == null) return;
setState(() {
_tabController = TabController(
length: _controller.days!.length,
vsync: this,
initialIndex:
min(_tabController.index, max(_controller.days!.length - 1, 0)),
);
if (initial ||
_controller.previousWeekId != _controller.currentWeekId) {
_tabController
.animateTo(_getDayIndex(widget.initialDay ?? DateTime.now()));
}
initial = false;
// Empty is updated once every week change
empty = Empty(subtitle: "empty".i18n);
});
});
if (mounted) {
if (widget.initialWeek != null) {
_controller.jump(widget.initialWeek!, context: context, initial: true);
} else {
_controller.jump(_controller.currentWeek,
context: context, initial: true, skip: true);
}
}
// Listen for user changes
user = Provider.of<UserProvider>(context, listen: false);
user.addListener(_userListener);
}
@override
void dispose() {
_tabController.dispose();
_controller.dispose();
user.removeListener(_userListener);
super.dispose();
}
String dayTitle(int index) {
// Sometimes when changing weeks really fast,
// controller.days might be null or won't include index
try {
return DateFormat("EEEE", I18n.of(context).locale.languageCode)
.format(_controller.days![index].first.date);
} catch (e) {
return "timetable".i18n;
}
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
timetableProvider = Provider.of<TimetableProvider>(context);
updateProvider = Provider.of<UpdateProvider>(context);
// First name
List<String> nameParts = user.name?.split(" ") ?? ["?"];
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
return Scaffold(
body: Padding(
padding: const EdgeInsets.only(top: 18.0),
child: Column(
children: [
Expanded(
child: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return FadeThroughTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
fillColor: Theme.of(context).scaffoldBackgroundColor,
child: child,
);
},
child: _controller.days != null
?
// Week view
_tabController.length > 0
? CupertinoScrollbar(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _controller.days!.length,
itemBuilder: (context, tab) => SizedBox(
width: 400,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Day Title
Padding(
padding: const EdgeInsets.only(
left: 24.0,
right: 28.0,
top: 18.0,
bottom: 8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
dayTitle(tab).capital(),
style: const TextStyle(
fontSize: 32.0,
fontWeight: FontWeight.w600,
),
),
Text(
"${"${_controller.days![tab].first.date.day}"
.padLeft(2, '0')}.",
style: TextStyle(
color: AppColors.of(context)
.text
.withOpacity(.5),
fontWeight: FontWeight.w500,
),
),
],
),
),
// Lessons
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
physics: const BouncingScrollPhysics(),
itemCount:
_controller.days![tab].length + 2,
itemBuilder: (context, index) {
if (_controller.days == null) {
return Container();
}
// Header
if (index == 0) {
return const Padding(
padding: EdgeInsets.only(
top: 8.0,
left: 24.0,
right: 24.0),
child: PanelHeader(
padding: EdgeInsets.only(
top: 12.0)),
);
}
// Footer
if (index ==
_controller.days![tab].length +
1) {
return const Padding(
padding: EdgeInsets.only(
bottom: 8.0,
left: 24.0,
right: 24.0),
child: PanelFooter(
padding: EdgeInsets.only(
top: 12.0)),
);
}
// Body
final Lesson lesson =
_controller.days![tab][index - 1];
final bool swapDescDay = _controller
.days![tab]
.map(
(l) => l.swapDesc ? 1 : 0)
.reduce((a, b) => a + b) >=
_controller.days![tab].length *
.5;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0),
child: PanelBody(
padding:
const EdgeInsets.symmetric(
horizontal: 10.0),
child: LessonViewable(
lesson,
swapDesc: swapDescDay,
),
),
);
},
),
),
],
),
),
),
)
// Empty week
: Expanded(
child: Center(child: empty),
)
: Center(
child: CircularProgressIndicator(
color: Theme.of(context).colorScheme.secondary,
),
),
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Previous week
IconButton(
onPressed: _controller.currentWeekId == 0
? null
: () => setState(() {
_controller.previous(context);
}),
splashRadius: 24.0,
icon: const Icon(FeatherIcons.chevronLeft),
color: Theme.of(context).colorScheme.secondary),
// Week selector
InkWell(
borderRadius: BorderRadius.circular(6.0),
onTap: () => setState(() {
_controller.current();
if (mounted) {
_controller.jump(_controller.currentWeek,
context: context,
loader: _controller.currentWeekId !=
_controller.previousWeekId);
}
}),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"${_controller.currentWeekId + 1}. ${"week".i18n} (${DateFormat(
"${_controller.currentWeek.start.year !=
DateTime.now().year
? "yy. "
: ""}MMM d.",
I18n.of(context).locale.languageCode)
.format(_controller.currentWeek.start)} - ${DateFormat(
"${_controller.currentWeek.start.year !=
DateTime.now().year
? "yy. "
: ""}MMM d.",
I18n.of(context).locale.languageCode)
.format(_controller.currentWeek.end)})",
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.0,
),
),
),
),
// Next week
IconButton(
onPressed: _controller.currentWeekId == 51
? null
: () => setState(() {
_controller.next(context);
}),
splashRadius: 24.0,
icon: const Icon(FeatherIcons.chevronRight),
color: Theme.of(context).colorScheme.secondary),
],
),
),
],
),
),
);
}
}