From 2d57c9a4e7989021e631255aadf1025508cc0679 Mon Sep 17 00:00:00 2001 From: Username * Date: Mon, 21 Jul 2025 19:12:24 +0100 Subject: [PATCH 01/13] Update gradle version to 8.3.0 --- android/settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/settings.gradle b/android/settings.gradle index bd473bb..ee4d135 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.2.1" apply false + id "com.android.application" version "8.3.0" apply false id "org.jetbrains.kotlin.android" version "1.9.0" apply false } From 1ab75193ffda283387dc1df53d68162f960a4c29 Mon Sep 17 00:00:00 2001 From: Username * Date: Mon, 21 Jul 2025 19:17:52 +0100 Subject: [PATCH 02/13] rework day implementation to add the ability to change week starting day --- assets/translations/en-US.json | 1 + assets/translations/fr-FR.json | 1 + lib/core/constants/basic_subject.dart | 4 +- lib/core/constants/days.dart | 34 ++++---- lib/core/constants/grid_properties.dart | 8 -- lib/core/db/database.g.dart | 30 +++---- lib/core/db/services/service.dart | 2 +- lib/core/db/tables/subject.dart | 2 +- lib/core/models/settings.dart | 7 ++ lib/core/services/day.dart | 45 ++++++++++ lib/core/utils/subject_validation.dart | 2 +- lib/features/settings/providers/settings.dart | 8 ++ .../settings/widgets/customize_timetable.dart | 11 +++ .../widgets/week_start_day_options.dart | 51 ++++++++++++ .../subjects/screens/subject_screen.dart | 6 +- lib/features/subjects/widgets/day.dart | 4 +- .../widgets/day_time_week_tb_config.dart | 2 +- lib/features/timetable/screens/day.dart | 16 ++-- lib/features/timetable/screens/grid.dart | 32 ++++---- lib/features/timetable/screens/timetable.dart | 2 +- .../widgets/{day_view => }/days_bar.dart | 48 +++++------ ...rid_view_overlapping_subjects_builder.dart | 7 +- .../grid_view/grid_view_subject_builder.dart | 8 +- .../grid_view_subject_container_builder.dart | 5 +- lib/shared/providers/day.dart | 15 ++++ lib/shared/widgets/bottom_sheets/days.dart | 8 +- pubspec.lock | 82 +++++++++---------- 27 files changed, 288 insertions(+), 153 deletions(-) create mode 100644 lib/core/services/day.dart create mode 100644 lib/features/settings/widgets/week_start_day_options.dart rename lib/features/timetable/widgets/{day_view => }/days_bar.dart (82%) create mode 100644 lib/shared/providers/day.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 3b31491..c0bc560 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -38,6 +38,7 @@ "auto_complete_colors": "Auto Complete Colors", "auto_complete_colors_description": "auto assigns colors from previously made subjects that have matching names", "default_tb_view": "Default View", + "week_start_day": "Initial Day", "view": "View", "grid": "Grid", "default_subject_duration": "Default Subject Duration", diff --git a/assets/translations/fr-FR.json b/assets/translations/fr-FR.json index 546869e..ae15c68 100644 --- a/assets/translations/fr-FR.json +++ b/assets/translations/fr-FR.json @@ -38,6 +38,7 @@ "auto_complete_colors": "Saisie Automatique des Couleurs", "auto_complete_colors_description": "attribue automatiquement les couleurs des matières créés précédemment qui portent des noms correspondants", "default_tb_view": "Vue par défaut", + "week_start_day": "Jour initial", "view": "Vue", "grid": "Grille", "default_subject_duration": "Durée par défaut d'une matière", diff --git a/lib/core/constants/basic_subject.dart b/lib/core/constants/basic_subject.dart index ef859d3..458ce3f 100644 --- a/lib/core/constants/basic_subject.dart +++ b/lib/core/constants/basic_subject.dart @@ -11,8 +11,8 @@ const basicSubject = Subject( location: "", color: Colors.black, startTime: TimeOfDay(hour: 8, minute: 0), - endTime: TimeOfDay(hour: 18, minute: 0), - day: Days.monday, + endTime: TimeOfDay(hour: 9, minute: 0), + day: Day.sunday, rotationWeek: RotationWeeks.all, note: "", timetable: "1", diff --git a/lib/core/constants/days.dart b/lib/core/constants/days.dart index e064047..3d51a07 100644 --- a/lib/core/constants/days.dart +++ b/lib/core/constants/days.dart @@ -1,23 +1,17 @@ /// The week's days. -enum Days { - monday, - tuesday, - wednesday, - thursday, - friday, - saturday, - sunday, -} +enum Day { + sunday('sunday', 7), + monday('monday', 1), + tuesday('tuesday', 2), + wednesday('wednesday', 3), + thursday('thursday', 4), + friday('friday', 5), + saturday('saturday', 6); -/// The week's days as a list of strings. -List days = [ - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - 'sunday' -]; + final String name; + final int isoValue; + const Day(this.name, this.isoValue); -const List daysList = Days.values; + String get shortened => name.substring(0, 3); + String get initial => name[0]; +} diff --git a/lib/core/constants/grid_properties.dart b/lib/core/constants/grid_properties.dart index 830d7b1..e823bc0 100644 --- a/lib/core/constants/grid_properties.dart +++ b/lib/core/constants/grid_properties.dart @@ -4,14 +4,6 @@ import 'package:timetable/features/settings/providers/settings.dart'; /// Width of the time column. const double timeColumnWidth = 22.5; -/// Number of columns in the grid view of the timetable. -int columns(WidgetRef ref) { - final hideSunday = ref.watch(settingsProvider).hideSunday; - - if (!hideSunday) return 7; - return 6; -} - /// Number of rows in the grid view based on the custom start time and custom end time (if customTimePeriod is true), /// otherwise uses the default time period. (8:00 -> 18:00) /// if hour difference is 0, there will be 24 rows diff --git a/lib/core/db/database.g.dart b/lib/core/db/database.g.dart index 9d7721b..7f9bd7b 100644 --- a/lib/core/db/database.g.dart +++ b/lib/core/db/database.g.dart @@ -225,10 +225,10 @@ class $SubjectsTable extends Subjects with TableInfo<$SubjectsTable, Subject> { .withConverter($SubjectsTable.$converterrotationWeek); static const VerificationMeta _dayMeta = const VerificationMeta('day'); @override - late final GeneratedColumnWithTypeConverter day = + late final GeneratedColumnWithTypeConverter day = GeneratedColumn('day', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true) - .withConverter($SubjectsTable.$converterday); + .withConverter($SubjectsTable.$converterday); static const VerificationMeta _startTimeMeta = const VerificationMeta('startTime'); @override @@ -345,8 +345,8 @@ class $SubjectsTable extends Subjects with TableInfo<$SubjectsTable, Subject> { const ColorConverter(); static JsonTypeConverter2 $converterrotationWeek = const EnumIndexConverter(RotationWeeks.values); - static JsonTypeConverter2 $converterday = - const EnumIndexConverter(Days.values); + static JsonTypeConverter2 $converterday = + const EnumIndexConverter(Day.values); static TypeConverter $converterstartTime = const TimeOfDayConverter(); static TypeConverter $converterendTime = @@ -360,7 +360,7 @@ class Subject extends DataClass implements Insertable { final String? note; final material.Color color; final RotationWeeks rotationWeek; - final Days day; + final Day day; final material.TimeOfDay startTime; final material.TimeOfDay endTime; final String timetable; @@ -480,7 +480,7 @@ class Subject extends DataClass implements Insertable { Value note = const Value.absent(), material.Color? color, RotationWeeks? rotationWeek, - Days? day, + Day? day, material.TimeOfDay? startTime, material.TimeOfDay? endTime, String? timetable}) => @@ -556,7 +556,7 @@ class SubjectsCompanion extends UpdateCompanion { final Value note; final Value color; final Value rotationWeek; - final Value day; + final Value day; final Value startTime; final Value endTime; final Value timetable; @@ -579,7 +579,7 @@ class SubjectsCompanion extends UpdateCompanion { this.note = const Value.absent(), required material.Color color, required RotationWeeks rotationWeek, - required Days day, + required Day day, required material.TimeOfDay startTime, required material.TimeOfDay endTime, required String timetable, @@ -623,7 +623,7 @@ class SubjectsCompanion extends UpdateCompanion { Value? note, Value? color, Value? rotationWeek, - Value? day, + Value? day, Value? startTime, Value? endTime, Value? timetable}) { @@ -832,7 +832,7 @@ typedef $$SubjectsTableCreateCompanionBuilder = SubjectsCompanion Function({ Value note, required material.Color color, required RotationWeeks rotationWeek, - required Days day, + required Day day, required material.TimeOfDay startTime, required material.TimeOfDay endTime, required String timetable, @@ -844,7 +844,7 @@ typedef $$SubjectsTableUpdateCompanionBuilder = SubjectsCompanion Function({ Value note, Value color, Value rotationWeek, - Value day, + Value day, Value startTime, Value endTime, Value timetable, @@ -881,7 +881,7 @@ class $$SubjectsTableFilterComposer column: $table.rotationWeek, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters get day => $composableBuilder( + ColumnWithTypeConverterFilters get day => $composableBuilder( column: $table.day, builder: (column) => ColumnWithTypeConverterFilters(column)); @@ -968,7 +968,7 @@ class $$SubjectsTableAnnotationComposer $composableBuilder( column: $table.rotationWeek, builder: (column) => column); - GeneratedColumnWithTypeConverter get day => + GeneratedColumnWithTypeConverter get day => $composableBuilder(column: $table.day, builder: (column) => column); GeneratedColumnWithTypeConverter get startTime => @@ -1010,7 +1010,7 @@ class $$SubjectsTableTableManager extends RootTableManager< Value note = const Value.absent(), Value color = const Value.absent(), Value rotationWeek = const Value.absent(), - Value day = const Value.absent(), + Value day = const Value.absent(), Value startTime = const Value.absent(), Value endTime = const Value.absent(), Value timetable = const Value.absent(), @@ -1034,7 +1034,7 @@ class $$SubjectsTableTableManager extends RootTableManager< Value note = const Value.absent(), required material.Color color, required RotationWeeks rotationWeek, - required Days day, + required Day day, required material.TimeOfDay startTime, required material.TimeOfDay endTime, required String timetable, diff --git a/lib/core/db/services/service.dart b/lib/core/db/services/service.dart index d0726f1..dff2b9a 100644 --- a/lib/core/db/services/service.dart +++ b/lib/core/db/services/service.dart @@ -75,7 +75,7 @@ Future restoreData( note: drift.Value(element["note"]), color: Color(element["color"]), rotationWeek: RotationWeeks.values[element["rotationWeek"]], - day: Days.values[element["day"]], + day: Day.values[element["day"]], startTime: TimeOfDay( hour: element["startTimeHour"], minute: element["startTimeMinute"]), diff --git a/lib/core/db/tables/subject.dart b/lib/core/db/tables/subject.dart index 428af5a..b74ea38 100644 --- a/lib/core/db/tables/subject.dart +++ b/lib/core/db/tables/subject.dart @@ -15,7 +15,7 @@ class Subjects extends Table { // to maintain the Color type instead of using int. IntColumn get color => integer().map(const ColorConverter())(); IntColumn get rotationWeek => intEnum()(); - IntColumn get day => intEnum()(); + IntColumn get day => intEnum()(); TextColumn get startTime => text().map(const TimeOfDayConverter())(); TextColumn get endTime => text().map(const TimeOfDayConverter())(); // added in v3 diff --git a/lib/core/models/settings.dart b/lib/core/models/settings.dart index f2da9ff..c1dcfdf 100644 --- a/lib/core/models/settings.dart +++ b/lib/core/models/settings.dart @@ -21,6 +21,7 @@ class Settings { final TbViews defaultTimetableView; final Color appThemeColor; final Duration defaultSubjectDuration; + final int weekStartDay; // settings defaults Settings({ @@ -41,6 +42,7 @@ class Settings { this.defaultTimetableView = TbViews.grid, this.appThemeColor = Colors.deepPurple, this.defaultSubjectDuration = const Duration(minutes: 60), + this.weekStartDay = 1, }); Settings copyWith({ @@ -61,6 +63,7 @@ class Settings { TbViews? defaultTimetableView, Color? appThemeColor, Duration? defaultSubjectDuration, + int? weekStartDay, }) => Settings( customTimePeriod: customTimePeriod ?? this.customTimePeriod, @@ -82,6 +85,7 @@ class Settings { appThemeColor: appThemeColor ?? this.appThemeColor, defaultSubjectDuration: defaultSubjectDuration ?? this.defaultSubjectDuration, + weekStartDay: weekStartDay ?? this.weekStartDay, ); Map toJson() => { @@ -104,6 +108,7 @@ class Settings { 'defaultTimetableView': defaultTimetableView.name, 'appThemeColorValue': appThemeColor.toInt(), 'defaultSubjectDuration': defaultSubjectDuration.inMinutes, + 'weekStartDay': weekStartDay, }; factory Settings.fromJson(Map json) { @@ -147,6 +152,8 @@ class Settings { defaultSubjectDuration: json['defaultSubjectDuration'] != null ? Duration(minutes: json['defaultSubjectDuration'] as int) : null, + weekStartDay: + json['weekStartDay'] != null ? json['weekStartDay'] as int : null, ); } } diff --git a/lib/core/services/day.dart b/lib/core/services/day.dart new file mode 100644 index 0000000..8c0118e --- /dev/null +++ b/lib/core/services/day.dart @@ -0,0 +1,45 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:timetable/core/constants/days.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; + +/// Service to provide the current day's index and ordered days. +class DaysService { + const DaysService(this.ref); + final Ref ref; + + List get orderedDays { + final settings = ref.watch(settingsProvider); + var days = Day.values; + + if (settings.hideSunday) { + days = days.where((day) => day != Day.sunday).toList(); + } + + final rotatedDays = [ + ...days.sublist(settings.weekStartDay), + ...days.sublist(0, settings.weekStartDay), + ]; + + return rotatedDays; + } + + int get currentDayIndex { + final settings = ref.watch(settingsProvider); + final now = DateTime.now(); + final currentIsoWeekday = now.weekday; + + var dayIndex = currentIsoWeekday - 1; + + dayIndex = (dayIndex - settings.weekStartDay) % Day.values.length; + if (dayIndex < 0) dayIndex += Day.values.length; + + if (settings.hideSunday) { + if (currentIsoWeekday == 7) return -1; + if (currentIsoWeekday > settings.weekStartDay) { + dayIndex -= 1; + } + } + + return dayIndex; + } +} diff --git a/lib/core/utils/subject_validation.dart b/lib/core/utils/subject_validation.dart index 88cafa6..69da28c 100644 --- a/lib/core/utils/subject_validation.dart +++ b/lib/core/utils/subject_validation.dart @@ -15,7 +15,7 @@ class SubjectValidation { final Color color; final TimeOfDay startTime; final TimeOfDay endTime; - final Days day; + final Day day; final RotationWeeks rotationWeek; final String? note; final String timetable; diff --git a/lib/features/settings/providers/settings.dart b/lib/features/settings/providers/settings.dart index ebee850..74a24bb 100644 --- a/lib/features/settings/providers/settings.dart +++ b/lib/features/settings/providers/settings.dart @@ -11,6 +11,14 @@ class SettingsNotifier extends StateNotifier { loadSettings(); } + void updateWeekStartDay(int weekStartDay) { + final newState = state.copyWith( + weekStartDay: weekStartDay, + ); + state = newState; + saveSettings(); + } + void updateDefaultSubjectDuration(Duration defaultSubjectDuration) { final newState = state.copyWith( defaultSubjectDuration: defaultSubjectDuration, diff --git a/lib/features/settings/widgets/customize_timetable.dart b/lib/features/settings/widgets/customize_timetable.dart index 66d2407..969ab4a 100644 --- a/lib/features/settings/widgets/customize_timetable.dart +++ b/lib/features/settings/widgets/customize_timetable.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:timetable/features/settings/widgets/default_view_options.dart'; import 'package:timetable/features/settings/providers/settings.dart'; +import 'package:timetable/features/settings/widgets/week_start_day_options.dart'; /// All the settings that allow for customizing the timetable. class CustomizeTimetableOptions extends ConsumerWidget { @@ -19,6 +20,7 @@ class CustomizeTimetableOptions extends ConsumerWidget { ref.watch(settingsProvider).hideTransparentSubject; final defaultTimetableView = ref.watch(settingsProvider).defaultTimetableView; + final defaultWeekStartDay = ref.watch(settingsProvider).weekStartDay; final switchItems = [ { @@ -64,6 +66,15 @@ class CustomizeTimetableOptions extends ConsumerWidget { ), onTap: () {}, ), + ListTile( + horizontalTitleGap: 8, + leading: const Icon(Icons.calendar_month_outlined, size: 20), + title: WeekStartDayOptions( + settings: settings, + defaultWeekStartDay: defaultWeekStartDay, + ), + onTap: () {}, + ), ...switchItems.map( (item) => SwitchListTile( secondary: Icon(item['icon'] as IconData, size: 20), diff --git a/lib/features/settings/widgets/week_start_day_options.dart b/lib/features/settings/widgets/week_start_day_options.dart new file mode 100644 index 0000000..ed7f52b --- /dev/null +++ b/lib/features/settings/widgets/week_start_day_options.dart @@ -0,0 +1,51 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:timetable/core/constants/days.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; + +/// Week starting day options dropdown menu. +class WeekStartDayOptions extends StatelessWidget { + final SettingsNotifier settings; + final int defaultWeekStartDay; + + const WeekStartDayOptions({ + super.key, + required this.settings, + required this.defaultWeekStartDay, + }); + + @override + Widget build(BuildContext context) { + List> weekStartDaysEntries() { + final entries = >[]; + const options = 2; + + for (int i = 0; i < options; i++) { + final day = Day.values[i]; + final label = day.name.tr(); + + entries.add( + DropdownMenuEntry(value: i, label: label), + ); + } + + return entries.toList(); + } + + return Row( + children: [ + const Text('week_start_day').tr(), + const Spacer(), + DropdownMenu( + width: 130, + dropdownMenuEntries: weekStartDaysEntries(), + label: const Text("day").plural(1), + initialSelection: defaultWeekStartDay, + onSelected: (value) { + settings.updateWeekStartDay(value!); + }, + ), + ], + ); + } +} diff --git a/lib/features/subjects/screens/subject_screen.dart b/lib/features/subjects/screens/subject_screen.dart index 71a332e..19ed5cf 100644 --- a/lib/features/subjects/screens/subject_screen.dart +++ b/lib/features/subjects/screens/subject_screen.dart @@ -16,6 +16,7 @@ import 'package:timetable/features/subjects/providers/overlapping_subjects.dart' import 'package:timetable/features/settings/providers/settings.dart'; import 'package:timetable/features/subjects/providers/subjects.dart'; import 'package:timetable/features/timetable/providers/timetables.dart'; +import 'package:timetable/shared/providers/day.dart'; /// Subject creation/modification UI. class SubjectScreen extends HookConsumerWidget { @@ -40,6 +41,7 @@ class SubjectScreen extends HookConsumerWidget { final overlappingSubjects = ref.watch(overlappingSubjectsProvider); final autoCompleteColor = ref.watch(settingsProvider).autoCompleteColor; final timetables = ref.watch(timetableProvider); + final orderedDays = ref.watch(orderedDaysProvider); final tfHours = ref.watch(settingsProvider).twentyFourHours; final customStartTimeHour = ref.watch(settingsProvider).customStartTime.hour; @@ -76,8 +78,8 @@ class SubjectScreen extends HookConsumerWidget { final ValueNotifier label = useState(subject?.label ?? ""); final ValueNotifier location = useState(subject?.location ?? ""); final ValueNotifier note = useState(subject?.note ?? ""); - final ValueNotifier day = - useState(Days.values[subject?.day.index ?? columnIndex!]); + final ValueNotifier day = + useState(subject?.day ?? orderedDays[columnIndex ?? 0]); final ValueNotifier color = useState(subject?.color ?? Colors.black); final ValueNotifier rotationWeek = useState(subject?.rotationWeek ?? RotationWeeks.none); diff --git a/lib/features/subjects/widgets/day.dart b/lib/features/subjects/widgets/day.dart index a797c8a..ee05a42 100644 --- a/lib/features/subjects/widgets/day.dart +++ b/lib/features/subjects/widgets/day.dart @@ -7,7 +7,7 @@ import 'package:timetable/core/constants/days.dart'; /// Day configuration part of the Subject creation screen. class DayConfig extends StatelessWidget { /// the subject day that will be manipulated - final ValueNotifier day; + final ValueNotifier day; const DayConfig({super.key, required this.day}); @@ -30,7 +30,7 @@ class DayConfig extends StatelessWidget { }, ); }, - label: Text(days[day.value.index]).tr(), + label: Text(Day.values[day.value.index].name.tr()), ), ], ); diff --git a/lib/features/subjects/widgets/day_time_week_tb_config.dart b/lib/features/subjects/widgets/day_time_week_tb_config.dart index f9a23f8..2053b4f 100644 --- a/lib/features/subjects/widgets/day_time_week_tb_config.dart +++ b/lib/features/subjects/widgets/day_time_week_tb_config.dart @@ -17,7 +17,7 @@ import 'package:timetable/features/timetable/providers/timetables.dart'; class TimeDayRotationWeekTimetableConfig extends ConsumerWidget { final ValueNotifier startTime; final ValueNotifier endTime; - final ValueNotifier day; + final ValueNotifier day; final ValueNotifier rotationWeek; final ValueNotifier timetable; diff --git a/lib/features/timetable/screens/day.dart b/lib/features/timetable/screens/day.dart index e409d7e..9236b3a 100644 --- a/lib/features/timetable/screens/day.dart +++ b/lib/features/timetable/screens/day.dart @@ -6,7 +6,6 @@ import 'package:timetable/features/timetable/widgets/day_view/day_view_subject_b import 'package:timetable/core/constants/custom_times.dart'; import 'package:timetable/core/constants/days.dart'; import 'package:timetable/core/constants/error_emoticons.dart'; -import 'package:timetable/core/constants/grid_properties.dart'; import 'package:timetable/core/constants/rotation_weeks.dart'; import 'package:timetable/core/utils/rotation_weeks.dart'; import 'package:timetable/core/db/database.dart'; @@ -14,6 +13,7 @@ import 'package:timetable/core/utils/subjects.dart'; import 'package:timetable/core/utils/timetables.dart'; import 'package:timetable/features/settings/providers/settings.dart'; import 'package:timetable/features/timetable/providers/timetables.dart'; +import 'package:timetable/shared/providers/day.dart'; /// Timetable view that shows each day's subjects in a single screen each. class TimetableDayView extends HookConsumerWidget { @@ -37,6 +37,8 @@ class TimetableDayView extends HookConsumerWidget { final twentyFourHoursMode = ref.watch(settingsProvider).twentyFourHours; final chosenCustomStartTime = ref.watch(settingsProvider).customStartTime; final chosenCustomEndTime = ref.watch(settingsProvider).customEndTime; + final weekStartDay = ref.watch(settingsProvider).weekStartDay; + final orderedDays = ref.watch(orderedDaysProvider); final customStartTime = getCustomStartTime(chosenCustomStartTime, ref); final customEndTime = getCustomEndTime(chosenCustomEndTime, ref); @@ -44,7 +46,7 @@ class TimetableDayView extends HookConsumerWidget { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { - int day = controller.page?.toInt() ?? 0; + int day = controller.page?.toInt() ?? weekStartDay; Navigator.push( context, @@ -61,11 +63,13 @@ class TimetableDayView extends HookConsumerWidget { child: const Icon(Icons.add), ), body: PageView.builder( - itemCount: columns(ref), + itemCount: orderedDays.length, pageSnapping: true, controller: controller, itemBuilder: (context, index) { - int startDay = ((DateTime.monday + index - 1) % 7 + 1); + int dayIndex = (weekStartDay + index) % 7; + int startDay = dayIndex; + final filteredSubjects = sortSubjects( getFilteredByTimetablesSubjects( currentTimetable, @@ -76,7 +80,7 @@ class TimetableDayView extends HookConsumerWidget { subject, ), ), - ).where((s) => s.day.index == index).where( + ).where((s) => s.day.index == dayIndex).where( (e) { if (!twentyFourHoursMode) return true; @@ -90,7 +94,7 @@ class TimetableDayView extends HookConsumerWidget { padding: const EdgeInsets.all(10), children: [ Text( - days[startDay - 1], + Day.values[startDay].name, style: const TextStyle( fontWeight: FontWeight.w400, fontSize: 20, diff --git a/lib/features/timetable/screens/grid.dart b/lib/features/timetable/screens/grid.dart index b1b22e1..c5a59c8 100644 --- a/lib/features/timetable/screens/grid.dart +++ b/lib/features/timetable/screens/grid.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:timetable/features/timetable/widgets/grid_view/grid.dart'; import 'package:timetable/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart'; +import 'package:timetable/shared/providers/day.dart'; import 'package:timetable/shared/widgets/time_column.dart'; import 'package:timetable/core/constants/custom_times.dart'; import 'package:timetable/core/constants/grid_properties.dart'; @@ -32,12 +33,11 @@ class TimetableGridView extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final bool compactMode = ref.watch(settingsProvider).compactMode; - final bool multipleTimetables = - ref.watch(settingsProvider).multipleTimetables; - final bool twentyFourHoursMode = - ref.watch(settingsProvider).twentyFourHours; - final List timetables = ref.watch(timetableProvider); + final compactMode = ref.watch(settingsProvider).compactMode; + final multipleTimetables = ref.watch(settingsProvider).multipleTimetables; + final twentyFourHoursMode = ref.watch(settingsProvider).twentyFourHours; + final timetables = ref.watch(timetableProvider); + final orderedDays = ref.watch(orderedDaysProvider); final TimeOfDay chosenCustomStartTime = ref.watch(settingsProvider).customStartTime; @@ -71,10 +71,11 @@ class TimetableGridView extends HookConsumerWidget { final double tileHeight = compactMode ? 125 : 100; final double tileWidth = compactMode - ? (screenWidth / columns(ref) - ((timeColumnWidth + 10) / 10)) + ? (screenWidth / orderedDays.length - ((timeColumnWidth + 10) / 10)) : isPortrait ? 100 - : (screenWidth / columns(ref) - ((timeColumnWidth + 10) / 10)); + : (screenWidth / orderedDays.length - + ((timeColumnWidth + 10) / 10)); return SingleChildScrollView( scrollDirection: Axis.vertical, @@ -94,10 +95,9 @@ class TimetableGridView extends HookConsumerWidget { tileHeight: tileHeight, tileWidth: tileWidth, rows: rows(ref), - columns: columns(ref), + columns: orderedDays.length, grid: generate( subjects, - columns(ref), rows(ref), ref, ), @@ -114,17 +114,21 @@ class TimetableGridView extends HookConsumerWidget { /// generates the grid List> generate( List subjects, - int totalDays, int totalHours, WidgetRef ref, ) { final overlappingSubjects = ref.watch(overlappingSubjectsProvider); final timetables = ref.watch(timetableProvider); final customStartTime = ref.watch(settingsProvider).customStartTime; + final orderedDays = ref.watch(orderedDaysProvider); + + final dayToColumnIndex = { + for (var i = 0; i < orderedDays.length; i++) orderedDays[i]: i + }; // subjects' containers final List> grid = List.generate( - totalDays, + orderedDays.length, (columnIndex) => List.generate( totalHours, (rowIndex) => Tile( @@ -187,13 +191,13 @@ class TimetableGridView extends HookConsumerWidget { subjects, overlappingSubjects, )) { - var day = subject.day.index; + final columnIndex = dayToColumnIndex[subject.day]!; var start = subject.startTime.hour - getCustomStartTime(customStartTime, ref).hour; var end = subject.endTime.hour - getCustomStartTime(customStartTime, ref).hour; - var column = grid[day]; + var column = grid[columnIndex]; column.replaceRange( start, diff --git a/lib/features/timetable/screens/timetable.dart b/lib/features/timetable/screens/timetable.dart index 6a74c67..386a522 100644 --- a/lib/features/timetable/screens/timetable.dart +++ b/lib/features/timetable/screens/timetable.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:timetable/features/timetable/screens/day.dart'; import 'package:timetable/features/timetable/screens/grid.dart'; -import 'package:timetable/features/timetable/widgets/day_view/days_bar.dart'; +import 'package:timetable/features/timetable/widgets/days_bar.dart'; import 'package:timetable/features/timetable/widgets/view_toggle.dart'; import 'package:timetable/features/navigation/widgets/navigation_bar_toggle.dart'; import 'package:timetable/shared/widgets/rotation_week_toggle.dart'; diff --git a/lib/features/timetable/widgets/day_view/days_bar.dart b/lib/features/timetable/widgets/days_bar.dart similarity index 82% rename from lib/features/timetable/widgets/day_view/days_bar.dart rename to lib/features/timetable/widgets/days_bar.dart index 2490c25..6f1eb58 100644 --- a/lib/features/timetable/widgets/day_view/days_bar.dart +++ b/lib/features/timetable/widgets/days_bar.dart @@ -8,28 +8,25 @@ import 'package:timetable/core/constants/days.dart'; import 'package:timetable/core/constants/grid_properties.dart'; import 'package:timetable/core/constants/theme_options.dart'; import 'package:timetable/features/settings/providers/settings.dart'; +import 'package:timetable/shared/providers/day.dart'; import 'package:timetable/shared/providers/themes.dart'; /// Top navigation bar in the day view that allows to switch between days quickly -/// merged with the days row in the grid view. +/// also acts as the days row in the grid view. class DaysBar extends ConsumerWidget { final PageController controller; final bool? isGridView; - final int currentDay; const DaysBar({ super.key, required this.controller, this.isGridView = false, - required this.currentDay, }); @override Widget build(BuildContext context, WidgetRef ref) { double screenWidth = MediaQuery.of(context).size.width; - final hideSunday = ref.watch(settingsProvider).hideSunday; final singleLetterDays = ref.watch(settingsProvider).singleLetterDays; - int daysLength = hideSunday ? days.length - 1 : days.length; final bool isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; @@ -40,6 +37,8 @@ class DaysBar extends ConsumerWidget { Theme.of(context).colorScheme.onInverseSurface; final lightCurrentDayColorScheme = Theme.of(context).colorScheme.outlineVariant; + final days = ref.watch(orderedDaysProvider); + final currentDay = ref.watch(currentDayIndexProvider); return SizedBox( height: 48, @@ -51,7 +50,7 @@ class DaysBar extends ConsumerWidget { padding: EdgeInsets.only(left: isGridView! ? 20 : 0), child: ListView.builder( scrollDirection: Axis.horizontal, - itemCount: daysLength, + itemCount: days.length, shrinkWrap: true, itemBuilder: (context, index) { return buildDayButton( @@ -67,8 +66,9 @@ class DaysBar extends ConsumerWidget { singleLetterDays: singleLetterDays, isPortrait: isPortrait, isGridView: isGridView!, - daysLength: daysLength, + daysLength: days.length, screenWidth: screenWidth, + orderedDays: days, colorScheme: Theme.of(context).colorScheme, onTap: () { if (isGridView!) return; @@ -99,14 +99,17 @@ class DaysBar extends ConsumerWidget { required Color currentDayColor, required VoidCallback? onTap, required ColorScheme colorScheme, + required List orderedDays, }) { + if (index == -1) return const SizedBox.shrink(); final isCurrentDay = isGridView && (currentDay == index); + final day = orderedDays[index]; final dayText = singleLetterDays - ? days[index].tr()[0] + ? day.initial.tr() : isPortrait - ? days[index].tr().substring(0, 3) - : days[index].tr(); + ? day.name.tr().substring(0, 3) + : day.name.tr(); return SizedBox( width: isGridView @@ -136,30 +139,21 @@ class DayBarUpdater extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentDay = useState(DateTime.now().weekday - 1); + final currentDay = ref.watch(currentDayIndexProvider); useEffect(() { - Timer? timer; - - void updateTimer() { - final now = DateTime.now(); - final nextMidnight = DateTime(now.year, now.month, now.day + 1); - - timer = Timer(nextMidnight.difference(now), () { - currentDay.value = DateTime.now().weekday - 1; - updateTimer(); - }); - } - - updateTimer(); - - return () => timer?.cancel(); + final timer = Timer.periodic(const Duration(minutes: 1), (_) { + final newDay = ref.read(dayServiceProvider).currentDayIndex; + if (newDay != currentDay) { + ref.read(currentDayIndexProvider.notifier).state = newDay; + } + }); + return timer.cancel; }, []); return DaysBar( controller: controller, isGridView: isGridView, - currentDay: currentDay.value, ); } } diff --git a/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart index 3ca640d..e2587c9 100644 --- a/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:non_uniform_border/non_uniform_border.dart'; -import 'package:timetable/features/timetable/widgets/grid_view/grid_view_subject_builder.dart'; import 'package:timetable/core/constants/custom_times.dart'; -import 'package:timetable/core/constants/grid_properties.dart'; +import 'package:timetable/features/timetable/widgets/grid_view/grid_view_subject_builder.dart'; import 'package:timetable/core/db/database.dart'; import 'package:timetable/features/settings/providers/settings.dart'; +import 'package:timetable/shared/providers/day.dart'; /// A widget that builds overlapping subjects in the grid view. /// @@ -30,11 +30,12 @@ class OverlappingSubjBuilder extends ConsumerWidget { final customStartTime = ref.watch(settingsProvider).customStartTime; final customEndTime = ref.watch(settingsProvider).customEndTime; final compactMode = ref.watch(settingsProvider).compactMode; + final orderedDays = ref.watch(orderedDaysProvider); final shape = NonUniformBorder( // both subjects should theoretically be in the same day so it doesn't matter which one we choose leftWidth: subjects[0].day.index == 0 ? 0 : 1, - rightWidth: subjects[0].day.index == (columns(ref) - 1) ? 0 : 1, + rightWidth: subjects[0].day.index == (orderedDays.length - 1) ? 0 : 1, topWidth: earlierStartTimeHour == getCustomStartTime(customStartTime, ref).hour ? 0 diff --git a/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart index cf8c7b0..72622a6 100644 --- a/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:non_uniform_border/non_uniform_border.dart'; +import 'package:timetable/shared/providers/day.dart'; import 'package:timetable/shared/widgets/bottom_sheets/subject_management.dart'; import 'package:timetable/core/constants/custom_times.dart'; import 'package:timetable/core/utils/rotation_weeks.dart'; import 'package:timetable/core/db/database.dart'; import 'package:timetable/features/settings/providers/settings.dart'; -import 'package:timetable/core/constants/grid_properties.dart'; /// Subject builder for the grid view. /// also builds for the overlapping subjects with some visual tweaks @@ -31,6 +31,8 @@ class SubjectBuilder extends ConsumerWidget { final hideTransparentSubject = ref.watch(settingsProvider).hideTransparentSubject; final compactMode = ref.watch(settingsProvider).compactMode; + final weekStartDay = ref.watch(settingsProvider).weekStartDay; + final orderedDays = ref.watch(orderedDaysProvider); final bool isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; @@ -49,8 +51,8 @@ class SubjectBuilder extends ConsumerWidget { : Colors.white.withValues(alpha: .75); final shape = NonUniformBorder( - leftWidth: subject.day.index == 0 ? 0 : 1, - rightWidth: subject.day.index == (columns(ref) - 1) ? 0 : 1, + leftWidth: subject.day.index == weekStartDay ? 0 : 1, + rightWidth: subject.day.index == (orderedDays.length - 1) ? 0 : 1, topWidth: subject.startTime.hour == getCustomStartTime(customStartTime, ref).hour ? 0 diff --git a/lib/features/timetable/widgets/grid_view/grid_view_subject_container_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_subject_container_builder.dart index 2e3057a..fed76d6 100644 --- a/lib/features/timetable/widgets/grid_view/grid_view_subject_container_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_subject_container_builder.dart @@ -4,6 +4,7 @@ import 'package:non_uniform_border/non_uniform_border.dart'; import 'package:timetable/core/constants/grid_properties.dart'; import 'package:timetable/features/subjects/screens/subject_screen.dart'; import 'package:timetable/core/db/database.dart'; +import 'package:timetable/shared/providers/day.dart'; /// Subject container tile builder. /// (the one you click on to create a Subject in the grid view.) @@ -21,9 +22,11 @@ class SubjectContainerBuilder extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final orderedDays = ref.watch(orderedDaysProvider); + final shape = NonUniformBorder( leftWidth: columnIndex == 0 ? 0 : 1, - rightWidth: columnIndex == (columns(ref) - 1) ? 0 : 1, + rightWidth: columnIndex == (orderedDays.length - 1) ? 0 : 1, topWidth: rowIndex == 0 ? 0 : 1, bottomWidth: rowIndex == (rows(ref) - 1) ? 0 : 1, strokeAlign: BorderSide.strokeAlignCenter, diff --git a/lib/shared/providers/day.dart b/lib/shared/providers/day.dart new file mode 100644 index 0000000..af5d912 --- /dev/null +++ b/lib/shared/providers/day.dart @@ -0,0 +1,15 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:timetable/core/constants/days.dart'; +import 'package:timetable/core/services/day.dart'; + +final dayServiceProvider = Provider((ref) { + return DaysService(ref); +}); + +final orderedDaysProvider = Provider>((ref) { + return ref.watch(dayServiceProvider).orderedDays; +}); + +final currentDayIndexProvider = StateProvider((ref) { + return ref.watch(dayServiceProvider).currentDayIndex; +}); diff --git a/lib/shared/widgets/bottom_sheets/days.dart b/lib/shared/widgets/bottom_sheets/days.dart index 13d7ecd..e4437ab 100644 --- a/lib/shared/widgets/bottom_sheets/days.dart +++ b/lib/shared/widgets/bottom_sheets/days.dart @@ -5,7 +5,7 @@ import 'package:timetable/core/constants/days.dart'; /// Bottom Sheet Modal Widget used to select a subject's day. class DaysModalBottomSheet extends StatelessWidget { - final ValueNotifier day; + final ValueNotifier day; const DaysModalBottomSheet({ super.key, @@ -17,15 +17,15 @@ class DaysModalBottomSheet extends StatelessWidget { return SubjectDataBottomSheet( title: "week_days".tr(), children: List.generate( - daysList.length, + Day.values.length, (i) { - final d = daysList[i]; + final d = Day.values[i]; bool isSelected = (d == day.value); return ListTile( title: Row( children: [ - Text(days[d.index]).tr(), + Text(d.name).tr(), const Spacer(), Visibility( visible: isSelected, diff --git a/pubspec.lock b/pubspec.lock index 978edf2..4434b2a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,18 +42,18 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: @@ -122,10 +122,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" charcode: dependency: transitive description: @@ -154,10 +154,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -170,10 +170,10 @@ packages: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -242,10 +242,10 @@ packages: dependency: "direct main" description: name: easy_localization - sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.7+1" easy_logger: dependency: transitive description: @@ -258,10 +258,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: @@ -406,10 +406,10 @@ packages: dependency: transitive description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -438,18 +438,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -486,10 +486,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -502,10 +502,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -534,10 +534,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: @@ -795,10 +795,10 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -835,10 +835,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" state_notifier: dependency: transitive description: @@ -851,10 +851,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -867,26 +867,26 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" timing: dependency: transitive description: @@ -955,10 +955,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "15.0.0" watcher: dependency: transitive description: @@ -1016,5 +1016,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.22.0" From 7b4af6b4fa7533e1e014d9eda67e12912d3e6c23 Mon Sep 17 00:00:00 2001 From: Username * Date: Mon, 21 Jul 2025 19:19:30 +0100 Subject: [PATCH 03/13] fix dropdown menu initial selections not updating with the locale --- lib/features/settings/widgets/default_view_options.dart | 2 ++ lib/features/settings/widgets/theme_options.dart | 2 ++ lib/features/settings/widgets/week_start_day_options.dart | 2 ++ 3 files changed, 6 insertions(+) diff --git a/lib/features/settings/widgets/default_view_options.dart b/lib/features/settings/widgets/default_view_options.dart index 55f8996..e5c4bf5 100644 --- a/lib/features/settings/widgets/default_view_options.dart +++ b/lib/features/settings/widgets/default_view_options.dart @@ -36,6 +36,8 @@ class DefaultViewOptions extends StatelessWidget { const Text('default_tb_view').tr(), const Spacer(), DropdownMenu( + // this is needed to change the initial selection with the new locale + key: ValueKey(context.locale), width: 130, dropdownMenuEntries: viewsEntries(), label: const Text("view").tr(), diff --git a/lib/features/settings/widgets/theme_options.dart b/lib/features/settings/widgets/theme_options.dart index b39d7d8..ec53b35 100644 --- a/lib/features/settings/widgets/theme_options.dart +++ b/lib/features/settings/widgets/theme_options.dart @@ -34,6 +34,8 @@ class ThemeOptions extends StatelessWidget { const Text('theme_mode').tr(), const Spacer(), DropdownMenu( + // this is needed to change the initial selection with the new locale + key: ValueKey(context.locale), width: 130, dropdownMenuEntries: themeEntries(), label: const Text("theme").tr(), diff --git a/lib/features/settings/widgets/week_start_day_options.dart b/lib/features/settings/widgets/week_start_day_options.dart index ed7f52b..a1925ab 100644 --- a/lib/features/settings/widgets/week_start_day_options.dart +++ b/lib/features/settings/widgets/week_start_day_options.dart @@ -37,6 +37,8 @@ class WeekStartDayOptions extends StatelessWidget { const Text('week_start_day').tr(), const Spacer(), DropdownMenu( + // this is needed to change the initial selection with the new locale + key: ValueKey(context.locale), width: 130, dropdownMenuEntries: weekStartDaysEntries(), label: const Text("day").plural(1), From 6e13d84a4ad79dff29005d3d935ac09717bda877 Mon Sep 17 00:00:00 2001 From: Username * Date: Mon, 21 Jul 2025 19:43:40 +0100 Subject: [PATCH 04/13] fix overlapping subjects builder not using weekStartDay --- .../grid_view/grid_view_overlapping_subjects_builder.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart index e2587c9..442a0da 100644 --- a/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart @@ -31,10 +31,11 @@ class OverlappingSubjBuilder extends ConsumerWidget { final customEndTime = ref.watch(settingsProvider).customEndTime; final compactMode = ref.watch(settingsProvider).compactMode; final orderedDays = ref.watch(orderedDaysProvider); + final weekStartDay = ref.watch(settingsProvider).weekStartDay; final shape = NonUniformBorder( // both subjects should theoretically be in the same day so it doesn't matter which one we choose - leftWidth: subjects[0].day.index == 0 ? 0 : 1, + leftWidth: subjects[0].day.index == weekStartDay ? 0 : 1, rightWidth: subjects[0].day.index == (orderedDays.length - 1) ? 0 : 1, topWidth: earlierStartTimeHour == getCustomStartTime(customStartTime, ref).hour From 1ec64ed52ae75a9806f4fb81204fee3051ca5cd7 Mon Sep 17 00:00:00 2001 From: Username * Date: Mon, 21 Jul 2025 19:45:13 +0100 Subject: [PATCH 05/13] fix null issue: change week start to mond if "hideSunday" is enabled while having sunday as the week start --- lib/core/services/day.dart | 10 +++--- .../widgets/week_start_day_options.dart | 22 ++++++++++-- lib/features/timetable/screens/grid.dart | 35 ++++++++++++------- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/lib/core/services/day.dart b/lib/core/services/day.dart index 8c0118e..467280d 100644 --- a/lib/core/services/day.dart +++ b/lib/core/services/day.dart @@ -11,15 +11,15 @@ class DaysService { final settings = ref.watch(settingsProvider); var days = Day.values; - if (settings.hideSunday) { - days = days.where((day) => day != Day.sunday).toList(); - } - - final rotatedDays = [ + var rotatedDays = [ ...days.sublist(settings.weekStartDay), ...days.sublist(0, settings.weekStartDay), ]; + if (settings.hideSunday) { + rotatedDays = rotatedDays.where((day) => day != Day.sunday).toList(); + } + return rotatedDays; } diff --git a/lib/features/settings/widgets/week_start_day_options.dart b/lib/features/settings/widgets/week_start_day_options.dart index a1925ab..62f7dae 100644 --- a/lib/features/settings/widgets/week_start_day_options.dart +++ b/lib/features/settings/widgets/week_start_day_options.dart @@ -1,10 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:timetable/core/constants/days.dart'; import 'package:timetable/features/settings/providers/settings.dart'; /// Week starting day options dropdown menu. -class WeekStartDayOptions extends StatelessWidget { +class WeekStartDayOptions extends HookConsumerWidget { final SettingsNotifier settings; final int defaultWeekStartDay; @@ -15,7 +17,20 @@ class WeekStartDayOptions extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final settingsState = ref.watch(settingsProvider); + final hideSunday = settingsState.hideSunday; + final weekStartDay = settingsState.weekStartDay; + + useEffect(() { + if (hideSunday && weekStartDay == 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + settings.updateWeekStartDay(1); + }); + } + return null; + }, [hideSunday, weekStartDay]); + List> weekStartDaysEntries() { final entries = >[]; const options = 2; @@ -40,9 +55,10 @@ class WeekStartDayOptions extends StatelessWidget { // this is needed to change the initial selection with the new locale key: ValueKey(context.locale), width: 130, + enabled: !hideSunday, dropdownMenuEntries: weekStartDaysEntries(), label: const Text("day").plural(1), - initialSelection: defaultWeekStartDay, + initialSelection: hideSunday ? 1 : weekStartDay, onSelected: (value) { settings.updateWeekStartDay(value!); }, diff --git a/lib/features/timetable/screens/grid.dart b/lib/features/timetable/screens/grid.dart index c5a59c8..710a12a 100644 --- a/lib/features/timetable/screens/grid.dart +++ b/lib/features/timetable/screens/grid.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:timetable/core/constants/days.dart'; import 'package:timetable/features/timetable/widgets/grid_view/grid.dart'; import 'package:timetable/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart'; import 'package:timetable/shared/providers/day.dart'; @@ -117,6 +118,7 @@ class TimetableGridView extends HookConsumerWidget { int totalHours, WidgetRef ref, ) { + final settings = ref.watch(settingsProvider); final overlappingSubjects = ref.watch(overlappingSubjectsProvider); final timetables = ref.watch(timetableProvider); final customStartTime = ref.watch(settingsProvider).customStartTime; @@ -126,6 +128,10 @@ class TimetableGridView extends HookConsumerWidget { for (var i = 0; i < orderedDays.length; i++) orderedDays[i]: i }; + final visibleSubjects = subjects.where((subject) { + return !settings.hideSunday || subject.day != Day.sunday; + }).toList(); + // subjects' containers final List> grid = List.generate( orderedDays.length, @@ -141,26 +147,27 @@ class TimetableGridView extends HookConsumerWidget { ), ); - overlappingSubjects.addAll(findOverlappingSubjects(subjects)); + final visibleOverlappingSubjects = findOverlappingSubjects(visibleSubjects); + overlappingSubjects.addAll(visibleOverlappingSubjects); // overlapping subjects - if (overlappingSubjects.isNotEmpty && - overlappingSubjects.any( - (e) => e.length == 2, - )) { + if (visibleOverlappingSubjects.isNotEmpty && + visibleOverlappingSubjects.any((e) => e.length == 2)) { filterOverlappingSubjectsByRotationWeeks( - overlappingSubjects, + visibleOverlappingSubjects, rotationWeek, ); filterOverlappingSubjectsByTimetable( - overlappingSubjects, + visibleOverlappingSubjects, currentTimetable, timetables, ); } - for (final subjects in overlappingSubjects) { - var day = subjects[0].day.index; + for (final subjects in visibleOverlappingSubjects) { + final columnIndex = dayToColumnIndex[subjects[0].day]; + if (columnIndex == null) continue; + var earlierStartTimeHour = getEarliestSubject(subjects).startTime.hour; var laterEndTimeHour = getLatestSubject(subjects).endTime.hour; @@ -168,7 +175,7 @@ class TimetableGridView extends HookConsumerWidget { getCustomStartTime(customStartTime, ref).hour; var endTime = getLatestSubject(subjects).endTime.hour - getCustomStartTime(customStartTime, ref).hour; - var column = grid[day]; + var column = grid[columnIndex]; column.replaceRange( startTime, @@ -188,10 +195,12 @@ class TimetableGridView extends HookConsumerWidget { // normal subjects for (final subject in filterOverlappingSubjects( - subjects, - overlappingSubjects, + visibleSubjects, + visibleOverlappingSubjects, )) { - final columnIndex = dayToColumnIndex[subject.day]!; + final columnIndex = dayToColumnIndex[subject.day]; + if (columnIndex == null) continue; + var start = subject.startTime.hour - getCustomStartTime(customStartTime, ref).hour; var end = From a04d2663965871b70e7e7cbd9293b263d98201f4 Mon Sep 17 00:00:00 2001 From: Username * Date: Mon, 21 Jul 2025 19:52:46 +0100 Subject: [PATCH 06/13] update doc --- .../grid_view/grid_view_overlapping_subjects_builder.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart index 442a0da..0e54344 100644 --- a/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart @@ -9,10 +9,10 @@ import 'package:timetable/shared/providers/day.dart'; /// A widget that builds overlapping subjects in the grid view. /// -/// takes a list of [SubjectData] and the earlier start time hour and +/// takes a list of [Subject] and the earlier start time hour and /// later end time hour of the overlapping subjects. /// -/// uses the [GridViewSubjectBuilder] to build each subject. +/// uses the [SubjectBuilder] to build each subject. class OverlappingSubjBuilder extends ConsumerWidget { final List subjects; final int earlierStartTimeHour; From 465362edd7e86140b07139ebb924b0b28e6c226e Mon Sep 17 00:00:00 2001 From: Username * Date: Mon, 21 Jul 2025 21:11:27 +0100 Subject: [PATCH 07/13] fix annoying error when swapping time when creating a subject --- lib/features/subjects/widgets/time.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/features/subjects/widgets/time.dart b/lib/features/subjects/widgets/time.dart index 0d6bc1f..9d92c9f 100644 --- a/lib/features/subjects/widgets/time.dart +++ b/lib/features/subjects/widgets/time.dart @@ -91,8 +91,9 @@ class TimeConfig extends ConsumerWidget { final temp = endTime.value; endTime.value = selectedTime; startTime.value = temp; + } else { + showInvalidTimePeriodDialog(); } - showInvalidTimePeriodDialog(); }, uses24HoursFormat: uses24HoursFormat, context: context, @@ -115,8 +116,9 @@ class TimeConfig extends ConsumerWidget { final temp = startTime.value; startTime.value = selectedTime; endTime.value = temp; + } else { + showInvalidTimePeriodDialog(); } - showInvalidTimePeriodDialog(); }, uses24HoursFormat: uses24HoursFormat, context: context, From bdee6804a2313db16b9d37d36b30da1607f6df3d Mon Sep 17 00:00:00 2001 From: Username * Date: Mon, 21 Jul 2025 21:14:40 +0100 Subject: [PATCH 08/13] FINALLY find a solution for overlapping subjects!!! (but overlapping time slots can't exceed 2 otherwise it's unreadable) --- lib/core/utils/overlapping_subjects.dart | 5 + lib/core/utils/subject_validation.dart | 53 +++++----- .../timetable/models/subject_positions.dart | 19 ++++ ...rid_view_overlapping_subjects_builder.dart | 98 ++++++++++++------- 4 files changed, 114 insertions(+), 61 deletions(-) create mode 100644 lib/features/timetable/models/subject_positions.dart diff --git a/lib/core/utils/overlapping_subjects.dart b/lib/core/utils/overlapping_subjects.dart index 66cbf01..8c80835 100644 --- a/lib/core/utils/overlapping_subjects.dart +++ b/lib/core/utils/overlapping_subjects.dart @@ -141,3 +141,8 @@ List> findOverlappingSubjects(List subjects) { return overlappingSubjects.where((e) => e.length > 1).toList(); } + +/// checks if 2 subjects overlap in hours +bool overlaps(Subject a, Subject b) { + return a.startTime.hour < b.endTime.hour && b.startTime.hour < a.endTime.hour; +} diff --git a/lib/core/utils/subject_validation.dart b/lib/core/utils/subject_validation.dart index 69da28c..233d51d 100644 --- a/lib/core/utils/subject_validation.dart +++ b/lib/core/utils/subject_validation.dart @@ -52,17 +52,6 @@ class SubjectValidation { timetable: timetable, ); - bool get isInOverlappingList => overlappingSubjects.any( - (group) => group.any((subject) => _tempSubject == subject), - ); - - bool get hasMultipleOccupants => - !isInOverlappingList && getConflictingSubjects().length > 1; - - bool get isOccupiedByOverlapping => getOverlappingConflicts().isNotEmpty; - - bool get isOccupiedByRegular => getRegularConflicts().isNotEmpty; - List getConflictingSubjects() { return subjectsInSameDay .where((s) => s != currentSubject) @@ -71,29 +60,37 @@ class SubjectValidation { .toList(); } - List getOverlappingConflicts() { - return subjectsInSameDay - .where((s) => overlappingSubjects.any((group) => group.contains(s))) - .where((s) => s.timetable == _tempSubject.timetable) - .where((s) => hasTimeConflict(s)) - .toList(); + bool hasTimeConflict(Subject subject) { + final subjectHours = getHoursList(subject.startTime, subject.endTime); + return hasTimeOverlap(subjectHours, inputHours); } - List getRegularConflicts() { - return subjectsInSameDay + bool get wouldExceedMaxOverlap { + if (currentSubject != null) return false; + + final allSubjectsInDay = subjectsInSameDay .where((s) => s != currentSubject) - .where((s) => !overlappingSubjects.any((group) => group.contains(s))) .where((s) => s.timetable == _tempSubject.timetable) - .where((s) => hasTimeConflict(s)) .toList(); - } - bool hasTimeConflict(Subject subject) { - final subjectHours = getHoursList(subject.startTime, subject.endTime); - return hasTimeOverlap(subjectHours, inputHours); + if (allSubjectsInDay.isEmpty) return false; + + for (final hour in inputHours) { + int overlapCount = 1; + + for (final subject in allSubjectsInDay) { + final subjectHours = getHoursList(subject.startTime, subject.endTime); + if (subjectHours.contains(hour)) { + overlapCount++; + } + } + + if (overlapCount > 3) return true; + } + + return false; } - bool get hasConflicts => isOccupiedByOverlapping && hasMultipleOccupants; - bool get hasConflictsForExisting => - isOccupiedByRegular || hasMultipleOccupants; + bool get hasConflicts => wouldExceedMaxOverlap; + bool get hasConflictsForExisting => false; } diff --git a/lib/features/timetable/models/subject_positions.dart b/lib/features/timetable/models/subject_positions.dart new file mode 100644 index 0000000..93f807a --- /dev/null +++ b/lib/features/timetable/models/subject_positions.dart @@ -0,0 +1,19 @@ +import 'package:timetable/core/db/database.dart'; + +class SubjectPosition { + final Subject subject; + final double left; + final double top; + final double width; + final double height; + final int column; + + SubjectPosition({ + required this.subject, + required this.left, + required this.top, + required this.width, + required this.height, + required this.column, + }); +} diff --git a/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart index 0e54344..4a2e0b8 100644 --- a/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:non_uniform_border/non_uniform_border.dart'; import 'package:timetable/core/constants/custom_times.dart'; +import 'package:timetable/core/utils/overlapping_subjects.dart'; +import 'package:timetable/features/timetable/models/subject_positions.dart'; import 'package:timetable/features/timetable/widgets/grid_view/grid_view_subject_builder.dart'; import 'package:timetable/core/db/database.dart'; import 'package:timetable/features/settings/providers/settings.dart'; @@ -54,44 +56,74 @@ class OverlappingSubjBuilder extends ConsumerWidget { return Container( decoration: ShapeDecoration(shape: shape), height: totalHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: subjects.map((subject) { - final offset = subject.startTime.hour - earlierStartTimeHour; - final duration = subject.endTime.hour - subject.startTime.hour; + child: LayoutBuilder( + builder: (context, constraints) { + final positions = + calculatePositions(subjects, earlierStartTimeHour, baseHeight); - return Expanded( - child: Column( - children: [ - if (offset > 0) SizedBox(height: offset * baseHeight), - Padding( - padding: const EdgeInsets.fromLTRB(1, 2, 1, 2), - child: SizedBox( - // we subtract 4 because it's the the top + bot padding - // shape decoration is 1 pixel on top if the later end time hour is equal to the custom timetable end time hour - // and is 1 pixel on the bottom if the earlier start time hour is equal to the custom timetable start time hour - // so we subtract all of that if the conditions are met and everything should be perfectly aligned! - height: duration * baseHeight - - 4 - - (laterEndTimeHour == - getCustomEndTime(customEndTime, ref).hour - ? 0 - : 1) - - (earlierStartTimeHour == - getCustomStartTime(customStartTime, ref).hour - ? 0 - : 1), - child: SubjectBuilder( - subject: subject, - isOverlapping: true, - ), + return Stack( + children: positions.map((pos) { + return Positioned( + left: pos.left * constraints.maxWidth, + top: pos.top, + width: pos.width * constraints.maxWidth, + height: pos.height, + child: Padding( + padding: const EdgeInsets.all(1), + child: SubjectBuilder( + subject: pos.subject, + isOverlapping: true, ), ), - ], - ), + ); + }).toList(), ); - }).toList(), + }, ), ); } + + List calculatePositions( + List subjects, int baseStartHour, double baseHeight) { + // sort subjects by start time then by duration + final sorted = subjects.toList() + ..sort((a, b) { + final startComp = a.startTime.hour.compareTo(b.startTime.hour); + if (startComp != 0) return startComp; + return (b.endTime.hour - b.startTime.hour) + .compareTo(a.endTime.hour - a.startTime.hour); + }); + + final positions = []; + final columns = >[]; + + for (final subject in sorted) { + // find first column where this subject doesn't overlap with the last subject + int columnIndex = 0; + while (columnIndex < columns.length && + overlaps(columns[columnIndex].last, subject)) { + columnIndex++; + } + + if (columnIndex >= columns.length) { + columns.add([]); + } + + columns[columnIndex].add(subject); + + final offset = subject.startTime.hour - baseStartHour; + final duration = subject.endTime.hour - subject.startTime.hour; + + positions.add(SubjectPosition( + subject: subject, + left: columnIndex / columns.length, + top: offset * baseHeight, + width: 1.0 / columns.length, + height: duration * baseHeight, + column: columnIndex, + )); + } + + return positions; + } } From f580b9eb957d15d656651dfbd9c702dffb28b194 Mon Sep 17 00:00:00 2001 From: Username * Date: Tue, 22 Jul 2025 17:37:48 +0100 Subject: [PATCH 09/13] fix cancel button not doing anything in a couple alert dialogs --- lib/features/settings/widgets/timetable_data.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/features/settings/widgets/timetable_data.dart b/lib/features/settings/widgets/timetable_data.dart index 734a1fd..e1dc457 100644 --- a/lib/features/settings/widgets/timetable_data.dart +++ b/lib/features/settings/widgets/timetable_data.dart @@ -38,6 +38,7 @@ class TimetableDataOptions extends ConsumerWidget { "${"restore_backup_dialog.dialog_1".tr()}\n${"restore_backup_dialog.dialog_2".tr()}", ), approveButtonText: "proceed".tr(), + onCancel: () => Navigator.of(context).pop(), onApprove: () async { final nav = Navigator.of(context); @@ -65,6 +66,7 @@ class TimetableDataOptions extends ConsumerWidget { return ShowAlertDialog( content: const Text("remove_all_subjects_dialog").tr(), approveButtonText: "delete".tr(), + onCancel: () => Navigator.of(context).pop(), onApprove: () async { final navigator = Navigator.of(context); await subjNotifier.resetData().then((_) => navigator.pop()); From cd23b4ab89986be26c6db63dd17dc7362b6c524a Mon Sep 17 00:00:00 2001 From: Username * Date: Tue, 22 Jul 2025 17:44:49 +0100 Subject: [PATCH 10/13] move rotation week indicator to bottom left for readability --- .../timetable/widgets/grid_view/grid_view_subject_builder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart index 72622a6..19901ab 100644 --- a/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart @@ -200,7 +200,7 @@ class SubjectBuilder extends ConsumerWidget { if (rotationWeeks) const Spacer(), if (rotationWeeks && !hideTransparentSubjects) Align( - alignment: Alignment.bottomRight, + alignment: Alignment.bottomLeft, child: RotatedBox( quarterTurns: quarterTurns, child: Text( From a383e3ecdb73641485f572e337e22ce3ef338734 Mon Sep 17 00:00:00 2001 From: Username * Date: Tue, 22 Jul 2025 17:48:56 +0100 Subject: [PATCH 11/13] fix subject validation with overlapping subjects --- lib/core/utils/subject_validation.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/core/utils/subject_validation.dart b/lib/core/utils/subject_validation.dart index 233d51d..16cea1e 100644 --- a/lib/core/utils/subject_validation.dart +++ b/lib/core/utils/subject_validation.dart @@ -66,8 +66,6 @@ class SubjectValidation { } bool get wouldExceedMaxOverlap { - if (currentSubject != null) return false; - final allSubjectsInDay = subjectsInSameDay .where((s) => s != currentSubject) .where((s) => s.timetable == _tempSubject.timetable) @@ -85,7 +83,7 @@ class SubjectValidation { } } - if (overlapCount > 3) return true; + if (overlapCount > 2) return true; } return false; From 5985a82e119d0d114dbd3b66cf7f39d5e4a886da Mon Sep 17 00:00:00 2001 From: Username * Date: Tue, 22 Jul 2025 19:03:34 +0100 Subject: [PATCH 12/13] remove useless/duplicate functions --- lib/core/constants/basic_subject.dart | 2 +- lib/core/constants/rotation_weeks.dart | 12 +++++--- lib/core/constants/theme_options.dart | 9 ++++-- lib/core/constants/timetable_views.dart | 11 +++++-- .../{constants => utils}/custom_times.dart | 0 lib/core/utils/overlapping_subjects.dart | 2 -- lib/core/utils/rotation_weeks.dart | 29 +------------------ lib/core/utils/subject_validation.dart | 28 +----------------- lib/core/utils/themes.dart | 13 --------- lib/core/utils/time_management.dart | 5 ---- lib/core/utils/views.dart | 12 -------- .../widgets/default_view_options.dart | 6 ++-- .../settings/widgets/theme_options.dart | 6 ++-- lib/features/subjects/widgets/time.dart | 2 +- lib/features/timetable/screens/day.dart | 2 +- lib/features/timetable/screens/grid.dart | 2 +- lib/features/timetable/screens/timetable.dart | 2 +- .../day_view/day_view_subject_builder.dart | 3 +- ...rid_view_overlapping_subjects_builder.dart | 2 +- .../grid_view/grid_view_subject_builder.dart | 5 ++-- .../widgets/bottom_sheets/rotation_week.dart | 2 +- lib/shared/widgets/rotation_week_toggle.dart | 6 ++-- 22 files changed, 42 insertions(+), 119 deletions(-) rename lib/core/{constants => utils}/custom_times.dart (100%) delete mode 100644 lib/core/utils/themes.dart delete mode 100644 lib/core/utils/views.dart diff --git a/lib/core/constants/basic_subject.dart b/lib/core/constants/basic_subject.dart index 458ce3f..96b2380 100644 --- a/lib/core/constants/basic_subject.dart +++ b/lib/core/constants/basic_subject.dart @@ -13,7 +13,7 @@ const basicSubject = Subject( startTime: TimeOfDay(hour: 8, minute: 0), endTime: TimeOfDay(hour: 9, minute: 0), day: Day.sunday, - rotationWeek: RotationWeeks.all, + rotationWeek: RotationWeeks.none, note: "", timetable: "1", ); diff --git a/lib/core/constants/rotation_weeks.dart b/lib/core/constants/rotation_weeks.dart index 9d87fd4..a15501f 100644 --- a/lib/core/constants/rotation_weeks.dart +++ b/lib/core/constants/rotation_weeks.dart @@ -1,7 +1,11 @@ /// Basic Rotation Weeks definition. enum RotationWeeks { - all, - none, - a, - b, + none("all"), + a("A"), + b("B"); + + final String name; + const RotationWeeks(this.name); + + String get displayName => name == "all" ? "" : name; } diff --git a/lib/core/constants/theme_options.dart b/lib/core/constants/theme_options.dart index 784d827..03465fa 100644 --- a/lib/core/constants/theme_options.dart +++ b/lib/core/constants/theme_options.dart @@ -1,6 +1,9 @@ /// Theme options. enum ThemeOption { - dark, - light, - auto, + dark("dark"), + light("light"), + auto("system"); + + final String name; + const ThemeOption(this.name); } diff --git a/lib/core/constants/timetable_views.dart b/lib/core/constants/timetable_views.dart index 56ba8ec..ef32a12 100644 --- a/lib/core/constants/timetable_views.dart +++ b/lib/core/constants/timetable_views.dart @@ -1,5 +1,12 @@ +import 'package:easy_localization/easy_localization.dart'; + /// Timetable Views enum TbViews { - grid, - day, + grid("grid"), + day("day"); + + final String name; + const TbViews(this.name); + + String get label => name.tr(); } diff --git a/lib/core/constants/custom_times.dart b/lib/core/utils/custom_times.dart similarity index 100% rename from lib/core/constants/custom_times.dart rename to lib/core/utils/custom_times.dart diff --git a/lib/core/utils/overlapping_subjects.dart b/lib/core/utils/overlapping_subjects.dart index 8c80835..59fbd19 100644 --- a/lib/core/utils/overlapping_subjects.dart +++ b/lib/core/utils/overlapping_subjects.dart @@ -49,8 +49,6 @@ void filterOverlappingSubjectsByRotationWeeks( return e.rotationWeek == RotationWeeks.b; case RotationWeeks.b: return e.rotationWeek == RotationWeeks.a; - case RotationWeeks.all: - return false; case RotationWeeks.none: return false; } diff --git a/lib/core/utils/rotation_weeks.dart b/lib/core/utils/rotation_weeks.dart index 6c65999..e2c6b92 100644 --- a/lib/core/utils/rotation_weeks.dart +++ b/lib/core/utils/rotation_weeks.dart @@ -12,8 +12,6 @@ String getRotationWeekLabel(RotationWeeks rotationWeek) { return "${"week".tr()} A"; case RotationWeeks.b: return "${"week".tr()} B"; - default: - return "all_weeks".tr(); } } @@ -26,8 +24,6 @@ String getRotationWeekButtonLabel(RotationWeeks rotationWeek) { return "A"; case RotationWeeks.b: return "B"; - default: - return "all".tr(); } } @@ -37,7 +33,7 @@ List getFilteredByRotationWeeksSubjects( List allSubjects, ) { switch (rotationWeek.value) { - case RotationWeeks.all: + case RotationWeeks.none: return allSubjects .where( (s) => @@ -62,28 +58,5 @@ List getFilteredByRotationWeeksSubjects( s.rotationWeek == RotationWeeks.b, ) .toList(); - default: - return allSubjects - .where( - (s) => - s.rotationWeek == RotationWeeks.none || - s.rotationWeek == RotationWeeks.a || - s.rotationWeek == RotationWeeks.b, - ) - .toList(); - } -} - -/// Returns rotation week label of a Subject. -String getSubjectRotationWeekLabel(Subject subject) { - switch (subject.rotationWeek) { - case RotationWeeks.a: - return "A"; - case RotationWeeks.b: - return "B"; - case RotationWeeks.none: - return ""; - default: - return ""; } } diff --git a/lib/core/utils/subject_validation.dart b/lib/core/utils/subject_validation.dart index 16cea1e..e77ef59 100644 --- a/lib/core/utils/subject_validation.dart +++ b/lib/core/utils/subject_validation.dart @@ -39,36 +39,10 @@ class SubjectValidation { required this.overlappingSubjects, }); - Subject get _tempSubject => Subject( - id: subjectId, - label: label, - location: location, - color: color, - startTime: startTime, - endTime: endTime, - day: day, - rotationWeek: rotationWeek, - note: note, - timetable: timetable, - ); - - List getConflictingSubjects() { - return subjectsInSameDay - .where((s) => s != currentSubject) - .where((s) => s.timetable == _tempSubject.timetable) - .where((s) => hasTimeConflict(s)) - .toList(); - } - - bool hasTimeConflict(Subject subject) { - final subjectHours = getHoursList(subject.startTime, subject.endTime); - return hasTimeOverlap(subjectHours, inputHours); - } - bool get wouldExceedMaxOverlap { final allSubjectsInDay = subjectsInSameDay .where((s) => s != currentSubject) - .where((s) => s.timetable == _tempSubject.timetable) + .where((s) => s.timetable == timetable) .toList(); if (allSubjectsInDay.isEmpty) return false; diff --git a/lib/core/utils/themes.dart b/lib/core/utils/themes.dart deleted file mode 100644 index 7262a23..0000000 --- a/lib/core/utils/themes.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:timetable/core/constants/theme_options.dart'; - -/// for each value of the enum it will give a corresponding label. -String getThemeLabel(ThemeOption theme) { - switch (theme) { - case ThemeOption.dark: - return 'dark'; - case ThemeOption.auto: - return 'system'; - case ThemeOption.light: - return 'light'; - } -} diff --git a/lib/core/utils/time_management.dart b/lib/core/utils/time_management.dart index 734995b..665cb92 100644 --- a/lib/core/utils/time_management.dart +++ b/lib/core/utils/time_management.dart @@ -21,8 +21,3 @@ List getHoursList(TimeOfDay start, TimeOfDay end) { (index) => index + start.hour, ); } - -// check if two time slots overlap -bool hasTimeOverlap(List hours1, List hours2) { - return hours1.any((hour) => hours2.contains(hour)); -} diff --git a/lib/core/utils/views.dart b/lib/core/utils/views.dart deleted file mode 100644 index 33162d4..0000000 --- a/lib/core/utils/views.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:timetable/core/constants/timetable_views.dart'; - -/// for each value of the enum it will give a corresponding label. -String getViewLabel(TbViews view) { - switch (view) { - case TbViews.grid: - return 'grid'.tr(); - case TbViews.day: - return 'day'.plural(1); - } -} diff --git a/lib/features/settings/widgets/default_view_options.dart b/lib/features/settings/widgets/default_view_options.dart index e5c4bf5..89ef17d 100644 --- a/lib/features/settings/widgets/default_view_options.dart +++ b/lib/features/settings/widgets/default_view_options.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:timetable/core/constants/timetable_views.dart'; -import 'package:timetable/core/utils/views.dart'; import 'package:timetable/features/settings/providers/settings.dart'; /// default timetable view options dropdown menu. @@ -22,10 +21,9 @@ class DefaultViewOptions extends StatelessWidget { final viewEntries = >[]; for (final TbViews option in TbViews.values) { - final label = getViewLabel(option); - viewEntries.add( - DropdownMenuEntry(value: option, label: label), + DropdownMenuEntry( + value: option, label: option.name.plural(1)), ); } return viewEntries.toList(); diff --git a/lib/features/settings/widgets/theme_options.dart b/lib/features/settings/widgets/theme_options.dart index ec53b35..b5173cc 100644 --- a/lib/features/settings/widgets/theme_options.dart +++ b/lib/features/settings/widgets/theme_options.dart @@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:timetable/core/constants/theme_options.dart'; import 'package:timetable/shared/providers/themes.dart'; -import 'package:timetable/core/utils/themes.dart'; /// app theme me options dropdown menu. class ThemeOptions extends StatelessWidget { @@ -20,10 +19,9 @@ class ThemeOptions extends StatelessWidget { final themeEntries = >[]; for (final ThemeOption option in ThemeOption.values) { - final label = getThemeLabel(option).tr(); - themeEntries.add( - DropdownMenuEntry(value: option, label: label), + DropdownMenuEntry( + value: option, label: option.name.tr()), ); } return themeEntries.toList(); diff --git a/lib/features/subjects/widgets/time.dart b/lib/features/subjects/widgets/time.dart index 9d92c9f..ebc7eb5 100644 --- a/lib/features/subjects/widgets/time.dart +++ b/lib/features/subjects/widgets/time.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:timetable/core/utils/time_formatter.dart'; import 'package:timetable/shared/widgets/act_chip.dart'; import 'package:timetable/core/utils/time_management.dart'; -import 'package:timetable/core/constants/custom_times.dart'; +import 'package:timetable/core/utils/custom_times.dart'; import 'package:timetable/features/settings/providers/settings.dart'; /// Time configuration part of the Subject creation screen. diff --git a/lib/features/timetable/screens/day.dart b/lib/features/timetable/screens/day.dart index 9236b3a..f072047 100644 --- a/lib/features/timetable/screens/day.dart +++ b/lib/features/timetable/screens/day.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:timetable/features/subjects/screens/subject_screen.dart'; import 'package:timetable/features/timetable/widgets/day_view/day_view_subject_builder.dart'; -import 'package:timetable/core/constants/custom_times.dart'; +import 'package:timetable/core/utils/custom_times.dart'; import 'package:timetable/core/constants/days.dart'; import 'package:timetable/core/constants/error_emoticons.dart'; import 'package:timetable/core/constants/rotation_weeks.dart'; diff --git a/lib/features/timetable/screens/grid.dart b/lib/features/timetable/screens/grid.dart index 710a12a..5d56679 100644 --- a/lib/features/timetable/screens/grid.dart +++ b/lib/features/timetable/screens/grid.dart @@ -5,7 +5,7 @@ import 'package:timetable/features/timetable/widgets/grid_view/grid.dart'; import 'package:timetable/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart'; import 'package:timetable/shared/providers/day.dart'; import 'package:timetable/shared/widgets/time_column.dart'; -import 'package:timetable/core/constants/custom_times.dart'; +import 'package:timetable/core/utils/custom_times.dart'; import 'package:timetable/core/constants/grid_properties.dart'; import 'package:timetable/core/utils/overlapping_subjects.dart'; import 'package:timetable/core/constants/rotation_weeks.dart'; diff --git a/lib/features/timetable/screens/timetable.dart b/lib/features/timetable/screens/timetable.dart index 386a522..143c6a2 100644 --- a/lib/features/timetable/screens/timetable.dart +++ b/lib/features/timetable/screens/timetable.dart @@ -30,7 +30,7 @@ class TimetableScreen extends HookConsumerWidget { final timetables = ref.watch(timetableProvider); final isGridView = useState(defaultTimetableView == TbViews.grid); - final rotationWeek = useState(RotationWeeks.all); + final rotationWeek = useState(RotationWeeks.none); final currentTimetable = useState(timetables[0]); final PageController controller; diff --git a/lib/features/timetable/widgets/day_view/day_view_subject_builder.dart b/lib/features/timetable/widgets/day_view/day_view_subject_builder.dart index d556eb9..97a008b 100644 --- a/lib/features/timetable/widgets/day_view/day_view_subject_builder.dart +++ b/lib/features/timetable/widgets/day_view/day_view_subject_builder.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/core/utils/rotation_weeks.dart'; import 'package:timetable/core/db/database.dart'; import 'package:timetable/features/settings/providers/settings.dart'; import 'package:timetable/features/subjects/screens/subject_screen.dart'; @@ -82,7 +81,7 @@ class DayViewSubjectBuilder extends ConsumerWidget { ), if (rotationWeeks) Text( - getSubjectRotationWeekLabel(subject), + subject.rotationWeek.displayName, style: TextStyle( color: labelColor, fontSize: 20, diff --git a/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart index 4a2e0b8..d218396 100644 --- a/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:non_uniform_border/non_uniform_border.dart'; -import 'package:timetable/core/constants/custom_times.dart'; +import 'package:timetable/core/utils/custom_times.dart'; import 'package:timetable/core/utils/overlapping_subjects.dart'; import 'package:timetable/features/timetable/models/subject_positions.dart'; import 'package:timetable/features/timetable/widgets/grid_view/grid_view_subject_builder.dart'; diff --git a/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart index 19901ab..e8f5edb 100644 --- a/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart @@ -3,8 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:non_uniform_border/non_uniform_border.dart'; import 'package:timetable/shared/providers/day.dart'; import 'package:timetable/shared/widgets/bottom_sheets/subject_management.dart'; -import 'package:timetable/core/constants/custom_times.dart'; -import 'package:timetable/core/utils/rotation_weeks.dart'; +import 'package:timetable/core/utils/custom_times.dart'; import 'package:timetable/core/db/database.dart'; import 'package:timetable/features/settings/providers/settings.dart'; @@ -204,7 +203,7 @@ class SubjectBuilder extends ConsumerWidget { child: RotatedBox( quarterTurns: quarterTurns, child: Text( - getSubjectRotationWeekLabel(subject), + subject.rotationWeek.displayName, style: TextStyle( color: subLabelsColor, fontWeight: FontWeight.bold, diff --git a/lib/shared/widgets/bottom_sheets/rotation_week.dart b/lib/shared/widgets/bottom_sheets/rotation_week.dart index 343a488..9b821cc 100644 --- a/lib/shared/widgets/bottom_sheets/rotation_week.dart +++ b/lib/shared/widgets/bottom_sheets/rotation_week.dart @@ -18,7 +18,7 @@ class RotationWeekModalBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { /// actual usable rotation weeks - final rws = rotationWeeks.where((r) => r != RotationWeeks.all).toList(); + final rws = rotationWeeks.where((r) => r != RotationWeeks.none).toList(); return SubjectDataBottomSheet( title: "rotation_week".plural(2), diff --git a/lib/shared/widgets/rotation_week_toggle.dart b/lib/shared/widgets/rotation_week_toggle.dart index cf7e665..f15a628 100644 --- a/lib/shared/widgets/rotation_week_toggle.dart +++ b/lib/shared/widgets/rotation_week_toggle.dart @@ -1,7 +1,7 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:timetable/core/constants/rotation_weeks.dart'; -import 'package:timetable/core/utils/rotation_weeks.dart'; import 'package:timetable/core/utils/subjects.dart'; /// Toggles between all rotation weeks. @@ -18,7 +18,7 @@ class RotationWeekToggle extends HookWidget { Widget build(BuildContext context) { final clickCount = useState(0); List labels = [ - RotationWeeks.all, + RotationWeeks.none, RotationWeeks.a, RotationWeeks.b, ]; @@ -34,7 +34,7 @@ class RotationWeekToggle extends HookWidget { height: 40, child: Center( child: Text( - getRotationWeekButtonLabel(labels[clickCount.value]), + labels[clickCount.value].name.tr(), overflow: TextOverflow.clip, softWrap: false, style: const TextStyle( From 6d56cc9b7a2cf43b20b36d4a83136195cf59e719 Mon Sep 17 00:00:00 2001 From: Username * Date: Tue, 19 Aug 2025 14:23:35 +0100 Subject: [PATCH 13/13] fix: missing rotation week option when creating subject removed a filter that removed `RotationWeeks.none` (leftover after merging `RotationWeeks.all` and `RotationWeeks.none`) --- lib/shared/widgets/bottom_sheets/rotation_week.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/shared/widgets/bottom_sheets/rotation_week.dart b/lib/shared/widgets/bottom_sheets/rotation_week.dart index 9b821cc..b461f12 100644 --- a/lib/shared/widgets/bottom_sheets/rotation_week.dart +++ b/lib/shared/widgets/bottom_sheets/rotation_week.dart @@ -17,13 +17,10 @@ class RotationWeekModalBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { - /// actual usable rotation weeks - final rws = rotationWeeks.where((r) => r != RotationWeeks.none).toList(); - return SubjectDataBottomSheet( title: "rotation_week".plural(2), - children: List.generate(rws.length, (i) { - final rw = rws[i]; + children: List.generate(rotationWeeks.length, (i) { + final rw = rotationWeeks[i]; bool isSelected = (rw == rotationWeek.value); return ListTile(