Merge pull request #31 from refilc/dev

everything from dev
This commit is contained in:
Márton Kiss 2023-09-02 21:03:49 +02:00 committed by GitHub
commit 79370f6a02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
126 changed files with 7259 additions and 2482 deletions

14
.gitignore vendored
View File

@ -1,5 +1,7 @@
# See https://www.dartlang.org/guides/libraries/private-files
.gitignore
termek.txt
.DS_Store
filc3.properties
@ -25,9 +27,6 @@ doc/api/
*.js.map
*.txt
filcnaplo/linux/flutter/generated_plugin_registrant.cc
filcnaplo/linux/flutter/generated_plugin_registrant.h
filcnaplo/linux/flutter/generated_plugins.cmake
filcnaplo/macos/Flutter/GeneratedPluginRegistrant.swift
filcnaplo/windows/flutter/ephemeral/.plugin_symlinks/connectivity_plus
filcnaplo/windows/flutter/ephemeral/.plugin_symlinks/dynamic_color
@ -43,10 +42,9 @@ filcnaplo/windows/flutter/generated_plugins.cmake
filcnaplo/linux/flutter/generated_plugin_registrant.cc
filcnaplo/linux/flutter/generated_plugin_registrant.h
filcnaplo/linux/flutter/generated_plugins.cmake
filcnaplo/macos/Flutter/GeneratedPluginRegistrant.swift
filcnaplo/linux/flutter/generated_plugin_registrant.cc
filcnaplo/linux/flutter/generated_plugin_registrant.h
filcnaplo/linux/flutter/generated_plugins.cmake
filcnaplo/macos/Flutter/GeneratedPluginRegistrant.swift
filcnaplo/macos/Flutter/*
filcnaplo/ios/Podfile.lock
.vscode/
key.properties
.flutter-plugins*

1286
.idea/libraries/Dart_Packages.xml generated Normal file

File diff suppressed because it is too large Load Diff

27
.idea/libraries/Dart_SDK.xml generated Normal file
View File

@ -0,0 +1,27 @@
<component name="libraryTable">
<library name="Dart SDK">
<CLASSES>
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/async" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/cli" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/collection" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/convert" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/core" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/developer" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/ffi" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/html" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/indexed_db" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/io" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/isolate" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/js" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/js_util" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/math" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/mirrors" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/svg" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/typed_data" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/web_audio" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/web_gl" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

4
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Android API 33, extension level 3 Platform" project-jdk-type="Android SDK" />
</project>

View File

@ -12,7 +12,7 @@
### Clone the project
```sh
git clone --recursive https://github.com/refilc/naplo
git clone https://github.com/refilc/naplo
cd naplo
```

View File

@ -2,12 +2,14 @@
xmlns:tools="http://schemas.android.com/tools" package="hu.refilc.naplo">
<application android:name="${applicationName}" android:label="reFilc" tools:replace="android:label" android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true">
<activity android:exported="true" android:name=".MainActivity"
<activity android:exported="true" android:name="hu.refilc.naplo.MainActivity"
android:launchMode="singleTop" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"
android:showWhenLocked="true"
android:turnScreenOn="true">
<meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />
<meta-data android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
@ -18,16 +20,26 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with https://api.filcnaplo.hu -->
<!-- Accepts URIs that begin with https://api.refilcapp.hu -->
<data
android:scheme="https"
android:host="api.filcnaplo.hu"
android:pathPrefix="/callback" />
android:host="api.refilcapp.hu"
android:pathPrefix="/v1/auth/callback" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with https://refilc.hu -->
<data
android:scheme="https"
android:host="refilc.hu"
android:pathPrefix="/app" />
</intent-filter>
</activity>
<meta-data android:name="flutterEmbedding" android:value="2" />
<receiver android:name=".widget_timetable.WidgetTimetable"
<receiver android:name="hu.refilc.naplo.widget_timetable.WidgetTimetable"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -43,7 +55,7 @@
android:resource="@xml/home_widget_test_info" />
</receiver>
<service android:name=".widget_timetable.WidgetTimetableService"
<service android:name="hu.refilc.naplo.widget_timetable.WidgetTimetableService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<receiver android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver"
@ -57,6 +69,8 @@
android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true" />
</application>
<meta-data android:name="flutterEmbedding" android:value="2" />
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />

View File

@ -1 +0,0 @@
../../../../../../../../../filcnaplo_premium/android/database

View File

@ -1 +0,0 @@
../../../../../../../../../filcnaplo_premium/android/utils

View File

@ -1 +0,0 @@
../../../../../../../../../filcnaplo_premium/android/widget_timetable

View File

@ -2,8 +2,6 @@ package hu.refilc.naplo;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
public class MainActivity extends FlutterActivity {
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<gradient
android:startColor="#123323"
android:endColor="#20AC9B"
android:angle="135" />
</shape>
</item>
</selector>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/launch_gradient_background" />
<item>
<bitmap android:gravity="center" android:src="@mipmap/ic_splash"/>
</item>
</layer-list>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<gradient
android:startColor="#123323"
android:endColor="#20AC9B"
android:angle="135" />
</shape>
</item>
</selector>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<gradient
android:startColor="#123323"
android:endColor="#20AC9B"
android:angle="135" />
</shape>
</item>
</selector>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:id="@+id/main_lay"
android:layout_height="50dp"
@ -15,12 +16,14 @@
android:id="@+id/tt_item_num"
android:layout_width="50dp"
android:layout_height="match_parent"
android:fontFamily="@font/montserrat_medium"
android:gravity="center"
android:text="1."
android:textColor="@color/filc"
android:textColorLink="#ff3D7BF4"
android:textSize="30sp"
android:textStyle="bold"
android:fontFamily="@font/montserrat_medium"
android:text="1."
android:gravity="center"
android:textColor="@color/filc"></TextView>
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/tt_item_name"
@ -33,10 +36,11 @@
android:text="Óra neve"
android:maxLines="1"
android:ellipsize="end"
android:layout_toLeftOf="@id/tt_item_room"
android:layout_toStartOf="@id/tt_item_room"
android:gravity="center_vertical"
android:layout_toRightOf="@id/tt_item_num"
android:textColor="@color/text"></TextView>
android:layout_toEndOf="@id/tt_item_num"
android:textColor="@color/text"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/tt_item_name_nodesc"
@ -47,13 +51,14 @@
android:fontFamily="@font/montserrat_medium"
android:visibility="gone"
android:layout_marginTop="2.5dp"
android:layout_toLeftOf="@id/tt_item_room"
android:layout_toStartOf="@id/tt_item_room"
android:text="Óra neve"
android:maxLines="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:layout_toRightOf="@id/tt_item_num"
android:textColor="@color/text"></TextView>
android:layout_toEndOf="@id/tt_item_num"
android:textColor="@color/text"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/tt_item_desc"
@ -67,10 +72,11 @@
android:maxLines="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:layout_toRightOf="@id/tt_item_num"
android:layout_toLeftOf="@id/tt_item_room"
android:layout_toEndOf="@id/tt_item_num"
android:layout_toStartOf="@id/tt_item_room"
android:layout_below="@id/tt_item_name"
android:textColor="@color/text_desc"></TextView>
android:textColor="@color/text_desc"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/tt_item_room"
@ -84,8 +90,9 @@
android:ellipsize="end"
android:maxLines="2"
android:gravity="center"
android:layout_toLeftOf="@id/tt_item_time"
android:textColor="@color/text_desc"></TextView>
android:layout_toStartOf="@id/tt_item_time"
android:textColor="@color/text_desc"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/tt_item_time"
@ -95,11 +102,12 @@
android:textFontWeight="500"
android:fontFamily="@font/montserrat_medium"
android:textStyle="bold"
android:layout_marginLeft="2dp"
android:layout_marginStart="2dp"
android:layout_marginTop="-2dp"
android:text="8:30\n9:10"
android:gravity="center"
android:layout_alignParentRight="true"
android:textColor="@color/white"></TextView>
android:layout_alignParentEnd="true"
android:textColor="@color/white"
tools:ignore="HardcodedText" />
</RelativeLayout>

View File

@ -19,10 +19,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="Empty"
android:text="Üres / Empty"
android:background="@drawable/widget_card_bottom_dark"
android:textColor="@color/text"
android:textSize="20sp"
android:textStyle="bold" />
android:textStyle="bold"
tools:ignore="HardcodedText" />
<ListView
android:id="@+id/widget_list"
@ -43,57 +45,63 @@
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@drawable/widget_card_top_dark">
<ImageView
android:id="@+id/nav_refresh"
android:layout_width="40dp"
android:layout_height="match_parent"
android:layout_toStartOf="@id/nav_to_left"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground"
android:padding="10dp"
android:src="@drawable/ic_refresh_cw"
android:layout_toLeftOf="@id/nav_to_left"
android:tint="@color/text_desc"
tools:ignore="UseAppTint"
android:padding="10dp" />
android:focusable="true" />
<ImageView
android:id="@+id/nav_to_left"
android:layout_width="50dp"
android:layout_width="45dp"
android:layout_height="match_parent"
android:layout_toStartOf="@id/nav_to_right"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground"
android:padding="10dp"
android:src="@drawable/ic_chevron_left"
android:layout_toLeftOf="@id/nav_to_right"
android:tint="@color/text_desc"
tools:ignore="UseAppTint"
android:padding="10dp" />
android:focusable="true" />
<ImageView
android:id="@+id/nav_to_right"
android:layout_width="50dp"
android:layout_width="45dp"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_marginEnd="5dp"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground"
android:padding="10dp"
android:src="@drawable/ic_chevron_right"
android:layout_alignParentRight="true"
android:layout_marginRight="5dp"
android:tint="@color/text_desc"
tools:ignore="UseAppTint"
android:padding="10dp" />
android:focusable="true" />
<TextView
android:id="@+id/nav_current"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_marginLeft="20sp"
android:layout_toLeftOf="@id/nav_refresh"
android:layout_alignParentStart="true"
android:layout_marginStart="15sp"
android:layout_marginTop="2sp"
android:layout_toStartOf="@id/nav_refresh"
android:fontFamily="@font/montserrat_medium"
android:gravity="center_vertical"
android:maxLines="1"
android:text="Timetable"
android:text="Órarend"
android:textColor="@color/text"
android:textSize="24sp"
android:textStyle="bold" />
android:textSize="22sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
</RelativeLayout>
</RelativeLayout>
@ -131,8 +139,9 @@
android:text="A widget használatához, bejelentkezés szükséges."
android:textColor="@color/black"
android:paddingTop="10dp"
android:textSize="17dp"
android:textStyle="bold" />
android:textSize="17sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
<Button
android:id="@+id/open_login"
@ -145,8 +154,9 @@
android:layout_margin="10dp"
android:textColor="@color/white"
android:fontFamily="@font/montserrat_medium"
android:textSize="16dp"
android:textStyle="bold"/>
android:textSize="16sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
</RelativeLayout>
@ -185,8 +195,9 @@
android:text="Órák a kezdőképernyőd kényelméből."
android:textColor="@color/black"
android:paddingTop="10dp"
android:textSize="16dp"
android:textStyle="bold" />
android:textSize="16sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
@ -197,7 +208,8 @@
android:textColor="@color/black"
android:layout_marginTop="0dp"
android:layout_marginHorizontal="15dp"
android:textSize="14dp" />
android:textSize="14sp"
tools:ignore="HardcodedText" />
<Button
android:id="@+id/buy_premium"
@ -211,8 +223,9 @@
android:layout_margin="10dp"
android:textColor="#ff691A9B"
android:fontFamily="@font/montserrat_medium"
android:textSize="16dp"
android:textStyle="bold"/>
android:textSize="16sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
</RelativeLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,12 +0,0 @@
<resources>
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_background</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_inner_view_background</item>
</style>
</resources>

View File

@ -27,7 +27,7 @@
<color name="yellow_light">#ffFFCC00</color>
<color name="light_yellow_light">#40FFD60A</color>
<color name="green_light">#ff34C759</color>
<color name="filc_light">#ff247665</color>
<color name="filc_light">#ff3D7BF4</color>
<color name="teal_light">#ff5AC8FA</color>
<color name="blue_light">#ff007AFF</color>
<color name="indigo_light">#ff5856D6</color>
@ -49,7 +49,7 @@
<color name="yellow">#ffFFD60A</color>
<color name="light_yellow">#40FFD60A</color>
<color name="green">#ff32D74B</color>
<color name="filc">#ff29826F</color>
<color name="filc">#ff3D7BF4</color>
<color name="teal">#ff64D2FF</color>
<color name="blue">#ff0A84FF</color>
<color name="indigo">#ff5E5CE6</color>

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,237 +0,0 @@
PODS:
- app_group_directory (1.0.0):
- Flutter
- background_fetch (1.1.6):
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- DKImagePickerController/Core (4.3.4):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.4)
- DKImagePickerController/PhotoGallery (4.3.4):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.4)
- DKPhotoGallery (0.0.17):
- DKPhotoGallery/Core (= 0.0.17)
- DKPhotoGallery/Model (= 0.0.17)
- DKPhotoGallery/Preview (= 0.0.17)
- DKPhotoGallery/Resource (= 0.0.17)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.17):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.17):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
- flutter_custom_tabs (0.0.1):
- Flutter
- flutter_image_compress (1.0.0):
- Flutter
- Mantle
- SDWebImage
- SDWebImageWebPCoder
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_image (0.0.1):
- Flutter
- flutter_native_splash (0.0.1):
- Flutter
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- home_widget (0.0.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- libwebp (1.2.4):
- libwebp/demux (= 1.2.4)
- libwebp/mux (= 1.2.4)
- libwebp/webp (= 1.2.4)
- libwebp/demux (1.2.4):
- libwebp/webp
- libwebp/mux (1.2.4):
- libwebp/demux
- libwebp/webp (1.2.4)
- live_activities (0.0.1):
- Flutter
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- open_file (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.0.4):
- Flutter
- quick_actions_ios (0.0.1):
- Flutter
- ReachabilitySwift (5.0.0)
- SDWebImage (5.13.2):
- SDWebImage/Core (= 5.13.2)
- SDWebImage/Core (5.13.2)
- SDWebImageWebPCoder (0.9.1):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.13)
- share_plus (0.0.1):
- Flutter
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
- SwiftyGif (5.4.3)
- uni_links (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- workmanager (0.0.1):
- Flutter
DEPENDENCIES:
- app_group_directory (from `.symlinks/plugins/app_group_directory/ios`)
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- DKImagePickerController/PhotoGallery (from `https://github.com/zhangao0086/DKImagePickerController.git`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_custom_tabs (from `.symlinks/plugins/flutter_custom_tabs/ios`)
- flutter_image_compress (from `.symlinks/plugins/flutter_image_compress/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_image (from `.symlinks/plugins/flutter_native_image/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- live_activities (from `.symlinks/plugins/live_activities/ios`)
- open_file (from `.symlinks/plugins/open_file/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- quick_actions_ios (from `.symlinks/plugins/quick_actions_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- workmanager (from `.symlinks/plugins/workmanager/ios`)
SPEC REPOS:
trunk:
- DKPhotoGallery
- FMDB
- libwebp
- Mantle
- ReachabilitySwift
- SDWebImage
- SDWebImageWebPCoder
- SwiftyGif
EXTERNAL SOURCES:
app_group_directory:
:path: ".symlinks/plugins/app_group_directory/ios"
background_fetch:
:path: ".symlinks/plugins/background_fetch/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
DKImagePickerController:
:git: https://github.com/zhangao0086/DKImagePickerController.git
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_custom_tabs:
:path: ".symlinks/plugins/flutter_custom_tabs/ios"
flutter_image_compress:
:path: ".symlinks/plugins/flutter_image_compress/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_image:
:path: ".symlinks/plugins/flutter_native_image/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
home_widget:
:path: ".symlinks/plugins/home_widget/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
live_activities:
:path: ".symlinks/plugins/live_activities/ios"
open_file:
:path: ".symlinks/plugins/open_file/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
quick_actions_ios:
:path: ".symlinks/plugins/quick_actions_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
uni_links:
:path: ".symlinks/plugins/uni_links/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
workmanager:
:path: ".symlinks/plugins/workmanager/ios"
CHECKOUT OPTIONS:
DKImagePickerController:
:commit: a727e44718a67e300089174e5591166045815ba4
:git: https://github.com/zhangao0086/DKImagePickerController.git
SPEC CHECKSUMS:
app_group_directory: 7bf9f8f9819ead554de29da7c25fb7a680d6a9a0
background_fetch: bc9b44b0bf8b434e282a2ac9be8662800a0296ed
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_custom_tabs: 7a10a08686955cb748e5d26e0ae586d30689bf89
flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_native_image: 9c0b7451838484458e5b0fae007b86a4c2d4bdfe
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
home_widget: 2829415127ee92e876f816cbbe44c0b6601b8a37
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
live_activities: 9ff56a06a2d43ecd68f56deeed13b18a8304789c
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
quick_actions_ios: 9e80dcfadfbc5d47d9cf8f47bcf428b11cf383d4
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866
SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: efe5e2e257c61a1c3d5be693ef1a8ea1c4b81d8f
COCOAPODS: 1.12.1

View File

@ -198,9 +198,9 @@
isa = PBXNativeTarget;
buildConfigurationList = 3127F79F28EAEDE300C2EFB3 /* Build configuration list for PBXNativeTarget "livecard" */;
buildPhases = (
3127F78A28EAEDE200C2EFB3 /* Resources */,
3127F78828EAEDE200C2EFB3 /* Sources */,
3127F78928EAEDE200C2EFB3 /* Frameworks */,
3127F78A28EAEDE200C2EFB3 /* Resources */,
);
buildRules = (
);
@ -299,7 +299,7 @@
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
buildActionMask = 12;
files = (
);
inputPaths = (
@ -310,11 +310,11 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
};
71459C0EB905E05018E3D78F /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 12;
files = (
);
inputFileListPaths = (
@ -364,7 +364,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
/* End PBXShellScriptBuildPhase section */

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.refilcnaplo.livecard</string>

View File

@ -1,6 +1,7 @@
import Foundation
class LessonData {
var color: String
var icon: String
var index: String
var title: String
@ -15,6 +16,7 @@ class LessonData {
init?() {
let sharedDefault = UserDefaults(suiteName: "group.refilc.livecard")!
self.color = sharedDefault.string(forKey: "color")!
self.icon = sharedDefault.string(forKey: "icon")!
self.index = sharedDefault.string(forKey: "index")!
self.title = sharedDefault.string(forKey: "title")!

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.refilcnaplo.livecard</string>

View File

@ -11,6 +11,33 @@ struct Widgets: WidgetBundle {
}
}
// Color Converter
extension Color {
init(hex: String, alpha: Double = 1.0) {
var hexValue = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if hexValue.hasPrefix("#") {
hexValue.remove(at: hexValue.startIndex)
}
var rgbValue: UInt64 = 0
Scanner(string: hexValue).scanHexInt64(&rgbValue)
let red = Double((rgbValue & 0xFF0000) >> 16) / 255.0
let green = Double((rgbValue & 0x00FF00) >> 8) / 255.0
let blue = Double(rgbValue & 0x0000FF) / 255.0
self.init(
.sRGB,
red: red,
green: green,
blue: blue,
opacity: alpha
)
}
}
// We need to redefined live activities pipe
struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable {
public struct ContentState: Codable, Hashable { }
@ -68,6 +95,11 @@ struct LockScreenLiveActivityView: View {
.monospacedDigit()
.padding(.trailing, CGFloat(24))
}
.activityBackgroundTint(
lesson!.color == "#676767"
? nil
: Color(hex: lesson!.color)
)
}
}
@ -144,7 +176,11 @@ struct LiveCardWidget: Widget {
.font(.system(size: CGFloat(10)))
}
}
.keylineTint(.accentColor)
.keylineTint(
lesson!.color == "#676767"
? nil
: Color(hex: lesson!.color)
)
}
}
}

View File

@ -47,7 +47,7 @@ class LiveCardProvider extends ChangeNotifier {
required SettingsProvider settings,
}) : _timetable = timetable,
_settings = settings {
// Check if live card is enabled .areActivitiesEnabled()
if (Platform.isIOS) {
_liveActivitiesPlugin.areActivitiesEnabled().then((value) {
// Console log
if (kDebugMode) {
@ -69,6 +69,7 @@ class LiveCardProvider extends ChangeNotifier {
: Duration.zero;
update();
}
}
@override
void dispose() {
@ -87,6 +88,7 @@ class LiveCardProvider extends ChangeNotifier {
// Debugging
static DateTime _now() {
// return DateTime(2023, 8, 31, 8, 0);
return DateTime.now();
}
@ -110,6 +112,8 @@ class LiveCardProvider extends ChangeNotifier {
switch (currentState) {
case LiveCardState.duringLesson:
return {
"color":
'#${_settings.liveActivityColor.toString().substring(10, 16)}',
"icon": currentLesson != null
? SubjectIcon.resolveName(subject: currentLesson?.subject)
: "book",
@ -142,6 +146,8 @@ class LiveCardProvider extends ChangeNotifier {
final diff = getFloorDifference();
return {
"color":
'#${_settings.liveActivityColor.toString().substring(10, 16)}',
"icon": iconFloorMap[diff] ?? "cup.and.saucer",
"title": "Szünet",
"description": "go $diff".i18n.fill([

View File

@ -1,7 +1,4 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:math';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo/models/settings.dart';
@ -11,7 +8,7 @@ import 'package:provider/provider.dart';
class NewsProvider extends ChangeNotifier {
// Private
late List<News> _news;
late int _state;
//late int _state;
late int _fresh;
bool show = false;
late BuildContext _context;
@ -30,56 +27,83 @@ class NewsProvider extends ChangeNotifier {
Future<void> restore() async {
// Load news state from the database
var state_ = Provider.of<SettingsProvider>(_context, listen: false).newsState;
var seen_ = Provider.of<SettingsProvider>(_context, listen: false).seenNews;
if (state_ == -1) {
if (seen_.isEmpty) {
var news_ = await FilcAPI.getNews();
if (news_ != null) {
state_ = news_.length;
_news = news_;
show = true;
}
}
_state = state_;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state);
//_state = seen_;
// Provider.of<SettingsProvider>(_context, listen: false)
// .update(seenNewsId: news_.id);
}
Future<void> fetch() async {
var news_ = await FilcAPI.getNews();
if (news_ == null) return;
show = false;
_news = news_;
_fresh = news_.length - _state;
if (_fresh < 0) {
_state = news_.length;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state);
}
_fresh = max(_fresh, 0);
if (_fresh > 0) {
for (var news in news_) {
if (news.expireDate.isAfter(DateTime.now()) &&
Provider.of<SettingsProvider>(_context, listen: false)
.seenNews
.contains(news.id) ==
false) {
show = true;
Provider.of<SettingsProvider>(_context, listen: false)
.update(seenNewsId: news.id);
notifyListeners();
}
}
// print(news_.length);
// print(_state);
// _news = news_;
// _fresh = news_.length - _state;
// if (_fresh < 0) {
// _state = news_.length;
// Provider.of<SettingsProvider>(_context, listen: false)
// .update(newsState: _state);
// }
// _fresh = max(_fresh, 0);
// if (_fresh > 0) {
// show = true;
// notifyListeners();
// }
// print(_fresh);
// print(_state);
// print(show);
}
void lock() => show = false;
void release() {
if (_fresh == 0) return;
// if (_fresh == 0) return;
_fresh--;
_state++;
// _fresh--;
// //_state++;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state);
// // Provider.of<SettingsProvider>(_context, listen: false)
// // .update(seenNewsId: _state);
if (_fresh > 0) {
show = true;
} else {
show = false;
}
// if (_fresh > 0) {
// show = true;
// } else {
// show = false;
// }
notifyListeners();
// notifyListeners();
}
}

View File

@ -12,6 +12,7 @@ import 'package:filcnaplo/theme/observer.dart';
import 'package:filcnaplo/theme/theme.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_premium/providers/goal_provider.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -89,6 +90,7 @@ class App extends StatelessWidget {
return MultiProvider(
providers: [
// refilc providers
ChangeNotifierProvider<PremiumProvider>(create: (_) => premium),
ChangeNotifierProvider<SettingsProvider>(create: (_) => settings),
ChangeNotifierProvider<UserProvider>(create: (_) => user),
@ -103,7 +105,7 @@ class App extends StatelessWidget {
ChangeNotifierProvider<UpdateProvider>(
create: (context) => UpdateProvider(context: context)),
// User data providers
// user data (kreten) providers
ChangeNotifierProvider<GradeProvider>(
create: (_) => GradeProvider(
settings: settings,
@ -114,7 +116,8 @@ class App extends StatelessWidget {
ChangeNotifierProvider<ExamProvider>(
create: (context) => ExamProvider(context: context)),
ChangeNotifierProvider<HomeworkProvider>(
create: (context) => HomeworkProvider(context: context)),
create: (context) =>
HomeworkProvider(context: context, database: database)),
ChangeNotifierProvider<MessageProvider>(
create: (context) => MessageProvider(context: context)),
ChangeNotifierProvider<NoteProvider>(
@ -124,6 +127,7 @@ class App extends StatelessWidget {
ChangeNotifierProvider<AbsenceProvider>(
create: (context) => AbsenceProvider(context: context)),
// other providers
ChangeNotifierProvider<GradeCalculatorProvider>(
create: (_) => GradeCalculatorProvider(
settings: settings,
@ -132,7 +136,9 @@ class App extends StatelessWidget {
kreta: kreta)),
ChangeNotifierProvider<LiveCardProvider>(
create: (context) =>
LiveCardProvider(timetable: timetable, settings: settings))
LiveCardProvider(timetable: timetable, settings: settings)),
ChangeNotifierProvider<GoalProvider>(
create: (context) => GoalProvider(database: database, user: user)),
],
child: Consumer<ThemeModeObserver>(
builder: (context, themeMode, child) {

View File

@ -12,20 +12,27 @@ import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
const settingsDB = DatabaseStruct("settings", {
"language": String, "start_page": int, "rounding": int, "theme": int,
"accent_color": int, "news": int, "news_state": int, "developer_mode": int,
"accent_color": int, "news": int, "seen_news": String,
"developer_mode": int,
"update_channel": int, "config": String, "custom_accent_color": int,
"custom_background_color": int, "custom_highlight_color": int, // general
"grade_color1": int, "grade_color2": int, "grade_color3": int,
"grade_color4": int, "grade_color5": int, // grade colors
"vibration_strength": int, "ab_weeks": int, "swap_ab_weeks": int,
"notifications": int, "notifications_bitfield": int,
"notification_poll_interval": int, // notifications
"notification_poll_interval": int,
"notifications_grades": int,
"notifications_absences": int,
"notifications_messages": int,
"notifications_lessons": int, // notifications
"x_filc_id": String, "graph_class_avg": int, "presentation_mode": int,
"bell_delay": int, "bell_delay_enabled": int,
"grade_opening_fun": int, "icon_pack": String, "premium_scopes": String,
"premium_token": String, "premium_login": String,
"last_account_id": String, "renamed_subjects_enabled": int,
"renamed_subjects_italics": int,
"renamed_subjects_italics": int, "renamed_teachers_enabled": int,
"renamed_teachers_italics": int,
"live_activity_color": String,
});
// DON'T FORGET TO UPDATE DEFAULT VALUES IN `initDB` MIGRATION OR ELSE PARENTS WILL COMPLAIN ABOUT THEIR CHILDREN MISSING
// YOU'VE BEEN WARNED!!!
@ -40,8 +47,15 @@ const userDataDB = DatabaseStruct("user_data", {
"events": String, "absences": String, "group_averages": String,
// renamed subjects // non kreta data
"renamed_subjects": String,
// renamed teachers // non kreta data
"renamed_teachers": String,
// "subject_lesson_count": String, // non kreta data
"last_seen_grade": int,
// goal planning // non kreta data
"goal_plans": String,
"goal_averages": String,
"goal_befores": String,
"goal_pin_dates": String,
});
Future<void> createTable(Database db, DatabaseStruct struct) =>
@ -89,8 +103,15 @@ Future<Database> initDB(DatabaseProvider database) async {
"group_averages": "[]",
// renamed subjects // non kreta data
"renamed_subjects": "{}",
// renamed teachers // non kreta data
"renamed_teachers": "{}",
// "subject_lesson_count": "{}", // non kreta data
"last_seen_grade": 0,
// goal planning // non kreta data
"goal_plans": "{}",
"goal_averages": "{}",
"goal_befores": "{}",
"goal_pin_dates": "{}",
});
} catch (error) {
print("ERROR: migrateDB: $error");

View File

@ -26,7 +26,8 @@ class DatabaseQuery {
Future<SettingsProvider> getSettings(DatabaseProvider database) async {
Map settingsMap = (await db.query("settings")).elementAt(0);
SettingsProvider settings = SettingsProvider.fromMap(settingsMap, database: database);
SettingsProvider settings =
SettingsProvider.fromMap(settingsMap, database: database);
return settings;
}
@ -36,7 +37,10 @@ class DatabaseQuery {
for (var user in usersMap) {
userProvider.addUser(User.fromMap(user));
}
if (userProvider.getUsers().map((e) => e.id).contains(settings.lastAccountId)) {
if (userProvider
.getUsers()
.map((e) => e.id)
.contains(settings.lastAccountId)) {
userProvider.setUser(settings.lastAccountId);
} else {
if (usersMap.isNotEmpty) {
@ -54,100 +58,133 @@ class UserDatabaseQuery {
final Database db;
Future<List<Grade>> getGrades({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? gradesJson = userData.elementAt(0)["grades"] as String?;
if (gradesJson == null) return [];
List<Grade> grades = (jsonDecode(gradesJson) as List).map((e) => Grade.fromJson(e)).toList();
List<Grade> grades =
(jsonDecode(gradesJson) as List).map((e) => Grade.fromJson(e)).toList();
return grades;
}
Future<Map<Week, List<Lesson>>> getLessons({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? lessonsJson = userData.elementAt(0)["timetable"] as String?;
if (lessonsJson == null) return {};
if (jsonDecode(lessonsJson) is List) return {};
Map<Week, List<Lesson>> lessons = (jsonDecode(lessonsJson) as Map).cast<String, List>().map((key, value) {
return MapEntry(Week.fromId(int.parse(key)), value.cast<Map<String, Object?>>().map((e) => Lesson.fromJson(e)).toList());
Map<Week, List<Lesson>> lessons =
(jsonDecode(lessonsJson) as Map).cast<String, List>().map((key, value) {
return MapEntry(
Week.fromId(int.parse(key)),
value
.cast<Map<String, Object?>>()
.map((e) => Lesson.fromJson(e))
.toList());
}).cast();
return lessons;
}
Future<List<Exam>> getExams({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? examsJson = userData.elementAt(0)["exams"] as String?;
if (examsJson == null) return [];
List<Exam> exams = (jsonDecode(examsJson) as List).map((e) => Exam.fromJson(e)).toList();
List<Exam> exams =
(jsonDecode(examsJson) as List).map((e) => Exam.fromJson(e)).toList();
return exams;
}
Future<List<Homework>> getHomework({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? homeworkJson = userData.elementAt(0)["homework"] as String?;
if (homeworkJson == null) return [];
List<Homework> homework = (jsonDecode(homeworkJson) as List).map((e) => Homework.fromJson(e)).toList();
List<Homework> homework = (jsonDecode(homeworkJson) as List)
.map((e) => Homework.fromJson(e))
.toList();
return homework;
}
Future<List<Message>> getMessages({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? messagesJson = userData.elementAt(0)["messages"] as String?;
if (messagesJson == null) return [];
List<Message> messages = (jsonDecode(messagesJson) as List).map((e) => Message.fromJson(e)).toList();
List<Message> messages = (jsonDecode(messagesJson) as List)
.map((e) => Message.fromJson(e))
.toList();
return messages;
}
Future<List<Note>> getNotes({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? notesJson = userData.elementAt(0)["notes"] as String?;
if (notesJson == null) return [];
List<Note> notes = (jsonDecode(notesJson) as List).map((e) => Note.fromJson(e)).toList();
List<Note> notes =
(jsonDecode(notesJson) as List).map((e) => Note.fromJson(e)).toList();
return notes;
}
Future<List<Event>> getEvents({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? eventsJson = userData.elementAt(0)["events"] as String?;
if (eventsJson == null) return [];
List<Event> events = (jsonDecode(eventsJson) as List).map((e) => Event.fromJson(e)).toList();
List<Event> events =
(jsonDecode(eventsJson) as List).map((e) => Event.fromJson(e)).toList();
return events;
}
Future<List<Absence>> getAbsences({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? absencesJson = userData.elementAt(0)["absences"] as String?;
if (absencesJson == null) return [];
List<Absence> absences = (jsonDecode(absencesJson) as List).map((e) => Absence.fromJson(e)).toList();
List<Absence> absences = (jsonDecode(absencesJson) as List)
.map((e) => Absence.fromJson(e))
.toList();
return absences;
}
Future<List<GroupAverage>> getGroupAverages({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? groupAveragesJson = userData.elementAt(0)["group_averages"] as String?;
String? groupAveragesJson =
userData.elementAt(0)["group_averages"] as String?;
if (groupAveragesJson == null) return [];
List<GroupAverage> groupAverages = (jsonDecode(groupAveragesJson) as List).map((e) => GroupAverage.fromJson(e)).toList();
List<GroupAverage> groupAverages = (jsonDecode(groupAveragesJson) as List)
.map((e) => GroupAverage.fromJson(e))
.toList();
return groupAverages;
}
Future<SubjectLessonCount> getSubjectLessonCount({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
Future<SubjectLessonCount> getSubjectLessonCount(
{required String userId}) async {
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return SubjectLessonCount.fromMap({});
String? lessonCountJson = userData.elementAt(0)["subject_lesson_count"] as String?;
String? lessonCountJson =
userData.elementAt(0)["subject_lesson_count"] as String?;
if (lessonCountJson == null) return SubjectLessonCount.fromMap({});
SubjectLessonCount lessonCount = SubjectLessonCount.fromMap(jsonDecode(lessonCountJson) as Map);
SubjectLessonCount lessonCount =
SubjectLessonCount.fromMap(jsonDecode(lessonCountJson) as Map);
return lessonCount;
}
Future<DateTime> lastSeenGrade({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return DateTime(0);
int? lastSeenDate = userData.elementAt(0)["last_seen_grade"] as int?;
if (lastSeenDate == null) return DateTime(0);
@ -155,11 +192,71 @@ class UserDatabaseQuery {
return lastSeen;
}
// renamed things
Future<Map<String, String>> renamedSubjects({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? renamedSubjectsJson = userData.elementAt(0)["renamed_subjects"] as String?;
String? renamedSubjectsJson =
userData.elementAt(0)["renamed_subjects"] as String?;
if (renamedSubjectsJson == null) return {};
return (jsonDecode(renamedSubjectsJson) as Map).map((key, value) => MapEntry(key.toString(), value.toString()));
return (jsonDecode(renamedSubjectsJson) as Map)
.map((key, value) => MapEntry(key.toString(), value.toString()));
}
Future<Map<String, String>> renamedTeachers({required String userId}) async {
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? renamedTeachersJson =
userData.elementAt(0)["renamed_teachers"] as String?;
if (renamedTeachersJson == null) return {};
return (jsonDecode(renamedTeachersJson) as Map)
.map((key, value) => MapEntry(key.toString(), value.toString()));
}
// goal planner
Future<Map<String, String>> subjectGoalPlans({required String userId}) async {
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? goalPlansJson = userData.elementAt(0)["goal_plans"] as String?;
if (goalPlansJson == null) return {};
return (jsonDecode(goalPlansJson) as Map)
.map((key, value) => MapEntry(key.toString(), value.toString()));
}
Future<Map<String, String>> subjectGoalAverages(
{required String userId}) async {
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? goalAvgsJson = userData.elementAt(0)["goal_averages"] as String?;
if (goalAvgsJson == null) return {};
return (jsonDecode(goalAvgsJson) as Map)
.map((key, value) => MapEntry(key.toString(), value.toString()));
}
Future<Map<String, String>> subjectGoalBefores(
{required String userId}) async {
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? goalBeforesJson = userData.elementAt(0)["goal_befores"] as String?;
if (goalBeforesJson == null) return {};
return (jsonDecode(goalBeforesJson) as Map)
.map((key, value) => MapEntry(key.toString(), value.toString()));
}
Future<Map<String, String>> subjectGoalPinDates(
{required String userId}) async {
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? goalPinDatesJson =
userData.elementAt(0)["goal_pin_dates"] as String?;
if (goalPinDatesJson == null) return {};
return (jsonDecode(goalPinDatesJson) as Map)
.map((key, value) => MapEntry(key.toString(), value.toString()));
}
}

View File

@ -27,9 +27,11 @@ class DatabaseStore {
}
Future<void> storeUser(User user) async {
List userRes = await db.query("users", where: "id = ?", whereArgs: [user.id]);
List userRes =
await db.query("users", where: "id = ?", whereArgs: [user.id]);
if (userRes.isNotEmpty) {
await db.update("users", user.toMap(), where: "id = ?", whereArgs: [user.id]);
await db
.update("users", user.toMap(), where: "id = ?", whereArgs: [user.id]);
} else {
await db.insert("users", user.toMap());
await db.insert("user_data", {"id": user.id});
@ -49,64 +51,123 @@ class UserDatabaseStore {
Future<void> storeGrades(List<Grade> grades, {required String userId}) async {
String gradesJson = jsonEncode(grades.map((e) => e.json).toList());
await db.update("user_data", {"grades": gradesJson}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"grades": gradesJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeLessons(Map<Week, List<Lesson>?> lessons, {required String userId}) async {
Future<void> storeLessons(Map<Week, List<Lesson>?> lessons,
{required String userId}) async {
final map = lessons.map<String, List<Map<String, Object?>>>(
(k, v) => MapEntry(k.id.toString(), v!.where((e) => e.json != null).map((e) => e.json!).toList().cast()),
(k, v) => MapEntry(k.id.toString(),
v!.where((e) => e.json != null).map((e) => e.json!).toList().cast()),
);
String lessonsJson = jsonEncode(map);
await db.update("user_data", {"timetable": lessonsJson}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"timetable": lessonsJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeExams(List<Exam> exams, {required String userId}) async {
String examsJson = jsonEncode(exams.map((e) => e.json).toList());
await db.update("user_data", {"exams": examsJson}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"exams": examsJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeHomework(List<Homework> homework, {required String userId}) async {
Future<void> storeHomework(List<Homework> homework,
{required String userId}) async {
String homeworkJson = jsonEncode(homework.map((e) => e.json).toList());
await db.update("user_data", {"homework": homeworkJson}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"homework": homeworkJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeMessages(List<Message> messages, {required String userId}) async {
Future<void> storeMessages(List<Message> messages,
{required String userId}) async {
String messagesJson = jsonEncode(messages.map((e) => e.json).toList());
await db.update("user_data", {"messages": messagesJson}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"messages": messagesJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeNotes(List<Note> notes, {required String userId}) async {
String notesJson = jsonEncode(notes.map((e) => e.json).toList());
await db.update("user_data", {"notes": notesJson}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"notes": notesJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeEvents(List<Event> events, {required String userId}) async {
String eventsJson = jsonEncode(events.map((e) => e.json).toList());
await db.update("user_data", {"events": eventsJson}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"events": eventsJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeAbsences(List<Absence> absences, {required String userId}) async {
Future<void> storeAbsences(List<Absence> absences,
{required String userId}) async {
String absencesJson = jsonEncode(absences.map((e) => e.json).toList());
await db.update("user_data", {"absences": absencesJson}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"absences": absencesJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeGroupAverages(List<GroupAverage> groupAverages, {required String userId}) async {
String groupAveragesJson = jsonEncode(groupAverages.map((e) => e.json).toList());
await db.update("user_data", {"group_averages": groupAveragesJson}, where: "id = ?", whereArgs: [userId]);
Future<void> storeGroupAverages(List<GroupAverage> groupAverages,
{required String userId}) async {
String groupAveragesJson =
jsonEncode(groupAverages.map((e) => e.json).toList());
await db.update("user_data", {"group_averages": groupAveragesJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeSubjectLessonCount(SubjectLessonCount lessonCount, {required String userId}) async {
Future<void> storeSubjectLessonCount(SubjectLessonCount lessonCount,
{required String userId}) async {
String lessonCountJson = jsonEncode(lessonCount.toMap());
await db.update("user_data", {"subject_lesson_count": lessonCountJson}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"subject_lesson_count": lessonCountJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeLastSeenGrade(DateTime date, {required String userId}) async {
Future<void> storeLastSeenGrade(DateTime date,
{required String userId}) async {
int lastSeenDate = date.millisecondsSinceEpoch;
await db.update("user_data", {"last_seen_grade": lastSeenDate}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"last_seen_grade": lastSeenDate},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeRenamedSubjects(Map<String, String> subjects, {required String userId}) async {
// renamed things
Future<void> storeRenamedSubjects(Map<String, String> subjects,
{required String userId}) async {
String renamedSubjectsJson = jsonEncode(subjects);
await db.update("user_data", {"renamed_subjects": renamedSubjectsJson}, where: "id = ?", whereArgs: [userId]);
await db.update("user_data", {"renamed_subjects": renamedSubjectsJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeRenamedTeachers(Map<String, String> teachers,
{required String userId}) async {
String renamedTeachersJson = jsonEncode(teachers);
await db.update("user_data", {"renamed_teachers": renamedTeachersJson},
where: "id = ?", whereArgs: [userId]);
}
// goal planner
Future<void> storeSubjectGoalPlans(Map<String, String> plans,
{required String userId}) async {
String goalPlansJson = jsonEncode(plans);
await db.update("user_data", {"goal_plans": goalPlansJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeSubjectGoalAverages(Map<String, String> avgs,
{required String userId}) async {
String goalAvgsJson = jsonEncode(avgs);
await db.update("user_data", {"goal_averages": goalAvgsJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeSubjectGoalBefores(Map<String, String> befores,
{required String userId}) async {
String goalBeforesJson = jsonEncode(befores);
await db.update("user_data", {"goal_befores": goalBeforesJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeSubjectGoalPinDates(Map<String, String> dates,
{required String userId}) async {
String goalPinDatesJson = jsonEncode(dates);
await db.update("user_data", {"goal_pin_dates": goalPinDatesJson},
where: "id = ?", whereArgs: [userId]);
}
}

View File

@ -1,34 +1,88 @@
import 'dart:math';
import 'dart:ui';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/helpers/notification_helper.i18n.dart';
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
hide Message;
import 'package:i18n_extension/i18n_widget.dart';
import 'package:intl/intl.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
class NotificationsHelper {
late DatabaseProvider database;
late SettingsProvider settingsProvider;
late UserProvider userProvider;
late KretaClient kretaClient;
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
List<T> combineLists<T, K>(
List<T> list1,
List<T> list2,
K Function(T) keyExtractor,
) {
Set<K> uniqueKeys = <K>{};
List<T> combinedList = [];
for (T item in list1) {
K key = keyExtractor(item);
if (!uniqueKeys.contains(key)) {
uniqueKeys.add(key);
combinedList.add(item);
}
}
for (T item in list2) {
K key = keyExtractor(item);
if (!uniqueKeys.contains(key)) {
uniqueKeys.add(key);
combinedList.add(item);
}
}
return combinedList;
}
String dayTitle(DateTime date) {
try {
return DateFormat("EEEE", I18n.locale.languageCode).format(date);
} catch (e) {
return "Unknown";
}
}
@pragma('vm:entry-point')
void backgroundJob() async {
// initialize providers
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
DatabaseProvider database = DatabaseProvider();
database = DatabaseProvider();
await database.init();
SettingsProvider settingsProvider =
await database.query.getSettings(database);
UserProvider userProvider = await database.query.getUsers(settingsProvider);
settingsProvider = await database.query.getSettings(database);
userProvider = await database.query.getUsers(settingsProvider);
if (userProvider.id != null && settingsProvider.notificationsEnabled) {
// refresh grades
// refresh kreta login
final status = StatusProvider();
final kretaClient = KretaClient(
kretaClient = KretaClient(
user: userProvider, settings: settingsProvider, status: status);
kretaClient.refreshLogin();
if (settingsProvider.notificationsGradesEnabled) gradeNotification();
if (settingsProvider.notificationsAbsencesEnabled) absenceNotification();
if (settingsProvider.notificationsMessagesEnabled) messageNotification();
if (settingsProvider.notificationsLessonsEnabled) lessonNotification();
}
}
void gradeNotification() async {
// fetch grades
GradeProvider gradeProvider = GradeProvider(
settings: settingsProvider,
user: userProvider,
@ -42,35 +96,506 @@ class NotificationsHelper {
// loop through grades and see which hasn't been seen yet
for (Grade grade in grades) {
// if grade is not a normal grade (1-5), don't show it
if ([1, 2, 3, 4, 5].contains(grade.value.value)) {
// if the grade was added over a week ago, don't show it to avoid notification spam
if (grade.seenDate.isAfter(lastSeenGrade) &&
grade.date.difference(DateTime.now()).inDays * -1 < 7) {
// send notificiation about new grade
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('GRADES', 'Jegyek',
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'GRADES',
'Jegyek',
channelDescription: 'Értesítés jegyek beírásakor',
importance: Importance.max,
priority: Priority.max,
color: Color(0xFF3D7BF4),
ticker: 'Jegyek');
const NotificationDetails notificationDetails =
color: settingsProvider.customAccentColor,
ticker: 'Jegyek',
);
NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails);
if (userProvider.getUsers().length == 1) {
await flutterLocalNotificationsPlugin.show(
// probably shouldn't use a random int
Random().nextInt(432234 * 2),
"title".i18n,
"body".i18n.fill([
grade.id.hashCode,
"title_grade".i18n,
"body_grade".i18n.fill(
[
grade.value.value.toString(),
grade.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled
? grade.subject.renamedTo!
: grade.subject.name
]),
notificationDetails);
],
),
notificationDetails,
);
} else {
// multiple users are added, also display student name
await flutterLocalNotificationsPlugin.show(
grade.id.hashCode,
"title_grade".i18n,
"body_grade_multiuser".i18n.fill(
[
userProvider.displayName!,
grade.value.value.toString(),
grade.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled
? grade.subject.renamedTo!
: grade.subject.name
],
),
notificationDetails,
);
}
}
}
}
// set grade seen status
gradeProvider.seenAll();
}
void absenceNotification() async {
// get absences from api
List? absenceJson = await kretaClient
.getAPI(KretaAPI.absences(userProvider.instituteCode ?? ""));
List<Absence> storedAbsences =
await database.userQuery.getAbsences(userId: userProvider.id!);
if (absenceJson == null) {
return;
}
// format api absences to correct format while preserving isSeen value
List<Absence> absences = absenceJson.map((e) {
Absence apiAbsence = Absence.fromJson(e);
Absence storedAbsence = storedAbsences.firstWhere(
(stored) => stored.id == apiAbsence.id,
orElse: () => apiAbsence);
apiAbsence.isSeen = storedAbsence.isSeen;
return apiAbsence;
}).toList();
List<Absence> modifiedAbsences = [];
if (absences != storedAbsences) {
// remove absences that are not new
absences.removeWhere((element) => storedAbsences.contains(element));
for (Absence absence in absences) {
if (!absence.isSeen) {
absence.isSeen = true;
modifiedAbsences.add(absence);
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'ABSENCES',
'Hiányzások',
channelDescription: 'Értesítés hiányzások beírásakor',
importance: Importance.max,
priority: Priority.max,
color: settingsProvider.customAccentColor,
ticker: 'Hiányzások',
);
NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails);
if (userProvider.getUsers().length == 1) {
await flutterLocalNotificationsPlugin.show(
absence.id.hashCode,
"title_absence".i18n,
"body_absence".i18n.fill(
[
DateFormat("yyyy-MM-dd").format(absence.date),
absence.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled
? absence.subject.renamedTo!
: absence.subject.name
],
),
notificationDetails,
);
} else {
await flutterLocalNotificationsPlugin.show(
absence.id.hashCode,
"title_absence".i18n,
"body_absence_multiuser".i18n.fill(
[
userProvider.displayName!,
DateFormat("yyyy-MM-dd").format(absence.date),
absence.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled
? absence.subject.renamedTo!
: absence.subject.name
],
),
notificationDetails,
);
}
}
}
}
// combine modified absences and storedabsences list and save them to the database
List<Absence> combinedAbsences = combineLists(
modifiedAbsences,
storedAbsences,
(Absence absence) => absence.id,
);
await database.userStore
.storeAbsences(combinedAbsences, userId: userProvider.id!);
}
void messageNotification() async {
// get messages from api
List? messageJson =
await kretaClient.getAPI(KretaAPI.messages("beerkezett"));
List<Message> storedmessages =
await database.userQuery.getMessages(userId: userProvider.id!);
if (messageJson == null) {
return;
}
// format api messages to correct format while preserving isSeen value
// Parse messages
List<Message> messages = [];
await Future.wait(List.generate(messageJson.length, (index) {
return () async {
Map message = messageJson.cast<Map>()[index];
Map? innerMessageJson = await kretaClient
.getAPI(KretaAPI.message(message["azonosito"].toString()));
if (innerMessageJson != null) {
messages.add(
Message.fromJson(innerMessageJson, forceType: MessageType.inbox));
}
}();
}));
for (Message message in messages) {
for (Message storedMessage in storedmessages) {
if (message.id == storedMessage.id) {
message.isSeen = storedMessage.isSeen;
}
}
}
List<Message> modifiedmessages = [];
if (messages != storedmessages) {
// remove messages that are not new
messages.removeWhere((element) => storedmessages.contains(element));
for (Message message in messages) {
if (!message.isSeen) {
message.isSeen = true;
modifiedmessages.add(message);
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'MESSAGES',
'Üzenetek',
channelDescription: 'Értesítés kapott üzenetekkor',
importance: Importance.max,
priority: Priority.max,
color: settingsProvider.customAccentColor,
ticker: 'Üzenetek',
);
NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails);
if (userProvider.getUsers().length == 1) {
await flutterLocalNotificationsPlugin.show(
message.id.hashCode,
message.author,
message.content.replaceAll(RegExp(r'<[^>]*>'), ''),
notificationDetails,
);
} else {
await flutterLocalNotificationsPlugin.show(
message.id.hashCode,
"(${userProvider.displayName!}) ${message.author}",
message.content.replaceAll(RegExp(r'<[^>]*>'), ''),
notificationDetails,
);
}
}
}
}
// combine modified messages and storedmessages list and save them to the database
List<Message> combinedmessages = combineLists(
modifiedmessages,
storedmessages,
(Message message) => message.id,
);
await database.userStore
.storeMessages(combinedmessages, userId: userProvider.id!);
}
void lessonNotification() async {
// get lesson from api
TimetableProvider timetableProvider = TimetableProvider(
user: userProvider, database: database, kreta: kretaClient);
List<Lesson> storedlessons =
timetableProvider.lessons[Week.current()] ?? [];
List? apilessons = timetableProvider.getWeek(Week.current()) ?? [];
for (Lesson lesson in apilessons) {
for (Lesson storedLesson in storedlessons) {
if (lesson.id == storedLesson.id) {
lesson.isSeen = storedLesson.isSeen;
}
}
}
List<Lesson> modifiedlessons = [];
if (apilessons != storedlessons) {
// remove lessons that are not new
apilessons.removeWhere((element) => storedlessons.contains(element));
for (Lesson lesson in apilessons) {
if (!lesson.isSeen && lesson.isChanged) {
lesson.isSeen = true;
modifiedlessons.add(lesson);
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'LESSONS',
'Órák',
channelDescription:
'Értesítés órák elmaradásáról, helyettesítésről',
importance: Importance.max,
priority: Priority.max,
color: settingsProvider.customAccentColor,
ticker: 'Órák',
);
NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails);
if (userProvider.getUsers().length == 1) {
if (lesson.status?.name == "Elmaradt") {
switch (I18n.localeStr) {
case "en_en":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill(
[
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date)
],
),
notificationDetails,
);
break;
}
case "hu_hu":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill(
[
dayTitle(lesson.date),
lesson.lessonIndex,
lesson.name
],
),
notificationDetails,
);
break;
}
default:
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill(
[
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date)
],
),
notificationDetails,
);
break;
}
}
} else if (lesson.substituteTeacher?.name != "") {
switch (I18n.localeStr) {
case "en_en":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill(
[
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date),
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
],
),
notificationDetails,
);
break;
}
case "hu_hu":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill(
[
dayTitle(lesson.date),
lesson.lessonIndex,
lesson.name,
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
],
),
notificationDetails,
);
break;
}
default:
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill(
[
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date),
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
],
),
notificationDetails,
);
break;
}
}
}
} else {
if (lesson.status?.name == "Elmaradt") {
switch (I18n.localeStr) {
case "en_en":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill(
[
userProvider.displayName!,
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date)
],
),
notificationDetails,
);
break;
}
case "hu_hu":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill(
[
userProvider.displayName!,
dayTitle(lesson.date),
lesson.lessonIndex,
lesson.name
],
),
notificationDetails,
);
break;
}
default:
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill(
[
userProvider.displayName!,
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date)
],
),
notificationDetails,
);
break;
}
}
} else if (lesson.substituteTeacher?.name != "") {
switch (I18n.localeStr) {
case "en_en":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill(
[
userProvider.displayName!,
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date),
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
],
),
notificationDetails,
);
break;
}
case "hu_hu":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill(
[
userProvider.displayName!,
dayTitle(lesson.date),
lesson.lessonIndex,
lesson.name,
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
],
),
notificationDetails,
);
break;
}
default:
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill(
[
userProvider.displayName!,
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date),
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
],
),
notificationDetails,
);
break;
}
}
}
}
}
}
// combine modified lesson and storedlesson list and save them to the database
List<Lesson> combinedlessons = combineLists(
modifiedlessons,
storedlessons,
(Lesson message) => message.id,
);
Map<Week, List<Lesson>> timetableLessons = timetableProvider.lessons;
timetableLessons[Week.current()] = combinedlessons;
await database.userStore
.storeLessons(timetableLessons, userId: userProvider.id!);
}
}
}

View File

@ -4,16 +4,43 @@ extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"title": "New grade",
"body": "You got a %s in %s"
"title_grade": "New grade",
"body_grade": "You got a %s in %s",
"body_grade_multiuser": "%s got a %s in %s",
"title_absence": "Absence recorded",
"body_absence": "An absence was recorded on %s for %s",
"body_absence_multiuser": "An absence was recorded for %s on %s for the subject %s",
"title_lesson": "Timetable modified",
"body_lesson_canceled": "Lesson #%s (%s) has been canceled on %s",
"body_lesson_canceled_multiuser": "(%s) Lesson #%s (%s) has been canceled on %s",
"body_lesson_substituted": "Lesson #%s (%s) on %s will be substituted by %s",
"body_lesson_substituted_multiuser": "(%s) Lesson #%s (%s) on %s will be substituted by %s"
},
"hu_hu": {
"title": "Új jegy",
"body": "%s-st kaptál %s tantárgyból"
"title_grade": "Új jegy",
"body_grade": "%s-st kaptál %s tantárgyból",
"body_grade_multiuser": "%s tanuló %s-st kapott %s tantárgyból",
"title_absence": "Új hiányzás",
"body_absence": "Új hiányzást kaptál %s napon %s tantárgyból",
"body_absence_multiuser": "%s tanuló új hiányzást kapott %s napon %s tantárgyból",
"title_lesson": "Órarend szerkesztve",
"body_lesson_canceled": "%s-i %s. óra (%s) elmarad",
"body_lesson_canceled_multiuser": "(%s) %s-i %s. óra (%s) elmarad",
"body_lesson_substituted": "%s-i %s. (%s) órát %s helyettesíti",
"body_lesson_substituted_multiuser": "(%s) %s-i %s. (%s) órát %s helyettesíti"
},
"de_de": {
"title": "Neue Note",
"body": "Du hast eine %s in %s"
"title_grade": "Neue Note",
"body_grade": "Du hast eine %s in %s",
"body_grade_multiuser": "%s hast eine %s in %s",
"title_absence": "Abwesenheit aufgezeichnet",
"body_absence": "Auf %s für %s wurde eine Abwesenheit aufgezeichnet",
"body_absence_multiuser": "Für %s wurde am %s für das Thema Mathematik eine Abwesenheit aufgezeichnet",
"title_lesson": "Fahrplan geändert",
"body_lesson_canceled": "Lektion Nr. %s (%s) wurde am %s abgesagt",
"body_lesson_canceled_multiuser": "(%s) Lektion Nr. %s (%s) wurde am %s abgesagt",
"body_lesson_substituted": "Lektion Nr. %s (%s) wird am %s durch %s ersetzt",
"body_lesson_substituted_multiuser": "(%s) Lektion Nr. %s (%s) wird am %s durch %s ersetzt"
},
};

View File

@ -1,30 +1,36 @@
class News {
String id;
String title;
String content;
String link;
String openLabel;
String platform;
bool emergency;
DateTime expireDate;
Map? json;
News({
required this.id,
required this.title,
required this.content,
required this.link,
required this.openLabel,
required this.platform,
required this.emergency,
required this.expireDate,
this.json,
});
factory News.fromJson(Map json) {
return News(
id: json["id"] ?? "",
title: json["title"] ?? "",
content: json["content"] ?? "",
link: json["link"] ?? "",
openLabel: json["open_label"] ?? "",
platform: json["platform"] ?? "",
emergency: json["emergency"] ?? false,
expireDate: DateTime.parse(json["expire_date"] ?? ''),
json: json,
);
}

View File

@ -29,8 +29,12 @@ class SettingsProvider extends ChangeNotifier {
// zero is one, ...
List<Color> _gradeColors;
bool _newsEnabled;
int _newsState;
String _seenNews;
bool _notificationsEnabled;
bool _notificationsGradesEnabled;
bool _notificationsAbsencesEnabled;
bool _notificationsMessagesEnabled;
bool _notificationsLessonsEnabled;
/*
notificationsBitfield values:
@ -69,6 +73,9 @@ class SettingsProvider extends ChangeNotifier {
String _lastAccountId;
bool _renamedSubjectsEnabled;
bool _renamedSubjectsItalics;
bool _renamedTeachersEnabled;
bool _renamedTeachersItalics;
Color _liveActivityColor;
SettingsProvider({
DatabaseProvider? database,
@ -79,8 +86,12 @@ class SettingsProvider extends ChangeNotifier {
required AccentColor accentColor,
required List<Color> gradeColors,
required bool newsEnabled,
required int newsState,
required String seenNews,
required bool notificationsEnabled,
required bool notificationsGradesEnabled,
required bool notificationsAbsencesEnabled,
required bool notificationsMessagesEnabled,
required bool notificationsLessonsEnabled,
required int notificationsBitfield,
required bool developerMode,
required int notificationPollInterval,
@ -106,6 +117,9 @@ class SettingsProvider extends ChangeNotifier {
required String lastAccountId,
required bool renameSubjectsEnabled,
required bool renameSubjectsItalics,
required bool renameTeachersEnabled,
required bool renameTeachersItalics,
required Color liveActivityColor,
}) : _database = database,
_language = language,
_startPage = startPage,
@ -114,8 +128,12 @@ class SettingsProvider extends ChangeNotifier {
_accentColor = accentColor,
_gradeColors = gradeColors,
_newsEnabled = newsEnabled,
_newsState = newsState,
_seenNews = seenNews,
_notificationsEnabled = notificationsEnabled,
_notificationsGradesEnabled = notificationsGradesEnabled,
_notificationsAbsencesEnabled = notificationsAbsencesEnabled,
_notificationsMessagesEnabled = notificationsMessagesEnabled,
_notificationsLessonsEnabled = notificationsLessonsEnabled,
_notificationsBitfield = notificationsBitfield,
_developerMode = developerMode,
_notificationPollInterval = notificationPollInterval,
@ -140,7 +158,10 @@ class SettingsProvider extends ChangeNotifier {
_premiumLogin = premiumLogin,
_lastAccountId = lastAccountId,
_renamedSubjectsEnabled = renameSubjectsEnabled,
_renamedSubjectsItalics = renameSubjectsItalics;
_renamedSubjectsItalics = renameSubjectsItalics,
_renamedTeachersEnabled = renameTeachersEnabled,
_renamedTeachersItalics = renameTeachersItalics,
_liveActivityColor = liveActivityColor;
factory SettingsProvider.fromMap(Map map,
{required DatabaseProvider database}) {
@ -167,8 +188,12 @@ class SettingsProvider extends ChangeNotifier {
Color(map["grade_color5"]),
],
newsEnabled: map["news"] == 1,
newsState: map["news_state"],
seenNews: map["seen_news"],
notificationsEnabled: map["notifications"] == 1,
notificationsGradesEnabled: map["notifications_grades"] == 1,
notificationsAbsencesEnabled: map["notifications_absences"] == 1,
notificationsMessagesEnabled: map["notifications_messages"] == 1,
notificationsLessonsEnabled: map["notifications_lessons"] == 1,
notificationsBitfield: map["notifications_bitfield"],
notificationPollInterval: map["notification_poll_interval"],
developerMode: map["developer_mode"] == 1,
@ -195,6 +220,9 @@ class SettingsProvider extends ChangeNotifier {
lastAccountId: map["last_account_id"],
renameSubjectsEnabled: map["renamed_subjects_enabled"] == 1,
renameSubjectsItalics: map["renamed_subjects_italics"] == 1,
renameTeachersEnabled: map["renamed_teachers_enabled"] == 1,
renameTeachersItalics: map["renamed_teachers_italics"] == 1,
liveActivityColor: Color(map["live_activity_color"]),
);
}
@ -206,8 +234,12 @@ class SettingsProvider extends ChangeNotifier {
"theme": _theme.index,
"accent_color": _accentColor.index,
"news": _newsEnabled ? 1 : 0,
"news_state": _newsState,
"seen_news": _seenNews,
"notifications": _notificationsEnabled ? 1 : 0,
"notifications_grades": _notificationsGradesEnabled ? 1 : 0,
"notifications_absences": _notificationsAbsencesEnabled ? 1 : 0,
"notifications_messages": _notificationsMessagesEnabled ? 1 : 0,
"notifications_lessons": _notificationsLessonsEnabled ? 1 : 0,
"notifications_bitfield": _notificationsBitfield,
"developer_mode": _developerMode ? 1 : 0,
"grade_color1": _gradeColors[0].value,
@ -236,7 +268,10 @@ class SettingsProvider extends ChangeNotifier {
"premium_login": _premiumLogin,
"last_account_id": _lastAccountId,
"renamed_subjects_enabled": _renamedSubjectsEnabled ? 1 : 0,
"renamed_subjects_italics": _renamedSubjectsItalics ? 1 : 0
"renamed_subjects_italics": _renamedSubjectsItalics ? 1 : 0,
"renamed_teachers_enabled": _renamedTeachersEnabled ? 1 : 0,
"renamed_teachers_italics": _renamedTeachersItalics ? 1 : 0,
"live_activity_color": _liveActivityColor.value,
};
}
@ -256,8 +291,12 @@ class SettingsProvider extends ChangeNotifier {
DarkMobileAppColors().gradeFive,
],
newsEnabled: true,
newsState: -1,
seenNews: '',
notificationsEnabled: true,
notificationsGradesEnabled: true,
notificationsAbsencesEnabled: true,
notificationsMessagesEnabled: true,
notificationsLessonsEnabled: true,
notificationsBitfield: 255,
developerMode: false,
notificationPollInterval: 1,
@ -283,6 +322,9 @@ class SettingsProvider extends ChangeNotifier {
lastAccountId: "",
renameSubjectsEnabled: false,
renameSubjectsItalics: false,
renameTeachersEnabled: false,
renameTeachersItalics: false,
liveActivityColor: const Color(0xFF676767),
);
}
@ -294,8 +336,12 @@ class SettingsProvider extends ChangeNotifier {
AccentColor get accentColor => _accentColor;
List<Color> get gradeColors => _gradeColors;
bool get newsEnabled => _newsEnabled;
int get newsState => _newsState;
List<String> get seenNews => _seenNews.split(',');
bool get notificationsEnabled => _notificationsEnabled;
bool get notificationsGradesEnabled => _notificationsGradesEnabled;
bool get notificationsAbsencesEnabled => _notificationsAbsencesEnabled;
bool get notificationsMessagesEnabled => _notificationsMessagesEnabled;
bool get notificationsLessonsEnabled => _notificationsLessonsEnabled;
int get notificationsBitfield => _notificationsBitfield;
bool get developerMode => _developerMode;
int get notificationPollInterval => _notificationPollInterval;
@ -324,6 +370,9 @@ class SettingsProvider extends ChangeNotifier {
String get lastAccountId => _lastAccountId;
bool get renamedSubjectsEnabled => _renamedSubjectsEnabled;
bool get renamedSubjectsItalics => _renamedSubjectsItalics;
bool get renamedTeachersEnabled => _renamedTeachersEnabled;
bool get renamedTeachersItalics => _renamedTeachersItalics;
Color get liveActivityColor => _liveActivityColor;
Future<void> update({
bool store = true,
@ -334,8 +383,12 @@ class SettingsProvider extends ChangeNotifier {
AccentColor? accentColor,
List<Color>? gradeColors,
bool? newsEnabled,
int? newsState,
String? seenNewsId,
bool? notificationsEnabled,
bool? notificationsGradesEnabled,
bool? notificationsAbsencesEnabled,
bool? notificationsMessagesEnabled,
bool? notificationsLessonsEnabled,
int? notificationsBitfield,
bool? developerMode,
int? notificationPollInterval,
@ -361,6 +414,9 @@ class SettingsProvider extends ChangeNotifier {
String? lastAccountId,
bool? renamedSubjectsEnabled,
bool? renamedSubjectsItalics,
bool? renamedTeachersEnabled,
bool? renamedTeachersItalics,
Color? liveActivityColor,
}) async {
if (language != null && language != _language) _language = language;
if (startPage != null && startPage != _startPage) _startPage = startPage;
@ -375,11 +431,31 @@ class SettingsProvider extends ChangeNotifier {
if (newsEnabled != null && newsEnabled != _newsEnabled) {
_newsEnabled = newsEnabled;
}
if (newsState != null && newsState != _newsState) _newsState = newsState;
if (seenNewsId != null && !_seenNews.split(',').contains(seenNewsId)) {
var tempList = _seenNews.split(',');
tempList.add(seenNewsId);
_seenNews = tempList.join(',');
}
if (notificationsEnabled != null &&
notificationsEnabled != _notificationsEnabled) {
_notificationsEnabled = notificationsEnabled;
}
if (notificationsGradesEnabled != null &&
notificationsGradesEnabled != _notificationsGradesEnabled) {
_notificationsGradesEnabled = notificationsGradesEnabled;
}
if (notificationsAbsencesEnabled != null &&
notificationsAbsencesEnabled != _notificationsAbsencesEnabled) {
_notificationsAbsencesEnabled = notificationsAbsencesEnabled;
}
if (notificationsMessagesEnabled != null &&
notificationsMessagesEnabled != _notificationsMessagesEnabled) {
_notificationsMessagesEnabled = notificationsMessagesEnabled;
}
if (notificationsLessonsEnabled != null &&
notificationsLessonsEnabled != _notificationsLessonsEnabled) {
_notificationsLessonsEnabled = notificationsLessonsEnabled;
}
if (notificationsBitfield != null &&
notificationsBitfield != _notificationsBitfield) {
_notificationsBitfield = notificationsBitfield;
@ -448,6 +524,17 @@ class SettingsProvider extends ChangeNotifier {
renamedSubjectsItalics != _renamedSubjectsItalics) {
_renamedSubjectsItalics = renamedSubjectsItalics;
}
if (renamedTeachersEnabled != null &&
renamedTeachersEnabled != _renamedTeachersEnabled) {
_renamedTeachersEnabled = renamedTeachersEnabled;
}
if (renamedTeachersItalics != null &&
renamedTeachersItalics != _renamedTeachersItalics) {
_renamedTeachersItalics = renamedTeachersItalics;
}
if (liveActivityColor != null && liveActivityColor != _liveActivityColor) {
_liveActivityColor = liveActivityColor;
}
if (store) await _database?.store.storeSettings(this);
notifyListeners();
}

View File

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

View File

@ -89,7 +89,9 @@ Future<List<DateWidget>> getFilterWidgets(FilterType activeData,
// Grades
case FilterType.grades:
if(!settingsProvider.gradeOpeningFun) {
gradeProvider.seenAll();
}
items = grade_filter.getWidgets(
gradeProvider.grades, gradeProvider.lastSeenDate);
if (settingsProvider.gradeOpeningFun) {

View File

@ -192,6 +192,7 @@ class GradeValueWidget extends StatelessWidget {
this.outline = false,
this.complemented = false,
this.nocolor = false,
this.color,
}) : super(key: key);
final GradeValue value;
@ -202,6 +203,7 @@ class GradeValueWidget extends StatelessWidget {
final bool outline;
final bool complemented;
final bool nocolor;
final Color? color;
@override
Widget build(BuildContext context) {
@ -209,7 +211,7 @@ class GradeValueWidget extends StatelessWidget {
bool isSubjectView = SubjectGradesContainer.of(context) != null;
Color color =
gradeColor(context: context, value: value.value, nocolor: nocolor);
this.color ?? gradeColor(context: context, value: value.value, nocolor: nocolor);
Widget valueText;
final percentage = value.percentage;

View File

@ -44,7 +44,7 @@ class LessonTile extends StatelessWidget {
fillLeading = true;
}
if (lesson.substituteTeacher != "") {
if (lesson.substituteTeacher?.name != "") {
fill = true;
accent = AppColors.of(context).yellow;
}
@ -113,7 +113,7 @@ class LessonTile extends StatelessWidget {
if (lesson.isChanged) {
if (lesson.status?.name == "Elmaradt") {
description = 'cancelled'.i18n;
} else if (lesson.substituteTeacher != "") {
} else if (lesson.substituteTeacher?.name != "") {
description = 'substitution'.i18n;
}
}
@ -161,8 +161,10 @@ class LessonTile extends StatelessWidget {
color: AppColors.of(context)
.text
.withOpacity(!lesson.isEmpty ? 1.0 : 0.5),
fontStyle:
lesson.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null),
fontStyle: lesson.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
),
subtitle: description != ""
? Text(

View File

@ -1,23 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
g_autoptr(FlPluginRegistrar) flutter_acrylic_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterAcrylicPlugin");
flutter_acrylic_plugin_register_with_registrar(flutter_acrylic_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -1,26 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
flutter_acrylic
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@ -1,28 +0,0 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import connectivity_plus
import dynamic_color
import flutter_local_notifications
import macos_window_utils
import package_info_plus
import path_provider_foundation
import share_plus
import sqflite
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@ -1,78 +0,0 @@
PODS:
- connectivity_plus (0.0.1):
- FlutterMacOS
- ReachabilitySwift
- dynamic_color (0.0.2):
- FlutterMacOS
- flutter_acrylic (0.1.0):
- FlutterMacOS
- flutter_local_notifications (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- ReachabilitySwift (5.0.0)
- share_plus_macos (0.0.1):
- FlutterMacOS
- sqflite (0.0.2):
- FlutterMacOS
- FMDB (>= 2.7.5)
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
- dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`)
- flutter_acrylic (from `Flutter/ephemeral/.symlinks/plugins/flutter_acrylic/macos`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
SPEC REPOS:
trunk:
- FMDB
- ReachabilitySwift
EXTERNAL SOURCES:
connectivity_plus:
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
dynamic_color:
:path: Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos
flutter_acrylic:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_acrylic/macos
flutter_local_notifications:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
FlutterMacOS:
:path: Flutter/ephemeral
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
share_plus_macos:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos
sqflite:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747
dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f
flutter_acrylic: c3df24ae52ab6597197837ce59ef2a8542640c17
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
COCOAPODS: 1.12.1

View File

@ -3,7 +3,7 @@ description: "Nem hivatalos e-napló alkalmazás az e-Kréta rendszerhez"
homepage: https://refilc.hu
publish_to: "none"
version: 4.1.1+216
version: 4.2.0+220
environment:
sdk: ">=2.17.0 <3.0.0"
@ -68,6 +68,9 @@ dependencies:
screenshot: ^2.1.0
flutter_staggered_grid_view: ^0.7.0
sqflite_common_ffi_web: ^0.4.0
image_crop:
git:
url: https://github.com/kimaah/image_crop.git
dev_dependencies:
flutter_lints: ^2.0.1
@ -83,6 +86,7 @@ flutter:
- assets/icons/ic_splash.png
- assets/animations/
- assets/images/
- assets/images/subject_covers/
fonts:
- family: FilcIcons

View File

@ -1,30 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
// // This is a basic Flutter widget test.
// //
// // To perform an interaction with a widget in your test, use the WidgetTester
// // utility in the flutter_test package. For example, you can send tap and scroll
// // gestures. You can also use WidgetTester to find child widgets in the widget
// // tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_test/flutter_test.dart';
import 'package:filcnaplo/main.dart';
// import 'package:filcnaplo/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// void main() {
// testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// // Build our app and trigger a frame.
// await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// // Verify that our counter starts at 0.
// expect(find.text('0'), findsOneWidget);
// expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// // Tap the '+' icon and trigger a frame.
// await tester.tap(find.byIcon(Icons.add));
// await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
// // Verify that our counter has incremented.
// expect(find.text('0'), findsNothing);
// expect(find.text('1'), findsOneWidget);
// });
// }

View File

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

View File

@ -12,6 +12,7 @@ import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_premium/providers/goal_provider.dart';
class NavigationScreen extends StatefulWidget {
const NavigationScreen({Key? key}) : super(key: key);
@ -29,6 +30,7 @@ class NavigationScreenState extends State<NavigationScreen>
late NavigationRoute selected;
late SettingsProvider settings;
late NewsProvider newsProvider;
late GoalProvider goalProvider;
double topInset = 0.0;
@override
@ -45,10 +47,14 @@ class NavigationScreenState extends State<NavigationScreen>
Provider.of<KretaClient>(context, listen: false).userAgent =
settings.config.userAgent;
// Get news
// get news
newsProvider = Provider.of<NewsProvider>(context, listen: false);
newsProvider.restore().then((value) => newsProvider.fetch());
// get goals
// goalProvider = Provider.of<GoalProvider>(context, listen: false);
// goalProvider.fetchDone();
// Initial sync
syncAll(context);
@ -98,13 +104,17 @@ class NavigationScreenState extends State<NavigationScreen>
Widget build(BuildContext context) {
settings = Provider.of<SettingsProvider>(context);
newsProvider = Provider.of<NewsProvider>(context);
goalProvider = Provider.of<GoalProvider>(context);
// Show news
// show news / complete goals
WidgetsBinding.instance.addPostFrameCallback((_) {
if (newsProvider.show) {
newsProvider.lock();
// NewsView.show(newsProvider.news[newsProvider.state], context: context).then((value) => newsProvider.release());
}
if (goalProvider.hasDoneGoals) {
// to-do
}
});
return Scaffold(

View File

@ -1,6 +1,4 @@
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo/theme/colors/accent.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';

View File

@ -66,8 +66,12 @@ class KretaClient {
for (int i = 0; i < 3; i++) {
if (autoHeader) {
if (!headerMap.containsKey("authorization") && accessToken != null) headerMap["authorization"] = "Bearer $accessToken";
if (!headerMap.containsKey("user-agent") && userAgent != null) headerMap["user-agent"] = "$userAgent";
if (!headerMap.containsKey("authorization") && accessToken != null) {
headerMap["authorization"] = "Bearer $accessToken";
}
if (!headerMap.containsKey("user-agent") && userAgent != null) {
headerMap["user-agent"] = "$userAgent";
}
}
res = await client.get(Uri.parse(url), headers: headerMap);
@ -94,7 +98,8 @@ class KretaClient {
return res.body;
}
} on http.ClientException catch (error) {
print("ERROR: KretaClient.getAPI ($url) ClientException: ${error.message}");
print(
"ERROR: KretaClient.getAPI ($url) ClientException: ${error.message}");
} catch (error) {
print("ERROR: KretaClient.getAPI ($url) ${error.runtimeType}: $error");
}
@ -120,9 +125,15 @@ class KretaClient {
for (int i = 0; i < 3; i++) {
if (autoHeader) {
if (!headerMap.containsKey("authorization") && accessToken != null) headerMap["authorization"] = "Bearer $accessToken";
if (!headerMap.containsKey("user-agent") && userAgent != null) headerMap["user-agent"] = "$userAgent";
if (!headerMap.containsKey("content-type")) headerMap["content-type"] = "application/json";
if (!headerMap.containsKey("authorization") && accessToken != null) {
headerMap["authorization"] = "Bearer $accessToken";
}
if (!headerMap.containsKey("user-agent") && userAgent != null) {
headerMap["user-agent"] = "$userAgent";
}
if (!headerMap.containsKey("content-type")) {
headerMap["content-type"] = "application/json";
}
}
res = await client.post(Uri.parse(url), headers: headerMap, body: body);
@ -142,9 +153,10 @@ class KretaClient {
return res.body;
}
} on http.ClientException catch (error) {
print("ERROR: KretaClient.getAPI ($url) ClientException: ${error.message}");
print(
"ERROR: KretaClient.postAPI ($url) ClientException: ${error.message}");
} catch (error) {
print("ERROR: KretaClient.getAPI ($url) ${error.runtimeType}: $error");
print("ERROR: KretaClient.postAPI ($url) ${error.runtimeType}: $error");
}
}
@ -157,7 +169,8 @@ class KretaClient {
};
String nonceStr = await getAPI(KretaAPI.nonce, json: false);
Nonce nonce = getNonce(nonceStr, loginUser.username, loginUser.instituteCode);
Nonce nonce =
getNonce(nonceStr, loginUser.username, loginUser.instituteCode);
headers.addAll(nonce.header());
if (_settings.presentationMode) {
@ -175,18 +188,28 @@ class KretaClient {
));
if (loginRes != null) {
if (loginRes.containsKey("access_token")) accessToken = loginRes["access_token"];
if (loginRes.containsKey("refresh_token")) refreshToken = loginRes["refresh_token"];
if (loginRes.containsKey("access_token")) {
accessToken = loginRes["access_token"];
}
if (loginRes.containsKey("refresh_token")) {
refreshToken = loginRes["refresh_token"];
}
// Update role
loginUser.role = JwtUtils.getRoleFromJWT(accessToken ?? "") ?? Role.student;
loginUser.role =
JwtUtils.getRoleFromJWT(accessToken ?? "") ?? Role.student;
}
if (refreshToken != null) {
Map? refreshRes = await postAPI(KretaAPI.login,
headers: headers, body: User.refreshBody(refreshToken: refreshToken!, instituteCode: loginUser.instituteCode));
headers: headers,
body: User.refreshBody(
refreshToken: refreshToken!,
instituteCode: loginUser.instituteCode));
if (refreshRes != null) {
if (refreshRes.containsKey("id_token")) idToken = refreshRes["id_token"];
if (refreshRes.containsKey("id_token")) {
idToken = refreshRes["id_token"];
}
}
}
}

View File

@ -24,7 +24,10 @@ class TimetableController extends ChangeNotifier {
current();
}
static int getWeekId(Week week) => (week.start.difference(getSchoolYearStart()).inDays / DateTime.daysPerWeek).ceil();
static int getWeekId(Week week) =>
(week.start.difference(getSchoolYearStart()).inDays /
DateTime.daysPerWeek)
.ceil();
static DateTime getSchoolYearStart() {
DateTime now = DateTime.now();
@ -48,8 +51,10 @@ class TimetableController extends ChangeNotifier {
}
// Jump shortcuts
Future<void> next(BuildContext context) => jump(Week.fromId(currentWeekId + 1), context: context);
Future<void> previous(BuildContext context) => jump(Week.fromId(currentWeekId - 1), context: context);
Future<void> next(BuildContext context) =>
jump(Week.fromId(currentWeekId + 1), context: context);
Future<void> previous(BuildContext context) =>
jump(Week.fromId(currentWeekId - 1), context: context);
void current() {
Week week = Week.current();
int id = getWeekId(week);
@ -60,7 +65,11 @@ class TimetableController extends ChangeNotifier {
_setWeek(Week.fromId(id));
}
Future<void> jump(Week week, {required BuildContext context, bool initial = false, bool skip = false, bool loader = true}) async {
Future<void> jump(Week week,
{required BuildContext context,
bool initial = false,
bool skip = false,
bool loader = true}) async {
if (_setWeek(week)) return;
loadType = LoadType.initial;
@ -81,7 +90,9 @@ class TimetableController extends ChangeNotifier {
notifyListeners();
try {
await _fetchWeek(week, context: context).timeout(const Duration(seconds: 5), onTimeout: (() => throw "timeout"));
await _fetchWeek(week, context: context).timeout(
const Duration(seconds: 5),
onTimeout: (() => throw "timeout"));
loadType = LoadType.online;
} catch (error, stack) {
print("ERROR: TimetableController.jump: $error\n$stack");
@ -95,7 +106,9 @@ class TimetableController extends ChangeNotifier {
if (week != currentWeek) return;
// Jump to next week on weekends
if (skip && (days?.length ?? 0) > 0 && days!.last.last.end.isBefore(DateTime.now())) return next(context);
if (skip &&
(days?.length ?? 0) > 0 &&
days!.last.last.end.isBefore(DateTime.now())) return next(context);
notifyListeners();
}
@ -106,7 +119,9 @@ class TimetableController extends ChangeNotifier {
if (id < 0) return true; // Min 1.
// Set week start to Sept. 1 of first week
if (!_differentDate(week.start, Week.fromId(0).start)) week.start = TimetableController.getSchoolYearStart();
if (!_differentDate(week.start, Week.fromId(0).start)) {
week.start = TimetableController.getSchoolYearStart();
}
currentWeek = week;
previousWeekId = currentWeekId;
@ -124,16 +139,17 @@ class TimetableController extends ChangeNotifier {
List<List<Lesson>> _sortDays(Week week, {required BuildContext context}) {
List<List<Lesson>> days = [];
try {
final timetableProvider = context.read<TimetableProvider>();
try {
List<Lesson> lessons = timetableProvider.getWeek(week) ?? [];
if (lessons.isNotEmpty) {
days.add([]);
lessons.sort((a, b) => a.date.compareTo(b.date));
for (var lesson in lessons) {
if (days.last.isNotEmpty && _differentDate(lesson.date, days.last.last.date)) days.add([]);
if (days.last.isNotEmpty &&
_differentDate(lesson.date, days.last.last.date)) days.add([]);
days.last.add(lesson);
}
@ -152,7 +168,8 @@ class TimetableController extends ChangeNotifier {
if (lessonIndexes.isNotEmpty) {
// Fill missing indexes with empty spaces
for (var i in List<int>.generate(maxIndex - minIndex + 1, (int i) => minIndex + i)) {
for (var i in List<int>.generate(
maxIndex - minIndex + 1, (int i) => minIndex + i)) {
List<Lesson> indexLessons = _getLessonsByIndex(_day, i);
// Empty lesson
@ -160,8 +177,13 @@ class TimetableController extends ChangeNotifier {
// Get start date by previous lesson
List<Lesson> prevLesson = _getLessonsByIndex(day, i - 1);
try {
DateTime? startDate = prevLesson.last.start.add(const Duration(seconds: 1));
indexLessons.add(Lesson.fromJson({'isEmpty': true, 'Oraszam': i, 'KezdetIdopont': startDate.toIso8601String()}));
DateTime startDate =
prevLesson.last.start.add(const Duration(seconds: 1));
indexLessons.add(Lesson.fromJson({
'isEmpty': true,
'Oraszam': i,
'KezdetIdopont': startDate.toIso8601String()
}));
// ignore: empty_catches
} catch (e) {}
}
@ -171,7 +193,8 @@ class TimetableController extends ChangeNotifier {
}
// Additional lessons
day.addAll(_day.where((l) => int.tryParse(l.lessonIndex) == null && l.subject.id != ''));
day.addAll(_day.where((l) =>
int.tryParse(l.lessonIndex) == null && l.subject.id != ''));
day.sort((a, b) => a.start.compareTo(b.start));
@ -213,5 +236,6 @@ class TimetableController extends ChangeNotifier {
return indexes;
}
bool _differentDate(DateTime a, DateTime b) => !(a.year == b.year && a.month == b.month && a.day == b.day);
bool _differentDate(DateTime a, DateTime b) =>
!(a.year == b.year && a.month == b.month && a.day == b.day);
}

View File

@ -1,5 +1,6 @@
import "category.dart";
import "subject.dart";
import "teacher.dart";
class Absence {
Map? json;
@ -7,7 +8,7 @@ class Absence {
DateTime date;
int delay;
DateTime submitDate;
String teacher;
Teacher teacher;
Justification state;
Category? justification;
Category? type;
@ -17,6 +18,14 @@ class Absence {
DateTime lessonEnd;
int? lessonIndex;
String group;
bool isSeen;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Absence && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
Absence({
required this.id,
@ -34,6 +43,7 @@ class Absence {
this.lessonIndex,
required this.group,
this.json,
this.isSeen = false,
});
factory Absence.fromJson(Map json) {
@ -41,8 +51,12 @@ class Absence {
DateTime lessonEnd;
int? lessonIndex;
if (json["Ora"] != null) {
lessonStart = json["Ora"]["KezdoDatum"] != null ? DateTime.parse(json["Ora"]["KezdoDatum"]).toLocal() : DateTime(0);
lessonEnd = json["Ora"]["VegDatum"] != null ? DateTime.parse(json["Ora"]["VegDatum"]).toLocal() : DateTime(0);
lessonStart = json["Ora"]["KezdoDatum"] != null
? DateTime.parse(json["Ora"]["KezdoDatum"]).toLocal()
: DateTime(0);
lessonEnd = json["Ora"]["VegDatum"] != null
? DateTime.parse(json["Ora"]["VegDatum"]).toLocal()
: DateTime(0);
lessonIndex = json["Ora"]["Oraszam"];
} else {
lessonStart = DateTime(0);
@ -51,23 +65,31 @@ class Absence {
return Absence(
id: json["Uid"],
date: json["Datum"] != null ? DateTime.parse(json["Datum"]).toLocal() : DateTime(0),
date: json["Datum"] != null
? DateTime.parse(json["Datum"]).toLocal()
: DateTime(0),
delay: json["KesesPercben"] ?? 0,
submitDate: json["KeszitesDatuma"] != null ? DateTime.parse(json["KeszitesDatuma"]).toLocal() : DateTime(0),
teacher: (json["RogzitoTanarNeve"] ?? "").trim(),
submitDate: json["KeszitesDatuma"] != null
? DateTime.parse(json["KeszitesDatuma"]).toLocal()
: DateTime(0),
teacher: Teacher.fromString((json["RogzitoTanarNeve"] ?? "").trim()),
state: json["IgazolasAllapota"] == "Igazolt"
? Justification.excused
: json["IgazolasAllapota"] == "Igazolando"
? Justification.pending
: Justification.unexcused,
justification: json["IgazolasTipusa"] != null ? Category.fromJson(json["IgazolasTipusa"]) : null,
justification: json["IgazolasTipusa"] != null
? Category.fromJson(json["IgazolasTipusa"])
: null,
type: json["Tipus"] != null ? Category.fromJson(json["Tipus"]) : null,
mode: json["Mod"] != null ? Category.fromJson(json["Mod"]) : null,
subject: Subject.fromJson(json["Tantargy"] ?? {}),
lessonStart: lessonStart,
lessonEnd: lessonEnd,
lessonIndex: lessonIndex,
group: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "",
group:
json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "",
isSeen: false,
json: json,
);
}

View File

@ -1,4 +1,5 @@
import 'category.dart';
import 'teacher.dart';
class Exam {
Map? json;
@ -7,7 +8,7 @@ class Exam {
Category? mode;
int? subjectIndex;
String subjectName;
String teacher;
Teacher teacher;
String description;
String group;
String id;
@ -28,14 +29,20 @@ class Exam {
factory Exam.fromJson(Map json) {
return Exam(
id: json["Uid"] ?? "",
date: json["BejelentesDatuma"] != null ? DateTime.parse(json["BejelentesDatuma"]).toLocal() : DateTime(0),
writeDate: json["Datum"] != null ? DateTime.parse(json["Datum"]).toLocal() : DateTime(0),
date: json["BejelentesDatuma"] != null
? DateTime.parse(json["BejelentesDatuma"]).toLocal()
: DateTime(0),
writeDate: json["Datum"] != null
? DateTime.parse(json["Datum"]).toLocal()
: DateTime(0),
mode: json["Modja"] != null ? Category.fromJson(json["Modja"]) : null,
subjectIndex: json["OrarendiOraOraszama"],
subjectName: json["TantargyNeve"] ?? "",
teacher: (json["RogzitoTanarNeve"] ?? "").trim(),
teacher: Teacher.fromString((json["RogzitoTanarNeve"] ?? "").trim()),
description: (json["Temaja"] ?? "").trim(),
group: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] ?? "" : "",
group: json["OsztalyCsoport"] != null
? json["OsztalyCsoport"]["Uid"] ?? ""
: "",
json: json,
);
}

View File

@ -1,13 +1,14 @@
import 'package:filcnaplo/utils/format.dart';
import 'category.dart';
import 'subject.dart';
import 'teacher.dart';
class Grade {
Map? json;
String id;
DateTime date;
GradeValue value;
String teacher;
Teacher teacher;
String description;
GradeType type;
String groupId;
@ -38,23 +39,35 @@ class Grade {
factory Grade.fromJson(Map json) {
return Grade(
id: json["Uid"] ?? "",
date: json["KeszitesDatuma"] != null ? DateTime.parse(json["KeszitesDatuma"]).toLocal() : DateTime(0),
date: json["KeszitesDatuma"] != null
? DateTime.parse(json["KeszitesDatuma"]).toLocal()
: DateTime(0),
value: GradeValue(
json["SzamErtek"] ?? 0,
json["SzovegesErtek"] ?? "",
json["SzovegesErtekelesRovidNev"] ?? "",
json["SulySzazalekErteke"] ?? 0,
percentage: json["ErtekFajta"] != null ? json["ErtekFajta"]["Uid"] == "3,Szazalekos" : false,
percentage: json["ErtekFajta"] != null
? json["ErtekFajta"]["Uid"] == "3,Szazalekos"
: false,
),
teacher: (json["ErtekeloTanarNeve"] ?? "").trim(),
teacher: Teacher.fromString((json["ErtekeloTanarNeve"] ?? "").trim()),
description: json["Tema"] ?? "",
type: json["Tipus"] != null ? Category.getGradeType(json["Tipus"]["Nev"]) : GradeType.unknown,
type: json["Tipus"] != null
? Category.getGradeType(json["Tipus"]["Nev"])
: GradeType.unknown,
groupId: (json["OsztalyCsoport"] ?? {})["Uid"] ?? "",
subject: Subject.fromJson(json["Tantargy"] ?? {}),
gradeType: json["ErtekFajta"] != null ? Category.fromJson(json["ErtekFajta"]) : null,
gradeType: json["ErtekFajta"] != null
? Category.fromJson(json["ErtekFajta"])
: null,
mode: Category.fromJson(json["Mod"] ?? {}),
writeDate: json["RogzitesDatuma"] != null ? DateTime.parse(json["RogzitesDatuma"]).toLocal() : DateTime(0),
seenDate: json["LattamozasDatuma"] != null ? DateTime.parse(json["LattamozasDatuma"]).toLocal() : DateTime(0),
writeDate: json["RogzitesDatuma"] != null
? DateTime.parse(json["RogzitesDatuma"]).toLocal()
: DateTime(0),
seenDate: json["LattamozasDatuma"] != null
? DateTime.parse(json["LattamozasDatuma"]).toLocal()
: DateTime(0),
form: (json["Jelleg"] ?? "Na") != "Na" ? json["Jelleg"] : "",
json: json,
);
@ -76,7 +89,8 @@ class GradeValue {
set value(int v) => _value = v;
int get value {
String _valueName = valueName.toLowerCase().specialChars();
if (_value == 0 && ["peldas", "jo", "valtozo", "rossz", "hanyag"].contains(_valueName)) {
if (_value == 0 &&
["peldas", "jo", "valtozo", "rossz", "hanyag"].contains(_valueName)) {
switch (_valueName) {
case "peldas":
return 5;
@ -87,7 +101,16 @@ class GradeValue {
case "rossz":
return 2;
case "hanyag":
return 1;
// other
case "jeles":
return 5;
case "kozepes":
return 3;
case "elegseges":
return 2;
case "elegtelen":
return 1;
}
}
return _value;
@ -101,7 +124,8 @@ class GradeValue {
set weight(int v) => _weight = v;
int get weight {
String _valueName = valueName.toLowerCase().specialChars();
if (_value == 0 && ["peldas", "jo", "valtozo", "rossz", "hanyag"].contains(_valueName)) {
if (_value == 0 &&
["peldas", "jo", "valtozo", "rossz", "hanyag"].contains(_valueName)) {
return 0;
}
return _weight;
@ -110,11 +134,23 @@ class GradeValue {
final bool _percentage;
bool get percentage => _percentage;
GradeValue(int value, String valueName, this.shortName, int weight, {bool percentage = false})
GradeValue(int value, String valueName, this.shortName, int weight,
{bool percentage = false})
: _value = value,
_valueName = valueName,
_weight = weight,
_percentage = percentage;
}
enum GradeType { midYear, firstQ, secondQ, halfYear, thirdQ, fourthQ, endYear, levelExam, ghost, unknown }
enum GradeType {
midYear,
firstQ,
secondQ,
halfYear,
thirdQ,
fourthQ,
endYear,
levelExam,
ghost,
unknown
}

View File

@ -1,6 +1,7 @@
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'subject.dart';
import 'teacher.dart';
class Homework {
Map? json;
@ -9,7 +10,7 @@ class Homework {
DateTime deadline;
bool byTeacher;
bool homeworkEnabled;
String teacher;
Teacher teacher;
String content;
Subject subject;
String group;
@ -45,7 +46,7 @@ class Homework {
: DateTime(0),
byTeacher: json["IsTanarRogzitette"] ?? true,
homeworkEnabled: json["IsTanuloHaziFeladatEnabled"] ?? false,
teacher: (json["RogzitoTanarNeve"] ?? "").trim(),
teacher: Teacher.fromString((json["RogzitoTanarNeve"] ?? "").trim()),
content: (json["Szoveg"] ?? "").trim(),
subject: Subject.fromJson(json["Tantargy"] ?? {}),
group: json["OsztalyCsoport"] != null

View File

@ -1,5 +1,6 @@
import 'subject.dart';
import 'category.dart';
import 'teacher.dart';
class Lesson {
Map? json;
@ -8,8 +9,8 @@ class Lesson {
Subject subject;
String lessonIndex;
int? lessonYearIndex;
String substituteTeacher;
String teacher;
Teacher? substituteTeacher;
Teacher teacher;
bool homeworkEnabled;
DateTime start;
DateTime end;
@ -24,6 +25,7 @@ class Lesson {
String name;
bool online;
bool isEmpty;
bool isSeen;
Lesson({
this.status,
@ -31,7 +33,7 @@ class Lesson {
required this.subject,
required this.lessonIndex,
this.lessonYearIndex,
this.substituteTeacher = "",
this.substituteTeacher,
required this.teacher,
this.homeworkEnabled = false,
required this.start,
@ -48,36 +50,56 @@ class Lesson {
this.online = false,
this.isEmpty = false,
this.json,
this.isSeen = false,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Lesson && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
factory Lesson.fromJson(Map json) {
return Lesson(
id: json["Uid"] ?? "",
status: json["Allapot"] != null ? Category.fromJson(json["Allapot"]) : null,
date: json["Datum"] != null ? DateTime.parse(json["Datum"]).toLocal() : DateTime(0),
status:
json["Allapot"] != null ? Category.fromJson(json["Allapot"]) : null,
date: json["Datum"] != null
? DateTime.parse(json["Datum"]).toLocal()
: DateTime(0),
subject: Subject.fromJson(json["Tantargy"] ?? {}),
lessonIndex: json["Oraszam"] != null ? json["Oraszam"].toString() : "+",
lessonYearIndex: json["OraEvesSorszama"],
substituteTeacher: (json["HelyettesTanarNeve"] ?? "").trim(),
teacher: (json["TanarNeve"] ?? "").trim(),
substituteTeacher:
Teacher.fromString((json["HelyettesTanarNeve"] ?? "").trim()),
teacher: Teacher.fromString((json["TanarNeve"] ?? "").trim()),
homeworkEnabled: json["IsTanuloHaziFeladatEnabled"] ?? false,
start: json["KezdetIdopont"] != null ? DateTime.parse(json["KezdetIdopont"]).toLocal() : DateTime(0),
start: json["KezdetIdopont"] != null
? DateTime.parse(json["KezdetIdopont"]).toLocal()
: DateTime(0),
studentPresence: json["TanuloJelenlet"] != null
? (json["TanuloJelenlet"]["Nev"] ?? "") == "Hianyzas"
? false
: true
: true,
end: json["VegIdopont"] != null ? DateTime.parse(json["VegIdopont"]).toLocal() : DateTime(0),
end: json["VegIdopont"] != null
? DateTime.parse(json["VegIdopont"]).toLocal()
: DateTime(0),
homeworkId: json["HaziFeladatUid"] ?? "",
exam: json["BejelentettSzamonkeresUid"] ?? "",
type: json["Tipus"] != null ? Category.fromJson(json["Tipus"]) : null,
description: json["Tema"] ?? "",
room: ((json["TeremNeve"] ?? "").split("_").join(" ") as String).replaceAll(RegExp(r" ?terem ?", caseSensitive: false), ""),
groupName: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Nev"] ?? "" : "",
room: ((json["TeremNeve"] ?? "").split("_").join(" ") as String)
.replaceAll(RegExp(r" ?terem ?", caseSensitive: false), ""),
groupName: json["OsztalyCsoport"] != null
? json["OsztalyCsoport"]["Nev"] ?? ""
: "",
name: json["Nev"] ?? "",
online: json["IsDigitalisOra"] ?? false,
isEmpty: json['isEmpty'] ?? false,
json: json,
isSeen: false
);
}
@ -92,6 +114,6 @@ class Lesson {
return null;
}
bool get isChanged => status?.name == "Elmaradt" || substituteTeacher != "";
bool get isChanged => status?.name == "Elmaradt" || substituteTeacher != null;
bool get swapDesc => room.length > 8;
}

View File

@ -16,6 +16,7 @@ class Message {
MessageType? type;
List<Recipient> recipients;
List<Attachment> attachments;
bool isSeen;
Message({
required this.id,
@ -32,7 +33,15 @@ class Message {
this.replyId,
this.conversationId,
this.json,
this.isSeen = false,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Message && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
factory Message.fromJson(Map json, {MessageType? forceType}) {
Map message = json["uzenet"];
@ -69,6 +78,7 @@ class Message {
replyId: message["elozoUzenetAzonosito"],
conversationId: message["beszelgetesAzonosito"],
json: json,
isSeen: false,
);
}

View File

@ -1,4 +1,5 @@
import 'category.dart';
import 'teacher.dart';
class Note {
Map? json;
@ -6,7 +7,7 @@ class Note {
String title;
DateTime date;
DateTime submitDate;
String teacher;
Teacher teacher;
DateTime seenDate;
String groupId;
String content;
@ -29,11 +30,19 @@ class Note {
return Note(
id: json["Uid"] ?? "",
title: json["Cim"] ?? "",
date: json["Datum"] != null ? DateTime.parse(json["Datum"]).toLocal() : DateTime(0),
submitDate: json["KeszitesDatuma"] != null ? DateTime.parse(json["KeszitesDatuma"]).toLocal() : DateTime(0),
teacher: (json["KeszitoTanarNeve"] ?? "").trim(),
seenDate: json["LattamozasDatuma"] != null ? DateTime.parse(json["LattamozasDatuma"]).toLocal() : DateTime(0),
groupId: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] ?? "" : "",
date: json["Datum"] != null
? DateTime.parse(json["Datum"]).toLocal()
: DateTime(0),
submitDate: json["KeszitesDatuma"] != null
? DateTime.parse(json["KeszitesDatuma"]).toLocal()
: DateTime(0),
teacher: Teacher.fromString((json["KeszitoTanarNeve"] ?? "").trim()),
seenDate: json["LattamozasDatuma"] != null
? DateTime.parse(json["LattamozasDatuma"]).toLocal()
: DateTime(0),
groupId: json["OsztalyCsoport"] != null
? json["OsztalyCsoport"]["Uid"] ?? ""
: "",
content: json["Tartalom"].replaceAll("\r", "") ?? "",
type: json["Tipus"] != null ? Category.fromJson(json["Tipus"]) : null,
json: json,

View File

@ -0,0 +1,34 @@
import 'package:filcnaplo/utils/format.dart';
class Teacher {
String id;
String name;
String? renamedTo;
bool get isRenamed => renamedTo != null;
Teacher({required this.id, required this.name, this.renamedTo});
factory Teacher.fromJson(Map json) {
return Teacher(
id: json["Uid"] ?? "",
name: (json["Nev"] ?? "").trim(),
);
}
factory Teacher.fromString(String string) {
return Teacher(
id: string.trim().replaceAll(' ', '').toLowerCase().specialChars(),
name: string.trim(),
);
}
@override
bool operator ==(other) {
if (other is! Teacher) return false;
return id == other.id;
}
@override
int get hashCode => id.hashCode;
}

View File

@ -27,7 +27,10 @@ class AbsenceProvider with ChangeNotifier {
// Load absences from the database
if (userId != null) {
var dbAbsences = await Provider.of<DatabaseProvider>(_context, listen: false).userQuery.getAbsences(userId: userId);
var dbAbsences =
await Provider.of<DatabaseProvider>(_context, listen: false)
.userQuery
.getAbsences(userId: userId);
_absences = dbAbsences;
await convertBySettings();
}
@ -36,12 +39,26 @@ class AbsenceProvider with ChangeNotifier {
// for renamed subjects
Future<void> convertBySettings() async {
final _database = Provider.of<DatabaseProvider>(_context, listen: false);
Map<String, String> renamedSubjects = (await _database.query.getSettings(_database)).renamedSubjectsEnabled
? await _database.userQuery.renamedSubjects(userId: Provider.of<UserProvider>(_context, listen: false).user!.id)
Map<String, String> renamedSubjects =
(await _database.query.getSettings(_database)).renamedSubjectsEnabled
? await _database.userQuery.renamedSubjects(
userId:
Provider.of<UserProvider>(_context, listen: false).user!.id)
: {};
Map<String, String> renamedTeachers =
(await _database.query.getSettings(_database)).renamedTeachersEnabled
? await _database.userQuery.renamedTeachers(
userId:
Provider.of<UserProvider>(_context, listen: false).user!.id)
: {};
for (Absence absence in _absences) {
absence.subject.renamedTo = renamedSubjects.isNotEmpty ? renamedSubjects[absence.subject.id] : null;
absence.subject.renamedTo = renamedSubjects.isNotEmpty
? renamedSubjects[absence.subject.id]
: null;
absence.teacher.renamedTo = renamedTeachers.isNotEmpty
? renamedTeachers[absence.teacher.id]
: null;
}
notifyListeners();
@ -53,9 +70,11 @@ class AbsenceProvider with ChangeNotifier {
if (user == null) throw "Cannot fetch Absences for User null";
String iss = user.instituteCode;
List? absencesJson = await Provider.of<KretaClient>(_context, listen: false).getAPI(KretaAPI.absences(iss));
List? absencesJson = await Provider.of<KretaClient>(_context, listen: false)
.getAPI(KretaAPI.absences(iss));
if (absencesJson == null) throw "Cannot fetch Absences for User ${user.id}";
List<Absence> absences = absencesJson.map((e) => Absence.fromJson(e)).toList();
List<Absence> absences =
absencesJson.map((e) => Absence.fromJson(e)).toList();
if (absences.isNotEmpty || _absences.isNotEmpty) await store(absences);
}
@ -66,7 +85,9 @@ class AbsenceProvider with ChangeNotifier {
if (user == null) throw "Cannot store Absences for User null";
String userId = user.id;
await Provider.of<DatabaseProvider>(_context, listen: false).userStore.storeAbsences(absences, userId: userId);
await Provider.of<DatabaseProvider>(_context, listen: false)
.userStore
.storeAbsences(absences, userId: userId);
_absences = absences;
await convertBySettings();
}

View File

@ -83,10 +83,15 @@ class GradeProvider with ChangeNotifier {
Map<String, String> renamedSubjects = _settings.renamedSubjectsEnabled
? await _database.userQuery.renamedSubjects(userId: _user.user!.id)
: {};
Map<String, String> renamedTeachers = _settings.renamedTeachersEnabled
? await _database.userQuery.renamedTeachers(userId: _user.user!.id)
: {};
for (Grade grade in _grades) {
grade.subject.renamedTo =
renamedSubjects.isNotEmpty ? renamedSubjects[grade.subject.id] : null;
grade.teacher.renamedTo =
renamedTeachers.isNotEmpty ? renamedTeachers[grade.teacher.id] : null;
grade.value.value =
_settings.goodStudent ? 5 : grade.json!["SzamErtek"] ?? 0;

View File

@ -22,9 +22,11 @@ class HomeworkProvider with ChangeNotifier {
HomeworkProvider({
List<Homework> initialHomework = const [],
required BuildContext context,
required DatabaseProvider database,
}) {
_homework = List.castFrom(initialHomework);
_context = context;
_database = database;
if (_homework.isEmpty) restore();
}
@ -48,11 +50,18 @@ class HomeworkProvider with ChangeNotifier {
(await _database.query.getSettings(_database)).renamedSubjectsEnabled
? await _database.userQuery.renamedSubjects(userId: _user.id!)
: {};
Map<String, String> renamedTeachers =
(await _database.query.getSettings(_database)).renamedTeachersEnabled
? await _database.userQuery.renamedTeachers(userId: _user.id!)
: {};
for (Homework homework in _homework) {
homework.subject.renamedTo = renamedSubjects.isNotEmpty
? renamedSubjects[homework.subject.id]
: null;
homework.teacher.renamedTo = renamedTeachers.isNotEmpty
? renamedTeachers[homework.teacher.id]
: null;
}
notifyListeners();

View File

@ -8,7 +8,7 @@ import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:flutter/material.dart';
class TimetableProvider with ChangeNotifier {
Map<Week, List<Lesson>> _lessons = {};
Map<Week, List<Lesson>> lessons = {};
late final UserProvider _user;
late final DatabaseProvider _database;
late final KretaClient _kreta;
@ -29,7 +29,7 @@ class TimetableProvider with ChangeNotifier {
// Load lessons from the database
if (userId != null) {
var dbLessons = await _database.userQuery.getLessons(userId: userId);
_lessons = dbLessons;
lessons = dbLessons;
await convertBySettings();
}
}
@ -40,17 +40,24 @@ class TimetableProvider with ChangeNotifier {
(await _database.query.getSettings(_database)).renamedSubjectsEnabled
? await _database.userQuery.renamedSubjects(userId: _user.id!)
: {};
Map<String, String> renamedTeachers =
(await _database.query.getSettings(_database)).renamedTeachersEnabled
? await _database.userQuery.renamedTeachers(userId: _user.id!)
: {};
for (Lesson lesson in _lessons.values.expand((e) => e)) {
for (Lesson lesson in lessons.values.expand((e) => e)) {
lesson.subject.renamedTo = renamedSubjects.isNotEmpty
? renamedSubjects[lesson.subject.id]
: null;
lesson.teacher.renamedTo = renamedTeachers.isNotEmpty
? renamedTeachers[lesson.teacher.id]
: null;
}
notifyListeners();
}
List<Lesson>? getWeek(Week week) => _lessons[week];
List<Lesson>? getWeek(Week week) => lessons[week];
// Fetches Lessons from the Kreta API then stores them in the database
Future<void> fetch({Week? week}) async {
@ -61,11 +68,11 @@ class TimetableProvider with ChangeNotifier {
List? lessonsJson = await _kreta
.getAPI(KretaAPI.timetable(iss, start: week.start, end: week.end));
if (lessonsJson == null) throw "Cannot fetch Lessons for User ${user.id}";
List<Lesson> lessons = lessonsJson.map((e) => Lesson.fromJson(e)).toList();
List<Lesson> lessonsList = lessonsJson.map((e) => Lesson.fromJson(e)).toList();
if (lessons.isEmpty && _lessons.isEmpty) return;
if (lessons.isEmpty && lessons.isEmpty) return;
_lessons[week] = lessons;
lessons[week] = lessonsList;
await store();
await convertBySettings();
@ -78,7 +85,7 @@ class TimetableProvider with ChangeNotifier {
String userId = user.id;
// -TODO: clear indexes with weeks outside of the current school year
await _database.userStore.storeLessons(_lessons, userId: userId);
await _database.userStore.storeLessons(lessons, userId: userId);
}
// Future<void> setLessonCount(SubjectLessonCount lessonCount, {bool store = true}) async {

View File

@ -0,0 +1,41 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class BetaChip extends StatelessWidget {
const BetaChip({Key? key, this.disabled = false}) : super(key: key);
final bool disabled;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 25,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: Center(
child: Text(
"BETA",
softWrap: true,
style: TextStyle(
fontSize: 10,
color: disabled
? AppColors.of(context).text.withOpacity(.5)
: Colors.white,
fontWeight: FontWeight.w600,
overflow: TextOverflow.ellipsis,
),
),
),
),
decoration: BoxDecoration(
color: !disabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
borderRadius: BorderRadius.circular(40),
),
),
);
}
}

View File

@ -75,6 +75,7 @@ class ProfileButton extends StatelessWidget {
snapSpec: const SnapSpec(
snap: true,
snappings: [1.0],
initialSnap: 1.0,
positioning: SnapPositioning.relativeToSheetHeight,
),
cornerRadius: 16,

View File

@ -1,10 +1,13 @@
import 'package:flutter/material.dart';
class ProgressBar extends StatelessWidget {
const ProgressBar({Key? key, required this.value, this.backgroundColor}) : super(key: key);
const ProgressBar(
{Key? key, required this.value, this.backgroundColor, this.height = 8.0})
: super(key: key);
final double value;
final Color? backgroundColor;
final double height;
@override
Widget build(BuildContext context) {
@ -13,11 +16,13 @@ class ProgressBar extends StatelessWidget {
// Background
Container(
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.light ? Colors.black.withOpacity(0.1) : Colors.white.withOpacity(0.1),
color: Theme.of(context).brightness == Brightness.light
? Colors.black.withOpacity(0.1)
: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(45.0),
),
width: double.infinity,
height: 8.0,
height: height,
),
// Slider
@ -26,8 +31,9 @@ class ProgressBar extends StatelessWidget {
width: double.infinity,
child: CustomPaint(
painter: ProgressPainter(
backgroundColor: backgroundColor ?? Theme.of(context).colorScheme.secondary,
height: 8.0,
backgroundColor:
backgroundColor ?? Theme.of(context).colorScheme.secondary,
height: height,
value: value.clamp(0, 1),
),
),
@ -38,7 +44,10 @@ class ProgressBar extends StatelessWidget {
}
class ProgressPainter extends CustomPainter {
ProgressPainter({required this.height, required this.value, required this.backgroundColor});
ProgressPainter(
{required this.height,
required this.value,
required this.backgroundColor});
final double height;
final double value;
@ -64,6 +73,8 @@ class ProgressPainter extends CustomPainter {
@override
bool shouldRepaint(ProgressPainter oldDelegate) {
return value != oldDelegate.value || height != oldDelegate.height || backgroundColor != oldDelegate.backgroundColor;
return value != oldDelegate.value ||
height != oldDelegate.height ||
backgroundColor != oldDelegate.backgroundColor;
}
}

View File

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class RoundBorderIcon extends StatelessWidget {
final Color color;
final double width;
final double padding;
final Widget icon;
const RoundBorderIcon(
{Key? key,
this.color = Colors.black,
this.width = 1.5,
this.padding = 5.0,
required this.icon})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: color, width: width),
borderRadius: BorderRadius.circular(50.0),
),
child: Padding(
padding: EdgeInsets.all(padding),
child: icon,
),
);
}
}

View File

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

View File

@ -17,19 +17,23 @@ import 'package:provider/provider.dart';
import 'absence_view.i18n.dart';
class AbsenceView extends StatelessWidget {
const AbsenceView(this.absence, {Key? key, this.outsideContext, this.viewable = false}) : super(key: key);
const AbsenceView(this.absence,
{Key? key, this.outsideContext, this.viewable = false})
: super(key: key);
final Absence absence;
final BuildContext? outsideContext;
final bool viewable;
static show(Absence absence, {required BuildContext context}) {
showBottomCard(context: context, child: AbsenceView(absence, outsideContext: context));
showBottomCard(
context: context, child: AbsenceView(absence, outsideContext: context));
}
@override
Widget build(BuildContext context) {
Color color = AbsenceTile.justificationColor(absence.state, context: context);
Color color =
AbsenceTile.justificationColor(absence.state, context: context);
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
return Padding(
@ -41,7 +45,8 @@ class AbsenceView extends StatelessWidget {
ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 16.0, right: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0)),
leading: Container(
width: 44.0,
height: 44.0,
@ -60,10 +65,18 @@ class AbsenceView extends StatelessWidget {
absence.subject.renamedTo ?? absence.subject.name.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w700, fontStyle: absence.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null),
style: TextStyle(
fontWeight: FontWeight.w700,
fontStyle: absence.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
),
subtitle: Text(
absence.teacher,
(absence.teacher.isRenamed
? absence.teacher.renamedTo
: absence.teacher.name) ??
'',
// DateFormat("MM. dd. (EEEEE)", I18n.of(context).locale.toString()).format(absence.date),
style: const TextStyle(fontWeight: FontWeight.w500),
),
@ -77,12 +90,15 @@ class AbsenceView extends StatelessWidget {
if (absence.delay > 0)
Detail(
title: "delay".i18n,
description: absence.delay.toString() + " " + "minutes".i18n.plural(absence.delay),
description: absence.delay.toString() +
" " +
"minutes".i18n.plural(absence.delay),
),
if (absence.lessonIndex != null)
Detail(
title: "Lesson".i18n,
description: "${absence.lessonIndex}. (${absence.lessonStart.format(context, timeOnly: true)}"
description:
"${absence.lessonIndex}. (${absence.lessonStart.format(context, timeOnly: true)}"
" - "
"${absence.lessonEnd.format(context, timeOnly: true)})",
),
@ -91,13 +107,19 @@ class AbsenceView extends StatelessWidget {
title: "Excuse".i18n,
description: absence.justification?.description ?? "",
),
if (absence.mode != null) Detail(title: "Mode".i18n, description: absence.mode?.description ?? ""),
Detail(title: "Submit date".i18n, description: absence.submitDate.format(context)),
if (absence.mode != null)
Detail(
title: "Mode".i18n,
description: absence.mode?.description ?? ""),
Detail(
title: "Submit date".i18n,
description: absence.submitDate.format(context)),
// Show in timetable
if (!viewable)
Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 6.0, top: 12.0),
padding: const EdgeInsets.only(
left: 16.0, right: 16.0, bottom: 6.0, top: 12.0),
child: PanelActionButton(
leading: const Icon(FeatherIcons.calendar),
title: Text(
@ -109,12 +131,15 @@ class AbsenceView extends StatelessWidget {
Navigator.of(context).pop();
if (outsideContext != null) {
ReverseSearch.getLessonByAbsence(absence, context).then((lesson) {
ReverseSearch.getLessonByAbsence(absence, context)
.then((lesson) {
if (lesson != null) {
TimetablePage.jump(outsideContext!, lesson: lesson);
} else {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
content: Text("Cannot find lesson".i18n, style: const TextStyle(color: Colors.white)),
ScaffoldMessenger.of(context)
.showSnackBar(CustomSnackBar(
content: Text("Cannot find lesson".i18n,
style: const TextStyle(color: Colors.white)),
backgroundColor: AppColors.of(context).red,
context: context,
));

View File

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

View File

@ -12,7 +12,8 @@ class ExamView extends StatelessWidget {
final Exam exam;
static show(Exam exam, {required BuildContext context}) => showBottomCard(context: context, child: ExamView(exam));
static show(Exam exam, {required BuildContext context}) =>
showBottomCard(context: context, child: ExamView(exam));
@override
Widget build(BuildContext context) {
@ -27,7 +28,8 @@ class ExamView extends StatelessWidget {
leading: Padding(
padding: const EdgeInsets.only(left: 6.0),
child: Icon(
SubjectIcon.resolveVariant(subjectName: exam.subjectName, context: context),
SubjectIcon.resolveVariant(
subjectName: exam.subjectName, context: context),
size: 36.0,
color: AppColors.of(context).text.withOpacity(.75),
),
@ -39,7 +41,10 @@ class ExamView extends StatelessWidget {
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
exam.teacher,
(exam.teacher.isRenamed
? exam.teacher.renamedTo
: exam.teacher.name) ??
'',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
@ -51,9 +56,14 @@ class ExamView extends StatelessWidget {
),
// Details
if (exam.writeDate.year != 0) Detail(title: "date".i18n, description: exam.writeDate.format(context)),
if (exam.description != "") Detail(title: "description".i18n, description: exam.description),
if (exam.mode != null) Detail(title: "mode".i18n, description: exam.mode!.description),
if (exam.writeDate.year != 0)
Detail(
title: "date".i18n,
description: exam.writeDate.format(context)),
if (exam.description != "")
Detail(title: "description".i18n, description: exam.description),
if (exam.mode != null)
Detail(title: "mode".i18n, description: exam.mode!.description),
],
),
);

View File

@ -11,7 +11,8 @@ import 'grade_view.i18n.dart';
class GradeView extends StatelessWidget {
const GradeView(this.grade, {Key? key}) : super(key: key);
static show(Grade grade, {required BuildContext context}) => showBottomCard(context: context, child: GradeView(grade));
static show(Grade grade, {required BuildContext context}) =>
showBottomCard(context: context, child: GradeView(grade));
final Grade grade;
@ -30,10 +31,21 @@ class GradeView extends StatelessWidget {
grade.subject.renamedTo ?? grade.subject.name.capital(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: grade.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null),
style: TextStyle(
fontWeight: FontWeight.w600,
fontStyle: grade.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
),
subtitle: Text(
!Provider.of<SettingsProvider>(context, listen: false).presentationMode ? grade.teacher : "Tanár",
!Provider.of<SettingsProvider>(context, listen: false)
.presentationMode
? (grade.teacher.isRenamed
? grade.teacher.renamedTo
: grade.teacher.name) ??
''
: "Tanár",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
@ -49,13 +61,20 @@ class GradeView extends StatelessWidget {
title: "value".i18n,
description: "${grade.value.valueName} " + percentText(),
),
if (grade.description != "") Detail(title: "description".i18n, description: grade.description),
if (grade.mode.description != "") Detail(title: "mode".i18n, description: grade.mode.description),
if (grade.writeDate.year != 0) Detail(title: "date".i18n, description: grade.writeDate.format(context)),
if (grade.description != "")
Detail(title: "description".i18n, description: grade.description),
if (grade.mode.description != "")
Detail(title: "mode".i18n, description: grade.mode.description),
if (grade.writeDate.year != 0)
Detail(
title: "date".i18n,
description: grade.writeDate.format(context)),
],
),
);
}
String percentText() => grade.value.weight != 100 && grade.value.weight > 0 ? "${grade.value.weight}%" : "";
String percentText() => grade.value.weight != 100 && grade.value.weight > 0
? "${grade.value.weight}%"
: "";
}

View File

@ -51,10 +51,18 @@ class HomeworkView extends StatelessWidget {
homework.subject.renamedTo ?? homework.subject.name.capital(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: homework.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null),
style: TextStyle(
fontWeight: FontWeight.w600,
fontStyle: homework.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
),
subtitle: Text(
homework.teacher,
(homework.teacher.isRenamed
? homework.teacher.renamedTo
: homework.teacher.name) ??
'',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),

View File

@ -6,7 +6,8 @@ import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'changed_lesson_tile.i18n.dart';
class ChangedLessonTile extends StatelessWidget {
const ChangedLessonTile(this.lesson, {Key? key, this.onTap, this.padding}) : super(key: key);
const ChangedLessonTile(this.lesson, {Key? key, this.onTap, this.padding})
: super(key: key);
final Lesson lesson;
final void Function()? onTap;
@ -21,7 +22,7 @@ class ChangedLessonTile extends StatelessWidget {
Color accent = Theme.of(context).colorScheme.secondary;
if (lesson.substituteTeacher != "") {
if (lesson.substituteTeacher?.name != '') {
accent = AppColors.of(context).yellow;
}
@ -38,7 +39,8 @@ class ChangedLessonTile extends StatelessWidget {
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: SizedBox(
width: 44.0,
height: 44.0,
@ -55,7 +57,9 @@ class ChangedLessonTile extends StatelessWidget {
),
),
title: Text(
lesson.substituteTeacher != "" ? "substituted".i18n : "cancelled".i18n,
lesson.substituteTeacher?.name != ""
? "substituted".i18n
: "cancelled".i18n,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
@ -64,7 +68,9 @@ class ChangedLessonTile extends StatelessWidget {
lesson.subject.renamedTo ?? lesson.subject.name.capital(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w500, fontStyle: lesson.subject.isRenamed ? FontStyle.italic : null),
style: TextStyle(
fontWeight: FontWeight.w500,
fontStyle: lesson.subject.isRenamed ? FontStyle.italic : null),
),
trailing: const Icon(FeatherIcons.arrowRight),
minLeadingWidth: 0,

View File

@ -22,7 +22,7 @@ class LessonView extends StatelessWidget {
if (RegExp(r'\d').hasMatch(lesson.lessonIndex)) lessonIndexTrailing = ".";
if (lesson.substituteTeacher != "") {
if (lesson.substituteTeacher?.name != "") {
accent = AppColors.of(context).yellow;
}
@ -54,10 +54,23 @@ class LessonView extends StatelessWidget {
lesson.subject.renamedTo ?? lesson.subject.name.capital(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: lesson.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null),
style: TextStyle(
fontWeight: FontWeight.w600,
fontStyle: lesson.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
),
subtitle: Text(
lesson.substituteTeacher == "" ? lesson.teacher : lesson.substituteTeacher,
((lesson.substituteTeacher == null ||
lesson.substituteTeacher!.name == "")
? (lesson.teacher.isRenamed
? lesson.teacher.renamedTo
: lesson.teacher.name)
: (lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo
: lesson.substituteTeacher!.name)) ??
'',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
@ -69,10 +82,18 @@ class LessonView extends StatelessWidget {
),
// Details
if (lesson.room != "") Detail(title: "Room".i18n, description: lesson.room.replaceAll("_", " ")),
if (lesson.description != "") Detail(title: "Description".i18n, description: lesson.description),
if (lesson.lessonYearIndex != null) Detail(title: "Lesson Number".i18n, description: "${lesson.lessonYearIndex}."),
if (lesson.groupName != "") Detail(title: "Group".i18n, description: lesson.groupName),
if (lesson.room != "")
Detail(
title: "Room".i18n,
description: lesson.room.replaceAll("_", " ")),
if (lesson.description != "")
Detail(title: "Description".i18n, description: lesson.description),
if (lesson.lessonYearIndex != null)
Detail(
title: "Lesson Number".i18n,
description: "${lesson.lessonYearIndex}."),
if (lesson.groupName != "")
Detail(title: "Group".i18n, description: lesson.groupName),
],
),
);

View File

@ -4,7 +4,8 @@ import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:flutter/material.dart';
class NoteTile extends StatelessWidget {
const NoteTile(this.note, {Key? key, this.onTap, this.padding}) : super(key: key);
const NoteTile(this.note, {Key? key, this.onTap, this.padding})
: super(key: key);
final Note note;
final void Function()? onTap;
@ -20,11 +21,20 @@ class NoteTile extends StatelessWidget {
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: ProfileImage(
name: note.teacher,
name: (note.teacher.isRenamed
? note.teacher.renamedTo
: note.teacher.name) ??
'',
radius: 22.0,
backgroundColor: ColorUtils.stringToColor(note.teacher),
backgroundColor: ColorUtils.stringToColor(
(note.teacher.isRenamed
? note.teacher.renamedTo
: note.teacher.name) ??
'',
),
),
title: Text(
note.title,

View File

@ -12,7 +12,8 @@ class NoteView extends StatelessWidget {
final Note note;
static void show(Note note, {required BuildContext context}) => showSlidingBottomSheet(context: context, child: NoteView(note));
static void show(Note note, {required BuildContext context}) =>
showSlidingBottomSheet(context: context, child: NoteView(note));
@override
Widget build(BuildContext context) {
@ -25,9 +26,17 @@ class NoteView extends StatelessWidget {
// Header
ListTile(
leading: ProfileImage(
name: note.teacher,
name: (note.teacher.isRenamed
? note.teacher.renamedTo
: note.teacher.name) ??
'',
radius: 22.0,
backgroundColor: ColorUtils.stringToColor(note.teacher),
backgroundColor: ColorUtils.stringToColor(
(note.teacher.isRenamed
? note.teacher.renamedTo
: note.teacher.name) ??
'',
),
),
title: Text(
note.title,
@ -36,7 +45,10 @@ class NoteView extends StatelessWidget {
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
note.teacher,
(note.teacher.isRenamed
? note.teacher.renamedTo
: note.teacher.name) ??
'',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),

View File

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

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:filcnaplo_kreta_api/models/category.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_kreta_api/models/teacher.dart';
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
import 'package:filcnaplo_mobile_ui/common/material_action_button.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
@ -42,7 +43,8 @@ class _GradeCalculatorState extends State<GradeCalculator> {
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"Grade Calculator".i18n,
style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w600),
style:
const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w600),
),
),
@ -63,7 +65,9 @@ class _GradeCalculatorState extends State<GradeCalculator> {
Container(
width: 80.0,
padding: const EdgeInsets.only(right: 12.0),
child: Center(child: GradeValueWidget(GradeValue(newValue.toInt(), "", "", 0))),
child: Center(
child: GradeValueWidget(
GradeValue(newValue.toInt(), "", "", 0))),
),
]),
@ -90,7 +94,8 @@ class _GradeCalculatorState extends State<GradeCalculator> {
child: Center(
child: TextField(
controller: _weightController,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 22.0),
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 22.0),
autocorrect: false,
textAlign: TextAlign.right,
keyboardType: TextInputType.number,
@ -121,8 +126,9 @@ class _GradeCalculatorState extends State<GradeCalculator> {
child: MaterialActionButton(
child: Text("Add Grade".i18n),
onPressed: () {
if (calculatorProvider.ghosts.length >= 30) {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(content: Text("limit_reached".i18n), context: context));
if (calculatorProvider.ghosts.length >= 50) {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
content: Text("limit_reached".i18n), context: context));
return;
}
@ -133,7 +139,11 @@ class _GradeCalculatorState extends State<GradeCalculator> {
grades.sort((a, b) => -a.writeDate.compareTo(b.writeDate));
date = grades.first.date.add(const Duration(days: 7));
} else {
List<Grade> grades = calculatorProvider.grades.where((e) => e.type == GradeType.midYear && e.subject == widget.subject).toList();
List<Grade> grades = calculatorProvider.grades
.where((e) =>
e.type == GradeType.midYear &&
e.subject == widget.subject)
.toList();
grades.sort((a, b) => -a.writeDate.compareTo(b.writeDate));
date = grades.first.date;
}
@ -143,8 +153,9 @@ class _GradeCalculatorState extends State<GradeCalculator> {
date: date,
writeDate: date,
description: "Ghost Grade".i18n,
value: GradeValue(newValue.toInt(), "", "", newWeight.toInt()),
teacher: "Ghost",
value:
GradeValue(newValue.toInt(), "", "", newWeight.toInt()),
teacher: Teacher.fromString("Ghost"),
type: GradeType.ghost,
form: "",
subject: widget.subject,

View File

@ -1,6 +1,8 @@
import 'dart:math';
import 'package:animations/animations.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
@ -21,6 +23,11 @@ import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_pro
import 'package:filcnaplo_mobile_ui/pages/grades/grades_count.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/graph.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner_screen.dart';
import 'package:filcnaplo_premium/models/premium_scopes.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_state_screen.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
@ -58,12 +65,16 @@ class _GradeSubjectViewState extends State<GradeSubjectView> {
late GradeProvider gradeProvider;
late GradeCalculatorProvider calculatorProvider;
late SettingsProvider settingsProvider;
late DatabaseProvider dbProvider;
late UserProvider user;
late double average;
late Widget gradeGraph;
bool gradeCalcMode = false;
String plan = '';
List<Grade> getSubjectGrades(Subject subject) => !gradeCalcMode
? gradeProvider.grades.where((e) => e.subject == subject).toList()
: calculatorProvider.grades.where((e) => e.subject == subject).toList();
@ -147,6 +158,20 @@ class _GradeSubjectViewState extends State<GradeSubjectView> {
gradeTiles = List.castFrom(tiles);
}
@override
void initState() {
super.initState();
user = Provider.of<UserProvider>(context, listen: false);
dbProvider = Provider.of<DatabaseProvider>(context, listen: false);
}
void fetchGoalPlans() async {
plan = (await dbProvider.userQuery
.subjectGoalPlans(userId: user.id!))[widget.subject.id] ??
'';
setState(() {});
}
@override
Widget build(BuildContext context) {
gradeProvider = Provider.of<GradeProvider>(context);
@ -192,6 +217,8 @@ class _GradeSubjectViewState extends State<GradeSubjectView> {
buildTiles(ghostGrades);
}
fetchGoalPlans();
return Scaffold(
key: _scaffoldKey,
floatingActionButtonLocation: ExpandableFab.location,
@ -209,31 +236,33 @@ class _GradeSubjectViewState extends State<GradeSubjectView> {
),
children: [
FloatingActionButton.small(
heroTag: "btn_ghost_grades",
child: const Icon(FeatherIcons.plus),
backgroundColor: Theme.of(context).colorScheme.secondary,
onPressed: () {
gradeCalc(context);
},
),
// FloatingActionButton.small(
// child: const Icon(FeatherIcons.flag, size: 20.0),
// backgroundColor: Theme.of(context).colorScheme.secondary,
// onPressed: () {
// if (!Provider.of<PremiumProvider>(context, listen: false)
// .hasScope(PremiumScopes.goalPlanner)) {
// PremiumLockedFeatureUpsell.show(
// context: context, feature: PremiumFeature.goalplanner);
// return;
// }
FloatingActionButton.small(
heroTag: "btn_goal_planner",
child: const Icon(FeatherIcons.flag, size: 20.0),
backgroundColor: Theme.of(context).colorScheme.secondary,
onPressed: () {
if (!Provider.of<PremiumProvider>(context, listen: false)
.hasScope(PremiumScopes.goalPlanner)) {
PremiumLockedFeatureUpsell.show(
context: context, feature: PremiumFeature.goalplanner);
return;
}
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text("Hamarosan...")));
// Navigator.of(context).push(CupertinoPageRoute(
// builder: (context) => PremiumGoalplannerNewGoalScreen(
// subject: widget.subject)));
// },
// ),
Navigator.of(context).push(CupertinoPageRoute(
builder: (context) =>
GoalPlannerScreen(subject: widget.subject)));
},
),
],
),
),
@ -257,13 +286,42 @@ class _GradeSubjectViewState extends State<GradeSubjectView> {
const SizedBox(width: 6.0),
if (average != 0)
Center(child: AverageDisplay(average: average)),
const SizedBox(width: 6.0),
if (plan != '')
Center(
child: GestureDetector(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(
builder: (context) =>
GoalStateScreen(subject: widget.subject)));
},
child: Container(
width: 54.0,
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(45.0),
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(.15),
),
child: Icon(
FeatherIcons.flag,
size: 17.0,
weight: 2.5,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
const SizedBox(width: 12.0),
],
icon: SubjectIcon.resolveVariant(
subject: widget.subject, context: context),
scrollController: _scrollController,
title: widget.subject.renamedTo ?? widget.subject.name.capital(),
italic: settingsProvider.renamedSubjectsItalics && widget.subject.isRenamed,
italic: settingsProvider.renamedSubjectsItalics &&
widget.subject.isRenamed,
child: SubjectGradesContainer(
child: CupertinoScrollbar(
child: ListView.builder(

View File

@ -356,14 +356,14 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
// confetti 🎊
if (_confettiController != null)
Align(
alignment: Alignment.bottomCenter,
alignment: Alignment.topCenter,
child: ConfettiWidget(
confettiController: _confettiController!,
blastDirection: -pi / 2,
emissionFrequency: 0.01,
numberOfParticles: 80,
maxBlastForce: 100,
minBlastForce: 90,
blastDirectionality: BlastDirectionality.explosive,
emissionFrequency: 0.02,
numberOfParticles: 120,
maxBlastForce: 20,
minBlastForce: 10,
gravity: 0.3,
minimumSize: const Size(5, 5),
maximumSize: const Size(20, 20),

View File

@ -3,6 +3,7 @@ import 'package:filcnaplo/helpers/quick_actions.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_mobile_ui/common/system_chrome.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/nabar.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/navbar_item.dart';
@ -12,6 +13,7 @@ import 'package:filcnaplo/icons/filc_icons.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/status_bar.dart';
import 'package:filcnaplo_mobile_ui/screens/news/news_view.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_complete_modal.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -23,6 +25,7 @@ import 'package:filcnaplo/api/providers/sync.dart';
import 'package:home_widget/home_widget.dart';
import 'package:wtf_sliding_sheet/wtf_sliding_sheet.dart';
import 'package:background_fetch/background_fetch.dart';
import 'package:filcnaplo_premium/providers/goal_provider.dart';
class NavigationScreen extends StatefulWidget {
const NavigationScreen({Key? key}) : super(key: key);
@ -42,7 +45,9 @@ class NavigationScreenState extends State<NavigationScreen>
late SettingsProvider settings;
late NewsProvider newsProvider;
late GoalProvider goalProvider;
late UpdateProvider updateProvider;
late GradeProvider gradeProvicer;
NavigatorState? get navigator => _navigatorState.currentState;
@ -82,6 +87,7 @@ class NavigationScreenState extends State<NavigationScreen>
snapSpec: const SnapSpec(
snap: true,
snappings: [1.0],
initialSnap: 1.0,
positioning: SnapPositioning.relativeToSheetHeight,
),
cornerRadius: 16,
@ -139,7 +145,6 @@ class NavigationScreenState extends State<NavigationScreen>
void initState() {
super.initState();
HomeWidget.setAppGroupId('hu.refilc.naplo.group');
_checkForWidgetLaunch();
@ -156,15 +161,22 @@ class NavigationScreenState extends State<NavigationScreen>
Provider.of<KretaClient>(context, listen: false).userAgent =
settings.config.userAgent;
// Get news
// get news
newsProvider = Provider.of<NewsProvider>(context, listen: false);
newsProvider.restore().then((value) => newsProvider.fetch());
// Get releases
// init grade provider (for goals)
gradeProvicer = Provider.of<GradeProvider>(context, listen: false);
// get goals
goalProvider = Provider.of<GoalProvider>(context, listen: false);
goalProvider.fetchDone(gradeProvider: gradeProvicer);
// get releases
updateProvider = Provider.of<UpdateProvider>(context, listen: false);
updateProvider.fetch();
// Initial sync
// initial sync
syncAll(context);
setupQuickActions();
}
@ -195,13 +207,19 @@ class NavigationScreenState extends State<NavigationScreen>
setSystemChrome(context);
settings = Provider.of<SettingsProvider>(context);
newsProvider = Provider.of<NewsProvider>(context);
goalProvider = Provider.of<GoalProvider>(context);
// Show news
// show news and complete goals
WidgetsBinding.instance.addPostFrameCallback((_) {
if (newsProvider.show) {
newsProvider.lock();
NewsView.show(newsProvider.news[newsProvider.state], context: context)
NewsView.show(newsProvider.news[0], context: context)
.then((value) => newsProvider.release());
newsProvider.lock();
}
if (goalProvider.hasDoneGoals) {
GoalCompleteModal.show(goalProvider.doneSubject!, context: context);
goalProvider.lock();
}
});

View File

@ -31,23 +31,30 @@ class NewsView extends StatelessWidget {
physics: const BouncingScrollPhysics(),
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 6.0, top: 14.0, bottom: 8.0),
padding: const EdgeInsets.only(
left: 8.0, right: 6.0, top: 14.0, bottom: 8.0),
child: Text(
news.title,
maxLines: 3,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18.0),
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 18.0),
),
),
SelectableLinkify(
text: news.content.escapeHtml(),
options: const LinkifyOptions(looseUrl: true, removeWww: true),
options:
const LinkifyOptions(looseUrl: true, removeWww: true),
onOpen: (link) {
launch(
link.url,
customTabsOption: CustomTabsOption(showPageTitle: true, toolbarColor: Theme.of(context).scaffoldBackgroundColor),
customTabsOption: CustomTabsOption(
showPageTitle: true,
toolbarColor:
Theme.of(context).scaffoldBackgroundColor),
);
},
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14.0),
style: const TextStyle(
fontWeight: FontWeight.w500, fontSize: 14.0),
),
],
),
@ -59,10 +66,15 @@ class NewsView extends StatelessWidget {
children: [
if (news.link != "")
DialogButton(
label: news.openLabel != "" ? news.openLabel : "open".i18n.toUpperCase(),
label: news.openLabel != ""
? news.openLabel
: "open".i18n.toUpperCase(),
onTap: () => launch(
news.link,
customTabsOption: CustomTabsOption(showPageTitle: true, toolbarColor: Theme.of(context).scaffoldBackgroundColor),
customTabsOption: CustomTabsOption(
showPageTitle: true,
toolbarColor:
Theme.of(context).scaffoldBackgroundColor),
),
),
DialogButton(
@ -78,12 +90,15 @@ class NewsView extends StatelessWidget {
);
}
static Future<T?> show<T>(News news, {required BuildContext context, bool force = false}) {
static Future<T?> show<T>(News news,
{required BuildContext context, bool force = false}) {
if (news.title == "") return Future<T?>.value(null);
bool popup = news.platform == '' || force;
bool popup = news.platform == 'all' || force;
if (Provider.of<SettingsProvider>(context, listen: false).newsEnabled || news.emergency || force) {
if (Provider.of<SettingsProvider>(context, listen: false).newsEnabled ||
news.emergency ||
force) {
switch (news.platform.trim().toLowerCase()) {
case "android":
if (Platform.isAndroid) popup = true;
@ -100,6 +115,9 @@ class NewsView extends StatelessWidget {
case "macos":
if (Platform.isMacOS) popup = true;
break;
case "all":
popup = true;
break;
default:
popup = true;
}
@ -108,7 +126,10 @@ class NewsView extends StatelessWidget {
}
if (popup) {
return showDialog<T?>(context: context, builder: (context) => NewsView(news), barrierDismissible: true);
return showDialog<T?>(
context: context,
builder: (context) => NewsView(news),
barrierDismissible: true);
} else {
return Future<T?>.value(null);
}

View File

@ -0,0 +1,226 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_mobile_ui/common/beta_chip.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'notifications_screen.i18n.dart';
class MenuNotifications extends StatelessWidget {
const MenuNotifications({Key? key, required this.settings}) : super(key: key);
final SettingsProvider settings;
@override
Widget build(BuildContext context) {
return PanelButton(
padding: const EdgeInsets.only(left: 14.0),
onPressed: () {
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(builder: (context) => const NotificationsScreen()),
);
},
title: Row(
children: [
Text(
"notifications_screen".i18n,
style: TextStyle(
color: AppColors.of(context)
.text
.withOpacity(settings.notificationsEnabled ? 1.0 : .5)),
),
const SizedBox(width: 5.0),
BetaChip(
disabled: !settings.notificationsEnabled,
),
],
),
leading: settings.notificationsEnabled
? const Icon(FeatherIcons.messageSquare)
: Icon(FeatherIcons.messageSquare,
color: AppColors.of(context).text.withOpacity(.25)),
trailingDivider: true,
trailing: Switch(
onChanged: (v) async {
settings.update(notificationsEnabled: v);
},
value: settings.notificationsEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
);
}
}
class NotificationsScreen extends StatelessWidget {
const NotificationsScreen({super.key});
@override
Widget build(BuildContext context) {
SettingsProvider settings = Provider.of<SettingsProvider>(context);
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"notifications_screen".i18n,
style: TextStyle(color: AppColors.of(context).text),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Panel(
child: Column(
children: [
Material(
type: MaterialType.transparency,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsGradesEnabled,
onChanged: (v) =>
settings.update(notificationsGradesEnabled: v),
title: Row(
children: [
Icon(
FeatherIcons.bookmark,
color: settings.notificationsGradesEnabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 14.0),
Expanded(
child: Text(
"grades".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsGradesEnabled
? 1.0
: .5,
),
),
),
),
],
),
),
),
Material(
type: MaterialType.transparency,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsAbsencesEnabled,
onChanged: (v) =>
settings.update(notificationsAbsencesEnabled: v),
title: Row(
children: [
Icon(
FeatherIcons.clock,
color: settings.notificationsAbsencesEnabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 14.0),
Expanded(
child: Text(
"absences".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsAbsencesEnabled
? 1.0
: .5,
),
),
),
),
],
),
),
),
Material(
type: MaterialType.transparency,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsMessagesEnabled,
onChanged: (v) =>
settings.update(notificationsMessagesEnabled: v),
title: Row(
children: [
Icon(
FeatherIcons.messageSquare,
color: settings.notificationsMessagesEnabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 14.0),
Expanded(
child: Text(
"messages".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsMessagesEnabled
? 1.0
: .5,
),
),
),
),
],
),
),
),
Material(
type: MaterialType.transparency,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsLessonsEnabled,
onChanged: (v) =>
settings.update(notificationsLessonsEnabled: v),
title: Row(
children: [
Icon(
FeatherIcons.calendar,
color: settings.notificationsLessonsEnabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 14.0),
Expanded(
child: Text(
"lessons".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsLessonsEnabled
? 1.0
: .5,
),
),
),
),
],
),
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"notifications_screen": "Notifications",
"grades": "Grades",
"absences": "Absences",
"messages": "Messages",
"lessons": "Lessons"
},
"hu_hu": {
"notifications_screen": "Értesítések",
"grades": "Jegyek",
"absences": "Hiányzások",
"messages": "Üzenetek",
"lessons": "Órák"
},
"de_de": {
"notifications_screen": "Mitteilung",
"grades": "Noten",
"absences": "Fehlen",
"messages": "Nachrichten",
"lessons": "Unterricht"
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -28,27 +28,22 @@ class PrivacyView extends StatelessWidget {
),
SelectableLinkify(
text: """
A reFilc egy kliensalkalmazás, segítségével az e-Kréta rendszeréből letöltheted és felhasználóbarát módon megjelenítheted az adataidat.
Tanulmányi adataid csak közvetlenül az alkalmazás és a Kréta-szerverek között közlekednek, titkosított kapcsolaton keresztül.
A reFilc (továbbiakban alkalmazás) egy mobilos, asztali és webes kliensalkalmazás, segítségével az e-Kréta rendszeréből letöltheted és felhasználóbarát módon megjelenítheted az adataidat. Tanulmányi adataid csak közvetlenül az alkalmazás és a Kréta-szerverek között közlekednek, titkosított kapcsolaton keresztül.
A reFilc fejlesztői és üzemeltetői a tanulmányi adataidat semmilyen célból nem másolják, nem tárolják és harmadik félnek nem továbbítják. Ezeket így az e-Kréta Informatikai Zrt. kezeli, az ő tájékoztatójukat itt találod: https://tudasbazis.ekreta.hu/pages/viewpage.action?pageId=4065038.
Azok törlésével vagy módosítával kapcsolatban keresd az osztályfőnöködet vagy az iskolád rendszergazdáját.
A reFilc fejlesztői és/vagy üzemeltetői, valamint az alkalmazás a tanulmányi és személyes adataidat semmilyen célból és semmilyen körülmények között nem másolják, nem tárolják és harmadik félnek nem továbbítják. Ezeket így az Educational Development Informatikai Zrt. kezeli, az Ő adatkezeléssel kapcsolatos tájékoztatójukat itt találod: https://tudasbazis.ekreta.hu/pages/viewpage.action?pageId=4065038
Az alkalmazás névtelen használati statisztikákat gyűjt, ezek alapján tudjuk meghatározni a felhasználók és a telepítések számát. Ezt a beállításokban kikapcsolhatod.
Kérünk, hogy ha csak teheted, hagyd ezt a funkciót bekapcsolva.
Azok törlésével vagy módosítával kapcsolatban keresd az osztályfőnöködet vagy az iskolád rendszergazdáját.
Amikor az alkalmazás hibába ütközik, lehetőség van hibajelentés küldésére.
Ez személyes- vagy tanulmányi adatokat nem tartalmaz, viszont részletes információval szolgál a hibáról és eszközödről.
A küldés előtt megjelenő képernyőn a te felelősséged átnézni a továbbításra kerülő adatsort.
A hibajelentéseket a reFilc fejlesztői felületén és egy privát Discord szobában tároljuk, ezekhez csak az app fejlesztői férnek hozzá.
Az alkalmazás belépéskor a GitHub API segítségével ellenőrzi, hogy elérhető-e új verzió, és kérésre innen is tölti le a telepítőt.
Az alkalmazás névtelen használati statisztikákat gyűjt, ezek alapján tudjuk meghatározni a felhasználók és a telepítések számát, valamint az eszközük platformját. Ezt a beállításokban kikapcsolhatod. Kérünk, hogy ha csak teheted, hagyd ezt a funkciót bekapcsolva, hogy pontosabb információnk legyen a felhasználóink platform-megoszlásáról.
Ha az adataiddal kapcsolatban bármilyen kérdésed van (törlés, módosítás, adathordozás), keress minket a filcnaplo@filcnaplo.hu címen.
Amikor az alkalmazás hibába ütközik, lehetőség van hibajelentés küldésére. Ez személyes- és/vagy tanulmányi adatokat nem tartalmaz, viszont részletes információval szolgál a hibáról, annak okáról és eszközödről. A küldés előtt megjelenő képernyőn a te felelősséged átnézni a továbbításra kerülő adatsort. A hibajelentéseket a reFilc fejlesztői felületén és egy privát Discord szobában tároljuk, ezekhez csak az app fejlesztői férnek hozzá.
Az alkalmazás használatával jelzed, hogy ezt a tájékoztatót tudomásul vetted.
Az alkalmazás (az alábbi platformokon: Android, Linux, Windows) minden egyes indításakor a reFilc API, valamint a Github API segítségével ellenőrzi, hogy elérhető-e új verzió, és kérésre innen letölti és telepíti a frissítést.
Utolsó módosítás: 2023. 05. 28.
""",
Amennyiben az adataiddal kapcsolatban bármilyen kérdésed van (megtekintés, törlés, módosítás, adathordozás), keress minket a social@refilc.hu e-mail címen, vagy Discord szerverünkön!
A kliensalkalmazás bármely eszközön és platformon történő használatával tudomásul vetted és elfogadod a jelen adatkezelési tájékoztatót. A reFilc csapata fenntartja a jogot a tájékoztató módosítására és a módosításokról nem köteles értesíteni a felhasználóit!
""",
onOpen: (link) => launch(link.url,
customTabsOption: CustomTabsOption(
toolbarColor: Theme.of(context).scaffoldBackgroundColor,

Some files were not shown because too many files have changed in this diff Show More