diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5499de9..d1739fc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,36 +1,34 @@ name: Android Build and Artifact - on: push: branches: - main - dev - jobs: build: name: Android Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: "17" distribution: "zulu" cache: "gradle" - - uses: subosito/flutter-action@v2 with: channel: "stable" cache: true - - name: Set up Flutter run: flutter pub get - - name: Build APKs run: flutter build apk --release --split-per-abi - + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + - name: Install build tools + run: | + echo "y" | sdkmanager "build-tools;33.0.0" "platforms;android-33" - uses: ilharp/sign-android-release@v1 name: Sign app APK with: @@ -40,21 +38,18 @@ jobs: keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} buildToolsVersion: 33.0.0 - - name: Archive arm64-v8a uses: actions/upload-artifact@v4 with: name: android-arm64-v8a path: | build/app/outputs/flutter-apk/app-arm64-v8a-release-signed.apk - - name: Archive armeabi-v7a uses: actions/upload-artifact@v4 with: name: android-armeabi-v7a path: | build/app/outputs/flutter-apk/*-armeabi-v7a-release-signed.apk - - name: Archive x86_64 uses: actions/upload-artifact@v4 with: diff --git a/android/settings.gradle b/android/settings.gradle index 54b690d..bd473bb 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.1.0" apply false + id "com.android.application" version "8.2.1" apply false id "org.jetbrains.kotlin.android" version "1.9.0" apply false } diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 065bad1..3b31491 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -110,6 +110,7 @@ "proceed": "Proceed", "delete": "Delete", "cancel": "Cancel", + "undo": "Undo", "manage": "Manage", "configuration": "Configuration", diff --git a/assets/translations/fr-FR.json b/assets/translations/fr-FR.json index 65320cc..546869e 100644 --- a/assets/translations/fr-FR.json +++ b/assets/translations/fr-FR.json @@ -110,6 +110,7 @@ "proceed": "Continuer", "delete": "Supprimer", "cancel": "Annuler", + "undo": "Défaire", "manage": "Gérer", "configuration": "Configuration", diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..9fd2b53 --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,50 @@ +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:timetable/features/navigation/bottom_navigation.dart'; +import 'package:timetable/shared/widgets/eager_initilization.dart'; +import 'package:timetable/core/utils/theme_helper.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; +import 'package:timetable/shared/providers/themes.dart'; + +/// The main class of the application. +class TimetableApp extends ConsumerWidget { + const TimetableApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.watch(themeProvider); + final monetTheming = ref.watch(settingsProvider).monetTheming; + final appThemeColor = ref.watch(settingsProvider).appThemeColor; + final Brightness systemBrightness = + MediaQuery.of(context).platformBrightness; + + return DynamicColorBuilder( + builder: ( + ColorScheme? lightDynamic, + ColorScheme? darkDynamic, + ) { + return MaterialApp( + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + title: 'Timetable', + color: Colors.white, + theme: ThemeData( + colorScheme: ThemeHelper.getColorScheme( + monetTheming: monetTheming, + theme: theme, + systemBrightness: systemBrightness, + lightDynamic: lightDynamic, + darkDynamic: darkDynamic, + appThemeColor: appThemeColor, + ), + useMaterial3: true, + ), + home: const EagerInitialization(child: BottomNavigation()), + ); + }, + ); + } +} diff --git a/lib/components/settings/customize_timetable.dart b/lib/components/settings/customize_timetable.dart deleted file mode 100644 index 6dd270d..0000000 --- a/lib/components/settings/customize_timetable.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/settings/default_view_options.dart'; -import 'package:timetable/provider/settings.dart'; - -/// All the settings that allow for customizing the timetable. -class CustomizeTimetableOptions extends ConsumerWidget { - const CustomizeTimetableOptions({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final settings = ref.read(settingsProvider.notifier); - final hideSunday = ref.watch(settingsProvider).hideSunday; - final compactMode = ref.watch(settingsProvider).compactMode; - final hideLocation = ref.watch(settingsProvider).hideLocation; - final singleLetterDays = ref.watch(settingsProvider).singleLetterDays; - final hideTransparentSubject = - ref.watch(settingsProvider).hideTransparentSubject; - final defaultTimetableView = - ref.watch(settingsProvider).defaultTimetableView; - - return Column( - children: [ - ListTile( - leading: const Icon( - Icons.table_chart_outlined, - size: 20, - ), - horizontalTitleGap: 8, - title: DefaultViewOptions( - settings: settings, - defaultTimetableView: defaultTimetableView, - ), - onTap: () {}, - ), - SwitchListTile( - secondary: const Icon( - Icons.close_fullscreen_outlined, - size: 20, - ), - visualDensity: const VisualDensity(horizontal: -4), - title: const Text("compact_mode").tr(), - value: compactMode, - onChanged: (bool value) { - settings.updateCompactMode(value); - }, - ), - SwitchListTile( - secondary: const Icon( - Icons.title_outlined, - size: 20, - ), - visualDensity: const VisualDensity(horizontal: -4), - title: const Text("single_letter_days").tr(), - value: singleLetterDays, - onChanged: (bool value) { - settings.updateSingleLetterDays(value); - }, - ), - SwitchListTile( - secondary: const Icon( - Icons.location_off_outlined, - size: 20, - ), - visualDensity: const VisualDensity(horizontal: -4), - title: const Text("hide_locations").tr(), - value: hideLocation, - onChanged: (bool value) { - settings.updateHideLocation(value); - }, - ), - SwitchListTile( - secondary: const Icon( - Icons.visibility_off_outlined, - size: 20, - ), - visualDensity: const VisualDensity(horizontal: -4), - title: const Text("hide_sunday").tr(), - value: hideSunday, - onChanged: (bool value) { - settings.updateHideSunday(value); - }, - ), - SwitchListTile( - secondary: const Icon( - Icons.visibility_off_outlined, - size: 20, - ), - visualDensity: const VisualDensity(horizontal: -4), - title: const Text("hide_transparent_subjects").tr(), - value: hideTransparentSubject, - onChanged: (bool value) { - settings.updateHideTransparentSubject(value); - }, - ), - ], - ); - } -} diff --git a/lib/components/settings/language_options.dart b/lib/components/settings/language_options.dart deleted file mode 100644 index df34993..0000000 --- a/lib/components/settings/language_options.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:timetable/constants/languages.dart'; -import 'package:timetable/helpers/languages.dart'; -import 'package:timetable/provider/language.dart'; - -/// app language options dropdown menu. -class LanguageOptions extends StatelessWidget { - final LanguageNotifier language; - - const LanguageOptions({ - super.key, - required this.language, - }); - - @override - Widget build(BuildContext context) { - /// The dropdown menu entries of each language. - List> languageEntries() { - final themeEntries = >[]; - - for (final Locale option in languages) { - final label = getLanguageLabel(option); - - themeEntries.add( - DropdownMenuEntry( - value: option, - label: label, - ), - ); - } - return themeEntries.toList(); - } - - return Row( - children: [ - const Text('language').tr(), - const Spacer(), - DropdownMenu( - width: 130, - dropdownMenuEntries: languageEntries(), - label: const Text("language").tr(), - initialSelection: language.getLanguage(), - onSelected: (value) { - language.changeLanguage(value!); - context.setLocale(value); - // https://github.com/aissat/easy_localization/issues/370 - // https://github.com/aissat/easy_localization/issues/370#issuecomment-1312480842 - // couldn't find any other solution but reloading the app when changing the language - // maybe an issue from easy localization itself (?) - - final engine = WidgetsFlutterBinding.ensureInitialized(); - engine.performReassemble(); - }, - ), - ], - ); - } -} diff --git a/lib/components/subject_management/subject_configs/colors_screens/preset_colors.dart b/lib/components/subject_management/subject_configs/colors_screens/preset_colors.dart deleted file mode 100644 index 1cd0fd2..0000000 --- a/lib/components/subject_management/subject_configs/colors_screens/preset_colors.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:timetable/constants/colors.dart'; - -/// Preset colors screen for the color configuration screen. -class PresetColorsScreen extends HookWidget { - /// the color that will be changed by one of the presets - final ValueNotifier color; - final List colors; - final int colorsPerRow; - - const PresetColorsScreen({ - super.key, - required this.color, - required this.colors, - this.colorsPerRow = 3, - }); - - @override - Widget build(BuildContext context) { - /// used for highlighting the selected color by default no color is selected - // the default color is black, maybe i should have it as the default selected color - final selectedColorIndex = useState(-1); - const int colorsPerRow = 3; - final int rowCount = (colors.length / colorsPerRow).ceil(); - - return Scaffold( - body: ListView( - padding: const EdgeInsets.all(16.0), - scrollDirection: Axis.vertical, - children: [ - Column( - children: List.generate( - rowCount, - (rowIndex) { - return Row( - children: List.generate( - colorsPerRow, - (colIndex) { - final index = (rowIndex * colorsPerRow + colIndex); - final isCurrentColor = colors[index].color == color.value; - - const topleft = - BorderRadius.only(topLeft: Radius.circular(10)); - const topRight = - BorderRadius.only(topRight: Radius.circular(10)); - const bottomRight = - BorderRadius.only(bottomRight: Radius.circular(10)); - const bottomLeft = - BorderRadius.only(bottomLeft: Radius.circular(10)); - - final isTopLeft = index == 0; - final isTopRight = index == (colorsPerRow - 1); - final isBottomRight = - index == ((rowCount * colorsPerRow) - 1); - final isBottomLeft = - index == ((rowCount * colorsPerRow) - (colorsPerRow)); - - final borderRadius = isTopLeft - ? topleft - : isTopRight - ? topRight - : isBottomRight - ? bottomRight - : isBottomLeft - ? bottomLeft - : null; - - return InkWell( - onTap: () { - selectedColorIndex.value = index; - color.value = colors[index].color; - }, - borderRadius: borderRadius, - child: Stack( - alignment: Alignment.center, - children: [ - Ink( - decoration: BoxDecoration( - borderRadius: borderRadius, - color: colors[index].color, - ), - height: 50, - width: - (MediaQuery.of(context).size.width - 32.0) / - 3.0, - ), - Visibility( - visible: isCurrentColor, - child: Icon( - Icons.check, - color: - colors[index].color.computeLuminance() > .7 - ? Colors.black - : Colors.white, - size: 30, - ), - ), - ], - ), - ); - }, - ), - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/components/subject_management/subject_configs/time.dart b/lib/components/subject_management/subject_configs/time.dart deleted file mode 100644 index 6e10718..0000000 --- a/lib/components/subject_management/subject_configs/time.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/widgets/act_chip.dart'; -import 'package:timetable/helpers/time_management.dart'; -import 'package:timetable/constants/custom_times.dart'; -import 'package:timetable/provider/settings.dart'; - -/// Time configuration part of the Subject creation screen. -class TimeConfig extends ConsumerWidget { - /// whether or not the current time slot ((endTime - startTime) - tbCustomStartTime) is occupied - /// used to show the error icon when the time is not valid/unavailable - final bool occupied; - final ValueNotifier startTime; - final ValueNotifier endTime; - - const TimeConfig({ - super.key, - required this.occupied, - required this.startTime, - required this.endTime, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final settings = ref.watch(settingsProvider); - final chosenCustomStartTime = settings.customStartTime; - final chosenCustomEndTime = settings.customEndTime; - - final customStartTime = getCustomStartTime(chosenCustomStartTime, ref); - final customEndTime = getCustomEndTime(chosenCustomEndTime, ref); - - final uses24HoursFormat = MediaQuery.of(context).alwaysUse24HourFormat; - - void showInvalidTimePeriodDialog() { - final String customStartTimeHour = getCustomTimeHour(customStartTime); - final String customStartTimeMinute = getCustomTimeMinute(customStartTime); - final String customEndTimeHour = getCustomTimeHour(customEndTime); - final String customEndTimeMinute = getCustomTimeMinute(customEndTime); - - final String startTime = "$customStartTimeHour:$customStartTimeMinute"; - final String endTime = "$customEndTimeHour:$customEndTimeMinute"; - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('invalid_time').tr(), - content: - const Text('invalid_time_config').tr(args: [startTime, endTime]), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('ok').tr(), - ), - ], - ), - ); - } - - void showInvalidEqualTimeDialog() { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('invalid_time').tr(), - content: const Text('invalid_equal_time_error').tr(), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('ok').tr(), - ), - ], - ), - ); - } - - void switchStartWithEndTime(TimeOfDay newTime) { - if (!newTime.isAfter(customEndTime)) { - final temp = endTime.value; - endTime.value = newTime; - startTime.value = temp; - return; - } - showInvalidTimePeriodDialog(); - } - - void switchEndWithStartTime(TimeOfDay newTime) { - if (!newTime.isBefore(customStartTime)) { - final temp = startTime.value; - startTime.value = newTime; - endTime.value = temp; - return; - } - showInvalidTimePeriodDialog(); - } - - // TODO: this is extremely similar to what i have done already in the [getTime()] helper - // maybe add a check to remove the backspace when specified - String getTime(TimeOfDay time) { - final hour = uses24HoursFormat - ? time.hour - : (time.hour > 12 ? time.hour - 12 : time.hour); - final minute = time.minute.toString().padLeft(2, '0'); - final period = - uses24HoursFormat ? '' : ' ${time.hour > 12 ? "PM" : "AM"}'; - return '$hour:$minute$period'; - } - - return Row( - children: [ - const Text("time").tr(), - const Spacer(), - ActChip( - onPressed: () async { - final TimeOfDay? selectedTime = await timePicker( - context, - TimeOfDay( - hour: startTime.value.hour, - minute: 0, - ), - ); - - if (selectedTime == null) return; - - if (selectedTime.isBefore(customStartTime)) { - showInvalidTimePeriodDialog(); - return; - } - - if (selectedTime.hour == endTime.value.hour) { - showInvalidEqualTimeDialog(); - return; - } - - if (selectedTime.isAfter(endTime.value)) { - switchStartWithEndTime(selectedTime); - return; - } - - startTime.value = selectedTime; - }, - label: Text(getTime(startTime.value)), - ), - const Padding( - padding: EdgeInsets.symmetric( - horizontal: 10, - ), - child: Icon(Icons.arrow_forward), - ), - ActChip( - onPressed: () async { - final TimeOfDay? selectedTime = await timePicker( - context, - TimeOfDay( - hour: endTime.value.hour, - minute: 0, - ), - ); - - if (selectedTime == null) return; - - if (selectedTime.isAfter(customEndTime)) { - showInvalidTimePeriodDialog(); - return; - } - - if (selectedTime.hour == startTime.value.hour) { - showInvalidEqualTimeDialog(); - return; - } - if (selectedTime.isBefore(startTime.value)) { - switchEndWithStartTime(selectedTime); - return; - } - - endTime.value = selectedTime; - }, - label: Text(getTime(endTime.value)), - ), - if (occupied) - const Padding( - padding: EdgeInsets.only(left: 10), - child: Icon( - Icons.cancel, - color: Colors.redAccent, - ), - ), - ], - ); - } -} diff --git a/lib/components/subject_management/subjects_list.dart b/lib/components/subject_management/subjects_list.dart deleted file mode 100644 index 149ce79..0000000 --- a/lib/components/subject_management/subjects_list.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:timetable/components/widgets/color_indicator.dart'; -import 'package:timetable/components/widgets/list_item_group.dart'; -import 'package:timetable/constants/error_emoticons.dart'; -import 'package:timetable/db/database.dart'; - -/// lists all subjects to choose a label & -/// color from an already existing one -class SubjectsList extends HookWidget { - final List subjects; - final TextEditingController controller; - final ValueNotifier label; - final ValueNotifier location; - final ValueNotifier color; - - const SubjectsList({ - super.key, - required this.subjects, - required this.controller, - required this.label, - required this.location, - required this.color, - }); - - @override - Widget build(BuildContext context) { - final ValueNotifier labelFilterEnabled = useState(false); - final ValueNotifier locationFilterEnabled = useState(false); - final ValueNotifier colorFilterEnabled = useState(false); - Set uniqueSubjects = {}; - - /// places 1 subject from duplicates in a set (filtered by label) - List filteredSubjects = []; - for (SubjectData subject in subjects) { - final label = subject.label; - - if (!uniqueSubjects.contains(label)) { - uniqueSubjects.add(label); - filteredSubjects.add(subject); - } - } - - filteredSubjects = filteredSubjects.where((subject) { - bool passesLabelFilter = !labelFilterEnabled.value || - subject.label.trim() == label.value.trim(); - bool passesLocationFilter = !locationFilterEnabled.value || - subject.location?.trim() == location.value?.trim(); - bool passesColorFilter = - !colorFilterEnabled.value || subject.color == color.value; - - return passesLabelFilter && passesLocationFilter && passesColorFilter; - }).toList(); - - return Scaffold( - appBar: AppBar( - title: const Text('choose_subject').tr(), - actions: [ - // filtering system - MenuAnchor( - menuChildren: [ - CheckboxMenuButton( - value: labelFilterEnabled.value, - onChanged: (value) => labelFilterEnabled.value = value ?? false, - child: const Text('Label').tr(), - ), - CheckboxMenuButton( - value: locationFilterEnabled.value, - onChanged: (value) => - locationFilterEnabled.value = value ?? false, - child: const Text('Location').tr(), - ), - CheckboxMenuButton( - value: colorFilterEnabled.value, - onChanged: (value) => colorFilterEnabled.value = value ?? false, - child: const Text('Color').tr(), - ), - ], - builder: ( - BuildContext context, - MenuController controller, - Widget? child, - ) { - return IconButton( - icon: const Icon(Icons.filter_list_outlined), - tooltip: "filter_by".tr(), - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - ); - }, - ), - ], - ), - body: LayoutBuilder( - builder: (context, constraints) => ListView( - padding: const EdgeInsets.all(16), - children: [ - if (filteredSubjects.isEmpty) - Container( - padding: const EdgeInsets.all(10), - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: Center( - child: Column( - children: [ - Text( - getRandomErrorEmoticon(), - style: const TextStyle(fontSize: 25), - ), - const SizedBox(height: 10), - const Text( - "no_subjects_error", - style: TextStyle(fontSize: 18), - ).tr(), - ], - ), - ), - ), - if (filteredSubjects.isNotEmpty) - ListItemGroup( - children: List.generate(filteredSubjects.length, (i) { - final subj = filteredSubjects[i]; - return ListItem( - leading: ColorIndicator(color: subj.color), - title: Text(subj.label), - subtitle: (subj.location != null && - subj.location!.isNotEmpty) || - (subj.note != null && subj.note!.isNotEmpty) - ? Column( - children: [ - if (subj.location != null && - subj.location!.isNotEmpty) - Row( - children: [ - const Icon( - Icons.location_on_outlined, - size: 15, - ), - const SizedBox(width: 2.5), - Expanded( - child: Text( - subj.location.toString(), - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - if (subj.note != null && subj.note!.isNotEmpty) - Row( - children: [ - const Icon( - Icons.sticky_note_2_outlined, - size: 15, - ), - const SizedBox(width: 2.5), - Expanded( - child: Text( - subj.note.toString(), - maxLines: 4, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ], - ) - : null, - onTap: () { - controller.text = subj.label; - label.value = subj.label; - color.value = subj.color; - Navigator.of(context).pop(); - }, - ); - }), - ), - ], - ), - ), - ); - } -} diff --git a/lib/components/widgets/views/day_view/days_bar.dart b/lib/components/widgets/views/day_view/days_bar.dart deleted file mode 100644 index d134dc8..0000000 --- a/lib/components/widgets/views/day_view/days_bar.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'dart:async'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/constants/days.dart'; -import 'package:timetable/constants/grid_properties.dart'; -import 'package:timetable/constants/theme_options.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/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. -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; - - final theme = ref.watch(themeProvider); - final Brightness systemBrightness = - MediaQuery.of(context).platformBrightness; - final darkCurrentDayColorScheme = - Theme.of(context).colorScheme.onInverseSurface; - final lightCurrentDayColorScheme = - Theme.of(context).colorScheme.outlineVariant; - - return SizedBox( - height: 48, - width: screenWidth, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - padding: EdgeInsets.only(left: isGridView! ? 20 : 0), - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: daysLength, - shrinkWrap: true, - itemBuilder: (context, index) { - return SizedBox( - width: isGridView! - ? ((screenWidth - (timeColumnWidth - 1)) / daysLength) - : (screenWidth / daysLength), - child: TextButton( - onPressed: () { - if (isGridView!) return; - - controller.animateToPage( - index, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }, - style: ButtonStyle( - foregroundColor: WidgetStateProperty.all( - Theme.of(context).colorScheme.onSurface, - ), - backgroundColor: isGridView! && (currentDay == index) - ? WidgetStateProperty.all( - theme == ThemeOption.auto - ? systemBrightness == Brightness.dark - ? darkCurrentDayColorScheme - : lightCurrentDayColorScheme - : theme == ThemeOption.dark - ? darkCurrentDayColorScheme - : lightCurrentDayColorScheme, - ) - : null, - ), - child: Text( - overflow: TextOverflow.clip, - softWrap: false, - singleLetterDays - ? days[index].tr()[0] - : isPortrait - ? days[index].tr().substring(0, 3) - : days[index].tr(), - ), - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class DayBarUpdater extends StatefulWidget { - final PageController controller; - final bool isGridView; - - const DayBarUpdater({ - super.key, - required this.controller, - required this.isGridView, - }); - - @override - State createState() => _DayBarUpdaterState(); -} - -class _DayBarUpdaterState extends State { - late Timer timer; - int currentDay = DateTime.now().weekday - 1; - - @override - void initState() { - super.initState(); - updateTimer(); - } - - @override - void dispose() { - timer.cancel(); - super.dispose(); - } - - void updateTimer() { - final nextMidnight = DateTime( - DateTime.now().add(const Duration(days: 1)).day, - 0, - 0, - 0, - ); - timer = Timer(nextMidnight.difference(DateTime.now()), updateDay); - } - - void updateDay() { - setState(() { - currentDay = DateTime.now().weekday - 1; - }); - updateTimer(); - } - - @override - Widget build(BuildContext context) { - return DaysBar( - controller: widget.controller, - isGridView: widget.isGridView, - currentDay: currentDay, - ); - } -} diff --git a/lib/constants/basic_subject.dart b/lib/core/constants/basic_subject.dart similarity index 65% rename from lib/constants/basic_subject.dart rename to lib/core/constants/basic_subject.dart index 0a787eb..ef859d3 100644 --- a/lib/constants/basic_subject.dart +++ b/lib/core/constants/basic_subject.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:timetable/constants/days.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/db/database.dart'; +import 'package:timetable/core/constants/days.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/db/database.dart'; /// A basic subject with no extra information. /// Used for the color autocomplete feature. -const basicSubject = SubjectData( +const basicSubject = Subject( id: 0, label: " ", location: "", diff --git a/lib/constants/colors.dart b/lib/core/constants/colors.dart similarity index 100% rename from lib/constants/colors.dart rename to lib/core/constants/colors.dart diff --git a/lib/constants/custom_times.dart b/lib/core/constants/custom_times.dart similarity index 95% rename from lib/constants/custom_times.dart rename to lib/core/constants/custom_times.dart index 2416328..068c215 100644 --- a/lib/constants/custom_times.dart +++ b/lib/core/constants/custom_times.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/provider/settings.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; /// Returns the custom start time set by the user if customTimePeriod is true, /// otherwise returns the default start time. (8:00) diff --git a/lib/constants/days.dart b/lib/core/constants/days.dart similarity index 100% rename from lib/constants/days.dart rename to lib/core/constants/days.dart diff --git a/lib/constants/durations.dart b/lib/core/constants/durations.dart similarity index 100% rename from lib/constants/durations.dart rename to lib/core/constants/durations.dart diff --git a/lib/constants/error_emoticons.dart b/lib/core/constants/error_emoticons.dart similarity index 100% rename from lib/constants/error_emoticons.dart rename to lib/core/constants/error_emoticons.dart diff --git a/lib/constants/grid_properties.dart b/lib/core/constants/grid_properties.dart similarity index 94% rename from lib/constants/grid_properties.dart rename to lib/core/constants/grid_properties.dart index 8d3cb4d..830d7b1 100644 --- a/lib/constants/grid_properties.dart +++ b/lib/core/constants/grid_properties.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/provider/settings.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; /// Width of the time column. const double timeColumnWidth = 22.5; diff --git a/lib/constants/languages.dart b/lib/core/constants/languages.dart similarity index 100% rename from lib/constants/languages.dart rename to lib/core/constants/languages.dart diff --git a/lib/constants/navigation_items.dart b/lib/core/constants/navigation_items.dart similarity index 100% rename from lib/constants/navigation_items.dart rename to lib/core/constants/navigation_items.dart diff --git a/lib/constants/rotation_weeks.dart b/lib/core/constants/rotation_weeks.dart similarity index 100% rename from lib/constants/rotation_weeks.dart rename to lib/core/constants/rotation_weeks.dart diff --git a/lib/constants/theme_options.dart b/lib/core/constants/theme_options.dart similarity index 100% rename from lib/constants/theme_options.dart rename to lib/core/constants/theme_options.dart diff --git a/lib/constants/time.dart b/lib/core/constants/time.dart similarity index 100% rename from lib/constants/time.dart rename to lib/core/constants/time.dart diff --git a/lib/constants/timetable_views.dart b/lib/core/constants/timetable_views.dart similarity index 100% rename from lib/constants/timetable_views.dart rename to lib/core/constants/timetable_views.dart diff --git a/lib/db/converters/time_of_day.dart b/lib/core/converters/converters.dart similarity index 65% rename from lib/db/converters/time_of_day.dart rename to lib/core/converters/converters.dart index fec2c45..51bb535 100644 --- a/lib/db/converters/time_of_day.dart +++ b/lib/core/converters/converters.dart @@ -1,5 +1,22 @@ import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; +import 'package:timetable/core/extensions/color.dart'; + +class ColorConverter extends TypeConverter { + const ColorConverter(); + + @override + Color fromSql(int fromDb) { + // Parse Color from int + return Color(fromDb); + } + + @override + int toSql(Color value) { + // get Color value (int) + return value.toInt(); + } +} class TimeOfDayConverter extends TypeConverter { const TimeOfDayConverter(); diff --git a/lib/db/connection/native.dart b/lib/core/db/connection/native.dart similarity index 100% rename from lib/db/connection/native.dart rename to lib/core/db/connection/native.dart diff --git a/lib/db/database.dart b/lib/core/db/database.dart similarity index 64% rename from lib/db/database.dart rename to lib/core/db/database.dart index 6c846b5..6df883f 100644 --- a/lib/db/database.dart +++ b/lib/core/db/database.dart @@ -1,14 +1,13 @@ import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter/material.dart' as material; -import 'package:timetable/db/connection/native.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/constants/days.dart'; -import 'package:timetable/db/converters/color.dart'; -import 'package:timetable/db/converters/time_of_day.dart'; -import 'package:timetable/db/models/timetable.dart'; -import 'package:timetable/db/models/subject.dart'; -import 'package:timetable/extensions/color.dart'; +import 'package:timetable/core/db/connection/native.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/constants/days.dart'; +import 'package:timetable/core/converters/converters.dart'; +import 'package:timetable/core/db/tables/timetable.dart'; +import 'package:timetable/core/db/tables/subject.dart'; +import 'package:timetable/core/extensions/color.dart'; part 'database.g.dart'; @@ -24,12 +23,12 @@ part 'database.g.dart'; /// migration strategy: /// - v1 -> v2: Color storage format change /// - v2 -> v3: Added timetable name and subject timetable columns -@DriftDatabase(tables: [Timetable, Subject]) +@DriftDatabase(tables: [Timetables, Subjects]) class AppDatabase extends _$AppDatabase { AppDatabase() : super(openConnection()); @override - int get schemaVersion => 3; + int get schemaVersion => 4; @override MigrationStrategy get migration { @@ -46,16 +45,20 @@ class AppDatabase extends _$AppDatabase { // to maintain the Color type instead of using int in v1. await m.alterTable( TableMigration( - subject, + subjects, columnTransformer: { - subject.color: subject.color, + subjects.color: subjects.color, }, ), ); } if (from < 3) { - await m.addColumn(timetable, timetable.name); - await m.addColumn(subject, subject.timetable); + await m.addColumn(timetables, timetables.name); + await m.addColumn(subjects, subjects.timetable); + } + if (from < 4) { + await m.renameTable(timetables, "timetable"); + await m.renameTable(subjects, "subject"); } }, ); diff --git a/lib/db/database.g.dart b/lib/core/db/database.g.dart similarity index 51% rename from lib/db/database.g.dart rename to lib/core/db/database.g.dart index 6f1e795..9d7721b 100644 --- a/lib/db/database.g.dart +++ b/lib/core/db/database.g.dart @@ -1,18 +1,14 @@ -/// current modifications: -/// in [SubjectData], specifically in the [toJson()] and [fromJson()] functions, -/// I changed the way it handles the elments with types Color and TimeOfDay -/// since there is no regular way to deal with them i seperate hours and minutes for TimeOfDay -/// then regroup them again and convert Color to int and int to Color again +// GENERATED CODE - DO NOT MODIFY BY HAND part of 'database.dart'; // ignore_for_file: type=lint -class $TimetableTable extends Timetable - with TableInfo<$TimetableTable, TimetableData> { +class $TimetablesTable extends Timetables + with TableInfo<$TimetablesTable, Timetable> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $TimetableTable(this.attachedDatabase, [this._alias]); + $TimetablesTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -33,9 +29,9 @@ class $TimetableTable extends Timetable String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'timetable'; + static const String $name = 'timetables'; @override - VerificationContext validateIntegrity(Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); @@ -54,9 +50,9 @@ class $TimetableTable extends Timetable @override Set get $primaryKey => {id}; @override - TimetableData map(Map data, {String? tablePrefix}) { + Timetable map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return TimetableData( + return Timetable( id: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}id'])!, name: attachedDatabase.typeMapping @@ -65,15 +61,15 @@ class $TimetableTable extends Timetable } @override - $TimetableTable createAlias(String alias) { - return $TimetableTable(attachedDatabase, alias); + $TimetablesTable createAlias(String alias) { + return $TimetablesTable(attachedDatabase, alias); } } -class TimetableData extends DataClass implements Insertable { +class Timetable extends DataClass implements Insertable { final int id; final String name; - const TimetableData({required this.id, required this.name}); + const Timetable({required this.id, required this.name}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -82,17 +78,17 @@ class TimetableData extends DataClass implements Insertable { return map; } - TimetableCompanion toCompanion(bool nullToAbsent) { - return TimetableCompanion( + TimetablesCompanion toCompanion(bool nullToAbsent) { + return TimetablesCompanion( id: Value(id), name: Value(name), ); } - factory TimetableData.fromJson(Map json, + factory Timetable.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return TimetableData( + return Timetable( id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), ); @@ -106,13 +102,20 @@ class TimetableData extends DataClass implements Insertable { }; } - TimetableData copyWith({int? id, String? name}) => TimetableData( + Timetable copyWith({int? id, String? name}) => Timetable( id: id ?? this.id, name: name ?? this.name, ); + Timetable copyWithCompanion(TimetablesCompanion data) { + return Timetable( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + ); + } + @override String toString() { - return (StringBuffer('TimetableData(') + return (StringBuffer('Timetable(') ..write('id: $id, ') ..write('name: $name') ..write(')')) @@ -124,23 +127,21 @@ class TimetableData extends DataClass implements Insertable { @override bool operator ==(Object other) => identical(this, other) || - (other is TimetableData && - other.id == this.id && - other.name == this.name); + (other is Timetable && other.id == this.id && other.name == this.name); } -class TimetableCompanion extends UpdateCompanion { +class TimetablesCompanion extends UpdateCompanion { final Value id; final Value name; - const TimetableCompanion({ + const TimetablesCompanion({ this.id = const Value.absent(), this.name = const Value.absent(), }); - TimetableCompanion.insert({ + TimetablesCompanion.insert({ this.id = const Value.absent(), required String name, }) : name = Value(name); - static Insertable custom({ + static Insertable custom({ Expression? id, Expression? name, }) { @@ -150,8 +151,8 @@ class TimetableCompanion extends UpdateCompanion { }); } - TimetableCompanion copyWith({Value? id, Value? name}) { - return TimetableCompanion( + TimetablesCompanion copyWith({Value? id, Value? name}) { + return TimetablesCompanion( id: id ?? this.id, name: name ?? this.name, ); @@ -171,7 +172,7 @@ class TimetableCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('TimetableCompanion(') + return (StringBuffer('TimetablesCompanion(') ..write('id: $id, ') ..write('name: $name') ..write(')')) @@ -179,11 +180,11 @@ class TimetableCompanion extends UpdateCompanion { } } -class $SubjectTable extends Subject with TableInfo<$SubjectTable, SubjectData> { +class $SubjectsTable extends Subjects with TableInfo<$SubjectsTable, Subject> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $SubjectTable(this.attachedDatabase, [this._alias]); + $SubjectsTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -214,34 +215,35 @@ class $SubjectTable extends Subject with TableInfo<$SubjectTable, SubjectData> { late final GeneratedColumnWithTypeConverter color = GeneratedColumn('color', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true) - .withConverter($SubjectTable.$convertercolor); + .withConverter($SubjectsTable.$convertercolor); static const VerificationMeta _rotationWeekMeta = const VerificationMeta('rotationWeek'); @override late final GeneratedColumnWithTypeConverter rotationWeek = GeneratedColumn('rotation_week', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true) - .withConverter($SubjectTable.$converterrotationWeek); + .withConverter($SubjectsTable.$converterrotationWeek); static const VerificationMeta _dayMeta = const VerificationMeta('day'); @override late final GeneratedColumnWithTypeConverter day = GeneratedColumn('day', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true) - .withConverter($SubjectTable.$converterday); + .withConverter($SubjectsTable.$converterday); static const VerificationMeta _startTimeMeta = const VerificationMeta('startTime'); @override late final GeneratedColumnWithTypeConverter startTime = GeneratedColumn('start_time', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter($SubjectTable.$converterstartTime); + .withConverter( + $SubjectsTable.$converterstartTime); static const VerificationMeta _endTimeMeta = const VerificationMeta('endTime'); @override late final GeneratedColumnWithTypeConverter endTime = GeneratedColumn('end_time', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter($SubjectTable.$converterendTime); + .withConverter($SubjectsTable.$converterendTime); static const VerificationMeta _timetableMeta = const VerificationMeta('timetable'); @override @@ -265,9 +267,9 @@ class $SubjectTable extends Subject with TableInfo<$SubjectTable, SubjectData> { String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'subject'; + static const String $name = 'subjects'; @override - VerificationContext validateIntegrity(Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); @@ -305,9 +307,9 @@ class $SubjectTable extends Subject with TableInfo<$SubjectTable, SubjectData> { @override Set get $primaryKey => {id}; @override - SubjectData map(Map data, {String? tablePrefix}) { + Subject map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return SubjectData( + return Subject( id: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}id'])!, label: attachedDatabase.typeMapping @@ -316,17 +318,17 @@ class $SubjectTable extends Subject with TableInfo<$SubjectTable, SubjectData> { .read(DriftSqlType.string, data['${effectivePrefix}location']), note: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}note']), - color: $SubjectTable.$convertercolor.fromSql(attachedDatabase.typeMapping + color: $SubjectsTable.$convertercolor.fromSql(attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}color'])!), - rotationWeek: $SubjectTable.$converterrotationWeek.fromSql( + rotationWeek: $SubjectsTable.$converterrotationWeek.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}rotation_week'])!), - day: $SubjectTable.$converterday.fromSql(attachedDatabase.typeMapping + day: $SubjectsTable.$converterday.fromSql(attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}day'])!), - startTime: $SubjectTable.$converterstartTime.fromSql(attachedDatabase + startTime: $SubjectsTable.$converterstartTime.fromSql(attachedDatabase .typeMapping .read(DriftSqlType.string, data['${effectivePrefix}start_time'])!), - endTime: $SubjectTable.$converterendTime.fromSql(attachedDatabase + endTime: $SubjectsTable.$converterendTime.fromSql(attachedDatabase .typeMapping .read(DriftSqlType.string, data['${effectivePrefix}end_time'])!), timetable: attachedDatabase.typeMapping @@ -335,8 +337,8 @@ class $SubjectTable extends Subject with TableInfo<$SubjectTable, SubjectData> { } @override - $SubjectTable createAlias(String alias) { - return $SubjectTable(attachedDatabase, alias); + $SubjectsTable createAlias(String alias) { + return $SubjectsTable(attachedDatabase, alias); } static TypeConverter $convertercolor = @@ -351,7 +353,7 @@ class $SubjectTable extends Subject with TableInfo<$SubjectTable, SubjectData> { const TimeOfDayConverter(); } -class SubjectData extends DataClass implements Insertable { +class Subject extends DataClass implements Insertable { final int id; final String label; final String? location; @@ -362,7 +364,7 @@ class SubjectData extends DataClass implements Insertable { final material.TimeOfDay startTime; final material.TimeOfDay endTime; final String timetable; - const SubjectData( + const Subject( {required this.id, required this.label, this.location, @@ -385,29 +387,29 @@ class SubjectData extends DataClass implements Insertable { map['note'] = Variable(note); } { - map['color'] = Variable($SubjectTable.$convertercolor.toSql(color)); + map['color'] = Variable($SubjectsTable.$convertercolor.toSql(color)); } { map['rotation_week'] = Variable( - $SubjectTable.$converterrotationWeek.toSql(rotationWeek)); + $SubjectsTable.$converterrotationWeek.toSql(rotationWeek)); } { - map['day'] = Variable($SubjectTable.$converterday.toSql(day)); + map['day'] = Variable($SubjectsTable.$converterday.toSql(day)); } { map['start_time'] = - Variable($SubjectTable.$converterstartTime.toSql(startTime)); + Variable($SubjectsTable.$converterstartTime.toSql(startTime)); } { map['end_time'] = - Variable($SubjectTable.$converterendTime.toSql(endTime)); + Variable($SubjectsTable.$converterendTime.toSql(endTime)); } map['timetable'] = Variable(timetable); return map; } - SubjectCompanion toCompanion(bool nullToAbsent) { - return SubjectCompanion( + SubjectsCompanion toCompanion(bool nullToAbsent) { + return SubjectsCompanion( id: Value(id), label: Value(label), location: location == null && nullToAbsent @@ -423,18 +425,18 @@ class SubjectData extends DataClass implements Insertable { ); } - factory SubjectData.fromJson(Map json, + factory Subject.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return SubjectData( + return Subject( id: serializer.fromJson(json['id']), label: serializer.fromJson(json['label']), location: serializer.fromJson(json['location']), note: serializer.fromJson(json['note']), color: serializer.fromJson(material.Color(json['color'])), - rotationWeek: $SubjectTable.$converterrotationWeek + rotationWeek: $SubjectsTable.$converterrotationWeek .fromJson(serializer.fromJson(json['rotationWeek'])), - day: $SubjectTable.$converterday + day: $SubjectsTable.$converterday .fromJson(serializer.fromJson(json['day'])), startTime: serializer.fromJson( material.TimeOfDay( @@ -451,7 +453,6 @@ class SubjectData extends DataClass implements Insertable { timetable: serializer.fromJson(json['timetable']), ); } - @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; @@ -462,8 +463,8 @@ class SubjectData extends DataClass implements Insertable { 'note': serializer.toJson(note), 'color': serializer.toJson(color.toInt()), 'rotationWeek': serializer.toJson( - $SubjectTable.$converterrotationWeek.toJson(rotationWeek)), - 'day': serializer.toJson($SubjectTable.$converterday.toJson(day)), + $SubjectsTable.$converterrotationWeek.toJson(rotationWeek)), + 'day': serializer.toJson($SubjectsTable.$converterday.toJson(day)), 'startTimeHour': serializer.toJson(startTime.hour), 'startTimeMinute': serializer.toJson(startTime.minute), 'endTimeHour': serializer.toJson(endTime.hour), @@ -472,7 +473,7 @@ class SubjectData extends DataClass implements Insertable { }; } - SubjectData copyWith( + Subject copyWith( {int? id, String? label, Value location = const Value.absent(), @@ -483,7 +484,7 @@ class SubjectData extends DataClass implements Insertable { material.TimeOfDay? startTime, material.TimeOfDay? endTime, String? timetable}) => - SubjectData( + Subject( id: id ?? this.id, label: label ?? this.label, location: location.present ? location.value : this.location, @@ -495,9 +496,26 @@ class SubjectData extends DataClass implements Insertable { endTime: endTime ?? this.endTime, timetable: timetable ?? this.timetable, ); + Subject copyWithCompanion(SubjectsCompanion data) { + return Subject( + id: data.id.present ? data.id.value : this.id, + label: data.label.present ? data.label.value : this.label, + location: data.location.present ? data.location.value : this.location, + note: data.note.present ? data.note.value : this.note, + color: data.color.present ? data.color.value : this.color, + rotationWeek: data.rotationWeek.present + ? data.rotationWeek.value + : this.rotationWeek, + day: data.day.present ? data.day.value : this.day, + startTime: data.startTime.present ? data.startTime.value : this.startTime, + endTime: data.endTime.present ? data.endTime.value : this.endTime, + timetable: data.timetable.present ? data.timetable.value : this.timetable, + ); + } + @override String toString() { - return (StringBuffer('SubjectData(') + return (StringBuffer('Subject(') ..write('id: $id, ') ..write('label: $label, ') ..write('location: $location, ') @@ -518,7 +536,7 @@ class SubjectData extends DataClass implements Insertable { @override bool operator ==(Object other) => identical(this, other) || - (other is SubjectData && + (other is Subject && other.id == this.id && other.label == this.label && other.location == this.location && @@ -531,7 +549,7 @@ class SubjectData extends DataClass implements Insertable { other.timetable == this.timetable); } -class SubjectCompanion extends UpdateCompanion { +class SubjectsCompanion extends UpdateCompanion { final Value id; final Value label; final Value location; @@ -542,7 +560,7 @@ class SubjectCompanion extends UpdateCompanion { final Value startTime; final Value endTime; final Value timetable; - const SubjectCompanion({ + const SubjectsCompanion({ this.id = const Value.absent(), this.label = const Value.absent(), this.location = const Value.absent(), @@ -554,7 +572,7 @@ class SubjectCompanion extends UpdateCompanion { this.endTime = const Value.absent(), this.timetable = const Value.absent(), }); - SubjectCompanion.insert({ + SubjectsCompanion.insert({ this.id = const Value.absent(), required String label, this.location = const Value.absent(), @@ -572,7 +590,7 @@ class SubjectCompanion extends UpdateCompanion { startTime = Value(startTime), endTime = Value(endTime), timetable = Value(timetable); - static Insertable custom({ + static Insertable custom({ Expression? id, Expression? label, Expression? location, @@ -598,7 +616,7 @@ class SubjectCompanion extends UpdateCompanion { }); } - SubjectCompanion copyWith( + SubjectsCompanion copyWith( {Value? id, Value? label, Value? location, @@ -609,7 +627,7 @@ class SubjectCompanion extends UpdateCompanion { Value? startTime, Value? endTime, Value? timetable}) { - return SubjectCompanion( + return SubjectsCompanion( id: id ?? this.id, label: label ?? this.label, location: location ?? this.location, @@ -640,22 +658,22 @@ class SubjectCompanion extends UpdateCompanion { } if (color.present) { map['color'] = - Variable($SubjectTable.$convertercolor.toSql(color.value)); + Variable($SubjectsTable.$convertercolor.toSql(color.value)); } if (rotationWeek.present) { map['rotation_week'] = Variable( - $SubjectTable.$converterrotationWeek.toSql(rotationWeek.value)); + $SubjectsTable.$converterrotationWeek.toSql(rotationWeek.value)); } if (day.present) { - map['day'] = Variable($SubjectTable.$converterday.toSql(day.value)); + map['day'] = Variable($SubjectsTable.$converterday.toSql(day.value)); } if (startTime.present) { map['start_time'] = Variable( - $SubjectTable.$converterstartTime.toSql(startTime.value)); + $SubjectsTable.$converterstartTime.toSql(startTime.value)); } if (endTime.present) { map['end_time'] = Variable( - $SubjectTable.$converterendTime.toSql(endTime.value)); + $SubjectsTable.$converterendTime.toSql(endTime.value)); } if (timetable.present) { map['timetable'] = Variable(timetable.value); @@ -665,7 +683,7 @@ class SubjectCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('SubjectCompanion(') + return (StringBuffer('SubjectsCompanion(') ..write('id: $id, ') ..write('label: $label, ') ..write('location: $location, ') @@ -683,11 +701,381 @@ class SubjectCompanion extends UpdateCompanion { abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); - late final $TimetableTable timetable = $TimetableTable(this); - late final $SubjectTable subject = $SubjectTable(this); + $AppDatabaseManager get managers => $AppDatabaseManager(this); + late final $TimetablesTable timetables = $TimetablesTable(this); + late final $SubjectsTable subjects = $SubjectsTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [timetable, subject]; + List get allSchemaEntities => [timetables, subjects]; +} + +typedef $$TimetablesTableCreateCompanionBuilder = TimetablesCompanion Function({ + Value id, + required String name, +}); +typedef $$TimetablesTableUpdateCompanionBuilder = TimetablesCompanion Function({ + Value id, + Value name, +}); + +class $$TimetablesTableFilterComposer + extends Composer<_$AppDatabase, $TimetablesTable> { + $$TimetablesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); +} + +class $$TimetablesTableOrderingComposer + extends Composer<_$AppDatabase, $TimetablesTable> { + $$TimetablesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); +} + +class $$TimetablesTableAnnotationComposer + extends Composer<_$AppDatabase, $TimetablesTable> { + $$TimetablesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); +} + +class $$TimetablesTableTableManager extends RootTableManager< + _$AppDatabase, + $TimetablesTable, + Timetable, + $$TimetablesTableFilterComposer, + $$TimetablesTableOrderingComposer, + $$TimetablesTableAnnotationComposer, + $$TimetablesTableCreateCompanionBuilder, + $$TimetablesTableUpdateCompanionBuilder, + (Timetable, BaseReferences<_$AppDatabase, $TimetablesTable, Timetable>), + Timetable, + PrefetchHooks Function()> { + $$TimetablesTableTableManager(_$AppDatabase db, $TimetablesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$TimetablesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$TimetablesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$TimetablesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + }) => + TimetablesCompanion( + id: id, + name: name, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + required String name, + }) => + TimetablesCompanion.insert( + id: id, + name: name, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$TimetablesTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $TimetablesTable, + Timetable, + $$TimetablesTableFilterComposer, + $$TimetablesTableOrderingComposer, + $$TimetablesTableAnnotationComposer, + $$TimetablesTableCreateCompanionBuilder, + $$TimetablesTableUpdateCompanionBuilder, + (Timetable, BaseReferences<_$AppDatabase, $TimetablesTable, Timetable>), + Timetable, + PrefetchHooks Function()>; +typedef $$SubjectsTableCreateCompanionBuilder = SubjectsCompanion Function({ + Value id, + required String label, + Value location, + Value note, + required material.Color color, + required RotationWeeks rotationWeek, + required Days day, + required material.TimeOfDay startTime, + required material.TimeOfDay endTime, + required String timetable, +}); +typedef $$SubjectsTableUpdateCompanionBuilder = SubjectsCompanion Function({ + Value id, + Value label, + Value location, + Value note, + Value color, + Value rotationWeek, + Value day, + Value startTime, + Value endTime, + Value timetable, +}); + +class $$SubjectsTableFilterComposer + extends Composer<_$AppDatabase, $SubjectsTable> { + $$SubjectsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get label => $composableBuilder( + column: $table.label, builder: (column) => ColumnFilters(column)); + + ColumnFilters get location => $composableBuilder( + column: $table.location, builder: (column) => ColumnFilters(column)); + + ColumnFilters get note => $composableBuilder( + column: $table.note, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters + get color => $composableBuilder( + column: $table.color, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters + get rotationWeek => $composableBuilder( + column: $table.rotationWeek, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters get day => $composableBuilder( + column: $table.day, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters + get startTime => $composableBuilder( + column: $table.startTime, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters + get endTime => $composableBuilder( + column: $table.endTime, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get timetable => $composableBuilder( + column: $table.timetable, builder: (column) => ColumnFilters(column)); +} + +class $$SubjectsTableOrderingComposer + extends Composer<_$AppDatabase, $SubjectsTable> { + $$SubjectsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get label => $composableBuilder( + column: $table.label, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get location => $composableBuilder( + column: $table.location, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get note => $composableBuilder( + column: $table.note, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get color => $composableBuilder( + column: $table.color, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rotationWeek => $composableBuilder( + column: $table.rotationWeek, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get day => $composableBuilder( + column: $table.day, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get startTime => $composableBuilder( + column: $table.startTime, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get endTime => $composableBuilder( + column: $table.endTime, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get timetable => $composableBuilder( + column: $table.timetable, builder: (column) => ColumnOrderings(column)); +} + +class $$SubjectsTableAnnotationComposer + extends Composer<_$AppDatabase, $SubjectsTable> { + $$SubjectsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get label => + $composableBuilder(column: $table.label, builder: (column) => column); + + GeneratedColumn get location => + $composableBuilder(column: $table.location, builder: (column) => column); + + GeneratedColumn get note => + $composableBuilder(column: $table.note, builder: (column) => column); + + GeneratedColumnWithTypeConverter get color => + $composableBuilder(column: $table.color, builder: (column) => column); + + GeneratedColumnWithTypeConverter get rotationWeek => + $composableBuilder( + column: $table.rotationWeek, builder: (column) => column); + + GeneratedColumnWithTypeConverter get day => + $composableBuilder(column: $table.day, builder: (column) => column); + + GeneratedColumnWithTypeConverter get startTime => + $composableBuilder(column: $table.startTime, builder: (column) => column); + + GeneratedColumnWithTypeConverter get endTime => + $composableBuilder(column: $table.endTime, builder: (column) => column); + + GeneratedColumn get timetable => + $composableBuilder(column: $table.timetable, builder: (column) => column); +} + +class $$SubjectsTableTableManager extends RootTableManager< + _$AppDatabase, + $SubjectsTable, + Subject, + $$SubjectsTableFilterComposer, + $$SubjectsTableOrderingComposer, + $$SubjectsTableAnnotationComposer, + $$SubjectsTableCreateCompanionBuilder, + $$SubjectsTableUpdateCompanionBuilder, + (Subject, BaseReferences<_$AppDatabase, $SubjectsTable, Subject>), + Subject, + PrefetchHooks Function()> { + $$SubjectsTableTableManager(_$AppDatabase db, $SubjectsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SubjectsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SubjectsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SubjectsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value label = const Value.absent(), + Value location = const Value.absent(), + Value note = const Value.absent(), + Value color = const Value.absent(), + Value rotationWeek = const Value.absent(), + Value day = const Value.absent(), + Value startTime = const Value.absent(), + Value endTime = const Value.absent(), + Value timetable = const Value.absent(), + }) => + SubjectsCompanion( + id: id, + label: label, + location: location, + note: note, + color: color, + rotationWeek: rotationWeek, + day: day, + startTime: startTime, + endTime: endTime, + timetable: timetable, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + required String label, + Value location = const Value.absent(), + Value note = const Value.absent(), + required material.Color color, + required RotationWeeks rotationWeek, + required Days day, + required material.TimeOfDay startTime, + required material.TimeOfDay endTime, + required String timetable, + }) => + SubjectsCompanion.insert( + id: id, + label: label, + location: location, + note: note, + color: color, + rotationWeek: rotationWeek, + day: day, + startTime: startTime, + endTime: endTime, + timetable: timetable, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SubjectsTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $SubjectsTable, + Subject, + $$SubjectsTableFilterComposer, + $$SubjectsTableOrderingComposer, + $$SubjectsTableAnnotationComposer, + $$SubjectsTableCreateCompanionBuilder, + $$SubjectsTableUpdateCompanionBuilder, + (Subject, BaseReferences<_$AppDatabase, $SubjectsTable, Subject>), + Subject, + PrefetchHooks Function()>; + +class $AppDatabaseManager { + final _$AppDatabase _db; + $AppDatabaseManager(this._db); + $$TimetablesTableTableManager get timetables => + $$TimetablesTableTableManager(_db, _db.timetables); + $$SubjectsTableTableManager get subjects => + $$SubjectsTableTableManager(_db, _db.subjects); } diff --git a/lib/db/services/service.dart b/lib/core/db/services/service.dart similarity index 84% rename from lib/db/services/service.dart rename to lib/core/db/services/service.dart index 7394a17..d0726f1 100644 --- a/lib/db/services/service.dart +++ b/lib/core/db/services/service.dart @@ -6,10 +6,10 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:timetable/constants/days.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/helpers/request_permission.dart'; +import 'package:timetable/core/constants/days.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/core/utils/request_permission.dart'; Future shareFile(File file) async { Share.shareXFiles([XFile(file.path)]); @@ -34,8 +34,8 @@ Future exportData(AppDatabase db) async { if (!isGranted) return; final Map> allData = { - 'subject': await db.subject.select().get(), - 'timetable': await db.timetable.select().get(), + 'subject': await db.subjects.select().get(), + 'timetable': await db.timetables.select().get(), }; final jsonData = jsonEncode(allData); @@ -61,15 +61,15 @@ Future restoreData( final decodedData = jsonDecode(jsonData) as Map; for (final table in decodedData.keys) { - final subjectsTable = db.subject; - final timetablesTable = db.timetable; + final subjectsTable = db.subjects; + final timetablesTable = db.timetables; if (table == 'subject') { subjectsTable.deleteAll(); for (final element in decodedData[table]!) { subjectsTable.insertOne( - SubjectCompanion.insert( + SubjectsCompanion.insert( label: element["label"], location: drift.Value(element["location"]), note: drift.Value(element["note"]), @@ -94,7 +94,7 @@ Future restoreData( for (final element in decodedData[table]!) { timetablesTable.insertOne( - TimetableCompanion.insert( + TimetablesCompanion.insert( name: element["name"], ), ); diff --git a/lib/db/models/subject.dart b/lib/core/db/tables/subject.dart similarity index 64% rename from lib/db/models/subject.dart rename to lib/core/db/tables/subject.dart index e4dce58..428af5a 100644 --- a/lib/db/models/subject.dart +++ b/lib/core/db/tables/subject.dart @@ -1,12 +1,12 @@ import 'package:drift/drift.dart'; -import 'package:timetable/constants/days.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/db/converters/color.dart'; -import 'package:timetable/db/converters/time_of_day.dart'; -import 'package:timetable/db/database.dart'; +import 'package:timetable/core/constants/days.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/core/converters/converters.dart'; /// Subject table definition and data model -class Subject extends Table { +// changed table name in v4 from Subject to Subjects +class Subjects extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get label => text()(); TextColumn get location => text().nullable()(); @@ -19,5 +19,5 @@ class Subject extends Table { TextColumn get startTime => text().map(const TimeOfDayConverter())(); TextColumn get endTime => text().map(const TimeOfDayConverter())(); // added in v3 - TextColumn get timetable => text().references($TimetableTable, #name)(); + TextColumn get timetable => text().references($TimetablesTable, #name)(); } diff --git a/lib/db/models/timetable.dart b/lib/core/db/tables/timetable.dart similarity index 65% rename from lib/db/models/timetable.dart rename to lib/core/db/tables/timetable.dart index 3b0aa82..5b6a73a 100644 --- a/lib/db/models/timetable.dart +++ b/lib/core/db/tables/timetable.dart @@ -1,7 +1,8 @@ import 'package:drift/drift.dart'; /// Timetable table definition and data model -class Timetable extends Table { +// changed table name in v4 from Timetable to Timetables +class Timetables extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text()(); } diff --git a/lib/extensions/color.dart b/lib/core/extensions/color.dart similarity index 100% rename from lib/extensions/color.dart rename to lib/core/extensions/color.dart diff --git a/lib/extensions/time_of_day.dart b/lib/core/extensions/time_of_day.dart similarity index 100% rename from lib/extensions/time_of_day.dart rename to lib/core/extensions/time_of_day.dart diff --git a/lib/db/models/settings.dart b/lib/core/models/settings.dart similarity index 98% rename from lib/db/models/settings.dart rename to lib/core/models/settings.dart index cdab53b..f2da9ff 100644 --- a/lib/db/models/settings.dart +++ b/lib/core/models/settings.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:timetable/constants/timetable_views.dart'; -import 'package:timetable/extensions/color.dart'; +import 'package:timetable/core/constants/timetable_views.dart'; +import 'package:timetable/core/extensions/color.dart'; /// Settings data model. class Settings { diff --git a/lib/helpers/get_os_version.dart b/lib/core/utils/get_os_version.dart similarity index 100% rename from lib/helpers/get_os_version.dart rename to lib/core/utils/get_os_version.dart diff --git a/lib/helpers/languages.dart b/lib/core/utils/languages.dart similarity index 100% rename from lib/helpers/languages.dart rename to lib/core/utils/languages.dart diff --git a/lib/helpers/overlapping_subjects.dart b/lib/core/utils/overlapping_subjects.dart similarity index 76% rename from lib/helpers/overlapping_subjects.dart rename to lib/core/utils/overlapping_subjects.dart index 6136b4d..66cbf01 100644 --- a/lib/helpers/overlapping_subjects.dart +++ b/lib/core/utils/overlapping_subjects.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/db/database.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/db/database.dart'; /// filters overlapping subjects from the list of all subjects. -List filterOverlappingSubjects( - List subjects, - List> overlappingSubjects, +List filterOverlappingSubjects( + List subjects, + List> overlappingSubjects, ) { return subjects .where( @@ -15,8 +15,8 @@ List filterOverlappingSubjects( } /// Returns the subject with the earlier starting time. -SubjectData getEarliestSubject(List subjects) { - SubjectData earliestSubject = subjects[0]; +Subject getEarliestSubject(List subjects) { + Subject earliestSubject = subjects[0]; for (final subject in subjects) { if (subject.startTime.hour < earliestSubject.startTime.hour) { earliestSubject = subject; @@ -26,8 +26,8 @@ SubjectData getEarliestSubject(List subjects) { } /// Returns the subject with the later ending time. -SubjectData getLatestSubject(List subjects) { - SubjectData latestSubject = subjects.first; +Subject getLatestSubject(List subjects) { + Subject latestSubject = subjects.first; for (final subject in subjects) { if (subject.endTime.hour > latestSubject.endTime.hour) { latestSubject = subject; @@ -38,7 +38,7 @@ SubjectData getLatestSubject(List subjects) { /// filters overlapping subjects by the current rotation week. void filterOverlappingSubjectsByRotationWeeks( - List> overlappingSubjects, + List> overlappingSubjects, ValueNotifier rotationWeek, ) { return overlappingSubjects.removeWhere( @@ -61,9 +61,9 @@ void filterOverlappingSubjectsByRotationWeeks( /// filters overlapping subjects by the current timetable. void filterOverlappingSubjectsByTimetable( - List> overlappingSubjects, - ValueNotifier currentTimetable, - List timetables, + List> overlappingSubjects, + ValueNotifier currentTimetable, + List timetables, ) { return overlappingSubjects.removeWhere( (elem) => elem.any( @@ -73,7 +73,7 @@ void filterOverlappingSubjectsByTimetable( } /// checks if 2 subjects overlap in time -bool doSubjectsOverlap(SubjectData a, SubjectData b, List group) { +bool doSubjectsOverlap(Subject a, Subject b, List group) { return a.day == b.day && (a.startTime.hour < b.endTime.hour || (a.startTime.hour == b.endTime.hour && @@ -85,8 +85,8 @@ bool doSubjectsOverlap(SubjectData a, SubjectData b, List group) { } void findOverlappingSubjectsWithinGroup( - List subjects, - List group, + List subjects, + List group, int startIndex, ) { for (int i = startIndex; i < subjects.length; i++) { @@ -108,8 +108,8 @@ void findOverlappingSubjectsWithinGroup( } } -List> findOverlappingSubjects(List subjects) { - final overlappingSubjects = >[]; +List> findOverlappingSubjects(List subjects) { + final overlappingSubjects = >[]; for (int i = 0; i < subjects.length; i++) { if (overlappingSubjects.any( @@ -118,7 +118,7 @@ List> findOverlappingSubjects(List subjects) { continue; } - final overlappingGroup = [subjects[i]]; + final overlappingGroup = [subjects[i]]; for (int j = i + 1; j < subjects.length; j++) { if (doSubjectsOverlap( diff --git a/lib/helpers/request_permission.dart b/lib/core/utils/request_permission.dart similarity index 89% rename from lib/helpers/request_permission.dart rename to lib/core/utils/request_permission.dart index 12e2e3e..a2b8163 100644 --- a/lib/helpers/request_permission.dart +++ b/lib/core/utils/request_permission.dart @@ -1,5 +1,5 @@ import 'package:permission_handler/permission_handler.dart'; -import 'package:timetable/helpers/get_os_version.dart'; +import 'package:timetable/core/utils/get_os_version.dart'; Future requestStoragePermission() async { final int androidVersion = await getAndroidVersion(); diff --git a/lib/helpers/rotation_weeks.dart b/lib/core/utils/rotation_weeks.dart similarity index 90% rename from lib/helpers/rotation_weeks.dart rename to lib/core/utils/rotation_weeks.dart index 2c7aa1a..6c65999 100644 --- a/lib/helpers/rotation_weeks.dart +++ b/lib/core/utils/rotation_weeks.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/db/database.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/db/database.dart'; /// Rotation Weeks button label. (used in the rotation week modal bottom sheet) String getRotationWeekLabel(RotationWeeks rotationWeek) { @@ -32,9 +32,9 @@ String getRotationWeekButtonLabel(RotationWeeks rotationWeek) { } /// Filters the list of subjects based on the selected rotation week. -List getFilteredByRotationWeeksSubjects( +List getFilteredByRotationWeeksSubjects( ValueNotifier rotationWeek, - List allSubjects, + List allSubjects, ) { switch (rotationWeek.value) { case RotationWeeks.all: @@ -75,7 +75,7 @@ List getFilteredByRotationWeeksSubjects( } /// Returns rotation week label of a Subject. -String getSubjectRotationWeekLabel(SubjectData subject) { +String getSubjectRotationWeekLabel(Subject subject) { switch (subject.rotationWeek) { case RotationWeeks.a: return "A"; diff --git a/lib/core/utils/subject_validation.dart b/lib/core/utils/subject_validation.dart new file mode 100644 index 0000000..88cafa6 --- /dev/null +++ b/lib/core/utils/subject_validation.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:timetable/core/constants/days.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/core/utils/time_management.dart'; + +/// Handles validation logic for subject scheduling conflicts +/// basically manages overlap detection between subjects +class SubjectValidation { + final List subjectsInSameDay; + final List inputHours; + final int subjectId; + final String label; + final String? location; + final Color color; + final TimeOfDay startTime; + final TimeOfDay endTime; + final Days day; + final RotationWeeks rotationWeek; + final String? note; + final String timetable; + final Subject? currentSubject; + final List> overlappingSubjects; + + SubjectValidation({ + required this.subjectsInSameDay, + required this.inputHours, + required this.subjectId, + required this.label, + required this.location, + required this.color, + required this.startTime, + required this.endTime, + required this.day, + required this.rotationWeek, + required this.note, + required this.timetable, + required this.currentSubject, + 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, + ); + + 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) + .where((s) => s.timetable == _tempSubject.timetable) + .where((s) => hasTimeConflict(s)) + .toList(); + } + + List getOverlappingConflicts() { + return subjectsInSameDay + .where((s) => overlappingSubjects.any((group) => group.contains(s))) + .where((s) => s.timetable == _tempSubject.timetable) + .where((s) => hasTimeConflict(s)) + .toList(); + } + + List getRegularConflicts() { + return 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); + } + + bool get hasConflicts => isOccupiedByOverlapping && hasMultipleOccupants; + bool get hasConflictsForExisting => + isOccupiedByRegular || hasMultipleOccupants; +} diff --git a/lib/helpers/subjects.dart b/lib/core/utils/subjects.dart similarity index 79% rename from lib/helpers/subjects.dart rename to lib/core/utils/subjects.dart index fbac7ed..483671c 100644 --- a/lib/helpers/subjects.dart +++ b/lib/core/utils/subjects.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:timetable/db/database.dart'; +import 'package:timetable/core/db/database.dart'; /// sorts subjects by time -List sortSubjects(List subjects) { +List sortSubjects(List subjects) { subjects.sort((a, b) => a.startTime.hour.compareTo(b.startTime.hour)); return subjects; diff --git a/lib/helpers/theme_helper.dart b/lib/core/utils/theme_helper.dart similarity index 96% rename from lib/helpers/theme_helper.dart rename to lib/core/utils/theme_helper.dart index 01de405..bd0723e 100644 --- a/lib/helpers/theme_helper.dart +++ b/lib/core/utils/theme_helper.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:timetable/constants/theme_options.dart'; +import 'package:timetable/core/constants/theme_options.dart'; /// Theme and color scheme management class ThemeHelper { diff --git a/lib/helpers/themes.dart b/lib/core/utils/themes.dart similarity index 82% rename from lib/helpers/themes.dart rename to lib/core/utils/themes.dart index 98c472c..7262a23 100644 --- a/lib/helpers/themes.dart +++ b/lib/core/utils/themes.dart @@ -1,4 +1,4 @@ -import 'package:timetable/constants/theme_options.dart'; +import 'package:timetable/core/constants/theme_options.dart'; /// for each value of the enum it will give a corresponding label. String getThemeLabel(ThemeOption theme) { diff --git a/lib/core/utils/time_formatter.dart b/lib/core/utils/time_formatter.dart new file mode 100644 index 0000000..b4378ff --- /dev/null +++ b/lib/core/utils/time_formatter.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class TimeFormatter { + static String getTime(TimeOfDay time, bool uses24HoursFormat) { + final minute = time.minute.toString().padLeft(2, '0'); + if (uses24HoursFormat) { + final hour = time.hour.toString().padLeft(2, '0'); + return "$hour:$minute"; + } + + final hour = + time.hour == 0 ? 12 : (time.hour > 12 ? time.hour - 12 : time.hour); + final period = time.hour >= 12 ? "PM" : "AM"; + final formattedHour = hour.toString().padLeft(2, '0'); + return "$formattedHour:$minute $period"; + } + + static String getTimeNoPadding(TimeOfDay time, bool uses24HoursFormat) { + final minute = time.minute.toString().padLeft(2, '0'); + if (uses24HoursFormat) return "${time.hour}:$minute"; + + final hour = + time.hour == 0 ? 12 : (time.hour > 12 ? time.hour - 12 : time.hour); + final period = time.hour >= 12 ? "PM" : "AM"; + return "$hour:$minute $period"; + } + + static ({String hour, String minute}) getTimeComponents(TimeOfDay time) { + return ( + hour: time.hour.toString().padLeft(2, '0'), + minute: time.minute.toString().padLeft(2, '0') + ); + } +} diff --git a/lib/helpers/time_management.dart b/lib/core/utils/time_management.dart similarity index 85% rename from lib/helpers/time_management.dart rename to lib/core/utils/time_management.dart index 09aea43..734995b 100644 --- a/lib/helpers/time_management.dart +++ b/lib/core/utils/time_management.dart @@ -3,7 +3,10 @@ import 'package:flutter/material.dart'; /// returns a time picker /// /// initialEntryMode is set by default to [TimePickerEntryMode.input] -Future timePicker(BuildContext context, TimeOfDay time) async { +Future timePicker({ + required TimeOfDay time, + required BuildContext context, +}) async { return await showTimePicker( context: context, initialEntryMode: TimePickerEntryMode.input, diff --git a/lib/helpers/timetables.dart b/lib/core/utils/timetables.dart similarity index 59% rename from lib/helpers/timetables.dart rename to lib/core/utils/timetables.dart index f5b1314..9273d6b 100644 --- a/lib/helpers/timetables.dart +++ b/lib/core/utils/timetables.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:timetable/db/database.dart'; +import 'package:timetable/core/db/database.dart'; /// filters subjects based on the current selected timetable // this should probably be in the subjects helper since it handles subjects -List getFilteredByTimetablesSubjects( - ValueNotifier currentTimetable, - List timetables, +List getFilteredByTimetablesSubjects( + ValueNotifier currentTimetable, + List timetables, bool multipleTimetables, - List allSubjects, + List allSubjects, ) { return allSubjects .where((s) => s.timetable == currentTimetable.value.name) diff --git a/lib/helpers/views.dart b/lib/core/utils/views.dart similarity index 81% rename from lib/helpers/views.dart rename to lib/core/utils/views.dart index 430effc..33162d4 100644 --- a/lib/helpers/views.dart +++ b/lib/core/utils/views.dart @@ -1,5 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:timetable/constants/timetable_views.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) { diff --git a/lib/db/converters/color.dart b/lib/db/converters/color.dart deleted file mode 100644 index eb97531..0000000 --- a/lib/db/converters/color.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:flutter/material.dart'; -import 'package:timetable/extensions/color.dart'; - -class ColorConverter extends TypeConverter { - const ColorConverter(); - - @override - Color fromSql(int fromDb) { - // Parse Color from int - return Color(fromDb); - } - - @override - int toSql(Color value) { - // get Color value (int) - return value.toInt(); - } -} diff --git a/lib/components/widgets/navigation/bottom_navigation_bar.dart b/lib/features/navigation/bottom_navigation.dart similarity index 77% rename from lib/components/widgets/navigation/bottom_navigation_bar.dart rename to lib/features/navigation/bottom_navigation.dart index 8244f96..8642a53 100644 --- a/lib/components/widgets/navigation/bottom_navigation_bar.dart +++ b/lib/features/navigation/bottom_navigation.dart @@ -3,11 +3,11 @@ 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/constants/navigation_items.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/subjects.dart'; -import 'package:timetable/screens/settings/settings.dart'; -import 'package:timetable/screens/timetable/timetable.dart'; +import 'package:timetable/core/constants/navigation_items.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; +import 'package:timetable/features/subjects/providers/subjects.dart'; +import 'package:timetable/features/settings/screens/settings.dart'; +import 'package:timetable/features/timetable/screens/timetable.dart'; /// The app's bottom navigation bar. class BottomNavigation extends HookConsumerWidget { @@ -23,7 +23,6 @@ class BottomNavigation extends HookConsumerWidget { void onTabTapped(int index) { if (currentPageIndex.value == index) return; - currentPageIndex.value = index; return; } @@ -47,10 +46,11 @@ class BottomNavigation extends HookConsumerWidget { duration: const Duration(milliseconds: 500), transitionBuilder: (child, animation, secondaryAnimation) { return SharedAxisTransition( - animation: animation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.horizontal, - child: child); + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.horizontal, + child: child, + ); }, child: _buildPage(currentPageIndex.value, subject), ), diff --git a/lib/components/widgets/navigation/navigation_bar_toggle.dart b/lib/features/navigation/widgets/navigation_bar_toggle.dart similarity index 93% rename from lib/components/widgets/navigation/navigation_bar_toggle.dart rename to lib/features/navigation/widgets/navigation_bar_toggle.dart index 0aa408c..7d43902 100644 --- a/lib/components/widgets/navigation/navigation_bar_toggle.dart +++ b/lib/features/navigation/widgets/navigation_bar_toggle.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/provider/settings.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; /// Toggles the navbar on and off to gain more screen space. class NavbarToggle extends HookConsumerWidget { diff --git a/lib/provider/settings.dart b/lib/features/settings/providers/settings.dart similarity index 96% rename from lib/provider/settings.dart rename to lib/features/settings/providers/settings.dart index d022d5a..ebee850 100644 --- a/lib/provider/settings.dart +++ b/lib/features/settings/providers/settings.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:timetable/constants/timetable_views.dart'; -import 'package:timetable/db/models/settings.dart'; +import 'package:timetable/core/constants/timetable_views.dart'; +import 'package:timetable/core/models/settings.dart'; /// App settings state management class SettingsNotifier extends StateNotifier { @@ -11,7 +11,6 @@ class SettingsNotifier extends StateNotifier { loadSettings(); } - // should be self-explanatory void updateDefaultSubjectDuration(Duration defaultSubjectDuration) { final newState = state.copyWith( defaultSubjectDuration: defaultSubjectDuration, diff --git a/lib/screens/settings/choose_app_color.dart b/lib/features/settings/screens/choose_app_color.dart similarity index 56% rename from lib/screens/settings/choose_app_color.dart rename to lib/features/settings/screens/choose_app_color.dart index 97728bf..52211fb 100644 --- a/lib/screens/settings/choose_app_color.dart +++ b/lib/features/settings/screens/choose_app_color.dart @@ -1,10 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/widgets/color_indicator.dart'; -import 'package:timetable/components/widgets/list_item_group.dart'; -import 'package:timetable/constants/colors.dart'; -import 'package:timetable/provider/settings.dart'; +import 'package:timetable/shared/widgets/color_indicator.dart'; +import 'package:timetable/shared/widgets/list_item_group.dart'; +import 'package:timetable/core/constants/colors.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; /// allows the user to choose the app color found in the settings screen of the app. class ChooseAppColor extends ConsumerWidget { @@ -28,30 +28,16 @@ class ChooseAppColor extends ConsumerWidget { return ListItem( title: Row( children: [ - Visibility( - visible: appThemeColor == colors[i].color, - child: const Row( - children: [ - Icon( - Icons.check, - size: 18, - ), - SizedBox( - width: 16, - ) - ], - ), + buildCheckmark( + appThemeColor: appThemeColor, + color: colors[i].color, ), ColorIndicator(color: colors[i].color), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Text(colors[i].label).tr(), ], ), - onTap: () { - settings.updateAppThemeColor(colors[i].color); - }, + onTap: () => settings.updateAppThemeColor(colors[i].color), ); }, ), @@ -60,4 +46,13 @@ class ChooseAppColor extends ConsumerWidget { ), ); } + + Widget buildCheckmark({required Color appThemeColor, required Color color}) { + return Visibility( + visible: appThemeColor == color, + child: const Row( + children: [Icon(Icons.check, size: 18), SizedBox(width: 16)], + ), + ); + } } diff --git a/lib/screens/settings/settings.dart b/lib/features/settings/screens/settings.dart similarity index 77% rename from lib/screens/settings/settings.dart rename to lib/features/settings/screens/settings.dart index b6cf741..05ea42c 100644 --- a/lib/screens/settings/settings.dart +++ b/lib/features/settings/screens/settings.dart @@ -1,13 +1,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/settings/customize_timetable.dart'; -import 'package:timetable/components/settings/general.dart'; -import 'package:timetable/components/settings/theme_options.dart'; -import 'package:timetable/components/settings/timetable_data.dart'; -import 'package:timetable/components/settings/timetable_features.dart'; -import 'package:timetable/components/widgets/navigation/navigation_bar_toggle.dart'; -import 'package:timetable/provider/settings.dart'; +import 'package:timetable/features/settings/widgets/customize_timetable.dart'; +import 'package:timetable/features/settings/widgets/general.dart'; +import 'package:timetable/features/settings/widgets/theme_options.dart'; +import 'package:timetable/features/settings/widgets/timetable_data.dart'; +import 'package:timetable/features/settings/widgets/timetable_features.dart'; +import 'package:timetable/features/navigation/widgets/navigation_bar_toggle.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; /// Settings Screen, groups all settings ([CustomizeTimetableOptions], [TimetableDataOptions], /// [TimetableFeaturesOptions] and [ThemeOptions]) together. diff --git a/lib/features/settings/screens/timetable_management.dart b/lib/features/settings/screens/timetable_management.dart new file mode 100644 index 0000000..fd11baa --- /dev/null +++ b/lib/features/settings/screens/timetable_management.dart @@ -0,0 +1,208 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:drift/drift.dart' as drift; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:timetable/shared/widgets/alert_dialog.dart'; +import 'package:timetable/shared/widgets/list_item_group.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; +import 'package:timetable/features/timetable/providers/timetables.dart'; + +class TimetableManagementScreen extends ConsumerWidget { + const TimetableManagementScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settingsNotifier = ref.read(settingsProvider.notifier); + final timetableNotifier = ref.read(timetableProvider.notifier); + final multipleTimetables = ref.watch(settingsProvider).multipleTimetables; + final timetables = ref.watch(timetableProvider); + + return Scaffold( + appBar: AppBar(title: const Text("manage_timetables").tr()), + body: ListView( + children: [ + buildMultipleTimetablesSwitch( + multipleTimetables: multipleTimetables, + settingsNotifier: settingsNotifier, + ), + buildResetButton( + context: context, + timetableNotifier: timetableNotifier, + ), + buildManageSection( + context: context, + multipleTimetables: multipleTimetables, + ), + buildTimetablesList( + timetables: timetables, + multipleTimetables: multipleTimetables, + timetableNotifier: timetableNotifier, + context: context, + ), + ], + ), + floatingActionButton: buildFAB( + timetables: timetables, + multipleTimetables: multipleTimetables, + timetableNotifier: timetableNotifier, + ), + ); + } + + Widget buildMultipleTimetablesSwitch({ + required bool multipleTimetables, + required SettingsNotifier settingsNotifier, + }) { + return SwitchListTile( + secondary: const Icon(Icons.backup_table_outlined, size: 20), + visualDensity: const VisualDensity(horizontal: -4), + title: const Text("multiple_timetables").tr(), + value: multipleTimetables, + onChanged: settingsNotifier.updateMultipleTimetables, + ); + } + + Widget buildResetButton({ + required BuildContext context, + required TimetableNotifier timetableNotifier, + }) { + return ListTile( + leading: const Icon(Icons.delete_forever_outlined, size: 20), + horizontalTitleGap: 8, + title: const Text("reset").tr(), + onTap: () => showResetDialog(context, timetableNotifier), + ); + } + + Widget buildManageSection({ + required BuildContext context, + required bool multipleTimetables, + }) { + return ListTile( + dense: true, + title: const Text("manage").tr(), + enabled: multipleTimetables, + textColor: Theme.of(context).colorScheme.primary, + ); + } + + Widget buildTimetablesList({ + required List timetables, + required bool multipleTimetables, + required TimetableNotifier timetableNotifier, + required BuildContext context, + }) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 75), + child: ListItemGroup( + children: List.generate( + timetables.length, + (i) => buildTimetableItem( + timetableItem: timetables[i], + firstTimetable: timetables[0], + multipleTimetables: multipleTimetables, + timetableNotifier: timetableNotifier, + context: context, + ), + ), + ), + ); + } + + ListItem buildTimetableItem({ + required Timetable timetableItem, + required Timetable firstTimetable, + required bool multipleTimetables, + required TimetableNotifier timetableNotifier, + required BuildContext context, + }) { + final isFirstTimetable = timetableItem == firstTimetable; + final opacity = isFirstTimetable ? 1.0 : (multipleTimetables ? 1.0 : 0.5); + final showDeleteButton = !isFirstTimetable && multipleTimetables; + + return ListItem( + title: Opacity( + opacity: opacity, + child: Row( + children: [ + Text("${"timetable".plural(1)} ${timetableItem.name}"), + const Spacer(), + if (showDeleteButton) + buildDeleteButton(context, timetableNotifier, timetableItem), + ], + ), + ), + onTap: () {}, + ); + } + + Widget buildDeleteButton( + BuildContext context, + TimetableNotifier timetable, + Timetable timetableItem, + ) { + return IconButton( + onPressed: () => showDeleteDialog(context, timetable, timetableItem), + icon: const Icon(Icons.delete_outline), + tooltip: "delete".tr(), + ); + } + + Widget? buildFAB({ + required List timetables, + required bool multipleTimetables, + required TimetableNotifier timetableNotifier, + }) { + final canAddTimetable = timetables.length < 5 && multipleTimetables; + if (!canAddTimetable) return null; + + return FloatingActionButton( + onPressed: () => addTimetable(timetables, timetableNotifier), + tooltip: "create".tr(), + child: const Icon(Icons.add), + ); + } + + void showResetDialog( + BuildContext context, + TimetableNotifier timetableNotifier, + ) { + showDialog( + context: context, + barrierDismissible: true, + builder: (_) => ShowAlertDialog( + content: const Text("reset_timetables_dialog").tr(), + approveButtonText: "reset".tr(), + onApprove: () { + timetableNotifier.resetData(); + Navigator.of(context).pop(); + }, + ), + ); + } + + void showDeleteDialog( + BuildContext context, + TimetableNotifier timetableNotifier, + Timetable timetableItem, + ) { + showDialog( + context: context, + barrierDismissible: true, + builder: (_) => ShowAlertDialog( + content: const Text("delete_timetable_dialog").tr(), + approveButtonText: "delete".tr(), + onApprove: () { + timetableNotifier.deleteTimetable(timetableItem); + Navigator.of(context).pop(); + }, + ), + ); + } + + void addTimetable(List timetables, TimetableNotifier timetable) { + final nextName = (int.parse(timetables.last.name) + 1).toString(); + timetable.addTimetable(TimetablesCompanion(name: drift.Value(nextName))); + } +} diff --git a/lib/features/settings/screens/timetable_period.dart b/lib/features/settings/screens/timetable_period.dart new file mode 100644 index 0000000..52d91e9 --- /dev/null +++ b/lib/features/settings/screens/timetable_period.dart @@ -0,0 +1,174 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:timetable/core/utils/time_formatter.dart'; +import 'package:timetable/core/utils/time_management.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; + +/// Screen to manage the period of the timetable. +/// +/// Changes the start time and end time of the timetable. +class TimetablePeriodScreen extends ConsumerWidget { + const TimetablePeriodScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final customTimePeriod = ref.watch(settingsProvider).customTimePeriod; + final customStartTime = ref.watch(settingsProvider).customStartTime; + final customEndTime = ref.watch(settingsProvider).customEndTime; + final twentyFourHours = ref.watch(settingsProvider).twentyFourHours; + final settings = ref.read(settingsProvider.notifier); + + final uses24HoursFormat = MediaQuery.of(context).alwaysUse24HourFormat; + + /// error dialog when the start time and end time have the same value (equal) + void showInvalidEqualTimeDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('invalid_time').tr(), + content: const Text('invalid_equal_time_error').tr(), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('ok').tr(), + ), + ], + ), + ); + } + + /// switches the start and end time of the timetable + /// + /// used when the start time is after the end time that + void switchStartWithEndTime(TimeOfDay newTime) { + final temp = customEndTime; + settings.updateCustomEndTime(newTime); + settings.updateCustomStartTime(temp); + return; + } + + /// switches the end and start time of the timetable + /// + /// used when the end time is before the start time that + void switchEndWithStartTime(TimeOfDay newTime) { + final temp = customStartTime; + settings.updateCustomStartTime(newTime); + settings.updateCustomEndTime(temp); + return; + } + + Future handleTimeSelection( + TimeOfDay currentTime, + TimeOfDay otherTime, + bool isStartTime, + ) async { + final selectedTime = await timePicker( + context: context, + time: isStartTime ? customStartTime : customEndTime, + ); + if (selectedTime == null) return; + + if (selectedTime.hour == otherTime.hour) { + showInvalidEqualTimeDialog(); + return; + } + + if (isStartTime && selectedTime.isAfter(otherTime)) { + switchStartWithEndTime(selectedTime); + } else if (!isStartTime && selectedTime.isBefore(otherTime)) { + switchEndWithStartTime(selectedTime); + } else { + isStartTime + ? settings.updateCustomStartTime(selectedTime) + : settings.updateCustomEndTime(selectedTime); + } + } + + return Scaffold( + appBar: AppBar(title: const Text("period_preferences").tr()), + body: ListView( + children: [ + buildSwitchListTile( + icon: Icons.edit_outlined, + titleKey: "custom_time_period", + value: customTimePeriod, + onChanged: (value) { + settings.updateCustomTimePeriod(value); + if (twentyFourHours) settings.update24Hours(!value); + }, + ), + buildSwitchListTile( + icon: Icons.schedule_outlined, + titleKey: "24_hour_period", + value: twentyFourHours, + onChanged: (value) { + settings.update24Hours(value); + if (customTimePeriod) settings.updateCustomTimePeriod(false); + }, + ), + ListTile( + dense: true, + title: const Text("configuration").tr(), + enabled: customTimePeriod, + textColor: Theme.of(context).colorScheme.primary, + ), + buildTimeListTile( + icon: Icons.play_arrow_outlined, + titleKey: "start_time", + time: customStartTime, + enabled: customTimePeriod, + getTime: TimeFormatter.getTime, + uses24HoursFormat: uses24HoursFormat, + onTap: () => + handleTimeSelection(customStartTime, customEndTime, true), + ), + buildTimeListTile( + icon: Icons.stop_outlined, + titleKey: "end_time", + time: customEndTime, + enabled: customTimePeriod, + getTime: TimeFormatter.getTime, + uses24HoursFormat: uses24HoursFormat, + onTap: () => + handleTimeSelection(customEndTime, customStartTime, false), + ), + ], + ), + ); + } + + Widget buildSwitchListTile({ + required IconData icon, + required String titleKey, + required bool value, + required ValueChanged onChanged, + }) { + return SwitchListTile( + secondary: Icon(icon, size: 20), + visualDensity: const VisualDensity(horizontal: -4), + title: Text(titleKey).tr(), + value: value, + onChanged: onChanged, + ); + } + + Widget buildTimeListTile({ + required IconData icon, + required String titleKey, + required TimeOfDay time, + required bool enabled, + required VoidCallback onTap, + required bool uses24HoursFormat, + required String Function(TimeOfDay time, bool uses24HoursFormat) getTime, + }) { + return ListTile( + leading: Icon(icon, size: 20), + horizontalTitleGap: 8, + title: Text(titleKey).tr(), + enabled: enabled, + subtitle: Text(getTime(time, uses24HoursFormat)), + onTap: enabled ? onTap : null, + ); + } +} diff --git a/lib/features/settings/widgets/customize_timetable.dart b/lib/features/settings/widgets/customize_timetable.dart new file mode 100644 index 0000000..66d2407 --- /dev/null +++ b/lib/features/settings/widgets/customize_timetable.dart @@ -0,0 +1,79 @@ +import 'package:easy_localization/easy_localization.dart'; +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'; + +/// All the settings that allow for customizing the timetable. +class CustomizeTimetableOptions extends ConsumerWidget { + const CustomizeTimetableOptions({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.read(settingsProvider.notifier); + final hideSunday = ref.watch(settingsProvider).hideSunday; + final compactMode = ref.watch(settingsProvider).compactMode; + final hideLocation = ref.watch(settingsProvider).hideLocation; + final singleLetterDays = ref.watch(settingsProvider).singleLetterDays; + final hideTransparentSubject = + ref.watch(settingsProvider).hideTransparentSubject; + final defaultTimetableView = + ref.watch(settingsProvider).defaultTimetableView; + + final switchItems = [ + { + 'icon': Icons.close_fullscreen_outlined, + 'title': 'compact_mode', + 'value': compactMode, + 'onChanged': settings.updateCompactMode, + }, + { + 'icon': Icons.title_outlined, + 'title': 'single_letter_days', + 'value': singleLetterDays, + 'onChanged': settings.updateSingleLetterDays, + }, + { + 'icon': Icons.location_off_outlined, + 'title': 'hide_locations', + 'value': hideLocation, + 'onChanged': settings.updateHideLocation, + }, + { + 'icon': Icons.visibility_off_outlined, + 'title': 'hide_sunday', + 'value': hideSunday, + 'onChanged': settings.updateHideSunday, + }, + { + 'icon': Icons.visibility_off_outlined, + 'title': 'hide_transparent_subjects', + 'value': hideTransparentSubject, + 'onChanged': settings.updateHideTransparentSubject, + }, + ]; + + return Column( + children: [ + ListTile( + horizontalTitleGap: 8, + leading: const Icon(Icons.table_chart_outlined, size: 20), + title: DefaultViewOptions( + settings: settings, + defaultTimetableView: defaultTimetableView, + ), + onTap: () {}, + ), + ...switchItems.map( + (item) => SwitchListTile( + secondary: Icon(item['icon'] as IconData, size: 20), + visualDensity: const VisualDensity(horizontal: -4), + title: Text(item['title'] as String).tr(), + value: item['value'] as bool, + onChanged: item['onChanged'] as Function(bool), + ), + ), + ], + ); + } +} diff --git a/lib/components/settings/default_view_options.dart b/lib/features/settings/widgets/default_view_options.dart similarity index 82% rename from lib/components/settings/default_view_options.dart rename to lib/features/settings/widgets/default_view_options.dart index 5820beb..55f8996 100644 --- a/lib/components/settings/default_view_options.dart +++ b/lib/features/settings/widgets/default_view_options.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/constants/timetable_views.dart'; -import 'package:timetable/helpers/views.dart'; -import 'package:timetable/provider/settings.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. class DefaultViewOptions extends StatelessWidget { @@ -25,10 +25,7 @@ class DefaultViewOptions extends StatelessWidget { final label = getViewLabel(option); viewEntries.add( - DropdownMenuEntry( - value: option, - label: label, - ), + DropdownMenuEntry(value: option, label: label), ); } return viewEntries.toList(); diff --git a/lib/components/settings/general.dart b/lib/features/settings/widgets/general.dart similarity index 56% rename from lib/components/settings/general.dart rename to lib/features/settings/widgets/general.dart index a38a2fd..6c81f9d 100644 --- a/lib/components/settings/general.dart +++ b/lib/features/settings/widgets/general.dart @@ -1,14 +1,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/settings/language_options.dart'; -import 'package:timetable/screens/settings/choose_app_color.dart'; -import 'package:timetable/components/settings/theme_options.dart'; -import 'package:timetable/components/widgets/color_indicator.dart'; -import 'package:timetable/helpers/get_os_version.dart'; -import 'package:timetable/provider/language.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/themes.dart'; +import 'package:timetable/features/settings/widgets/language_options.dart'; +import 'package:timetable/features/settings/screens/choose_app_color.dart'; +import 'package:timetable/features/settings/widgets/theme_options.dart'; +import 'package:timetable/shared/widgets/color_indicator.dart'; +import 'package:timetable/core/utils/get_os_version.dart'; +import 'package:timetable/shared/providers/language.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; +import 'package:timetable/shared/providers/themes.dart'; /// All the general app settings (mostly appearance). class GeneralOptions extends ConsumerWidget { @@ -25,47 +25,31 @@ class GeneralOptions extends ConsumerWidget { return Column( children: [ ListTile( - leading: const Icon( - Icons.translate, - size: 20, - ), + leading: const Icon(Icons.translate, size: 20), horizontalTitleGap: 8, - title: LanguageOptions( - language: language, - ), + title: LanguageOptions(language: language), onTap: () {}, ), ListTile( - leading: const Icon( - Icons.palette_outlined, - size: 20, - ), + leading: const Icon(Icons.palette_outlined, size: 20), horizontalTitleGap: 8, title: ThemeOptions(theme: theme), onTap: () {}, ), FutureBuilder( future: getAndroidVersion(), - builder: ( - BuildContext context, - AsyncSnapshot snapshot, - ) { + builder: (BuildContext context, AsyncSnapshot snapshot) { final version = snapshot.data; if (version != null && version >= 31) { return SwitchListTile( - secondary: const Icon( - Icons.wallpaper_outlined, - size: 20, - ), + secondary: const Icon(Icons.wallpaper_outlined, size: 20), // this is dumb.. I couldn't find a better way to change the gap size on switch list tiles visualDensity: const VisualDensity(horizontal: -4), title: const Text("monet_theming").tr(), subtitle: const Text("Android 12+"), value: monetTheming, - onChanged: (bool value) { - settings.updateMonetThemeing(value); - }, + onChanged: (bool value) => settings.updateMonetThemeing(value), ); } return Container(); @@ -73,20 +57,17 @@ class GeneralOptions extends ConsumerWidget { ), ListTile( enabled: !monetTheming, - leading: const Icon( - Icons.colorize_outlined, - size: 20, - ), + leading: const Icon(Icons.colorize_outlined, size: 20), horizontalTitleGap: 8, title: const Text("app_color").tr(), - trailing: - ColorIndicator(color: appThemeColor, inactive: monetTheming), + trailing: ColorIndicator( + color: appThemeColor, + inactive: monetTheming, + ), onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => const ChooseAppColor(), - ), + MaterialPageRoute(builder: (context) => const ChooseAppColor()), ); }, ) diff --git a/lib/features/settings/widgets/language_options.dart b/lib/features/settings/widgets/language_options.dart new file mode 100644 index 0000000..3297a85 --- /dev/null +++ b/lib/features/settings/widgets/language_options.dart @@ -0,0 +1,62 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:timetable/core/constants/languages.dart'; +import 'package:timetable/core/utils/languages.dart'; +import 'package:timetable/shared/providers/language.dart'; + +/// app language options dropdown menu. +class LanguageOptions extends StatelessWidget { + final LanguageNotifier language; + + const LanguageOptions({ + super.key, + required this.language, + }); + + @override + Widget build(BuildContext context) { + /// The dropdown menu entries of each language. + List> languageEntries() { + final themeEntries = >[]; + + for (final Locale option in languages) { + final label = getLanguageLabel(option); + + themeEntries.add( + DropdownMenuEntry(value: option, label: label), + ); + } + return themeEntries.toList(); + } + + return Row( + children: [ + const Text('language').tr(), + const Spacer(), + FutureBuilder( + future: language.getLanguage(), + builder: (context, snapshot) { + if (!snapshot.hasData) return Container(); + + return DropdownMenu( + width: 130, + dropdownMenuEntries: languageEntries(), + label: const Text("language").tr(), + initialSelection: snapshot.data, + onSelected: (value) { + language.changeLanguage(value!); + context.setLocale(value); + // https://github.com/aissat/easy_localization/issues/370 + // https://github.com/aissat/easy_localization/issues/370#issuecomment-1312480842 + // couldn't find any other solution but reassembling the app + + final engine = WidgetsFlutterBinding.ensureInitialized(); + engine.performReassemble(); + }, + ); + }, + ), + ], + ); + } +} diff --git a/lib/components/settings/theme_options.dart b/lib/features/settings/widgets/theme_options.dart similarity index 80% rename from lib/components/settings/theme_options.dart rename to lib/features/settings/widgets/theme_options.dart index e524068..b39d7d8 100644 --- a/lib/components/settings/theme_options.dart +++ b/lib/features/settings/widgets/theme_options.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/constants/theme_options.dart'; -import 'package:timetable/provider/themes.dart'; -import 'package:timetable/helpers/themes.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 { @@ -23,10 +23,7 @@ class ThemeOptions extends StatelessWidget { final label = getThemeLabel(option).tr(); themeEntries.add( - DropdownMenuEntry( - value: option, - label: label, - ), + DropdownMenuEntry(value: option, label: label), ); } return themeEntries.toList(); diff --git a/lib/components/settings/timetable_data.dart b/lib/features/settings/widgets/timetable_data.dart similarity index 77% rename from lib/components/settings/timetable_data.dart rename to lib/features/settings/widgets/timetable_data.dart index 77bc658..734a1fd 100644 --- a/lib/components/settings/timetable_data.dart +++ b/lib/features/settings/widgets/timetable_data.dart @@ -1,11 +1,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/widgets/alert_dialog.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/db/services/service.dart'; -import 'package:timetable/provider/subjects.dart'; -import 'package:timetable/provider/timetables.dart'; +import 'package:timetable/shared/widgets/alert_dialog.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/core/db/services/service.dart'; +import 'package:timetable/features/subjects/providers/subjects.dart'; +import 'package:timetable/features/timetable/providers/timetables.dart'; /// All the settings that allow for manipulating the timetable's data. class TimetableDataOptions extends ConsumerWidget { @@ -17,10 +17,6 @@ class TimetableDataOptions extends ConsumerWidget { final tbNotifier = ref.watch(timetableProvider.notifier); final db = ref.watch(AppDatabase.databaseProvider); - void pop() { - return Navigator.of(context).pop(); - } - return Column( children: [ ListTile( @@ -30,10 +26,7 @@ class TimetableDataOptions extends ConsumerWidget { title: const Text("create_backup").tr(), ), ListTile( - leading: const Icon( - Icons.file_upload_outlined, - size: 20, - ), + leading: const Icon(Icons.file_upload_outlined, size: 20), horizontalTitleGap: 8, onTap: () async { showDialog( @@ -46,11 +39,14 @@ class TimetableDataOptions extends ConsumerWidget { ), approveButtonText: "proceed".tr(), onApprove: () async { + final nav = Navigator.of(context); + await restoreData(db).then((_) { - tbNotifier.loadTimetables(); - subjNotifier.loadSubjects(); + tbNotifier.fetchTimetablesFromDatabase(); + subjNotifier.fetchSubjectsFromDatabase(); }); - pop(); + + nav.pop(); }, ); }, @@ -59,10 +55,7 @@ class TimetableDataOptions extends ConsumerWidget { title: const Text("restore_backup").tr(), ), ListTile( - leading: const Icon( - Icons.delete_forever_outlined, - size: 20, - ), + leading: const Icon(Icons.delete_forever_outlined, size: 20), horizontalTitleGap: 8, onTap: () { showDialog( diff --git a/lib/components/settings/timetable_features.dart b/lib/features/settings/widgets/timetable_features.dart similarity index 69% rename from lib/components/settings/timetable_features.dart rename to lib/features/settings/widgets/timetable_features.dart index 4e38a65..f3c2bd5 100644 --- a/lib/components/settings/timetable_features.dart +++ b/lib/features/settings/widgets/timetable_features.dart @@ -2,10 +2,10 @@ 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/screens/settings/timetable_management.dart'; -import 'package:timetable/components/widgets/bottom_sheets/subject_duration_modal_bottom_sheet.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/screens/settings/timetable_period.dart'; +import 'package:timetable/features/settings/screens/timetable_management.dart'; +import 'package:timetable/shared/widgets/bottom_sheets/subject_duration.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; +import 'package:timetable/features/settings/screens/timetable_period.dart'; /// All the settings for changing some timetable features. class TimetableFeaturesOptions extends HookConsumerWidget { @@ -23,10 +23,7 @@ class TimetableFeaturesOptions extends HookConsumerWidget { return Column( children: [ ListTile( - leading: const Icon( - Icons.schedule_outlined, - size: 20, - ), + leading: const Icon(Icons.schedule_outlined, size: 20), horizontalTitleGap: 8, title: const Text("timetable_period_config").tr(), onTap: () { @@ -39,10 +36,7 @@ class TimetableFeaturesOptions extends HookConsumerWidget { }, ), ListTile( - leading: const Icon( - Icons.table_view_outlined, - size: 20, - ), + leading: const Icon(Icons.table_view_outlined, size: 20), horizontalTitleGap: 8, onTap: () { Navigator.push( @@ -55,15 +49,12 @@ class TimetableFeaturesOptions extends HookConsumerWidget { title: const Text("manage_timetables").tr(), ), ListTile( - leading: const Icon( - Icons.timer_outlined, - size: 20, - ), + leading: const Icon(Icons.timer_outlined, size: 20), horizontalTitleGap: 8, title: const Text("default_subject_duration").tr(), trailing: Text("${defaultSubjectDuration.inMinutes}min"), onTap: () { - showModalBottomSheet( + showModalBottomSheet( showDragHandle: true, enableDrag: true, isDismissible: true, @@ -72,9 +63,7 @@ class TimetableFeaturesOptions extends HookConsumerWidget { builder: (context) { return Wrap( children: [ - SubjectDurationModalBottomSheet( - duration: duration, - ), + SubjectDurationModalBottomSheet(duration: duration), ], ); }, @@ -84,29 +73,19 @@ class TimetableFeaturesOptions extends HookConsumerWidget { }, ), SwitchListTile( - secondary: const Icon( - Icons.rotate_90_degrees_ccw_outlined, - size: 20, - ), + secondary: const Icon(Icons.rotate_90_degrees_ccw_outlined, size: 20), visualDensity: const VisualDensity(horizontal: -4), title: const Text("rotation_week").plural(2), value: rotationWeeks, - onChanged: (bool value) { - settings.updateRotationWeeks(value); - }, + onChanged: (bool value) => settings.updateRotationWeeks(value), ), SwitchListTile( - secondary: const Icon( - Icons.format_color_fill_outlined, - size: 20, - ), + secondary: const Icon(Icons.format_color_fill_outlined, size: 20), visualDensity: const VisualDensity(horizontal: -4), title: const Text("auto_complete_colors").tr(), subtitle: const Text("auto_complete_colors_description").tr(), value: autoCompleteColor, - onChanged: (bool value) { - settings.updateAutoCompleteColor(value); - }, + onChanged: (bool value) => settings.updateAutoCompleteColor(value), ), ], ); diff --git a/lib/provider/overlapping_subjects.dart b/lib/features/subjects/providers/overlapping_subjects.dart similarity index 81% rename from lib/provider/overlapping_subjects.dart rename to lib/features/subjects/providers/overlapping_subjects.dart index c1eaeeb..779baef 100644 --- a/lib/provider/overlapping_subjects.dart +++ b/lib/features/subjects/providers/overlapping_subjects.dart @@ -1,25 +1,23 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/db/database.dart'; +import 'package:timetable/core/db/database.dart'; /// Overlapping Subjects state management // idea: maybe i should save the list of overlapping subjects // instead of trying to find them everytime the app is opened // should help with performance, probably -class OverlappingSubjects extends StateNotifier>> { +class OverlappingSubjects extends StateNotifier>> { OverlappingSubjects() : super([]); /// adds a whole list of subjects to the state - void addInBulk(List> value) { + void addInBulk(List> value) { state = value; } /// deletes all current data, usually to fetch new data and avoid errors - void reset() { - state = []; - } + void reset() => state = []; } final overlappingSubjectsProvider = - StateNotifierProvider>>( + StateNotifierProvider>>( (ref) => OverlappingSubjects(), ); diff --git a/lib/features/subjects/providers/subjects.dart b/lib/features/subjects/providers/subjects.dart new file mode 100644 index 0000000..21d001c --- /dev/null +++ b/lib/features/subjects/providers/subjects.dart @@ -0,0 +1,83 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/features/subjects/providers/overlapping_subjects.dart'; + +/// Subject state management +/// manages CRUD operations for subjects, handles subject filtering and state updates +/// also manages subject-timetable relationships +class SubjectNotifier extends StateNotifier> { + final AppDatabase db; + final OverlappingSubjects overlappingSubjectsNotifier; + + late StreamSubscription _subscription; + + SubjectNotifier(this.db, this.overlappingSubjectsNotifier) : super([]) { + _subscription = db.subjects.select().watch().listen((subjects) { + state = subjects; + }); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + Future fetchSubjectsFromDatabase() async { + await db.subjects.select().get(); + } + + /// adds a subject ([SubjectsCompanion]) to the database ([$SubjectsTable]) + Future addSubject(SubjectsCompanion entry) async { + await db.subjects.insertOne(entry); + } + + /// updates an already existing [Subject] + /// + /// also resets the overlapping subjects notifier + /// so it refetches new data, otherwise there will be new + /// and old data at the same time + Future updateSubject(SubjectsCompanion entry) async { + await db.subjects.update().replace(entry); + + overlappingSubjectsNotifier.reset(); + } + + /// deletes a [Subject] from db ([$SubjectsTable]) + Future deleteSubject(Subject entry) async { + await db.subjects.deleteWhere((t) => t.id.equals(entry.id)); + + overlappingSubjectsNotifier.reset(); + } + + /// executed from the [TimetableNotifier] to delete all the subjects + /// in the deleted timetable to avoid errors + Future deleteTimetableSubjects( + List timetables, + Timetable timetable, + ) async { + var subjects = await db.subjects.select().get(); + + for (var subject in subjects.where((e) => e.timetable == timetable.name)) { + await db.subjects.deleteWhere((t) => t.id.equals(subject.id)); + } + } + + /// deletes [$SubjectsTable] and resets overlapping subjects and state. + Future resetData() async { + await db.delete($SubjectsTable(db)).go(); + state = []; + + overlappingSubjectsNotifier.reset(); + } +} + +final subjectProvider = StateNotifierProvider>( + (ref) => SubjectNotifier( + ref.watch(AppDatabase.databaseProvider), + ref.watch(overlappingSubjectsProvider.notifier), + ), +); diff --git a/lib/components/subject_management/subject_configs/colors_screens/colors.dart b/lib/features/subjects/screens/colors.dart similarity index 68% rename from lib/components/subject_management/subject_configs/colors_screens/colors.dart rename to lib/features/subjects/screens/colors.dart index 703f06c..5e4c408 100644 --- a/lib/components/subject_management/subject_configs/colors_screens/colors.dart +++ b/lib/features/subjects/screens/colors.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/components/subject_management/subject_configs/colors_screens/color_picker.dart'; -import 'package:timetable/components/subject_management/subject_configs/colors_screens/preset_colors.dart'; -import 'package:timetable/constants/colors.dart'; +import 'package:timetable/features/subjects/widgets/color_picker.dart'; +import 'package:timetable/features/subjects/widgets/preset_colors.dart'; +import 'package:timetable/core/constants/colors.dart'; /// Colors configuration screen, basically groups [ColorPickerScreen] and [PresetColorsScreen]. @@ -11,10 +11,7 @@ class ColorsScreen extends StatefulWidget { /// the color that will be manipulated final ValueNotifier color; - const ColorsScreen({ - super.key, - required this.color, - }); + const ColorsScreen({super.key, required this.color}); @override State createState() => _ColorsScreenState(); @@ -51,10 +48,7 @@ class _ColorsScreenState extends State icon: const Icon(Icons.color_lens_outlined), text: "presets".tr(), ), - Tab( - icon: const Icon(Icons.colorize), - text: "custom".tr(), - ), + Tab(icon: const Icon(Icons.colorize), text: "custom".tr()), ], ), ), @@ -62,16 +56,9 @@ class _ColorsScreenState extends State controller: _tabController, children: [ Center( - child: PresetColorsScreen( - color: widget.color, - colors: colors, - ), - ), - Center( - child: ColorPickerScreen( - color: widget.color, - ), + child: PresetColorsScreen(color: widget.color, colors: colors), ), + Center(child: ColorPickerScreen(color: widget.color)), ], ), ); diff --git a/lib/components/subject_management/subject_screen.dart b/lib/features/subjects/screens/subject_screen.dart similarity index 58% rename from lib/components/subject_management/subject_screen.dart rename to lib/features/subjects/screens/subject_screen.dart index 876b1ae..71a332e 100644 --- a/lib/components/subject_management/subject_screen.dart +++ b/lib/features/subjects/screens/subject_screen.dart @@ -1,26 +1,28 @@ +import 'package:drift/drift.dart' as drift; 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/components/subject_management/subject_configs/color.dart'; -import 'package:timetable/components/subject_management/subject_configs/day_time_week_tb_config.dart'; -import 'package:timetable/components/subject_management/subject_configs/label_location.dart'; -import 'package:timetable/components/subject_management/subject_configs/note.dart'; -import 'package:timetable/constants/days.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/helpers/time_management.dart'; -import 'package:timetable/provider/overlapping_subjects.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/subjects.dart'; -import 'package:timetable/provider/timetables.dart'; +import 'package:timetable/core/utils/subject_validation.dart'; +import 'package:timetable/features/subjects/widgets/color.dart'; +import 'package:timetable/features/subjects/widgets/day_time_week_tb_config.dart'; +import 'package:timetable/features/subjects/widgets/label_location.dart'; +import 'package:timetable/features/subjects/widgets/note.dart'; +import 'package:timetable/core/constants/days.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/core/utils/time_management.dart'; +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'; /// Subject creation/modification UI. class SubjectScreen extends HookConsumerWidget { final int? rowIndex; final int? columnIndex; - final SubjectData? subject; - final ValueNotifier? currentTimetable; + final Subject? subject; + final ValueNotifier? currentTimetable; const SubjectScreen({ super.key, @@ -35,7 +37,6 @@ class SubjectScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final subjects = ref.watch(subjectProvider); - final subjectNotifier = ref.watch(subjectProvider.notifier); final overlappingSubjects = ref.watch(overlappingSubjectsProvider); final autoCompleteColor = ref.watch(settingsProvider).autoCompleteColor; final timetables = ref.watch(timetableProvider); @@ -48,8 +49,11 @@ class SubjectScreen extends HookConsumerWidget { final bool isSubjectNull = (subject == null); final bool isCurrentTimetableNull = (currentTimetable == null); final int id = (isSubjectNull ? subjects.length : subject!.id); + final int baseHour = isSubjectNull + ? rowIndex! + (tfHours ? 0 : customStartTimeHour) + : (tfHours ? 0 : customStartTimeHour); - final ValueNotifier timetable = useState( + final ValueNotifier timetable = useState( isSubjectNull ? isCurrentTimetableNull ? timetables @@ -59,18 +63,12 @@ class SubjectScreen extends HookConsumerWidget { : timetables.where((t) => t.name == subject!.timetable).firstOrNull, ); final ValueNotifier startTime = useState( - TimeOfDay( - hour: subject?.startTime.hour ?? - (rowIndex! + (tfHours ? 0 : customStartTimeHour)), - minute: 0, - ), + TimeOfDay(hour: subject?.startTime.hour ?? baseHour, minute: 0), ); final ValueNotifier endTime = useState( TimeOfDay( - hour: subject?.endTime.hour ?? - (rowIndex! + - (tfHours ? 0 : customStartTimeHour) + - defaultSubjectDuration.inHours), + hour: + subject?.endTime.hour ?? baseHour + defaultSubjectDuration.inHours, minute: 0, ), ); @@ -84,10 +82,23 @@ class SubjectScreen extends HookConsumerWidget { final ValueNotifier rotationWeek = useState(subject?.rotationWeek ?? RotationWeeks.none); - // I DONT KNOW WHY I AM USING [SubjectData] I SHOULD BE USING [SubjectCompanion] - // update: using [SubjectCompanion] breaks a lot of stuff so i will not be using that - final SubjectData newSubject = SubjectData( - id: id, + final SubjectsCompanion newSubject = SubjectsCompanion( + id: isSubjectNull ? const drift.Value.absent() : drift.Value(subject!.id), + label: drift.Value(label.value), + location: drift.Value(location.value), + color: drift.Value(color.value), + startTime: drift.Value(startTime.value), + endTime: drift.Value(endTime.value), + day: drift.Value(day.value), + rotationWeek: drift.Value(rotationWeek.value), + note: drift.Value(note.value), + timetable: drift.Value(timetable.value!.name), + ); + + final validation = SubjectValidation( + subjectsInSameDay: subjects.where((e) => e.day == day.value).toList(), + inputHours: getHoursList(startTime.value, endTime.value), + subjectId: id, label: label.value, location: location.value, color: color.value, @@ -97,59 +108,10 @@ class SubjectScreen extends HookConsumerWidget { rotationWeek: rotationWeek.value, note: note.value, timetable: timetable.value!.name, + currentSubject: subject, + overlappingSubjects: overlappingSubjects, ); - final subjectsInSameDay = - subjects.where((e) => e.day == day.value).toList(); - -// all the upcomming variables are checks to -// limit the amount of overlapping subjects. -// i should really work and find a solution to that issue.. - - final inputHours = getHoursList(startTime.value, endTime.value); - - // Check if subject is in overlapping subjects list - final isInOverlappingList = overlappingSubjects.any((e) { - for (var subject in e) { - return newSubject == subject; - } - - return false; - }); - - // Check for multiple subjects in same time slot - final multipleOccupied = !isInOverlappingList && - (subjectsInSameDay - .where((s) { - final sHours = getHoursList(s.startTime, s.endTime); - return hasTimeOverlap(sHours, inputHours); - }) - .where((s) => s != subject) - .where((e) => e.timetable == newSubject.timetable) - .length > - 1); - - // Check if time slot is occupied by overlapping subjects - final isOccupied = subjectsInSameDay - .where((e) => overlappingSubjects.any((elem) => elem.contains(e))) - .where((e) => e.timetable == newSubject.timetable) - .any((e) { - final eHours = getHoursList(e.startTime, e.endTime); - return hasTimeOverlap(eHours, inputHours); - }); - - // Check if time slot is occupied by non-overlapping subjects (excluding self) - final isOccupiedExceptSelf = subjectsInSameDay - .where((e) => e != subject) - .where((e) => !overlappingSubjects.any((elem) => elem.contains(e))) - .where((e) => e.timetable == newSubject.timetable) - .any((e) { - final eHours = getHoursList(e.startTime, e.endTime); - return hasTimeOverlap(eHours, inputHours); - }); - -// end of checks - return Scaffold( appBar: AppBar( actions: [ @@ -167,21 +129,25 @@ class SubjectScreen extends HookConsumerWidget { onPressed: () async { if (!formKey.currentState!.validate()) return; - if (isOccupied && multipleOccupied) { + final hasConflitcs = isSubjectNull + ? validation.hasConflicts + : validation.hasConflictsForExisting; + + if (hasConflitcs) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('time_slots_occupied_error').tr(), ), ); - return; } final navigator = Navigator.of(context); + final subjectNotifier = ref.watch(subjectProvider.notifier); if (isSubjectNull) { await subjectNotifier - .addSubject(newSubject.toCompanion(true)) + .addSubject(newSubject) .then((_) => navigator.pop(newSubject)); } if (!isSubjectNull) { @@ -191,8 +157,9 @@ class SubjectScreen extends HookConsumerWidget { } }, icon: Icon( - isSubjectNull ? Icons.add_outlined : Icons.save_outlined), - label: Text(isSubjectNull ? "create".tr() : "save".tr()), + isSubjectNull ? Icons.add_outlined : Icons.save_outlined, + ), + label: Text(isSubjectNull ? "create" : "save").tr(), ), ), ], @@ -223,8 +190,8 @@ class SubjectScreen extends HookConsumerWidget { endTime: endTime, timetable: timetable, occupied: isSubjectNull - ? (isOccupied || multipleOccupied) - : (isOccupiedExceptSelf || multipleOccupied), + ? validation.hasConflicts + : validation.hasConflictsForExisting, ), Padding( padding: const EdgeInsets.symmetric(vertical: 10), diff --git a/lib/components/subject_management/subject_configs/color.dart b/lib/features/subjects/widgets/color.dart similarity index 87% rename from lib/components/subject_management/subject_configs/color.dart rename to lib/features/subjects/widgets/color.dart index 2882fed..b3e3293 100644 --- a/lib/components/subject_management/subject_configs/color.dart +++ b/lib/features/subjects/widgets/color.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/components/subject_management/subject_configs/colors_screens/colors.dart'; -import 'package:timetable/components/widgets/list_item_group.dart'; +import 'package:timetable/features/subjects/screens/colors.dart'; +import 'package:timetable/shared/widgets/list_item_group.dart'; /// Color configuration part of the Subject creation screen. class ColorsConfig extends StatelessWidget { diff --git a/lib/components/subject_management/subject_configs/colors_screens/color_picker.dart b/lib/features/subjects/widgets/color_picker.dart similarity index 84% rename from lib/components/subject_management/subject_configs/colors_screens/color_picker.dart rename to lib/features/subjects/widgets/color_picker.dart index f711b35..931bbac 100644 --- a/lib/components/subject_management/subject_configs/colors_screens/color_picker.dart +++ b/lib/features/subjects/widgets/color_picker.dart @@ -1,16 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; /// Custom color picker screen for the color configuration screen. -class ColorPickerScreen extends HookWidget { +class ColorPickerScreen extends StatelessWidget { /// this is the color that will be changed by the color picker. final ValueNotifier color; - const ColorPickerScreen({ - super.key, - required this.color, - }); + const ColorPickerScreen({super.key, required this.color}); @override Widget build(BuildContext context) { diff --git a/lib/components/subject_management/subject_configs/day.dart b/lib/features/subjects/widgets/day.dart similarity index 69% rename from lib/components/subject_management/subject_configs/day.dart rename to lib/features/subjects/widgets/day.dart index 4a8bfbd..a797c8a 100644 --- a/lib/components/subject_management/subject_configs/day.dart +++ b/lib/features/subjects/widgets/day.dart @@ -1,18 +1,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/components/widgets/act_chip.dart'; -import 'package:timetable/components/widgets/bottom_sheets/days_modal_bottom_sheet.dart'; -import 'package:timetable/constants/days.dart'; +import 'package:timetable/shared/widgets/act_chip.dart'; +import 'package:timetable/shared/widgets/bottom_sheets/days.dart'; +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; - const DayConfig({ - super.key, - required this.day, - }); + const DayConfig({super.key, required this.day}); @override Widget build(BuildContext context) { @@ -29,9 +26,7 @@ class DayConfig extends StatelessWidget { isScrollControlled: true, context: context, builder: (context) { - return Wrap( - children: [DaysModalBottomSheet(day: day)], - ); + return Wrap(children: [DaysModalBottomSheet(day: day)]); }, ); }, diff --git a/lib/components/subject_management/subject_configs/day_time_week_tb_config.dart b/lib/features/subjects/widgets/day_time_week_tb_config.dart similarity index 62% rename from lib/components/subject_management/subject_configs/day_time_week_tb_config.dart rename to lib/features/subjects/widgets/day_time_week_tb_config.dart index e93be98..f9a23f8 100644 --- a/lib/components/subject_management/subject_configs/day_time_week_tb_config.dart +++ b/lib/features/subjects/widgets/day_time_week_tb_config.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/subject_management/subject_configs/day.dart'; -import 'package:timetable/components/subject_management/subject_configs/rotation_week.dart'; -import 'package:timetable/components/subject_management/subject_configs/time.dart'; -import 'package:timetable/components/subject_management/subject_configs/timetable.dart'; -import 'package:timetable/components/widgets/list_item_group.dart'; -import 'package:timetable/constants/days.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/timetables.dart'; +import 'package:timetable/features/subjects/widgets/day.dart'; +import 'package:timetable/features/subjects/widgets/rotation_week.dart'; +import 'package:timetable/features/subjects/widgets/time.dart'; +import 'package:timetable/features/subjects/widgets/timetable.dart'; +import 'package:timetable/shared/widgets/list_item_group.dart'; +import 'package:timetable/core/constants/days.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; +import 'package:timetable/features/timetable/providers/timetables.dart'; /// Day, Time, Rotation Week & Timetable configuration part of the Subject creation screen. /// @@ -19,7 +19,7 @@ class TimeDayRotationWeekTimetableConfig extends ConsumerWidget { final ValueNotifier endTime; final ValueNotifier day; final ValueNotifier rotationWeek; - final ValueNotifier timetable; + final ValueNotifier timetable; /// whether or not the current time slot ((endTime - startTime) - tbCustomStartTime) is occupied /// used to show the error icon when the time is not valid/unavailable @@ -44,16 +44,12 @@ class TimeDayRotationWeekTimetableConfig extends ConsumerWidget { return ListItemGroup( children: [ ListItem( - title: DayConfig( - day: day, - ), + title: DayConfig(day: day), onTap: () {}, ), if (rotationWeeks) ListItem( - title: RotationWeekConfig( - rotationWeek: rotationWeek, - ), + title: RotationWeekConfig(rotationWeek: rotationWeek), onTap: () {}, ), ListItem( @@ -66,10 +62,7 @@ class TimeDayRotationWeekTimetableConfig extends ConsumerWidget { ), if (multipleTimetables && timetables.length > 1) ListItem( - title: TimetableConfig( - timetable: timetable, - ), - // these are empty to preserve the tapping effect + title: TimetableConfig(timetable: timetable), onTap: () {}, ), ], diff --git a/lib/components/subject_management/subject_configs/label_location.dart b/lib/features/subjects/widgets/label_location.dart similarity index 90% rename from lib/components/subject_management/subject_configs/label_location.dart rename to lib/features/subjects/widgets/label_location.dart index a598558..2657b80 100644 --- a/lib/components/subject_management/subject_configs/label_location.dart +++ b/lib/features/subjects/widgets/label_location.dart @@ -1,16 +1,16 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/components/subject_management/subjects_list.dart'; -import 'package:timetable/components/widgets/list_item_group.dart'; -import 'package:timetable/constants/basic_subject.dart'; -import 'package:timetable/db/database.dart'; +import 'package:timetable/features/subjects/widgets/subjects_list.dart'; +import 'package:timetable/shared/widgets/list_item_group.dart'; +import 'package:timetable/core/constants/basic_subject.dart'; +import 'package:timetable/core/db/database.dart'; /// Label & Location configuration part of the Subject creation screen. /// /// Groups the label (+ the [SubjectsList] button) & the /// location [TextFormField]s in a [ListItemGroup]. class LabelLocationConfig extends StatefulWidget { - final List subjects; + final List subjects; final ValueNotifier label; final ValueNotifier location; @@ -117,9 +117,7 @@ class _LabelLocationConfigState extends State { hintText: "location".tr(), border: InputBorder.none, ), - onChanged: (value) { - widget.location.value = value; - }, + onChanged: (value) => widget.location.value = value, ), ), ], diff --git a/lib/components/subject_management/subject_configs/note.dart b/lib/features/subjects/widgets/note.dart similarity index 92% rename from lib/components/subject_management/subject_configs/note.dart rename to lib/features/subjects/widgets/note.dart index ffe1c58..f55d08e 100644 --- a/lib/components/subject_management/subject_configs/note.dart +++ b/lib/features/subjects/widgets/note.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/widgets/list_item_group.dart'; +import 'package:timetable/shared/widgets/list_item_group.dart'; /// Note configuration part of the Subject creation screen. class NotesTile extends ConsumerWidget { diff --git a/lib/features/subjects/widgets/preset_colors.dart b/lib/features/subjects/widgets/preset_colors.dart new file mode 100644 index 0000000..5550e88 --- /dev/null +++ b/lib/features/subjects/widgets/preset_colors.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:timetable/core/constants/colors.dart'; + +/// Preset colors screen for the color configuration screen. +class PresetColorsScreen extends HookWidget { + /// the color that will be changed by one of the presets + final ValueNotifier color; + final List colors; + final int colorsPerRow; + + const PresetColorsScreen({ + super.key, + required this.color, + required this.colors, + this.colorsPerRow = 3, + }); + + @override + Widget build(BuildContext context) { + /// used for highlighting the selected color by default no color is selected + // the default color is black, maybe i should have it as the default selected color + final selectedColorIndex = useState(-1); + const int colorsPerRow = 3; + final int rowCount = (colors.length / colorsPerRow).ceil(); + final screenWidth = MediaQuery.of(context).size.width; + + BorderRadius? getBorderRadius( + int index, + int colorsPerRow, + int rowCount, + int totalColors, + ) { + const radius = Radius.circular(10); + + final isTopLeft = index == 0; + final isTopRight = index == (colorsPerRow - 1); + final isBottomLeft = index == + (totalColors - + ((totalColors % colorsPerRow == 0) + ? colorsPerRow + : totalColors % colorsPerRow)); + final isBottomRight = index == (totalColors - 1); + + if (isTopLeft) return const BorderRadius.only(topLeft: radius); + if (isTopRight) return const BorderRadius.only(topRight: radius); + if (isBottomLeft) return const BorderRadius.only(bottomLeft: radius); + if (isBottomRight) return const BorderRadius.only(bottomRight: radius); + + return null; + } + + return Scaffold( + body: ListView( + padding: const EdgeInsets.all(16.0), + scrollDirection: Axis.vertical, + children: [ + Wrap( + children: colors.asMap().entries.map((entry) { + final index = entry.key; + final colorItem = entry.value; + + return buildColorTile( + color: colorItem.color, + width: (screenWidth - 32.0) / 3.0, + isSelected: colorItem.color == color.value, + borderRadius: getBorderRadius( + index, + colorsPerRow, + rowCount, + colors.length, + ), + onTap: () { + selectedColorIndex.value = index; + color.value = colorItem.color; + }, + ); + }).toList(), + ), + ], + ), + ); + } + + Widget buildColorTile({ + required Color color, + required double width, + required bool isSelected, + required VoidCallback onTap, + required BorderRadius? borderRadius, + }) { + final iconColor = + color.computeLuminance() > 0.7 ? Colors.black : Colors.white; + final checkIcon = Icon(Icons.check, color: iconColor, size: 30); + + return Material( + color: color, + borderRadius: borderRadius, + child: InkWell( + onTap: onTap, + borderRadius: borderRadius, + child: SizedBox( + height: 56, + width: width, + child: isSelected ? checkIcon : null, + ), + ), + ); + } +} diff --git a/lib/components/subject_management/subject_configs/rotation_week.dart b/lib/features/subjects/widgets/rotation_week.dart similarity index 82% rename from lib/components/subject_management/subject_configs/rotation_week.dart rename to lib/features/subjects/widgets/rotation_week.dart index ba4b5fc..b11ab22 100644 --- a/lib/components/subject_management/subject_configs/rotation_week.dart +++ b/lib/features/subjects/widgets/rotation_week.dart @@ -1,9 +1,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/components/widgets/act_chip.dart'; -import 'package:timetable/components/widgets/bottom_sheets/rotation_week_modal_bottom_sheet.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/helpers/rotation_weeks.dart'; +import 'package:timetable/shared/widgets/act_chip.dart'; +import 'package:timetable/shared/widgets/bottom_sheets/rotation_week.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/utils/rotation_weeks.dart'; /// Rotation Week configuration part of the Subject creation screen. class RotationWeekConfig extends StatelessWidget { diff --git a/lib/features/subjects/widgets/subjects_list.dart b/lib/features/subjects/widgets/subjects_list.dart new file mode 100644 index 0000000..ff9b168 --- /dev/null +++ b/lib/features/subjects/widgets/subjects_list.dart @@ -0,0 +1,208 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:timetable/shared/widgets/color_indicator.dart'; +import 'package:timetable/shared/widgets/list_item_group.dart'; +import 'package:timetable/core/constants/error_emoticons.dart'; +import 'package:timetable/core/db/database.dart'; + +/// lists all subjects to choose a label & +/// color from an already existing one +class SubjectsList extends HookWidget { + final List subjects; + final TextEditingController controller; + final ValueNotifier label; + final ValueNotifier location; + final ValueNotifier color; + + const SubjectsList({ + super.key, + required this.subjects, + required this.controller, + required this.label, + required this.location, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final ValueNotifier labelFilterEnabled = useState(false); + final ValueNotifier locationFilterEnabled = useState(false); + final ValueNotifier colorFilterEnabled = useState(false); + Set uniqueSubjects = {}; + + /// places 1 subject from duplicates in a set (filtered by label) + List filteredSubjects = []; + for (Subject subject in subjects) { + final label = subject.label; + + if (!uniqueSubjects.contains(label)) { + uniqueSubjects.add(label); + filteredSubjects.add(subject); + } + } + + filteredSubjects = filteredSubjects.where((subject) { + bool passesLabelFilter = !labelFilterEnabled.value || + subject.label.trim() == label.value.trim(); + bool passesLocationFilter = !locationFilterEnabled.value || + subject.location?.trim() == location.value?.trim(); + bool passesColorFilter = + !colorFilterEnabled.value || subject.color == color.value; + + return passesLabelFilter && passesLocationFilter && passesColorFilter; + }).toList(); + + return Scaffold( + appBar: AppBar( + title: const Text('choose_subject').tr(), + actions: [ + // filtering system + buildFilterMenuAnchor( + labelFilterEnabled: labelFilterEnabled, + locationFilterEnabled: locationFilterEnabled, + colorFilterEnabled: colorFilterEnabled, + ) + ], + ), + body: LayoutBuilder( + builder: (context, constraints) => ListView( + padding: const EdgeInsets.all(16), + children: [ + if (filteredSubjects.isEmpty) + Container( + padding: const EdgeInsets.all(10), + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Text( + getRandomErrorEmoticon(), + style: const TextStyle(fontSize: 25), + ), + const SizedBox(height: 10), + const Text( + "no_subjects_error", + style: TextStyle(fontSize: 18), + ).tr(), + ], + ), + ), + ), + if (filteredSubjects.isNotEmpty) + buildSubjectsList( + filteredSubjects: filteredSubjects, + navigator: Navigator.of(context), + ) + ], + ), + ), + ); + } + + Widget buildSubjectsList({ + required NavigatorState navigator, + required List filteredSubjects, + }) { + return ListItemGroup( + children: List.generate(filteredSubjects.length, (i) { + final subj = filteredSubjects[i]; + final location = subj.location; + final note = subj.note; + + return ListItem( + leading: ColorIndicator(color: subj.color), + title: Text(subj.label), + subtitle: (location != null && location.isNotEmpty) || + (note != null && note.isNotEmpty) + ? Column( + children: [ + if (location != null && location.isNotEmpty) + buildSubjectListDataRow( + icon: Icons.location_on_outlined, + text: subj.location!, + maxLines: 2, + ), + if (note != null && note.isNotEmpty) + buildSubjectListDataRow( + icon: Icons.sticky_note_2_outlined, + text: subj.note!, + maxLines: 4, + ), + ], + ) + : null, + onTap: () { + controller.text = subj.label; + label.value = subj.label; + color.value = subj.color; + navigator.pop(); + }, + ); + }), + ); + } + + Widget buildSubjectListDataRow({ + required String text, + required IconData icon, + required int maxLines, + }) { + return Row( + children: [ + Icon(icon, size: 15), + const SizedBox(width: 2.5), + Expanded( + child: Text( + text, + maxLines: maxLines, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + ), + ], + ); + } + + Widget buildFilterMenuAnchor({ + required ValueNotifier labelFilterEnabled, + required ValueNotifier locationFilterEnabled, + required ValueNotifier colorFilterEnabled, + }) { + return MenuAnchor( + menuChildren: [ + CheckboxMenuButton( + value: labelFilterEnabled.value, + onChanged: (value) => labelFilterEnabled.value = value ?? false, + child: const Text('Label').tr(), + ), + CheckboxMenuButton( + value: locationFilterEnabled.value, + onChanged: (value) => locationFilterEnabled.value = value ?? false, + child: const Text('Location').tr(), + ), + CheckboxMenuButton( + value: colorFilterEnabled.value, + onChanged: (value) => colorFilterEnabled.value = value ?? false, + child: const Text('Color').tr(), + ), + ], + builder: ( + BuildContext context, + MenuController controller, + Widget? child, + ) { + return IconButton( + icon: const Icon(Icons.filter_list_outlined), + tooltip: "filter_by".tr(), + onPressed: () { + if (controller.isOpen) controller.close(); + controller.open(); + }, + ); + }, + ); + } +} diff --git a/lib/features/subjects/widgets/time.dart b/lib/features/subjects/widgets/time.dart new file mode 100644 index 0000000..0d6bc1f --- /dev/null +++ b/lib/features/subjects/widgets/time.dart @@ -0,0 +1,174 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +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/features/settings/providers/settings.dart'; + +/// Time configuration part of the Subject creation screen. +class TimeConfig extends ConsumerWidget { + /// whether or not the current time slot ((endTime - startTime) - tbCustomStartTime) is occupied + /// used to show the error icon when the time is not valid/unavailable + final bool occupied; + final ValueNotifier startTime; + final ValueNotifier endTime; + + const TimeConfig({ + super.key, + required this.occupied, + required this.startTime, + required this.endTime, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final chosenCustomStartTime = settings.customStartTime; + final chosenCustomEndTime = settings.customEndTime; + + final customStartTime = getCustomStartTime(chosenCustomStartTime, ref); + final customEndTime = getCustomEndTime(chosenCustomEndTime, ref); + + final uses24HoursFormat = MediaQuery.of(context).alwaysUse24HourFormat; + + void showInvalidTimePeriodDialog() { + final String customStartTimeHour = getCustomTimeHour(customStartTime); + final String customStartTimeMinute = getCustomTimeMinute(customStartTime); + final String customEndTimeHour = getCustomTimeHour(customEndTime); + final String customEndTimeMinute = getCustomTimeMinute(customEndTime); + + final String startTime = "$customStartTimeHour:$customStartTimeMinute"; + final String endTime = "$customEndTimeHour:$customEndTimeMinute"; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('invalid_time').tr(), + content: + const Text('invalid_time_config').tr(args: [startTime, endTime]), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('ok').tr(), + ), + ], + ), + ); + } + + void showInvalidEqualTimeDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('invalid_time').tr(), + content: const Text('invalid_equal_time_error').tr(), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('ok').tr(), + ), + ], + ), + ); + } + + return Row( + children: [ + const Text("time").tr(), + const Spacer(), + buildTimeChip( + time: startTime, + inverseTime: endTime, + isBoundaryInvalid: (selectedTime) => + selectedTime.isBefore(customStartTime), + showInvalidTimePeriodDialog: showInvalidTimePeriodDialog, + showInvalidEqualTimeDialog: showInvalidEqualTimeDialog, + isSwapNeeded: (selectedTime) => selectedTime.isAfter(endTime.value), + performSwap: (selectedTime) { + if (!selectedTime.isAfter(customEndTime)) { + final temp = endTime.value; + endTime.value = selectedTime; + startTime.value = temp; + } + showInvalidTimePeriodDialog(); + }, + uses24HoursFormat: uses24HoursFormat, + context: context, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Icon(Icons.arrow_forward), + ), + buildTimeChip( + time: endTime, + inverseTime: startTime, + isBoundaryInvalid: (selectedTime) => + selectedTime.isAfter(customEndTime), + showInvalidTimePeriodDialog: showInvalidTimePeriodDialog, + showInvalidEqualTimeDialog: showInvalidEqualTimeDialog, + isSwapNeeded: (selectedTime) => + selectedTime.isBefore(startTime.value), + performSwap: (selectedTime) { + if (!selectedTime.isBefore(customStartTime)) { + final temp = startTime.value; + startTime.value = selectedTime; + endTime.value = temp; + } + showInvalidTimePeriodDialog(); + }, + uses24HoursFormat: uses24HoursFormat, + context: context, + ), + if (occupied) + const Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.cancel, color: Colors.redAccent), + ), + ], + ); + } + + Widget buildTimeChip({ + required ValueNotifier time, + required ValueNotifier inverseTime, + required bool Function(TimeOfDay selectedTime) isBoundaryInvalid, + required void Function() showInvalidTimePeriodDialog, + required void Function() showInvalidEqualTimeDialog, + required bool Function(TimeOfDay selectedTime) isSwapNeeded, + required void Function(TimeOfDay selectedTime) performSwap, + required bool uses24HoursFormat, + required BuildContext context, + }) { + return ActChip( + onPressed: () async { + final TimeOfDay? selectedTime = await timePicker( + time: TimeOfDay(hour: time.value.hour, minute: 0), + context: context, + ); + + if (selectedTime == null) return; + + if (isBoundaryInvalid(selectedTime)) { + showInvalidTimePeriodDialog(); + return; + } + + if (selectedTime.hour == inverseTime.value.hour) { + showInvalidEqualTimeDialog(); + return; + } + + if (isSwapNeeded(selectedTime)) { + performSwap(selectedTime); + return; + } + + time.value = selectedTime; + }, + label: + Text(TimeFormatter.getTimeNoPadding(time.value, uses24HoursFormat)), + ); + } +} diff --git a/lib/components/subject_management/subject_configs/timetable.dart b/lib/features/subjects/widgets/timetable.dart similarity index 70% rename from lib/components/subject_management/subject_configs/timetable.dart rename to lib/features/subjects/widgets/timetable.dart index 796986a..42a14f1 100644 --- a/lib/components/subject_management/subject_configs/timetable.dart +++ b/lib/features/subjects/widgets/timetable.dart @@ -1,12 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/components/widgets/act_chip.dart'; -import 'package:timetable/components/widgets/bottom_sheets/timetables_modal_bottom_sheet.dart'; -import 'package:timetable/db/database.dart'; +import 'package:timetable/shared/widgets/act_chip.dart'; +import 'package:timetable/shared/widgets/bottom_sheets/timetables_modal.dart'; +import 'package:timetable/core/db/database.dart'; /// Timetable configuration part of the Subject creation screen. class TimetableConfig extends StatelessWidget { - final ValueNotifier timetable; + final ValueNotifier timetable; const TimetableConfig({ super.key, @@ -29,9 +29,7 @@ class TimetableConfig extends StatelessWidget { context: context, builder: (context) { return Wrap( - children: [ - TimetablesModalBottomSheet(timetable: timetable), - ], + children: [TimetablesModalBottomSheet(timetable: timetable)], ); }, ); diff --git a/lib/features/timetable/providers/timetables.dart b/lib/features/timetable/providers/timetables.dart new file mode 100644 index 0000000..0358a83 --- /dev/null +++ b/lib/features/timetable/providers/timetables.dart @@ -0,0 +1,94 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/features/subjects/providers/subjects.dart'; + +/// Timetable state management +/// multiple timetable support, manages timetable CRUD operations +/// and default timetable handling +class TimetableNotifier extends StateNotifier> { + final AppDatabase db; + final SubjectNotifier subjectsNotifier; + + late final StreamSubscription> _timetablesSubscription; + + TimetableNotifier(this.db, this.subjectsNotifier) : super([]) { + _timetablesSubscription = + db.timetables.select().watch().listen((timetables) async { + if (timetables.isEmpty) { + // If the table somehow becomes empty we ensure a default timetable exists by + // calling resetData. + await resetData(); + } else { + state = timetables; + } + }); + + // checkAndLoadInitialData(); + } + + // Future checkAndLoadInitialData() async { + // final currentTimetables = await db.timetables.select().get(); + // if (currentTimetables.isEmpty) { + // await resetData(); + // } + // } + + @override + void dispose() { + _timetablesSubscription.cancel(); + super.dispose(); + } + + /// returns the list of timetables from notifier + List get timetables => state; + + /// returns the list of timetables from database + Future> fetchTimetablesFromDatabase() async { + return db.timetables.select().get(); + } + + /// adds a [TimetablesCompanion] to db ([$TimetablesTable]) + Future addTimetable(TimetablesCompanion entry) async { + db.timetables.insertOne( + TimetablesCompanion.insert( + name: entry.name.value, + ), + ); + } + + /// updates preexisting timetables + // i don't think this ever gets used (yet) + Future updateTimetable(Timetable entry) async { + db.timetables.update().replace(entry); + } + + /// deletes timetable from db + Future deleteTimetable(Timetable entry) async { + db.timetables.deleteWhere((t) => t.id.equals(entry.id)); + subjectsNotifier.deleteTimetableSubjects(state, entry); + } + + /// resets the database ([$TimetableTable]) to its initial state + /// + /// also automatically adds an initial timetable to avoid errors + Future resetData() async { + final allTimetablesInDb = await db.timetables.select().get(); + for (var timetable in allTimetablesInDb) { + subjectsNotifier.deleteTimetableSubjects([timetable], timetable); + } + + await db.timetables.deleteAll(); + await db.timetables.insertOne(TimetablesCompanion.insert(name: "1")); + } +} + +final timetableProvider = + StateNotifierProvider>( + (ref) => TimetableNotifier( + ref.watch(AppDatabase.databaseProvider), + ref.watch(subjectProvider.notifier), + ), +); diff --git a/lib/screens/timetable/day.dart b/lib/features/timetable/screens/day.dart similarity index 82% rename from lib/screens/timetable/day.dart rename to lib/features/timetable/screens/day.dart index ee071d2..e409d7e 100644 --- a/lib/screens/timetable/day.dart +++ b/lib/features/timetable/screens/day.dart @@ -1,25 +1,25 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/subject_management/subject_screen.dart'; -import 'package:timetable/components/widgets/views/day_view/day_view_subject_builder.dart'; -import 'package:timetable/constants/custom_times.dart'; -import 'package:timetable/constants/days.dart'; -import 'package:timetable/constants/error_emoticons.dart'; -import 'package:timetable/constants/grid_properties.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/helpers/rotation_weeks.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/helpers/subjects.dart'; -import 'package:timetable/helpers/timetables.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/timetables.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/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'; +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'; /// Timetable view that shows each day's subjects in a single screen each. class TimetableDayView extends HookConsumerWidget { final ValueNotifier rotationWeek; - final List subject; - final ValueNotifier currentTimetable; + final List subject; + final ValueNotifier currentTimetable; final PageController controller; const TimetableDayView({ diff --git a/lib/screens/timetable/grid.dart b/lib/features/timetable/screens/grid.dart similarity index 79% rename from lib/screens/timetable/grid.dart rename to lib/features/timetable/screens/grid.dart index 9f59569..b1b22e1 100644 --- a/lib/screens/timetable/grid.dart +++ b/lib/features/timetable/screens/grid.dart @@ -1,27 +1,27 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/widgets/views/grid_view/grid.dart'; -import 'package:timetable/components/widgets/views/grid_view/grid_view_overlapping_subjects_builder.dart'; -import 'package:timetable/components/widgets/time_column.dart'; -import 'package:timetable/constants/custom_times.dart'; -import 'package:timetable/constants/grid_properties.dart'; -import 'package:timetable/helpers/overlapping_subjects.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/helpers/timetables.dart'; -import 'package:timetable/provider/overlapping_subjects.dart'; -import 'package:timetable/components/widgets/views/grid_view/grid_view_subject_builder.dart'; -import 'package:timetable/components/widgets/views/grid_view/grid_view_subject_container_builder.dart'; -import 'package:timetable/components/widgets/views/grid_view/tile.dart'; -import 'package:timetable/helpers/rotation_weeks.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/timetables.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/widgets/time_column.dart'; +import 'package:timetable/core/constants/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'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/core/utils/timetables.dart'; +import 'package:timetable/features/subjects/providers/overlapping_subjects.dart'; +import 'package:timetable/features/timetable/widgets/grid_view/grid_view_subject_builder.dart'; +import 'package:timetable/features/timetable/widgets/grid_view/grid_view_subject_container_builder.dart'; +import 'package:timetable/features/timetable/widgets/grid_view/tile.dart'; +import 'package:timetable/core/utils/rotation_weeks.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; +import 'package:timetable/features/timetable/providers/timetables.dart'; /// Timetable view that shows All the days' subjects in a grid form. class TimetableGridView extends HookConsumerWidget { final ValueNotifier rotationWeek; - final List subject; - final ValueNotifier currentTimetable; + final List subject; + final ValueNotifier currentTimetable; const TimetableGridView({ super.key, @@ -37,7 +37,7 @@ class TimetableGridView extends HookConsumerWidget { ref.watch(settingsProvider).multipleTimetables; final bool twentyFourHoursMode = ref.watch(settingsProvider).twentyFourHours; - final List timetables = ref.watch(timetableProvider); + final List timetables = ref.watch(timetableProvider); final TimeOfDay chosenCustomStartTime = ref.watch(settingsProvider).customStartTime; @@ -48,7 +48,7 @@ class TimetableGridView extends HookConsumerWidget { getCustomStartTime(chosenCustomStartTime, ref); final TimeOfDay customEndTime = getCustomEndTime(chosenCustomEndTime, ref); - final List subjects = getFilteredByTimetablesSubjects( + final List subjects = getFilteredByTimetablesSubjects( currentTimetable, timetables, multipleTimetables, @@ -113,7 +113,7 @@ class TimetableGridView extends HookConsumerWidget { /// generates the grid List> generate( - List subjects, + List subjects, int totalDays, int totalHours, WidgetRef ref, diff --git a/lib/screens/timetable/timetable.dart b/lib/features/timetable/screens/timetable.dart similarity index 72% rename from lib/screens/timetable/timetable.dart rename to lib/features/timetable/screens/timetable.dart index 340dc8f..6a74c67 100644 --- a/lib/screens/timetable/timetable.dart +++ b/lib/features/timetable/screens/timetable.dart @@ -2,18 +2,18 @@ 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/screens/timetable/day.dart'; -import 'package:timetable/screens/timetable/grid.dart'; -import 'package:timetable/components/widgets/views/day_view/days_bar.dart'; -import 'package:timetable/components/widgets/views/view_toggle.dart'; -import 'package:timetable/components/widgets/navigation/navigation_bar_toggle.dart'; -import 'package:timetable/components/widgets/rotation_week_toggle.dart'; -import 'package:timetable/components/widgets/timetable_toggle.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/constants/timetable_views.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/subjects.dart'; -import 'package:timetable/provider/timetables.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/view_toggle.dart'; +import 'package:timetable/features/navigation/widgets/navigation_bar_toggle.dart'; +import 'package:timetable/shared/widgets/rotation_week_toggle.dart'; +import 'package:timetable/shared/widgets/timetable_toggle.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/constants/timetable_views.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'; /// The main screen, displays the default timetable view. /// basically groups [TimetableGridView] & [TimetableDayView]. diff --git a/lib/components/widgets/views/day_view/day_view_subject_builder.dart b/lib/features/timetable/widgets/day_view/day_view_subject_builder.dart similarity index 95% rename from lib/components/widgets/views/day_view/day_view_subject_builder.dart rename to lib/features/timetable/widgets/day_view/day_view_subject_builder.dart index f9daf0f..d556eb9 100644 --- a/lib/components/widgets/views/day_view/day_view_subject_builder.dart +++ b/lib/features/timetable/widgets/day_view/day_view_subject_builder.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/helpers/rotation_weeks.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/components/subject_management/subject_screen.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'; /// Subject builder for the day view. class DayViewSubjectBuilder extends ConsumerWidget { - final SubjectData subject; + final Subject subject; const DayViewSubjectBuilder({super.key, required this.subject}); diff --git a/lib/features/timetable/widgets/day_view/days_bar.dart b/lib/features/timetable/widgets/day_view/days_bar.dart new file mode 100644 index 0000000..2490c25 --- /dev/null +++ b/lib/features/timetable/widgets/day_view/days_bar.dart @@ -0,0 +1,165 @@ +import 'dart:async'; + +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/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/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. +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; + + final theme = ref.watch(themeProvider); + final Brightness systemBrightness = + MediaQuery.of(context).platformBrightness; + final darkCurrentDayColorScheme = + Theme.of(context).colorScheme.onInverseSurface; + final lightCurrentDayColorScheme = + Theme.of(context).colorScheme.outlineVariant; + + return SizedBox( + height: 48, + width: screenWidth, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Container( + padding: EdgeInsets.only(left: isGridView! ? 20 : 0), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: daysLength, + shrinkWrap: true, + itemBuilder: (context, index) { + return buildDayButton( + index: index, + currentDay: currentDay, + currentDayColor: theme == ThemeOption.auto + ? systemBrightness == Brightness.dark + ? darkCurrentDayColorScheme + : lightCurrentDayColorScheme + : theme == ThemeOption.dark + ? darkCurrentDayColorScheme + : lightCurrentDayColorScheme, + singleLetterDays: singleLetterDays, + isPortrait: isPortrait, + isGridView: isGridView!, + daysLength: daysLength, + screenWidth: screenWidth, + colorScheme: Theme.of(context).colorScheme, + onTap: () { + if (isGridView!) return; + + controller.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ); + }, + ), + ), + ], + ), + ); + } + + Widget buildDayButton({ + required int index, + required bool isGridView, + required int currentDay, + required double screenWidth, + required int daysLength, + required bool singleLetterDays, + required bool isPortrait, + required Color currentDayColor, + required VoidCallback? onTap, + required ColorScheme colorScheme, + }) { + final isCurrentDay = isGridView && (currentDay == index); + + final dayText = singleLetterDays + ? days[index].tr()[0] + : isPortrait + ? days[index].tr().substring(0, 3) + : days[index].tr(); + + return SizedBox( + width: isGridView + ? ((screenWidth - (timeColumnWidth - 1)) / daysLength) + : (screenWidth / daysLength), + child: TextButton( + onPressed: onTap, + style: TextButton.styleFrom( + foregroundColor: colorScheme.onSurface, + backgroundColor: isCurrentDay ? currentDayColor : null, + ), + child: Text(dayText, overflow: TextOverflow.clip, softWrap: false), + ), + ); + } +} + +class DayBarUpdater extends HookConsumerWidget { + final PageController controller; + final bool isGridView; + + const DayBarUpdater({ + super.key, + required this.controller, + required this.isGridView, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentDay = useState(DateTime.now().weekday - 1); + + 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(); + }, []); + + return DaysBar( + controller: controller, + isGridView: isGridView, + currentDay: currentDay.value, + ); + } +} diff --git a/lib/components/widgets/views/grid_view/grid.dart b/lib/features/timetable/widgets/grid_view/grid.dart similarity index 93% rename from lib/components/widgets/views/grid_view/grid.dart rename to lib/features/timetable/widgets/grid_view/grid.dart index fd1aa9a..8888e39 100644 --- a/lib/components/widgets/views/grid_view/grid.dart +++ b/lib/features/timetable/widgets/grid_view/grid.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:timetable/components/widgets/views/grid_view/tile.dart'; +import 'package:timetable/features/timetable/widgets/grid_view/tile.dart'; /// Grid for the grid view of the Timetable screen. class Grid extends StatelessWidget { diff --git a/lib/components/widgets/views/grid_view/grid_view_overlapping_subjects_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart similarity index 90% rename from lib/components/widgets/views/grid_view/grid_view_overlapping_subjects_builder.dart rename to lib/features/timetable/widgets/grid_view/grid_view_overlapping_subjects_builder.dart index 3cf822f..3ca640d 100644 --- a/lib/components/widgets/views/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/components/widgets/views/grid_view/grid_view_subject_builder.dart'; -import 'package:timetable/constants/custom_times.dart'; -import 'package:timetable/constants/grid_properties.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/provider/settings.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/core/db/database.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; /// A widget that builds overlapping subjects in the grid view. /// @@ -14,7 +14,7 @@ import 'package:timetable/provider/settings.dart'; /// /// uses the [GridViewSubjectBuilder] to build each subject. class OverlappingSubjBuilder extends ConsumerWidget { - final List subjects; + final List subjects; final int earlierStartTimeHour; final int laterEndTimeHour; diff --git a/lib/components/widgets/views/grid_view/grid_view_subject_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart similarity index 93% rename from lib/components/widgets/views/grid_view/grid_view_subject_builder.dart rename to lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart index 008795b..cf8c7b0 100644 --- a/lib/components/widgets/views/grid_view/grid_view_subject_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_subject_builder.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:non_uniform_border/non_uniform_border.dart'; -import 'package:timetable/components/widgets/bottom_sheets/subject_management_bottom_sheet.dart'; -import 'package:timetable/constants/custom_times.dart'; -import 'package:timetable/helpers/rotation_weeks.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/constants/grid_properties.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 class SubjectBuilder extends ConsumerWidget { - final SubjectData subject; + final Subject subject; final bool isOverlapping; final int startTimeOffset; @@ -128,9 +128,7 @@ class SubjectBuilder extends ConsumerWidget { builder: (context) { return Wrap( children: [ - SubjectManagementBottomSheet( - subject: subject, - ), + SubjectManagementBottomSheet(subject: subject), ], ); }, diff --git a/lib/components/widgets/views/grid_view/grid_view_subject_container_builder.dart b/lib/features/timetable/widgets/grid_view/grid_view_subject_container_builder.dart similarity index 87% rename from lib/components/widgets/views/grid_view/grid_view_subject_container_builder.dart rename to lib/features/timetable/widgets/grid_view/grid_view_subject_container_builder.dart index 0fb3b94..2e3057a 100644 --- a/lib/components/widgets/views/grid_view/grid_view_subject_container_builder.dart +++ b/lib/features/timetable/widgets/grid_view/grid_view_subject_container_builder.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:non_uniform_border/non_uniform_border.dart'; -import 'package:timetable/constants/grid_properties.dart'; -import 'package:timetable/components/subject_management/subject_screen.dart'; -import 'package:timetable/db/database.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'; /// Subject container tile builder. /// (the one you click on to create a Subject in the grid view.) class SubjectContainerBuilder extends ConsumerWidget { final int rowIndex; final int columnIndex; - final ValueNotifier currentTimetable; + final ValueNotifier currentTimetable; const SubjectContainerBuilder({ super.key, diff --git a/lib/components/widgets/views/grid_view/tile.dart b/lib/features/timetable/widgets/grid_view/tile.dart similarity index 100% rename from lib/components/widgets/views/grid_view/tile.dart rename to lib/features/timetable/widgets/grid_view/tile.dart diff --git a/lib/components/widgets/views/view_toggle.dart b/lib/features/timetable/widgets/view_toggle.dart similarity index 100% rename from lib/components/widgets/views/view_toggle.dart rename to lib/features/timetable/widgets/view_toggle.dart diff --git a/lib/main.dart b/lib/main.dart index 78ac5e3..2936dd4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,7 @@ -import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/widgets/navigation/bottom_navigation_bar.dart'; -import 'package:timetable/components/widgets/eager_initilization.dart'; -import 'package:timetable/constants/languages.dart'; -import 'package:timetable/helpers/theme_helper.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/themes.dart'; +import 'package:timetable/app.dart'; +import 'package:timetable/core/constants/languages.dart'; import 'package:easy_localization/easy_localization.dart'; void main() async { @@ -25,46 +20,3 @@ void main() async { ), ); } - -/// The main class of the application. -class TimetableApp extends ConsumerWidget { - const TimetableApp({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = ref.watch(themeProvider); - final monetTheming = ref.watch(settingsProvider).monetTheming; - final appThemeColor = ref.watch(settingsProvider).appThemeColor; - final Brightness systemBrightness = - MediaQuery.of(context).platformBrightness; - - return DynamicColorBuilder( - builder: ( - ColorScheme? lightDynamic, - ColorScheme? darkDynamic, - ) { - return MaterialApp( - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - title: 'Timetable', - color: Colors.white, - theme: ThemeData( - colorScheme: ThemeHelper.getColorScheme( - monetTheming: monetTheming, - theme: theme, - systemBrightness: systemBrightness, - lightDynamic: lightDynamic, - darkDynamic: darkDynamic, - appThemeColor: appThemeColor, - ), - useMaterial3: true, - ), - home: const EagerInitialization( - child: BottomNavigation(), - ), - ); - }, - ); - } -} diff --git a/lib/provider/subjects.dart b/lib/provider/subjects.dart deleted file mode 100644 index ace1dea..0000000 --- a/lib/provider/subjects.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/provider/overlapping_subjects.dart'; - -/// Subject state management -/// manages CRUD operations for subjects, handles subject filtering and state updates -/// also manages subject-timetable relationships -class SubjectNotifier extends StateNotifier> { - AppDatabase db; - OverlappingSubjects overlappingSubjectsNotifier; - - SubjectNotifier( - this.db, - this.overlappingSubjectsNotifier, - ) : super([]) { - getSubjects(); - } - - /// load subjects from database - Future loadSubjects() async { - final subjects = await db.subject.select().get(); - state = subjects; - } - - /// returns the list of [SubjectData] from the database ([$SubjectTable]) - Future> getSubjects() async { - final subjects = await db.subject.select().get(); - state = subjects; - return subjects; - } - - /// adds a subject ([SubjectCompanion]) to the database ([$SubjectTable]) - // i use [SubjectData] in the subject creation screen so i have to convert it everytime - Future addSubject(SubjectCompanion entry) async { - db.subject.insertOne( - SubjectCompanion.insert( - label: entry.label.value, - location: entry.location, - note: entry.note, - color: entry.color.value, - rotationWeek: entry.rotationWeek.value, - day: entry.day.value, - startTime: entry.startTime.value, - endTime: entry.endTime.value, - timetable: entry.timetable.value, - ), - ); - - state = await getSubjects(); - } - - /// updates an already existing [SubjectData] - /// - /// also resets the overlapping subjects notifier - /// so it refetches new data, otherwise there will be new - /// and old data at the same time - Future updateSubject(SubjectData entry) async { - db.subject.update().replace(entry); - state = await getSubjects(); - - overlappingSubjectsNotifier.reset(); - } - - /// deletes a [SubjectData] from db ([$SubjectTable]) - Future deleteSubject(SubjectData entry) async { - db.subject.deleteWhere((t) => t.id.equals(entry.id)); - state = await getSubjects(); - - overlappingSubjectsNotifier.reset(); - } - - /// executed from the [TimetableNotifier] to delete all the subjects - /// in the deleted timetable to avoid errors - Future deleteTimetableSubjects( - List timetables, - TimetableData timetable, - ) async { - var subjects = await db.subject.select().get(); - - for (var subject in subjects.where( - (e) => e.timetable == timetable.name, - )) { - db.subject.deleteWhere( - (t) => t.id.equals(subject.id), - ); - } - - state = await getSubjects(); - } - - /// deletes all subjects from db ([$SubjectTable]) and state - /// - /// also resets the overlapping subjects notifier. - Future resetData() async { - await db.delete($SubjectTable(db)).go(); - state = []; - - overlappingSubjectsNotifier.reset(); - } -} - -final subjectProvider = - StateNotifierProvider>( - (ref) => SubjectNotifier( - ref.watch(AppDatabase.databaseProvider), - ref.watch(overlappingSubjectsProvider.notifier), - ), -); diff --git a/lib/provider/timetables.dart b/lib/provider/timetables.dart deleted file mode 100644 index 027ab21..0000000 --- a/lib/provider/timetables.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/provider/subjects.dart'; - -/// Timetable state management -/// multiple timetable support, manages timetable CRUD operations -/// and default timetable handling -class TimetableNotifier extends StateNotifier> { - AppDatabase db; - SubjectNotifier subjectsNotifier; - - TimetableNotifier( - this.db, - this.subjectsNotifier, - ) : super([]) { - loadTimetables(); - } - - /// load timetables from database - Future loadTimetables() async { - final timetables = await db.timetable.select().get(); - if (timetables.isEmpty) await resetData(); - state = timetables; - } - - /// returns the list of timetables from database - Future> getTimetables() async { - final timetables = await db.timetable.select().get(); - if (timetables.isEmpty) await resetData(); - return timetables; - } - - /// adds a [TimetableCompanion] to db ([$TimetableTable]) - Future addTimetable(TimetableCompanion entry) async { - db.timetable.insertOne( - TimetableCompanion.insert( - name: entry.name.value, - ), - ); - - state = await getTimetables(); - } - - /// updates preexisting timetables - // i don't think this ever gets used (yet) - Future updateTimetable(TimetableData entry) async { - db.timetable.update().replace(entry); - - state = await getTimetables(); - } - - /// deletes timetable from db - Future deleteTimetable(TimetableData entry) async { - db.timetable.deleteWhere((t) => t.id.equals(entry.id)); - subjectsNotifier.deleteTimetableSubjects(state, entry); - - state = await getTimetables(); - } - - /// resets the database ([$TimetableTable]) to its initial state - /// - /// also automatically adds an initial timetable to avoid errors - Future resetData() async { - db.timetable.deleteAll(); - for (var timetable in state) { - subjectsNotifier.deleteTimetableSubjects(state, timetable); - } - - db.timetable.insertOne(TimetableCompanion.insert(name: "1")); - state = await getTimetables(); - } -} - -final timetableProvider = - StateNotifierProvider>( - (ref) => TimetableNotifier( - ref.watch(AppDatabase.databaseProvider), - ref.watch(subjectProvider.notifier), - ), -); diff --git a/lib/screens/settings/timetable_management.dart b/lib/screens/settings/timetable_management.dart deleted file mode 100644 index 4120fe2..0000000 --- a/lib/screens/settings/timetable_management.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:drift/drift.dart' as drift; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/widgets/alert_dialog.dart'; -import 'package:timetable/components/widgets/list_item_group.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/timetables.dart'; - -/// Screen to manage the "multiple timetables" features. -class TimetableManagementScreen extends ConsumerWidget { - const TimetableManagementScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final multipleTimetables = ref.watch(settingsProvider).multipleTimetables; - final settings = ref.read(settingsProvider.notifier); - final timetables = ref.watch(timetableProvider); - final timetable = ref.watch(timetableProvider.notifier); - - return Scaffold( - appBar: AppBar( - title: const Text("manage_timetables").tr(), - ), - body: ListView( - children: [ - Column( - children: [ - SwitchListTile( - secondary: const Icon( - Icons.backup_table_outlined, - size: 20, - ), - visualDensity: const VisualDensity(horizontal: -4), - title: const Text("multiple_timetables").tr(), - value: multipleTimetables, - onChanged: (bool value) { - settings.updateMultipleTimetables(value); - }, - ), - ListTile( - leading: const Icon( - Icons.delete_forever_outlined, - size: 20, - ), - horizontalTitleGap: 8, - title: const Text("reset").tr(), - onTap: () { - showDialog( - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return ShowAlertDialog( - content: const Text("reset_timetables_dialog").tr(), - approveButtonText: "reset".tr(), - onApprove: () { - timetable.resetData(); - Navigator.of(context).pop(); - }, - ); - }, - ); - }, - ), - ListTile( - dense: true, - title: const Text("manage").tr(), - enabled: multipleTimetables ? true : false, - textColor: Theme.of(context).colorScheme.primary, - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 75), - child: ListItemGroup( - children: List.generate( - timetables.length, - (i) => ListItem( - title: Opacity( - opacity: timetables[i] != timetables[0] - ? multipleTimetables - ? 1 - : .5 - : 1, - child: Row( - children: [ - Text( - "${"timetable".plural(1)} ${timetables[i].name}", - ), - const Spacer(), - if (timetables[i] != timetables[0] && - multipleTimetables) - IconButton( - onPressed: () { - showDialog( - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return ShowAlertDialog( - content: const Text( - "delete_timetable_dialog") - .tr(), - approveButtonText: "delete".tr(), - onApprove: () { - timetable - .deleteTimetable(timetables[i]); - Navigator.of(context).pop(); - }, - ); - }, - ); - }, - icon: const Icon(Icons.delete_outline), - tooltip: "delete".tr(), - ), - ], - ), - ), - onTap: () {}, - ), - ), - ), - ), - ], - ), - ], - ), - floatingActionButton: Visibility( - visible: timetables.length < 5 && multipleTimetables, - child: FloatingActionButton( - onPressed: () { - if (timetables.length < 5 && multipleTimetables) { - timetable.addTimetable( - TimetableCompanion( - name: drift.Value( - (int.parse(timetables.last.name) + 1).toString(), - ), - ), - ); - } - }, - tooltip: "create".tr(), - child: const Icon(Icons.add), - ), - ), - ); - } -} diff --git a/lib/screens/settings/timetable_period.dart b/lib/screens/settings/timetable_period.dart deleted file mode 100644 index 50ead1b..0000000 --- a/lib/screens/settings/timetable_period.dart +++ /dev/null @@ -1,186 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/helpers/time_management.dart'; -import 'package:timetable/provider/settings.dart'; - -/// Screen to manage the period of the timetable. -/// -/// Changes the start time and end time of the timetable. -class TimetablePeriodScreen extends ConsumerWidget { - const TimetablePeriodScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final customTimePeriod = ref.watch(settingsProvider).customTimePeriod; - final customStartTime = ref.watch(settingsProvider).customStartTime; - final customEndTime = ref.watch(settingsProvider).customEndTime; - final twentyFourHours = ref.watch(settingsProvider).twentyFourHours; - final settings = ref.read(settingsProvider.notifier); - - final uses24HoursFormat = MediaQuery.of(context).alwaysUse24HourFormat; - - /// error dialog when the start time and end time have the same value (equal) - void showInvalidEqualTimeDialog() { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('invalid_time').tr(), - content: const Text('invalid_equal_time_error').tr(), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('ok').tr(), - ), - ], - ), - ); - } - - /// switches the start and end time of the timetable - /// - /// used when the start time is after the end time that - void switchStartWithEndTime(TimeOfDay newTime) { - final temp = customEndTime; - settings.updateCustomEndTime(newTime); - settings.updateCustomStartTime(temp); - return; - } - - /// switches the end and start time of the timetable - /// - /// used when the end time is before the start time that - void switchEndWithStartTime(TimeOfDay newTime) { - final temp = customStartTime; - settings.updateCustomStartTime(newTime); - settings.updateCustomEndTime(temp); - return; - } - - String getTime(TimeOfDay time) { - final formattedTimeHour = time.hour < 10 - ? "0${time.hour}" - : uses24HoursFormat - ? time.hour - : time.hour > 12 - ? time.hour - 12 - : time.hour; - final amORpm = time.hour > 12 ? "PM" : "AM"; - final formattedTimeMinute = time.minute == 0 - ? "00" - : time.minute < 10 - ? "0${time.minute}" - : "${time.minute}"; - - return "$formattedTimeHour:$formattedTimeMinute${uses24HoursFormat ? "" : " $amORpm"}"; - } - - return Scaffold( - appBar: AppBar( - title: const Text("period_preferences").tr(), - ), - body: ListView( - children: [ - Column( - children: [ - SwitchListTile( - secondary: const Icon( - Icons.edit_outlined, - size: 20, - ), - visualDensity: const VisualDensity(horizontal: -4), - title: const Text("custom_time_period").tr(), - value: customTimePeriod, - onChanged: (bool value) { - settings.updateCustomTimePeriod(value); - if (twentyFourHours) { - settings.update24Hours(!value); - } - }, - ), - SwitchListTile( - secondary: const Icon( - Icons.schedule_outlined, - size: 20, - ), - visualDensity: const VisualDensity(horizontal: -4), - title: const Text("24_hour_period").tr(), - value: twentyFourHours, - onChanged: (bool value) { - settings.update24Hours(value); - if (customTimePeriod) { - settings.updateCustomTimePeriod(false); - } - }, - ), - ListTile( - dense: true, - title: const Text("configuration").tr(), - enabled: customTimePeriod ? true : false, - textColor: Theme.of(context).colorScheme.primary, - ), - ListTile( - leading: const Icon( - Icons.play_arrow_outlined, - size: 20, - ), - horizontalTitleGap: 8, - title: const Text("start_time").tr(), - enabled: customTimePeriod ? true : false, - subtitle: Text(getTime(customStartTime)), - onTap: () async { - final TimeOfDay? selectedTime = await timePicker( - context, - customStartTime, - ); - if (selectedTime == null) return; - - if (selectedTime.hour == customEndTime.hour) { - showInvalidEqualTimeDialog(); - return; - } - - if (selectedTime.isAfter(customEndTime)) { - switchStartWithEndTime(selectedTime); - return; - } - - settings.updateCustomStartTime(selectedTime); - }, - ), - ListTile( - leading: const Icon( - Icons.stop_outlined, - size: 20, - ), - horizontalTitleGap: 8, - title: const Text("end_time").tr(), - enabled: customTimePeriod ? true : false, - subtitle: Text(getTime(customEndTime)), - onTap: () async { - final TimeOfDay? selectedTime = await timePicker( - context, - customEndTime, - ); - if (selectedTime == null) return; - - if (selectedTime.hour == customStartTime.hour) { - showInvalidEqualTimeDialog(); - return; - } - - if (selectedTime.isBefore(customStartTime)) { - switchEndWithStartTime(selectedTime); - return; - } - - settings.updateCustomEndTime(selectedTime); - }, - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/provider/language.dart b/lib/shared/providers/language.dart similarity index 80% rename from lib/provider/language.dart rename to lib/shared/providers/language.dart index 7fdfdd3..9182740 100644 --- a/lib/provider/language.dart +++ b/lib/shared/providers/language.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:timetable/constants/languages.dart'; +import 'package:timetable/core/constants/languages.dart'; /// Language state management class LanguageNotifier extends StateNotifier { @@ -16,7 +16,11 @@ class LanguageNotifier extends StateNotifier { } /// returns current language ([Locale]) - Locale getLanguage() { + Future getLanguage() async { + // fetch from sharepreferences then return + final prefs = await SharedPreferences.getInstance(); + final savedLanguageIndex = prefs.getInt('language'); + state = languages[savedLanguageIndex ?? 0]; return state; } diff --git a/lib/provider/themes.dart b/lib/shared/providers/themes.dart similarity index 95% rename from lib/provider/themes.dart rename to lib/shared/providers/themes.dart index 799a834..f197122 100644 --- a/lib/provider/themes.dart +++ b/lib/shared/providers/themes.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:timetable/constants/theme_options.dart'; +import 'package:timetable/core/constants/theme_options.dart'; /// Theme state management class ThemeNotifier extends StateNotifier { diff --git a/lib/components/widgets/act_chip.dart b/lib/shared/widgets/act_chip.dart similarity index 100% rename from lib/components/widgets/act_chip.dart rename to lib/shared/widgets/act_chip.dart diff --git a/lib/components/widgets/alert_dialog.dart b/lib/shared/widgets/alert_dialog.dart similarity index 82% rename from lib/components/widgets/alert_dialog.dart rename to lib/shared/widgets/alert_dialog.dart index 8d49478..200fa6a 100644 --- a/lib/components/widgets/alert_dialog.dart +++ b/lib/shared/widgets/alert_dialog.dart @@ -1,6 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/components/widgets/list_item_group.dart'; +import 'package:timetable/shared/widgets/list_item_group.dart'; /// Alert dialog widget class ShowAlertDialog extends StatelessWidget { @@ -9,6 +9,7 @@ class ShowAlertDialog extends StatelessWidget { final Widget? leading; final String approveButtonText; final void Function()? onApprove; + final void Function()? onCancel; const ShowAlertDialog({ super.key, @@ -16,6 +17,7 @@ class ShowAlertDialog extends StatelessWidget { required this.content, required this.approveButtonText, this.onApprove, + this.onCancel, this.leading, }); @@ -27,10 +29,7 @@ class ShowAlertDialog extends StatelessWidget { : content; return AlertDialog( - icon: const Icon( - Icons.warning_amber_outlined, - size: 30, - ), + icon: const Icon(Icons.warning_amber_outlined, size: 30), content: modifiedContent, contentTextStyle: TextStyle( fontSize: 20, @@ -40,11 +39,8 @@ class ShowAlertDialog extends StatelessWidget { ListItemGroup( children: [ ListItem( - onTap: () => Navigator.pop(context), - title: const Text( - "cancel", - textAlign: TextAlign.center, - ).tr(), + onTap: onCancel, + title: const Text("cancel", textAlign: TextAlign.center).tr(), ), ListItem( leading: leading, diff --git a/lib/components/widgets/bottom_sheets/days_modal_bottom_sheet.dart b/lib/shared/widgets/bottom_sheets/days.dart similarity index 88% rename from lib/components/widgets/bottom_sheets/days_modal_bottom_sheet.dart rename to lib/shared/widgets/bottom_sheets/days.dart index 3b5fc25..13d7ecd 100644 --- a/lib/components/widgets/bottom_sheets/days_modal_bottom_sheet.dart +++ b/lib/shared/widgets/bottom_sheets/days.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/components/widgets/bottom_sheets/subject_data_bottom_sheet.dart'; -import 'package:timetable/constants/days.dart'; +import 'package:timetable/shared/widgets/bottom_sheets/subject_data.dart'; +import 'package:timetable/core/constants/days.dart'; /// Bottom Sheet Modal Widget used to select a subject's day. class DaysModalBottomSheet extends StatelessWidget { diff --git a/lib/components/widgets/bottom_sheets/rotation_week_modal_bottom_sheet.dart b/lib/shared/widgets/bottom_sheets/rotation_week.dart similarity index 86% rename from lib/components/widgets/bottom_sheets/rotation_week_modal_bottom_sheet.dart rename to lib/shared/widgets/bottom_sheets/rotation_week.dart index 8e5915b..343a488 100644 --- a/lib/components/widgets/bottom_sheets/rotation_week_modal_bottom_sheet.dart +++ b/lib/shared/widgets/bottom_sheets/rotation_week.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/components/widgets/bottom_sheets/subject_data_bottom_sheet.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/helpers/rotation_weeks.dart'; +import 'package:timetable/shared/widgets/bottom_sheets/subject_data.dart'; +import 'package:timetable/core/constants/rotation_weeks.dart'; +import 'package:timetable/core/utils/rotation_weeks.dart'; /// Bottom Sheet Modal Widget used to select a subject's rotation week. class RotationWeekModalBottomSheet extends StatelessWidget { diff --git a/lib/components/widgets/bottom_sheets/subject_data_bottom_sheet.dart b/lib/shared/widgets/bottom_sheets/subject_data.dart similarity index 100% rename from lib/components/widgets/bottom_sheets/subject_data_bottom_sheet.dart rename to lib/shared/widgets/bottom_sheets/subject_data.dart diff --git a/lib/components/widgets/bottom_sheets/subject_duration_modal_bottom_sheet.dart b/lib/shared/widgets/bottom_sheets/subject_duration.dart similarity index 89% rename from lib/components/widgets/bottom_sheets/subject_duration_modal_bottom_sheet.dart rename to lib/shared/widgets/bottom_sheets/subject_duration.dart index 53f5f15..991ff64 100644 --- a/lib/components/widgets/bottom_sheets/subject_duration_modal_bottom_sheet.dart +++ b/lib/shared/widgets/bottom_sheets/subject_duration.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:timetable/components/widgets/bottom_sheets/subject_data_bottom_sheet.dart'; -import 'package:timetable/constants/durations.dart'; +import 'package:timetable/shared/widgets/bottom_sheets/subject_data.dart'; +import 'package:timetable/core/constants/durations.dart'; /// Bottom Sheet Modal Widget used to select the default subject duration. class SubjectDurationModalBottomSheet extends StatelessWidget { diff --git a/lib/components/widgets/bottom_sheets/subject_management_bottom_sheet.dart b/lib/shared/widgets/bottom_sheets/subject_management.dart similarity index 85% rename from lib/components/widgets/bottom_sheets/subject_management_bottom_sheet.dart rename to lib/shared/widgets/bottom_sheets/subject_management.dart index f9f9de7..db8bbc1 100644 --- a/lib/components/widgets/bottom_sheets/subject_management_bottom_sheet.dart +++ b/lib/shared/widgets/bottom_sheets/subject_management.dart @@ -1,19 +1,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/subject_management/subject_screen.dart'; -import 'package:timetable/components/widgets/alert_dialog.dart'; -import 'package:timetable/components/widgets/color_indicator.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/helpers/rotation_weeks.dart'; -import 'package:timetable/provider/overlapping_subjects.dart'; -import 'package:timetable/provider/settings.dart'; -import 'package:timetable/provider/subjects.dart'; -import 'package:timetable/provider/timetables.dart'; +import 'package:timetable/features/subjects/screens/subject_screen.dart'; +import 'package:timetable/shared/widgets/alert_dialog.dart'; +import 'package:timetable/shared/widgets/color_indicator.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/core/utils/rotation_weeks.dart'; +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'; /// Bottom sheet widget to quickly see the full subject properties class SubjectManagementBottomSheet extends ConsumerWidget { - final SubjectData subject; + final Subject subject; const SubjectManagementBottomSheet({ super.key, @@ -289,28 +289,33 @@ class SubjectManagementBottomSheet extends ConsumerWidget { try { if (subjects.any((s) => s.id == currentSubject.id)) { navigator.pop(); - showDialog( - context: context, - builder: (BuildContext context) { - return ShowAlertDialog( - content: const Text('delete_subject_dialog').tr(), - approveButtonText: "delete".tr(), - onApprove: () async { - await subjectNotifier - .deleteSubject(currentSubject) - .then((_) { - messenger.showSnackBar( - SnackBar( - content: - const Text('subject_deleted_snackbar').tr(), - ), - ); - }); - navigator.pop(); + final result = await showDialog( + context: context, + builder: (BuildContext context) { + return ShowAlertDialog( + content: const Text('delete_subject_dialog').tr(), + approveButtonText: "delete".tr(), + onCancel: () => Navigator.of(context).pop(false), + onApprove: () => Navigator.of(context).pop(true), + ); }, - ); - }, - ); + ) ?? + false; + + if (result) { + await subjectNotifier.deleteSubject(currentSubject); + messenger.showSnackBar( + SnackBar( + content: const Text('subject_deleted_snackbar').tr(), + duration: const Duration(seconds: 3), + action: SnackBarAction( + label: "undo", + onPressed: () => subjectNotifier + .addSubject(currentSubject.toCompanion(true)), + ), + ), + ); + } } } catch (e) { messenger.showSnackBar( diff --git a/lib/components/widgets/bottom_sheets/timetables_modal_bottom_sheet.dart b/lib/shared/widgets/bottom_sheets/timetables_modal.dart similarity index 84% rename from lib/components/widgets/bottom_sheets/timetables_modal_bottom_sheet.dart rename to lib/shared/widgets/bottom_sheets/timetables_modal.dart index 11f8212..880ce95 100644 --- a/lib/components/widgets/bottom_sheets/timetables_modal_bottom_sheet.dart +++ b/lib/shared/widgets/bottom_sheets/timetables_modal.dart @@ -1,13 +1,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/components/widgets/bottom_sheets/subject_data_bottom_sheet.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/provider/timetables.dart'; +import 'package:timetable/shared/widgets/bottom_sheets/subject_data.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/features/timetable/providers/timetables.dart'; /// Bottom Sheet Modal Widget used to select a subject's timetable. class TimetablesModalBottomSheet extends ConsumerWidget { - final ValueNotifier timetable; + final ValueNotifier timetable; const TimetablesModalBottomSheet({ super.key, diff --git a/lib/components/widgets/color_indicator.dart b/lib/shared/widgets/color_indicator.dart similarity index 100% rename from lib/components/widgets/color_indicator.dart rename to lib/shared/widgets/color_indicator.dart diff --git a/lib/components/widgets/eager_initilization.dart b/lib/shared/widgets/eager_initilization.dart similarity index 80% rename from lib/components/widgets/eager_initilization.dart rename to lib/shared/widgets/eager_initilization.dart index 7344bd4..9c6b29d 100644 --- a/lib/components/widgets/eager_initilization.dart +++ b/lib/shared/widgets/eager_initilization.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/provider/timetables.dart'; -import 'package:timetable/provider/subjects.dart'; +import 'package:timetable/features/timetable/providers/timetables.dart'; +import 'package:timetable/features/subjects/providers/subjects.dart'; /// https://riverpod.dev/docs/essentials/eager_initialization /// @@ -22,7 +22,7 @@ class EagerInitialization extends ConsumerWidget { final timetablesNotifier = ref.watch(timetableProvider.notifier); return FutureBuilder( - future: timetablesNotifier.getTimetables(), + future: timetablesNotifier.fetchTimetablesFromDatabase(), builder: (context, snapshot) { if (snapshot.hasData) return child; return const Center(child: CircularProgressIndicator()); diff --git a/lib/components/widgets/list_item_group.dart b/lib/shared/widgets/list_item_group.dart similarity index 100% rename from lib/components/widgets/list_item_group.dart rename to lib/shared/widgets/list_item_group.dart diff --git a/lib/components/widgets/rotation_week_toggle.dart b/lib/shared/widgets/rotation_week_toggle.dart similarity index 86% rename from lib/components/widgets/rotation_week_toggle.dart rename to lib/shared/widgets/rotation_week_toggle.dart index ce716d8..cf7e665 100644 --- a/lib/components/widgets/rotation_week_toggle.dart +++ b/lib/shared/widgets/rotation_week_toggle.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:timetable/constants/rotation_weeks.dart'; -import 'package:timetable/helpers/rotation_weeks.dart'; -import 'package:timetable/helpers/subjects.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. /// diff --git a/lib/components/widgets/time_column.dart b/lib/shared/widgets/time_column.dart similarity index 88% rename from lib/components/widgets/time_column.dart rename to lib/shared/widgets/time_column.dart index 8112603..ce9c71e 100644 --- a/lib/components/widgets/time_column.dart +++ b/lib/shared/widgets/time_column.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/constants/grid_properties.dart'; -import 'package:timetable/constants/time.dart'; -import 'package:timetable/provider/settings.dart'; +import 'package:timetable/core/constants/grid_properties.dart'; +import 'package:timetable/core/constants/time.dart'; +import 'package:timetable/features/settings/providers/settings.dart'; /// Widget that appears at the left side of the timetable grid view screen and shows the timetable time period. class TimeColumn extends ConsumerWidget { diff --git a/lib/components/widgets/timetable_toggle.dart b/lib/shared/widgets/timetable_toggle.dart similarity index 82% rename from lib/components/widgets/timetable_toggle.dart rename to lib/shared/widgets/timetable_toggle.dart index e3054ff..70e04b7 100644 --- a/lib/components/widgets/timetable_toggle.dart +++ b/lib/shared/widgets/timetable_toggle.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:timetable/db/database.dart'; -import 'package:timetable/helpers/subjects.dart'; -import 'package:timetable/provider/timetables.dart'; +import 'package:timetable/core/db/database.dart'; +import 'package:timetable/core/utils/subjects.dart'; +import 'package:timetable/features/timetable/providers/timetables.dart'; /// Toggles between all Timetables. /// /// used for filtering class TimetableToggle extends HookConsumerWidget { - final ValueNotifier timetable; + final ValueNotifier timetable; const TimetableToggle({ super.key, required this.timetable,