diff --git a/.github/workflows/build-patch.yml b/.github/workflows/build-patch.yml index 6022500..81874d8 100644 --- a/.github/workflows/build-patch.yml +++ b/.github/workflows/build-patch.yml @@ -23,7 +23,7 @@ jobs: with: fetch-depth: 0 - name: Verify Changed files - uses: tj-actions/changed-files@v37 + uses: tj-actions/changed-files@v41 id: verify-changed-files with: files: | diff --git a/.github/workflows/build-prerelease-apk.yml b/.github/workflows/build-prerelease-apk.yml index f21dfda..f2143cc 100644 --- a/.github/workflows/build-prerelease-apk.yml +++ b/.github/workflows/build-prerelease-apk.yml @@ -64,7 +64,8 @@ jobs: commitChange: true branch: 'build-prerelease' labels: 'bump' - message: 'Bump version to ${{ steps.semvers.outputs.patch }}' + message: 'Bump version to ${{ steps.semvers.outputs.patch }} [no ci]' + createPR: true description: 'Automatic version bump to ${{ steps.semvers.outputs.patch }} for prerelease build' - run: flutter pub get - run: flutter gen-l10n diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml index 0d0e1f4..fabee0f 100644 --- a/.github/workflows/build-release-apk.yml +++ b/.github/workflows/build-release-apk.yml @@ -64,7 +64,8 @@ jobs: commitChange: true branch: 'build-release' labels: 'bump' - message: 'Bump version to ${{ steps.semvers.outputs.minor }}' + createPR: true + message: 'Bump version to ${{ steps.semvers.outputs.minor }} [no ci]' description: 'Automatic version bump to ${{ steps.semvers.outputs.minor }} for release build' - run: flutter pub get - run: flutter gen-l10n diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 869c6d3..a567e88 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -20,10 +20,11 @@ jobs: with: fetch-depth: 0 - name: Verify Changed files - uses: tj-actions/changed-files@v37 + uses: tj-actions/changed-files@v41 id: verify-changed-files with: files: | + .github/workflows/test-coverage.yml pubspec.yaml pubspec.lock l10n.yaml @@ -40,30 +41,6 @@ jobs: with: fetch-depth: 0 - - name: Gradle cache - uses: gradle/gradle-build-action@v2 - - - name: AVD cache - uses: actions/cache@v3 - id: avd-cache - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-33 - - - name: create AVD and generate snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 31 - arch: x86_64 - profile: pixel_6_pro - force-avd-creation: false - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: false - script: echo "Generated AVD snapshot for caching." - - uses: actions/setup-java@v2 with: distribution: 'zulu' @@ -79,14 +56,16 @@ jobs: - run: flutter analyze . - run: flutter gen-l10n - run: dart pub global run full_coverage - - run: flutter test --coverage --dart-define=USERNAME=${{ secrets.USERNAME }} --dart-define=PASSWORD=${{ secrets.PASSWORD }} + - run: flutter test --coverage - name: run integration tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: 31 arch: x86_64 profile: pixel_6_pro + avd-name: Pixel_6_Pro_API_31 force-avd-creation: false + ram-size: 4096M emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true script: flutter test integration_test/app_test.dart --coverage --dart-define=USERNAME=${{ secrets.USERNAME }} --dart-define=PASSWORD=${{ secrets.PASSWORD }} --dart-define=NAME="${{ secrets.NAME }}" diff --git a/README.md b/README.md index 3dffdc3..98fd24d 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,19 @@

-[![build-release-android](https://github.com/DislikesSchool/EduPage2/actions/workflows/build-release-apk.yml/badge.svg)](https://github.com/DislikesSchool/EduPage2/actions/workflows/build-release-apk.yml) ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/dislikesschool/edupage2) ![Downloads](https://img.shields.io/github/downloads/DislikesSchool/EduPage2/total) ![Contributors](https://img.shields.io/github/contributors/DislikesSchool/EduPage2?color=dark-green) ![Issues](https://img.shields.io/github/issues/DislikesSchool/EduPage2) ![License](https://img.shields.io/github/license/DislikesSchool/EduPage2) [![codecov](https://codecov.io/github/DislikesSchool/EduPage2/branch/master/graph/badge.svg?token=HKP9WFL0LN)](https://codecov.io/github/DislikesSchool/EduPage2) +![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/dislikesschool/edupage2) ![Downloads](https://img.shields.io/github/downloads/DislikesSchool/EduPage2/total) ![Contributors](https://img.shields.io/github/contributors/DislikesSchool/EduPage2?color=dark-green) ![Issues](https://img.shields.io/github/issues/DislikesSchool/EduPage2) ![License](https://img.shields.io/github/license/DislikesSchool/EduPage2) [![codecov](https://codecov.io/github/DislikesSchool/EduPage2/branch/master/graph/badge.svg?token=HKP9WFL0LN)](https://codecov.io/github/DislikesSchool/EduPage2) [![Discord](https://discordapp.com/api/guilds/1143488418840584224/widget.png?style=banner2)](https://discord.gg/xy5nqWa2kQ) +[![test-coverage](https://github.com/DislikesSchool/EduPage2/actions/workflows/test-coverage.yml/badge.svg)](https://github.com/DislikesSchool/EduPage2/actions/workflows/test-coverage.yml) +[![build-patch-android](https://github.com/DislikesSchool/EduPage2/actions/workflows/build-patch.yml/badge.svg)](https://github.com/DislikesSchool/EduPage2/actions/workflows/build-patch.yml) + ## Table Of Contents - [Table Of Contents](#table-of-contents) - [About The Project](#about-the-project) +- [Backend Status](#backend-status) + - [Quick status](#quick-status) + - [Statuspage](#statuspage) - [Disclaimer](#disclaimer) - [Built With](#built-with) - [Getting Started](#getting-started) @@ -43,6 +49,19 @@ And that's why we made EduPage2. So far, EduPage2 lacks a pretty big amount of f EduPage2 uses local caching on your device, and a caching server with our own privte software, which periodically updates data from EduPage, strips it of all useless data (which EduPage includes for some reason), and finally sends out to your device when requested. +## Backend Status + +### Quick status + +| Host | Status | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| Render.com | [![Better Stack Badge](https://uptime.betterstack.com/status-badges/v1/monitor/w8hv.svg)](https://uptime.betterstack.com/?utm_source=status_badge) | +| Deta.space | [![Better Stack Badge](https://uptime.betterstack.com/status-badges/v1/monitor/wt8i.svg)](https://uptime.betterstack.com/?utm_source=status_badge) | + +### Statuspage + +Currently there are two status pages for the EduPage2 backend. The one on [Better Stack](https://ep2.betteruptime.com/) which we have confirmed to work, and the other one on [Statuspage](https://edupage2.statuspage.io/) which seems to work, but we will have to wait unitl an outage occurs to test that. + ## Disclaimer **EduPage2** is an open-source project with contributions from multiple individuals and is not affiliated with or endorsed by the creators of EduPage. EduPage is a separate and (possibly) trademarked platform owned by asc Applied Software Consultants, s.r.o. @@ -55,13 +74,12 @@ This project is open source and distributed under the [GPL-3.0 license](https:// This is a list of all the main tools, libraries and frameworks, that were used in this project +- [Flutter](https://flutter.dev/) - [Firebase](https://firebase.google.com/) - [OneSignal](https://onesignal.com/) -- [Flutter](https://flutter.dev/) -- [Express.js](https://expressjs.com/) -- [PlanetScale](https://planetscale.com/) -- [Passport.js](https://www.passportjs.org/) - [Shorebird](https://shorebird.dev/) +- [Golang](https://go.dev/) +- [Gin](https://gin-gonic.com/) ## Getting Started diff --git a/SECURITY.md b/SECURITY.md index 01eec1a..b5059ec 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,25 +4,29 @@ ### App versions -| Version | API | Supported | Remote patching \* | Changelog | +| Version | API | Supported | Remote patches \* | Changelog | | ------------- | --- | ------------------ | ------------------ | ----------------------------------------------------------------------------------- | -| > 1.7.3 | β13 | :white_check_mark: | :white_check_mark: | [1.7.3...1.7.6](https://github.com/DislikesSchool/EduPage2/compare/v1.7.1...v1.7.3) | -| 1.7.1 - 1.7.3 | β13 | :white_check_mark: | :x: | [1.7.1...1.7.3](https://github.com/DislikesSchool/EduPage2/compare/v1.7.1...v1.7.3) | -| 1.7.1 | β13 | :white_check_mark: | :x: | [1.7.0...1.7.1](https://github.com/DislikesSchool/EduPage2/compare/v1.7.0...v1.7.1) | -| 1.7.0 | β12 | :warning: | :x: | [1.6.0...1.7.0](https://github.com/DislikesSchool/EduPage2/compare/v1.6.0...v1.7.0) | -| 1.6.0 | β12 | :warning: | :x: | [1.5.2...1.6.0](https://github.com/DislikesSchool/EduPage2/compare/v1.5.2...v1.6.0) | +| 1.8.2 | v1 | :white_check_mark: | :white_check_mark: | [1.8.0...1.8.2](https://github.com/DislikesSchool/EduPage2/compare/v1.8.0...v1.8.2) | +| 1.8.0 | β14 | :x: | :white_check_mark: | [1.7.9...1.8.0](https://github.com/DislikesSchool/EduPage2/compare/v1.7.9...v1.8.0) | +| 1.7.3 - 1.7.9 | β13 | :x: | :white_check_mark: | [1.7.3...1.7.9](https://github.com/DislikesSchool/EduPage2/compare/v1.7.1...v1.7.9) | +| 1.7.1 - 1.7.3 | β13 | :x: | :x: | [1.7.1...1.7.3](https://github.com/DislikesSchool/EduPage2/compare/v1.7.1...v1.7.3) | +| 1.7.1 | β13 | :x: | :x: | [1.7.0...1.7.1](https://github.com/DislikesSchool/EduPage2/compare/v1.7.0...v1.7.1) | +| 1.7.0 | β12 | :x: | :x: | [1.6.0...1.7.0](https://github.com/DislikesSchool/EduPage2/compare/v1.6.0...v1.7.0) | +| 1.6.0 | β12 | :x: | :x: | [1.5.2...1.6.0](https://github.com/DislikesSchool/EduPage2/compare/v1.5.2...v1.6.0) | | 1.5.x | β11 | :x: | :x: | [1.5.0...1.5.2](https://github.com/DislikesSchool/EduPage2/compare/v1.5.0...v1.5.2) | | < 1.5 | | :x: | :x: | | -\* Remote patching allows us to quickly push patches and bug fixes directly to your device without you having to redownload the app +\* Remote patches allow us to quickly push patches and bug fixes directly to your device without you having to redownload the app ### API version | Version | Supported | New features in version | | ------- | ------------------ | ----------------------- | -| β13 | :white_check_mark: | | -| β12 | :warning: | | -| β11 | :warning: | iCanteen setup | +| v1 | :white_check_mark: | Complete rewrite | +| β14 | :x: | | +| β13 | :x: | | +| β12 | :x: | | +| β11 | :x: | iCanteen setup | | < β11 | :x: | | :white_check_mark: - Version running on server diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart index e173c32..003e93f 100644 --- a/integration_test/app_test.dart +++ b/integration_test/app_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:eduapge2/main.dart' as app; +import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'utils.dart'; @@ -25,20 +26,38 @@ void main() { await prep(tester, username, password, name); await tester.tap(find.byType(NavigationDestination).at(1)); - await pumpUntilFound(tester, find.textContaining("Today")); - expect(find.textContaining("TODAY"), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + String day = DateFormat('d', const Locale('en', 'US').toString()) + .format(DateTime.now()); + String month = DateFormat('MMMM', const Locale('en', 'US').toString()) + .format(DateTime.now()); + + await pumpUntilFound(tester, find.textContaining("$day $month")); + expect(find.textContaining("$day $month"), findsWidgets); }); testWidgets('Test TimeTable page scroll', (tester) async { await prep(tester, username, password, name); await tester.tap(find.byType(NavigationDestination).at(1)); - await pumpUntilFound(tester, find.textContaining("Today")); - expect(find.textContaining("TODAY"), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + String day = DateFormat('d', const Locale('en', 'US').toString()) + .format(DateTime.now()); + String month = DateFormat('MMMM', const Locale('en', 'US').toString()) + .format(DateTime.now()); + + await pumpUntilFound(tester, find.textContaining("$day $month")); + expect(find.textContaining("$day $month"), findsWidgets); await tester.tap(find.byKey(const Key("TimeTableScrollForward"))); - await pumpUntilFound(tester, find.textContaining("Tomorrow")); - expect(find.textContaining("TOMORROW"), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + day = DateFormat('d', const Locale('en').toString()) + .format(DateTime.now().add(const Duration(days: 1))); + month = DateFormat('MMMM', const Locale('en').toString()) + .format(DateTime.now().add(const Duration(days: 1))); + + await pumpUntilFound(tester, find.textContaining("$day $month")); + expect(find.textContaining("$day $month"), findsWidgets); }); }); } diff --git a/lib/api.dart b/lib/api.dart new file mode 100644 index 0000000..7edb3cc --- /dev/null +++ b/lib/api.dart @@ -0,0 +1,955 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:eduapge2/timetable.dart'; +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:intl/intl.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +Future isConnected() async { + var connectivityResult = await (Connectivity().checkConnectivity()); + if (connectivityResult == ConnectivityResult.mobile) { + return true; + } else if (connectivityResult == ConnectivityResult.wifi) { + return true; + } + return false; +} + +class EP2Data { + final Dio dio = Dio(); + late SharedPreferences sharedPreferences; + + String baseUrl = ""; + late User user; + late Timeline timeline; + late TimeTable timetable; + + static EP2Data? _instance; + + EP2Data._privateConstructor(); + + static EP2Data getInstance() { + _instance ??= EP2Data._privateConstructor(); + return _instance!; + } + + Future init({ + required Function(String, double) onProgressUpdate, + required AppLocalizations local, + }) async { + onProgressUpdate(local.loadCredentials, 0.1); + + sharedPreferences = await SharedPreferences.getInstance(); + + bool quickstart = sharedPreferences.getBool("quickstart") ?? false; + + String? endpoint = sharedPreferences.getString("customEndpoint"); + if (endpoint != null && endpoint != "") { + baseUrl = endpoint; + } else { + baseUrl = FirebaseRemoteConfig.instance.getString("testUrl"); + } + + bool isInternetAvailable = await isConnected(); + + user = (await User.loadFromCache()) ?? + User( + username: sharedPreferences.getString("email") ?? "", + password: sharedPreferences.getString("password") ?? "", + server: sharedPreferences.getString("server") ?? "", + ); + + if (isInternetAvailable && !quickstart) { + if (!await user.validate()) { + onProgressUpdate(local.loadLoggingIn, 0.2); + if (!await user.login()) { + return false; + } + } + } + onProgressUpdate(local.loadLoggedIn, 0.4); + + timeline = (await Timeline.loadFromCache()) ?? + Timeline( + homeworks: {}, + items: {}, + ); + + if (isInternetAvailable && !quickstart) { + onProgressUpdate(local.loadDownloadMessages, 0.6); + await timeline.loadMessages(); + } + + timetable = (await TimeTable.loadFromCache()) ?? TimeTable(); + + if (isInternetAvailable && !quickstart) { + onProgressUpdate(local.loadDownloadTimetable, 0.8); + await timetable.loadRecentTt(); + } + + if (quickstart && isInternetAvailable) { + loadInBackground(); + } + + return true; + } + + Future loadInBackground() async { + if (!await user.validate()) { + await user.login(); + } + await timeline.loadMessages(); + await timetable.loadRecentTt(); + } +} + +class User { + final EP2Data data = EP2Data.getInstance(); + + final String username; + final String password; + String? server = ""; + + String token = ""; + String name = ""; + + User({ + required this.username, + required this.password, + this.server, + }); + + Future login() async { + try { + Response resp = await data.dio.post( + "${data.baseUrl}/login", + data: { + "username": username, + "password": password, + "server": server ?? "", + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + + token = resp.data['token']; + name = resp.data["name"]; + + saveToCache(); + return true; + } catch (e) { + return false; + } + } + + Future validate() async { + try { + Response resp = await data.dio.get( + "${data.baseUrl}/validate-token", + options: Options(headers: {"Authorization": "Bearer $token"}), + ); + + if (resp.data['success'] != true) { + return false; + } + return true; + } catch (e) { + return false; + } + } + + Map toJson() { + return { + 'username': username, + 'password': password, + 'server': server, + 'token': token, + 'name': name, + }; + } + + factory User.fromJson(Map json) { + return User( + username: json['username'], + password: json['password'], + server: json['server'], + ) + ..token = json['token'] + ..name = json['name']; + } + + Future saveToCache() async { + final prefs = await SharedPreferences.getInstance(); + final userJson = jsonEncode(toJson()); + await prefs.setString('user', userJson); + } + + static Future loadFromCache() async { + final prefs = await SharedPreferences.getInstance(); + final userJson = prefs.getString('user'); + if (userJson != null) { + return User.fromJson(jsonDecode(userJson)); + } else { + return null; + } + } +} + +class TimeTable { + final EP2Data data = EP2Data.getInstance(); + + final Map timetables = {}; + List? periods; + + Future> loadPeriods(String token) async { + if (periods != null) { + return periods!; + } + + Response periodsResponse = await data.dio.get( + "${data.baseUrl}/api/periods", + options: Options( + headers: { + "Authorization": "Bearer $token", + }, + ), + ); + + periods = []; + for (Map period in periodsResponse.data.values) { + periods!.add(TimeTablePeriod(period["id"], period["starttime"], + period["endtime"], period["name"], period["short"])); + } + + return periods!; + } + + Future loadTt(DateTime date) async { + DateTime dateOnly = DateTime(date.year, date.month, date.day); + if (timetables.containsKey(dateOnly)) { + return timetables[dateOnly]!; + } + + Response response = await data.dio.get( + "${data.baseUrl}/api/timetable?to=${DateFormat('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'en_US').format(DateTime(date.year, date.month, date.day))}&from=${DateFormat('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'en_US').format(DateTime(date.year, date.month, date.day))}", + options: Options( + headers: { + "Authorization": "Bearer ${data.user.token}", + }, + ), + ); + + List ttClasses = []; + Map lessons = response.data["Days"]; + for (Map ttLesson + in lessons.values.isEmpty ? [] : lessons.values.first) { + if (ttLesson["studentids"] != null) { + ttClasses.add(TimeTableClass.fromJson(ttLesson)); + } + } + + periods = await loadPeriods(data.user.token); + + TimeTableData t = processTimeTable(TimeTableData( + DateTime.parse(response.data["Days"].keys.isEmpty + ? date.toString() + : response.data["Days"].keys.first), + ttClasses, + periods!)); + + timetables[dateOnly] = t; + await saveToCache(); + return t; + } + + Future> loadRecentTt() async { + Response response = await data.dio.get( + "${data.baseUrl}/api/timetable/recent", + options: Options( + headers: { + "Authorization": "Bearer ${data.user.token}", + }, + ), + ); + + List recentTimetables = []; + for (MapEntry day in response.data["Days"].entries) { + List ttClasses = []; + for (Map ttLesson in day.value) { + if (ttLesson["studentids"] != null) { + ttClasses.add(TimeTableClass.fromJson(ttLesson)); + } + } + periods = await loadPeriods(data.user.token); + DateTime date = DateTime.parse(day.key); + recentTimetables + .add(processTimeTable(TimeTableData(date, ttClasses, periods!))); + DateTime dateOnly = DateTime(date.year, date.month, date.day); + timetables[dateOnly] = recentTimetables.last; + } + + await saveToCache(); + return recentTimetables; + } + + Future today() async { + return await loadTt(DateTime.now()); + } + + Map toJson() => { + 'timetables': timetables.map( + (key, value) => MapEntry(key.toIso8601String(), value.toJson())), + 'periods': periods?.map((p) => p.toJson()).toList(), + }; + + static TimeTable fromJson(Map json) { + return TimeTable() + ..timetables.addAll((json['timetables'] as Map).map( + (key, value) => MapEntry( + DateTime.parse(key), + TimeTableData.fromJson(value), + ), + )) + ..periods = (json['periods'] as List) + .map((p) => TimeTablePeriod.fromJson(p as Map)) + .toList(); + } + + Future saveToCache() async { + final prefs = await SharedPreferences.getInstance(); + prefs.setString('timetable', jsonEncode(toJson())); + } + + static Future loadFromCache() async { + final prefs = await SharedPreferences.getInstance(); + if (!prefs.containsKey('timetable')) { + return null; + } + return fromJson(jsonDecode(prefs.getString('timetable')!)); + } +} + +class TimeTableData { + TimeTableData(this.date, this.classes, this.periods); + + final DateTime date; + final List classes; + final List periods; + + Map toJson() => { + 'date': date.toIso8601String(), + 'classes': classes.map((c) => c.toJson()).toList(), + 'periods': periods.map((p) => p.toJson()).toList(), + }; + + static TimeTableData fromJson(Map json) => + processTimeTable(TimeTableData( + DateTime.parse(json['date']), + (json['classes'] as List) + .map((c) => TimeTableClass.fromJson(c as Map)) + .toList(), + (json['periods'] as List) + .map((p) => TimeTablePeriod.fromJson(p as Map)) + .toList(), + )); +} + +class TimeTablePeriod { + final String id; + final String startTime; + final String endTime; + final String name; + final String short; + + TimeTablePeriod(this.id, this.startTime, this.endTime, this.name, this.short); + + Map toJson() => { + 'id': id, + 'starttime': startTime, + 'endtime': endTime, + 'name': name, + 'short': short, + }; + + static TimeTablePeriod fromJson(Map json) => TimeTablePeriod( + json['id'], + json['starttime'], + json['endtime'], + json['name'], + json['short'], + ); +} + +class TimeTableClass { + TimeTableClass({ + this.type = "", + this.date = "", + required this.period, + required this.startTime, + required this.endTime, + this.subject, + this.classes = const [], + this.groupNames = const [], + this.iGroupId = "", + this.teachers = const [], + this.classrooms = const [], + this.studentIds = const [], + this.colors = const [], + }); + + final String type; + final String date; + final String period; + final String startTime; + final String endTime; + final Subject? subject; + final List classes; + final List groupNames; + final String iGroupId; + final List teachers; + final List classrooms; + final List studentIds; + final List colors; + TimeTablePeriod? startPeriod; + TimeTablePeriod? endPeriod; + + Map toJson() => { + 'type': type, + 'date': date, + 'uniperiod': period, + 'starttime': startTime, + 'endtime': endTime, + 'subject': subject?.toJson(), + 'classes': classes.map((c) => c.toJson()).toList(), + 'groupnames': groupNames, + 'igroupid': iGroupId, + 'teachers': teachers.map((t) => t.toJson()).toList(), + 'classrooms': classrooms.map((c) => c.toJson()).toList(), + 'studentids': studentIds, + 'colors': colors, + }; + + static TimeTableClass fromJson(Map json) => TimeTableClass( + type: json['type'], + date: json['date'], + period: json['uniperiod'], + startTime: json['starttime'], + endTime: json['endtime'], + subject: Subject.fromJson(json['subject'] ?? + {"id": "", "name": "", "short": "", "cbhidden": false}), + classes: (json['classes'] as List) + .map((c) => Class.fromJson(c as Map)) + .toList(), + groupNames: List.from(json['groupnames']), + iGroupId: json['igroupid'], + teachers: (json['teachers'] as List) + .map((t) => Teacher.fromJson(t as Map)) + .toList(), + classrooms: (json['classrooms'] as List) + .map((c) => Classroom.fromJson(c as Map)) + .toList(), + studentIds: List.from(json['studentids']), + colors: List.from(json['colors'] ?? []), + ); +} + +class Teacher { + Teacher({ + required this.id, + required this.firstName, + required this.lastName, + required this.short, + required this.gender, + required this.classroomId, + required this.dateFrom, + required this.dateTo, + required this.isOut, + }); + + final String id; + final String firstName; + final String lastName; + final String short; + final String gender; + final String classroomId; + final String dateFrom; + final String dateTo; + final bool isOut; + + Map toJson() => { + 'id': id, + 'firstname': firstName, + 'lastname': lastName, + 'short': short, + 'gender': gender, + 'classroomid': classroomId, + 'datefrom': dateFrom, + 'dateto': dateTo, + 'isout': isOut, + }; + + static Teacher fromJson(Map json) => Teacher( + id: json['id'], + firstName: json['firstname'], + lastName: json['lastname'], + short: json['short'], + gender: json['gender'], + classroomId: json['classroomid'], + dateFrom: json['datefrom'], + dateTo: json['dateto'], + isOut: json['isout'], + ); +} + +class Class { + Class({ + required this.id, + required this.name, + required this.short, + required this.grade, + required this.teacherId, + required this.teacher2Id, + required this.classroomId, + }); + + final String id; + final String name; + final String short; + final String grade; + final String teacherId; + final String teacher2Id; + final String classroomId; + + Map toJson() => { + 'id': id, + 'name': name, + 'short': short, + 'grade': grade, + 'teacherid': teacherId, + 'teacher2id': teacher2Id, + 'classroomid': classroomId, + }; + + static Class fromJson(Map json) => Class( + id: json['id'], + name: json['name'], + short: json['short'], + grade: json['grade'], + teacherId: json['teacherid'], + teacher2Id: json['teacher2id'], + classroomId: json['classroomid'], + ); +} + +class Subject { + Subject({ + required this.id, + required this.name, + required this.short, + required this.cbHidden, + }); + + final String id; + final String name; + final String short; + final bool cbHidden; + + Map toJson() => { + 'id': id, + 'name': name, + 'short': short, + 'cbhidden': cbHidden, + }; + + static Subject fromJson(Map json) => Subject( + id: json['id'], + name: json['name'], + short: json['short'], + cbHidden: json['cbhidden'], + ); +} + +class Classroom { + Classroom({ + required this.id, + required this.name, + required this.short, + }); + + final String id; + final String name; + final String short; + + Map toJson() => { + 'id': id, + 'name': name, + 'short': short, + }; + + static Classroom fromJson(Map json) => Classroom( + id: json['id'], + name: json['name'], + short: json['short'], + ); +} + +class TimelineItem { + final String id; + final DateTime timestamp; + final String reactionTo; + final String type; + final String user; + final String targetUser; + final String userName; + final String otherId; + final String text; + final DateTime timeAdded; + final DateTime timeEvent; + final Map data; + final String owner; + final String ownerName; + final int reactionCount; + final String lastReaction; + final String pomocnyZaznam; + final num removed; + final DateTime timeAddedBTC; + final DateTime lastReactionBTC; + + TimelineItem({ + required this.id, + required this.timestamp, + required this.reactionTo, + required this.type, + required this.user, + required this.targetUser, + required this.userName, + required this.otherId, + required this.text, + required this.timeAdded, + required this.timeEvent, + required this.data, + required this.owner, + required this.ownerName, + required this.reactionCount, + required this.lastReaction, + required this.pomocnyZaznam, + required this.removed, + required this.timeAddedBTC, + required this.lastReactionBTC, + }); + + factory TimelineItem.fromJson(Map json) { + return TimelineItem( + id: json['timelineid'], + timestamp: DateTime.parse(json['timestamp']), + reactionTo: json['reakcia_na'], + type: json['typ'], + user: json['user'], + targetUser: json['target_user'], + userName: json['user_meno'], + otherId: json['ineid'], + text: json['text'], + timeAdded: DateTime.parse(json['cas_pridania']), + timeEvent: DateTime.parse(json['cas_udalosti']), + data: Map.from(json['data']), + owner: json['vlastnik'], + ownerName: json['vlastnik_meno'], + reactionCount: json['poct_reakcii'], + lastReaction: json['posledna_reakcia'], + pomocnyZaznam: json['pomocny_zaznam'], + removed: json['removed'], + timeAddedBTC: DateTime.parse(json['cas_pridania_btc']), + lastReactionBTC: DateTime.parse(json['cas_udalosti_btc']), + ); + } + + Map toJson() { + return { + 'timelineid': id, + 'timestamp': timestamp.toIso8601String(), + 'reakcia_na': reactionTo, + 'typ': type, + 'user': user, + 'target_user': targetUser, + 'user_meno': userName, + 'ineid': otherId, + 'text': text, + 'cas_pridania': timeAdded.toIso8601String(), + 'cas_udalosti': timeEvent.toIso8601String(), + 'data': data, + 'vlastnik': owner, + 'vlastnik_meno': ownerName, + 'poct_reakcii': reactionCount, + 'posledna_reakcia': lastReaction, + 'pomocny_zaznam': pomocnyZaznam, + 'removed': removed, + 'cas_pridania_btc': timeAddedBTC.toIso8601String(), + 'cas_udalosti_btc': lastReactionBTC.toIso8601String(), + }; + } +} + +class Homework { + final String id; + final String homeworkId; + final String eSuperId; + final String userId; + final num lessonId; + final String planId; + final String name; + final String details; + final String dateTo; + final String dateFrom; + final String datetimeTo; + final String datetimeFrom; + final String dateCreated; + final dynamic period; + final String timestamp; + final String testId; + final String type; + final num likeCount; + final num reactionCount; + final num doneCount; + final String state; + final String lastResult; + final List groups; + final int eTestCards; + final int eTestAnswerCards; + final bool studyTopics; + final dynamic gradeEventId; + final String studentsHidden; + final Map data; + final String evaluationStatus; + final dynamic ended; + final bool missingNextLesson; + final dynamic attachments; + final String authorName; + final String lessonName; + + Homework({ + required this.id, + required this.homeworkId, + required this.eSuperId, + required this.userId, + required this.lessonId, + required this.planId, + required this.name, + required this.details, + required this.dateTo, + required this.dateFrom, + required this.datetimeTo, + required this.datetimeFrom, + required this.dateCreated, + required this.period, + required this.timestamp, + required this.testId, + required this.type, + required this.likeCount, + required this.reactionCount, + required this.doneCount, + required this.state, + required this.lastResult, + required this.groups, + required this.eTestCards, + required this.eTestAnswerCards, + required this.studyTopics, + required this.gradeEventId, + required this.studentsHidden, + required this.data, + required this.evaluationStatus, + required this.ended, + required this.missingNextLesson, + required this.attachments, + required this.authorName, + required this.lessonName, + }); + + factory Homework.fromJson(Map json) { + return Homework( + id: json['hwkid'], + homeworkId: json['homeworkid'], + eSuperId: json['e_superid'], + userId: json['userid'], + lessonId: json['predmetid'], + planId: json['planid'], + name: json['name'], + details: json['details'], + dateTo: json['dateto'], + dateFrom: json['datefrom'], + datetimeTo: json['datetimeto'], + datetimeFrom: json['datetimefrom'], + dateCreated: json['datecreated'], + period: json['period'], + timestamp: json['timestamp'], + testId: json['testid'], + type: json['typ'], + likeCount: json['pocet_like'], + reactionCount: json['pocet_reakcii'], + doneCount: json['pocet_done'], + state: json['stav'], + lastResult: json['posledny_vysledok'], + groups: List.from(json['skupiny']), + eTestCards: json['etestCards'], + eTestAnswerCards: json['etestAnswerCards'], + studyTopics: json['studyTopics'], + gradeEventId: json['znamky_udalostid'], + studentsHidden: json['students_hidden'], + data: Map.from(json['data']), + evaluationStatus: json['stavhodnotetimelinePathd'], + ended: json['skoncil'], + missingNextLesson: json['missingNextLesson'], + attachments: json['attachements'], + authorName: json['autor_meno'], + lessonName: json['predmet_meno'], + ); + } + + Map toJson() { + return { + 'hwkid': id, + 'homeworkid': homeworkId, + 'e_superid': eSuperId, + 'userid': userId, + 'predmetid': lessonId, + 'planid': planId, + 'name': name, + 'details': details, + 'dateto': dateTo, + 'datefrom': dateFrom, + 'datetimeto': datetimeTo, + 'datetimefrom': datetimeFrom, + 'datecreated': dateCreated, + 'period': period, + 'timestamp': timestamp, + 'testid': testId, + 'typ': type, + 'pocet_like': likeCount, + 'pocet_reakcii': reactionCount, + 'pocet_done': doneCount, + 'stav': state, + 'posledny_vysledok': lastResult, + 'skupiny': groups, + 'etestCards': eTestCards, + 'etestAnswerCards': eTestAnswerCards, + 'studyTopics': studyTopics, + 'znamky_udalostid': gradeEventId, + 'students_hidden': studentsHidden, + 'data': data, + 'stavhodnotetimelinePathd': evaluationStatus, + 'skoncil': ended, + 'missingNextLesson': missingNextLesson, + 'attachements': attachments, + 'autor_meno': authorName, + 'predmet_meno': lessonName, + }; + } +} + +class Timeline { + EP2Data data = EP2Data.getInstance(); + + Map homeworks; + Map items; + + Timeline({ + required this.homeworks, + required this.items, + }); + + factory Timeline.fromJson(Map json) { + return Timeline( + homeworks: (json['Homeworks'] as Map).map( + (key, value) => MapEntry(key, Homework.fromJson(value)), + ), + items: (json['Items'] as Map).map( + (key, value) => MapEntry(key, TimelineItem.fromJson(value)), + ), + ); + } + + Map toJson() { + return { + 'Homeworks': homeworks.map((key, value) => MapEntry(key, value.toJson())), + 'Items': items.map((key, value) => MapEntry(key, value.toJson())), + }; + } + + Future saveToCache() async { + final timelineJson = jsonEncode(toJson()); + await data.sharedPreferences.setString('timeline', timelineJson); + } + + static Future loadFromCache() async { + final prefs = await SharedPreferences.getInstance(); + final timelineJson = prefs.getString('timeline'); + if (timelineJson != null) { + return Timeline.fromJson(jsonDecode(timelineJson)); + } else { + return null; + } + } + + Future loadMessages() async { + Response response = await data.dio.get( + "${data.baseUrl}/api/timeline/recent", + options: Options( + headers: { + "Authorization": "Bearer ${data.user.token}", + }, + ), + ); + + Map newHomeworks = response.data["Homeworks"]; + Map newItems = response.data["Items"]; + + newHomeworks.forEach((key, value) { + homeworks[key] = Homework.fromJson(value); + }); + + newItems.forEach((key, value) { + items[key] = TimelineItem.fromJson(value); + }); + + await saveToCache(); + } + + Future loadOlderMessages() async { + DateTime oldestTimestamp = + items.values.fold(DateTime.now(), (oldest, item) { + DateTime timestamp = item.timestamp; + return timestamp.isBefore(oldest) ? timestamp : oldest; + }); + + // Calculate from and to dates + DateTime from = oldestTimestamp.subtract(const Duration(days: 14)); + DateTime to = oldestTimestamp; + + // Add query parameters for from and to dates + Response response = await data.dio.get( + "${data.baseUrl}/api/timeline", + queryParameters: { + "from": from.toIso8601String(), + "to": to.toIso8601String(), + }, + options: Options( + headers: { + "Authorization": "Bearer ${data.user.token}", + }, + ), + ); + + Map newHomeworks = response.data["Homeworks"]; + Map newItems = response.data["Items"]; + + newHomeworks.forEach((key, value) { + homeworks[key] = Homework.fromJson(value); + }); + + newItems.forEach((key, value) { + items[key] = TimelineItem.fromJson(value); + }); + + await saveToCache(); + } +} diff --git a/lib/home.dart b/lib/home.dart index 1bd8792..4da371c 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -2,19 +2,16 @@ import 'dart:async'; import 'dart:convert'; import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; +import 'package:eduapge2/api.dart'; import 'package:eduapge2/icanteen_setup.dart'; import 'package:eduapge2/message.dart'; import 'package:eduapge2/messages.dart'; -import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_session_manager/flutter_session_manager.dart'; -import 'package:package_info/package_info.dart'; -import 'package:restart_app/restart_app.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:shorebird_code_push/shorebird_code_push.dart'; import 'package:url_launcher/url_launcher.dart'; class HomePage extends StatefulWidget { @@ -74,6 +71,11 @@ extension TimeOfDayExtension on TimeOfDay { return false; } } + + static TimeOfDay fromString(String timeString) { + List split = timeString.split(':'); + return TimeOfDay(hour: int.parse(split[0]), minute: int.parse(split[1])); + } } extension DateTimeExtension on DateTime { @@ -88,7 +90,8 @@ extension DateTimeExtension on DateTime { } } -LessonStatus getLessonStatus(List lessons, TimeOfDay currentTime) { +LessonStatus getLessonStatus( + List lessons, TimeOfDay currentTime) { // Check if the user has any lessons today final hasLessonsToday = lessons.isNotEmpty; @@ -96,9 +99,9 @@ LessonStatus getLessonStatus(List lessons, TimeOfDay currentTime) { final hasLesson = hasLessonsToday && lessons.any((lesson) { final startTime = TimeOfDay.fromDateTime( - DateTimeExtension.parseTime(lesson['period']['startTime'])); - final endTime = TimeOfDay.fromDateTime( - DateTimeExtension.parseTime(lesson['period']['endTime'])); + DateTimeExtension.parseTime(lesson.startTime)); + final endTime = + TimeOfDay.fromDateTime(DateTimeExtension.parseTime(lesson.endTime)); return startTime < endTime && startTime <= currentTime && endTime > currentTime; @@ -110,23 +113,21 @@ LessonStatus getLessonStatus(List lessons, TimeOfDay currentTime) { if (hasLesson) { final currentLesson = lessons.firstWhere((lesson) { final startTime = TimeOfDay.fromDateTime( - DateTimeExtension.parseTime(lesson['period']['startTime'])); - final endTime = TimeOfDay.fromDateTime( - DateTimeExtension.parseTime(lesson['period']['endTime'])); + DateTimeExtension.parseTime(lesson.startTime)); + final endTime = + TimeOfDay.fromDateTime(DateTimeExtension.parseTime(lesson.endTime)); return startTime < endTime && startTime <= currentTime && endTime > currentTime; }); - nextLessonTime = - DateTimeExtension.parseTime(currentLesson['period']['endTime']); + nextLessonTime = DateTimeExtension.parseTime(currentLesson.endTime); } else if (hasLessonsToday) { final nextLesson = lessons.firstWhere((lesson) { final startTime = TimeOfDay.fromDateTime( - DateTimeExtension.parseTime(lesson['period']['startTime'])); + DateTimeExtension.parseTime(lesson.startTime)); return startTime > currentTime; }); - nextLessonTime = - DateTimeExtension.parseTime(nextLesson['period']['startTime']); + nextLessonTime = DateTimeExtension.parseTime(nextLesson.startTime); } else { nextLessonTime = DateTime.now(); } @@ -144,99 +145,25 @@ LessonStatus getLessonStatus(List lessons, TimeOfDay currentTime) { } } -final _shorebirdCodePush = ShorebirdCodePush(); - class HomePageState extends State { final GlobalKey scaffoldKey = GlobalKey(); - late SharedPreferences sharedPreferences; - String baseUrl = FirebaseRemoteConfig.instance.getString("baseUrl"); - late Response response; - Dio dio = Dio(); - - bool error = false; //for error status - bool loading = true; //for data featching status - String errmsg = ""; //to assing any error message from API/runtime - dynamic apidata; //for decoded JSON data - bool refresh = false; + SharedPreferences? sharedPreferences; + bool updateAvailable = false; bool quickstart = false; - bool _isCheckingForUpdate = false; - late Map apidataTT; - List apidataMsg = []; - late String username; - late LessonStatus _lessonStatus; + List apidataMsg = []; + String username = ""; + LessonStatus _lessonStatus = LessonStatus( + hasLessonsToday: false, hasLesson: false, nextLessonTime: DateTime.now()); Timer? _timer; + TimeTableData t = TimeTableData(DateTime.now(), [], []); @override void initState() { super.initState(); - dio.interceptors - .add(DioCacheManager(CacheConfig(baseUrl: baseUrl)).interceptor); + getData(); fetchAndCompareBuildName(); - getData(); //fetching data - if (!_isCheckingForUpdate) _checkForUpdate(); // ik that it's not necessary - } - - Future _checkForUpdate() async { - setState(() { - _isCheckingForUpdate = true; - }); - - // Ask the Shorebird servers if there is a new patch available. - final isUpdateAvailable = - await _shorebirdCodePush.isNewPatchAvailableForDownload(); - - if (!mounted) return; - - setState(() { - _isCheckingForUpdate = false; - }); - - if (isUpdateAvailable) { - _downloadUpdate(); - } - } - - void _showDownloadingBanner() { - ScaffoldMessenger.of(context).showMaterialBanner( - const MaterialBanner( - content: Text('Downloading patch...'), - actions: [ - SizedBox( - height: 14, - width: 14, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - ], - ), - ); - } - - void _showRestartBanner() { - ScaffoldMessenger.of(context).showMaterialBanner( - const MaterialBanner( - content: Text('A new patch is ready!'), - actions: [ - TextButton( - // Restart the app for the new patch to take effect. - onPressed: Restart.restartApp, - child: Text('Restart app'), - ), - ], - ), - ); - } - - Future _downloadUpdate() async { - _showDownloadingBanner(); - await _shorebirdCodePush.downloadUpdateIfAvailable(); - if (!mounted) return; - - ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); - _showRestartBanner(); } @override @@ -254,55 +181,24 @@ class HomePageState extends State { } getData() async { - setState(() { - loading = true; - }); sharedPreferences = await SharedPreferences.getInstance(); - quickstart = sharedPreferences.getBool('quickstart') ?? false; - var msgs = await widget.sessionManager.get('messages'); - if (msgs != Null && msgs != null) { - setState(() { - apidataMsg = msgs; - }); - } + quickstart = sharedPreferences?.getBool('quickstart') ?? false; + apidataMsg = EP2Data.getInstance().timeline.items.values.toList(); + username = EP2Data.getInstance().user.name; - Map? user = await widget.sessionManager.get('user'); - if (user == null) { - apidataTT = {}; - setState(() { - loading = false; - }); - return; - } - username = user["firstname"] + " " + user["lastname"]; - String token = sharedPreferences.getString("token")!; - - Response response = await dio.get( - "$baseUrl/timetable/${getWeekDay().toString()}", - options: buildCacheOptions( - Duration.zero, - maxStale: const Duration(days: 7), - options: Options( - headers: { - "Authorization": "Bearer $token", - }, - ), - ), - ); - apidataTT = jsonDecode(response.data); - _lessonStatus = getLessonStatus(apidataTT["lessons"], TimeOfDay.now()); + t = await EP2Data.getInstance().timetable.today(); + + _lessonStatus = getLessonStatus(t.classes, TimeOfDay.now()); if (_lessonStatus.hasLessonsToday) { _startTimer(); } - setState(() { - loading = false; - }); //refresh UI + setState(() {}); //refresh UI } void _startTimer() { _timer = Timer.periodic(const Duration(seconds: 1), (_) { setState(() { - _lessonStatus = getLessonStatus(apidataTT["lessons"], TimeOfDay.now()); + _lessonStatus = getLessonStatus(t.classes, TimeOfDay.now()); if (!_lessonStatus.hasLessonsToday) { _timer?.cancel(); } @@ -356,23 +252,20 @@ class HomePageState extends State { Widget build(BuildContext context) { AppLocalizations? local = AppLocalizations.of(context); ThemeData theme = Theme.of(context); - if (loading) { - return Center( - child: Text(local!.loading), - ); - } int lunch = -1; DateTime orderLunchesFor = DateTime(1998, 4, 10); - String? l = sharedPreferences.getString("lunches"); + String? l = sharedPreferences?.getString("lunches"); if (l != null) { var lunches = jsonDecode(l) as List; if (lunches.isNotEmpty) { var lunchToday = lunches[0] as Map; - lunch = 0; - var todayLunches = lunchToday["lunches"]; - for (int i = 0; i < todayLunches.length; i++) { - if (todayLunches[i]["ordered"]) lunch = i + 1; + if (DateTime.parse(lunchToday["day"]).day != DateTime.now().day) { + lunch = 0; + var todayLunches = lunchToday["lunches"]; + for (int i = 0; i < todayLunches.length; i++) { + if (todayLunches[i]["ordered"]) lunch = i + 1; + } } for (Map li in lunches) { bool canOrder = false; @@ -386,22 +279,23 @@ class HomePageState extends State { } } if (canOrder && !hasOrdered) { - orderLunchesFor = DateTime.parse(li["day"]); + DateTime parsed = DateTime.parse(li["day"]); + orderLunchesFor = DateTime(parsed.year, parsed.month, parsed.day); break; } } } } - List msgs = - apidataMsg.where((msg) => msg["type"] == "sprava").toList(); - List msgsWOR = List.from(msgs); + List msgs = + apidataMsg.where((msg) => msg.type == "sprava").toList(); + List msgsWOR = List.from(msgs); List> bump = []; - for (Map msg in msgs) { - if (msg["replyOf"] != null) { + for (TimelineItem msg in msgs) { + if (msg.reactionTo != "") { if (!bump.any((element) => - element["id"]!.compareTo(int.parse(msg["replyOf"])) == 0)) { + element["id"]!.compareTo(int.parse(msg.reactionTo)) == 0)) { bump.add( - {"id": int.parse(msg["replyOf"]), "index": msgsWOR.indexOf(msg)}); + {"id": int.parse(msg.reactionTo), "index": msgsWOR.indexOf(msg)}); msgsWOR.remove(msg); } else { msgsWOR.remove(msg); @@ -409,9 +303,12 @@ class HomePageState extends State { } } for (Map b in bump) { + if (!msgsWOR.any((element) => element.id == b["ineid"].toString())) { + continue; + } msgsWOR.move( msgsWOR.indexOf(msgsWOR - .firstWhere((element) => int.parse(element["id"]) == b["id"])), + .firstWhere((element) => int.parse(element.id) == b["id"])), b["index"]!); } final remainingTime = @@ -480,7 +377,7 @@ class HomePageState extends State { ], ), ), - if (apidataTT["lessons"].length > 0) + if (t.classes.isNotEmpty) Container( width: MediaQuery.of(context).size.width, margin: const EdgeInsets.only(left: 20, right: 20, top: 10), @@ -491,12 +388,11 @@ class HomePageState extends State { Card( elevation: 5, child: SizedBox( - height: 100, + height: 110, child: ListView( scrollDirection: Axis.horizontal, children: [ - for (Map lesson - in apidataTT["lessons"]) + for (TimeTableClass ttclass in t.classes) GestureDetector( onTap: () { widget.onDestinationSelected(1); @@ -505,24 +401,48 @@ class HomePageState extends State { child: Padding( padding: const EdgeInsets.all(10), child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Text( - lesson["period"]["name"] + ".", - style: - const TextStyle(fontSize: 10), - ), - Text( - lesson["subject"]["short"], - style: - const TextStyle(fontSize: 20), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = int.tryParse( + ttclass.startPeriod! + .id) ?? + 0; + i <= + (int.tryParse(ttclass + .endPeriod! + .id) ?? + 0); + i++) + Text( + "$i${i != int.tryParse(ttclass.endPeriod!.id) ? " - " : ""}", + style: const TextStyle( + fontSize: 10, + color: Colors.grey), + ), + ], ), + if (ttclass.subject != null) + Text( + ttclass.subject!.short, + style: const TextStyle( + fontSize: 22), + ), + for (Classroom classroom + in ttclass.classrooms) + Text( + classroom.short, + style: const TextStyle( + fontSize: 14), + ), + const SizedBox(height: 2), Text( - lesson["classrooms"].length > 0 - ? lesson["classrooms"][0] - ["short"] - : "?", - style: - const TextStyle(fontSize: 14), + "${ttclass.startTime} - ${ttclass.endTime}", + style: const TextStyle( + fontSize: 10, + color: Colors.grey), ), ], ), @@ -586,7 +506,7 @@ class HomePageState extends State { ), ), ), - if (lunch != -1 && apidataTT["lessons"].length > 0) + if (lunch != -1) Container( width: MediaQuery.of(context).size.width, margin: const EdgeInsets.only(left: 20, right: 20, top: 10), @@ -632,9 +552,10 @@ class HomePageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - for (Map m in msgsWOR.length < 5 + for (TimelineItem m in msgsWOR.length < 5 ? msgsWOR - : msgsWOR.getRange(0, 4)) + : msgsWOR.getRange( + msgsWOR.length - 5, msgsWOR.length)) InkWell( highlightColor: Colors.transparent, splashColor: Colors.transparent, @@ -646,7 +567,7 @@ class HomePageState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: Text( - '${m["owner"]["firstname"]?.trim()} ${m["owner"]["lastname"]?.trim()}: ${m["text"]}' + '${m.ownerName.trim()}: ${m.text}' .replaceAll(RegExp(r'\s+'), ' '), softWrap: false, overflow: TextOverflow.ellipsis, @@ -663,7 +584,7 @@ class HomePageState extends State { builder: (context) => MessagePage( sessionManager: widget.sessionManager, - id: int.parse(m["id"])))); + id: int.parse(m.id)))); }, ), ], @@ -682,18 +603,39 @@ class HomePageState extends State { const Padding( padding: EdgeInsets.only(top: 15), ), + InkWell( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: ListTile( + leading: const Icon(Icons.lunch_dining_rounded), + title: Text(local!.homeSetupICanteen), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ICanteenSetupScreen( + sessionManager: widget.sessionManager, + loadedCallback: () { + widget.reLogin(); + }, + ), + ), + ); + }, + ), + ), InkWell( highlightColor: Colors.transparent, splashColor: Colors.transparent, child: ListTile( leading: const Icon(Icons.bolt_rounded), - title: Text(local!.homeQuickstart), + title: Text(local.homeQuickstart), trailing: Transform.scale( scale: 0.75, child: Switch( value: quickstart, onChanged: (bool value) { - sharedPreferences.setBool('quickstart', value); + sharedPreferences?.setBool('quickstart', value); setState(() { quickstart = value; }); @@ -701,68 +643,14 @@ class HomePageState extends State { ), ), onTap: () { - sharedPreferences.setBool('quickstart', !quickstart); + sharedPreferences?.setBool('quickstart', !quickstart); setState(() { quickstart = !quickstart; }); }, ), ), - /* - const Divider(), - ListTile( - leading: const Icon(Icons.language), - title: const Text('Language'), - trailing: SizedBox( - height: 32, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(4), - ), - padding: const EdgeInsets.symmetric(horizontal: 8), - child: DropdownButton( - value: Localizations.localeOf(context), - onChanged: (Locale? locale) { - if (locale != null) { - // Handle locale selection - } - }, - icon: const Icon(Icons.arrow_drop_down), - underline: Container(), - style: Theme.of(context).textTheme.titleMedium, - items: AppLocalizations.supportedLocales - .map((locale) => DropdownMenuItem( - value: locale, - child: Text(locale.languageCode), - )) - .toList(), - ), - ), - ), - ),*/ const Divider(), - InkWell( - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - child: ListTile( - leading: const Icon(Icons.lunch_dining_rounded), - title: Text(local.homeSetupICanteen), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ICanteenSetupScreen( - sessionManager: widget.sessionManager, - loadedCallback: () { - widget.reLogin(); - }, - ), - ), - ); - }, - ), - ), InkWell( highlightColor: Colors.transparent, splashColor: Colors.transparent, @@ -770,9 +658,9 @@ class HomePageState extends State { leading: const Icon(Icons.logout), title: Text(local.homeLogout), onTap: () { - sharedPreferences.remove('email'); - sharedPreferences.remove('password'); - sharedPreferences.remove('token'); + sharedPreferences?.remove('email'); + sharedPreferences?.remove('password'); + sharedPreferences?.remove('token'); widget.reLogin(); }, ), diff --git a/lib/homework.dart b/lib/homework.dart index 2a980d5..dd74e03 100644 --- a/lib/homework.dart +++ b/lib/homework.dart @@ -1,3 +1,4 @@ +import 'package:eduapge2/api.dart'; import 'package:flutter/material.dart'; import 'package:flutter_session_manager/flutter_session_manager.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -13,13 +14,13 @@ class HomeworkPage extends StatefulWidget { class HomeworkPageState extends State { bool loading = true; + bool loaded = false; late List apidataMsg; late Widget messages; @override void initState() { - getData(); //fetching data super.initState(); } @@ -34,8 +35,8 @@ class HomeworkPageState extends State { loading = true; //make loading true to show progressindicator }); - apidataMsg = await widget.sessionManager.get('messages'); - messages = getMessages(apidataMsg); + messages = + getMessages(EP2Data.getInstance().timeline.homeworks.values.toList()); loading = false; setState(() {}); //refresh UI @@ -43,6 +44,10 @@ class HomeworkPageState extends State { @override Widget build(BuildContext context) { + if (!loaded) { + loaded = true; + getData(); + } return Scaffold( appBar: AppBar( toolbarHeight: 0, @@ -60,35 +65,21 @@ class HomeworkPageState extends State { setState(() { loading = true; //make loading true to show progressindicator }); - - apidataMsg = await widget.sessionManager.get('messages'); - messages = getMessages(apidataMsg); + messages = + getMessages(EP2Data.getInstance().timeline.homeworks.values.toList()); loading = false; setState(() {}); //refresh UI } - Widget getMessages(var apidataMsg) { + Widget getMessages(List apidataMsg) { List rows = []; - apidataMsg ??= [ - { - "type": "testpridelenie", - "title": "Načítání...", - "text": "Nebude to trvat dlouho", - } - ]; - apidataMsg = apidataMsg - .where((msg) => - msg["type"] == "testpridelenie" || msg["type"] == "homework") - .toList(); - for (Map msg in apidataMsg) { + for (Homework msg in apidataMsg) { String textAsTitle = "This isn't supposed to happen..."; - if (msg.keys.contains("text") && msg["text"] != null) { - textAsTitle = msg["text"]; - } else if (msg.keys.contains("assignment") && msg["assignment"] != null) { - textAsTitle = msg["assignment"]["title"]; + if (msg.name != "") { + textAsTitle = msg.name; } else { - break; + continue; } rows.add(Card( child: Padding( @@ -98,13 +89,11 @@ class HomeworkPageState extends State { children: [ Row( children: [ - Text(msg["owner"]["firstname"] + - " " + - msg["owner"]["lastname"]), + Text(msg.authorName), const Icon(Icons.arrow_right_rounded), Expanded( child: Text( - msg["title"], + msg.lessonName, overflow: TextOverflow.fade, maxLines: 5, softWrap: false, @@ -117,7 +106,7 @@ class HomeworkPageState extends State { Expanded( child: Text( textAsTitle, - style: const TextStyle(fontSize: 10), + style: const TextStyle(fontSize: 12), overflow: TextOverflow.fade, maxLines: 5, softWrap: false, diff --git a/lib/icanteen.dart b/lib/icanteen.dart index 801543e..7794e4c 100644 --- a/lib/icanteen.dart +++ b/lib/icanteen.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; +import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter_session_manager/flutter_session_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -27,7 +27,7 @@ class ICanteenPageState extends State { Dio dio = Dio(); - String baseUrl = "https://lobster-app-z6jfk.ondigitalocean.app/api"; + String baseUrl = FirebaseRemoteConfig.instance.getString("testUrl"); bool loading = true; List lunches = []; @@ -46,31 +46,24 @@ class ICanteenPageState extends State { getData() async { sharedPreferences = await SharedPreferences.getInstance(); - dio.interceptors - .add(DioCacheManager(CacheConfig(baseUrl: baseUrl)).interceptor); setState(() { loading = true; //make loading true to show progressindicator }); - String token = sharedPreferences.getString("token")!; - Response response = await dio - .get( - "$baseUrl/lunches", - options: buildCacheOptions( - const Duration(days: 0), - forceRefresh: true, - options: Options( - headers: { - "Authorization": "Bearer $token", - }, - ), - ), + .post( + "$baseUrl/icanteen", + data: { + "username": sharedPreferences.getString("ic_email"), + "password": sharedPreferences.getString("ic_password"), + "server": sharedPreferences.getString("ic_server"), + }, + options: Options(contentType: Headers.formUrlEncodedContentType), ) .catchError((obj) { return Response( - requestOptions: RequestOptions(path: "$baseUrl/lunches"), + requestOptions: RequestOptions(path: "$baseUrl/icanteen"), statusCode: 500, ); }); diff --git a/lib/icanteen_setup.dart b/lib/icanteen_setup.dart index 9539c76..b6c3756 100644 --- a/lib/icanteen_setup.dart +++ b/lib/icanteen_setup.dart @@ -1,4 +1,3 @@ -import 'package:dio/dio.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter_session_manager/flutter_session_manager.dart'; @@ -21,9 +20,7 @@ class ICanteenSetupScreenState extends State { late SessionManager sessionManager; late SharedPreferences sharedPreferences; - Dio dio = Dio(); - - String baseUrl = FirebaseRemoteConfig.instance.getString("baseUrl"); + String baseUrl = FirebaseRemoteConfig.instance.getString("testUrl"); AppLocalizations? local; @@ -36,20 +33,9 @@ class ICanteenSetupScreenState extends State { setState(() { hasLogin = true; }); - String? token = sharedPreferences.getString("token"); - await dio.post( - "$baseUrl/set_icanteen", - data: { - 'email': email, - 'password': password, - 'server': server, - }, - options: Options( - headers: { - "Authorization": "Bearer $token", - }, - ), - ); + sharedPreferences.setString("ic_server", server); + sharedPreferences.setString("ic_email", email); + sharedPreferences.setString("ic_password", password); sharedPreferences.setBool("ice", true); } diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 9be6d6a..db476f3 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -90,10 +90,13 @@ "@loginPassword": { "description": "User's password" }, + "loginServer": "Server (např. skola.edupage.org)", "loginLogin": "Přihlásit se", "@loginLogin": { "description": "Login button" }, + "loginCustomEndpointCheckbox": "Použít valstní endpoint", + "loginCustomEndpoint": "URL vlastního endpointu", "today": "Dnes", "tomorrow": "Zítra", "loadCredentials": "Načítání přihlašovacích údajů", @@ -111,5 +114,14 @@ "iCanteenSetupServer": "Adresa URL", "iCanteenSetupEmail": "Přihlašovací jméno", "iCanteenSetupPassword": "Heslo", - "messagesLoadingAttachment": "Načítání pdf souboru..." + "messagesLoadingAttachment": "Načítání pdf souboru...", + "messagesAttachments": "{count, plural, =1{1 Přípona} few{{count} Přípony} other{{count} Přípon}}", + "@messagesAttachments": { + "placeholders": { + "count": { + "type": "num", + "format": "compact" + } + } + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f2678b5..7699bed 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -25,7 +25,10 @@ "loginUseExistingCredentials": "Use your existing EduPage credentials", "loginUsername": "Username", "loginPassword": "Password", + "loginServer": "Server (e.g., school.edupage.org)", "loginLogin": "Login", + "loginCustomEndpointCheckbox": "Use custom endpoint", + "loginCustomEndpoint": "Enter custom endpoint URL", "today": "Today", "tomorrow": "Tomorrow", "loadCredentials": "Loading credentials...", @@ -43,5 +46,14 @@ "iCanteenSetupServer": "Server address", "iCanteenSetupEmail": "Username", "iCanteenSetupPassword": "Password", - "messagesLoadingAttachment": "Loading pdf..." + "messagesLoadingAttachment": "Loading pdf...", + "messagesAttachments": "{count, plural, =1{1 Attachment} other{{count} Attachments}}", + "@messagesAttachments": { + "placeholders": { + "count": { + "type": "num", + "format": "compact" + } + } + } } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8a16bed..0bb320f 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1,47 +1,127 @@ { "loading": "Načítavam", + "@loading": { + "description": "When loading a page" + }, "mainHome": "Domov", - "mainTimetable": "Rozvrh hodín", + "@mainHome": { + "description": "Label for home page button" + }, + "mainTimetable": "Rozvrh", + "@mainTimetable": { + "description":"Label for time table page button" + }, "mainICanteen": "iCanteen", + "@mainICanteen": { + "description":"Label for iCanteen page button" + }, "mainMessages": "Správy", - "mainHomework": "Domáce úlohy", + "@mainMessages": { + "description":"Label for messages page button" + }, + "mainHomework": "Úlohy", + "@mainHomework": { + "description":"Label for homework page button" + }, "mainGrades": "Známky", + "@mainGrades": { + "description": "Label for grade page button" + }, "homeLunchesNotLoaded": "Nepodarilo sa načítať obedy", + "@homeLunchesNotLoaded": { + "description": "Displayed when lunches weren't loaded" + }, "homeNoLunchToday": "Dnes nemáte žiadny obed", + "@homeNoLunchToday": { + "description": "Displayed when the user hasn't ordered a lunch for today" + }, "homeLunchToday": "Dnes máte možnosť obedu číslo {lunch}", + "@homeLunchToday": { + "description": "Shows the user the lunch that is ordered", + "placeholders": { + "lunch": { + "type": "int" + } + } + }, "homeLunchDontForget": "Nezabudnite si objednať obed pre {date}", + "@homeLunchDontForget": { + "description": "Displayed to remind user to order lunch", + "placeholders": { + "date": { + "type": "DateTime", + "format": "yMd" + } + } + }, "homeLogout": "Odhlásiť sa", - "homeSetupICanteen": "Nastavenie iCanteen", + "@homeLogout": { + "description": "Logout button on home page" + }, + "homeSetupICanteen": "Nastaviť iCanteen", "homeNoClasses": "Dnes sa škola nekoná :D", "homeUpdateTitle": "Dostupná nová verzia", - "homeUpdateDescription": "Prosím, navštívte https://github.com/DislikesSchool/EduPage2/releases pre stiahnutie najnovšej verzie", + "homeUpdateDescription": "Prosím navštívte https://github.com/DislikesSchool/EduPage2/releases pre nejnovšiu verziu", "homeQuickstart": "Rýchly štart", "homePreview": "Náhľad", - "homePatchAvailable": "Inštaluje sa aktualizácia...", - "homePatchDownloaded": "Aktualizácia je stiahnutá, prosím reštartujte EduPage2", + "homePatchAvailable": "Inštaluje se nový patch...", + "homePatchDownloaded": "Patch bol stiahnutý, prosím reštartujte EduPage2", "homeworkTitle": "Domáce úlohy", + "@homeworkTitle": { + "description": "Title of homework page" + }, "messagesTitle": "Správy", - "loginPleaseLogin": "Prosím, prihláste sa do EduPage2", - "loginUseExistingCredentials": "Použite svoje existujúce prihlasovacie údaje do EduPage", + "@messagesTitle": { + "description": "Title of messages page" + }, + "loginPleaseLogin": "Prosím, prihláste sa", + "@loginPleaseLogin": { + "description": "Main text for login page" + }, + "loginUseExistingCredentials": "Použite svoje existujúce údaje do EduPage", + "@loginUseExistingCredentials": { + "description": "Asks user to use their existing EduPage credentials" + }, "loginUsername": "Prihlasovacie meno", + "@loginUsername": { + "description": "User's email or user name" + }, "loginPassword": "Heslo", + "@loginPassword": { + "description": "User's password" + }, + "loginServer": "Server (napr. skola.edupage.org)", "loginLogin": "Prihlásiť sa", + "@loginLogin": { + "description": "Login button" + }, + "loginCustomEndpointCheckbox": "Použíť valstny endpoint", + "loginCustomEndpoint": "URL vlastného endpointu", "today": "Dnes", "tomorrow": "Zajtra", - "loadCredentials": "Načítavam prihlasovacie údaje...", + "loadCredentials": "Načítavam prihlasovacie údaje", "loadLoggingIn": "Prihlasovanie...", "loadLoggedIn": "Prihlásený", - "loadAccessToken": "Získavam prístupový token...", - "loadVerify": "Overujem", - "loadDownloadTimetable": "Sťahujem rozvrh hodín...", - "loadDownloadMessages": "Sťahujem správy...", + "loadAccessToken": "Získavanie prístupového tokenu...", + "loadVerify": "Overovanie...", + "loadDownloadTimetable": "Sťahovanie rozvrhu...", + "loadDownloadMessages": "Sťahovanie zpráv...", "loadDone": "Hotovo!", - "iCanteenLoading": "Načítavam obedy (môže to chvíľu trvať)", + "iCanteenLoading": "Načítavanie obedov (môže to chvíľu trvať)", "iCanteenCantLoad": "Nepodarilo sa načítať obedy", "iCanteenSetupPleaseLogin": "Prihláste sa do iCanteen", - "iCanteenSetupDetails": "Adresa URL v tomto formáte: https://lunches.yourschool.com/login", - "iCanteenSetupServer": "Adresa servera", - "iCanteenSetupEmail": "Používateľské meno", + "iCanteenSetupDetails": "Adresu URL zadajte vo formáte https://stravovanie.skola.sk/login", + "iCanteenSetupServer": "Adresa URL", + "iCanteenSetupEmail": "Prihlasovacie meno", "iCanteenSetupPassword": "Heslo", - "messagesLoadingAttachment": "Načítavam PDF..." + "messagesLoadingAttachment": "Načítavanie pdf súboru...", + "messagesAttachments": "{count, plural, =1{1 Prípona} few{{count} Prípony} other{{count} Prípon}}", + "@messagesAttachments": { + "placeholders": { + "count": { + "type": "num", + "format": "compact" + } + } + } } diff --git a/lib/load.dart b/lib/load.dart index 4b1de6e..c12a23c 100644 --- a/lib/load.dart +++ b/lib/load.dart @@ -1,15 +1,11 @@ -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; +import 'dart:async'; +import 'package:eduapge2/api.dart'; import 'package:eduapge2/login.dart'; -import 'package:firebase_performance/firebase_performance.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter_session_manager/flutter_session_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:onesignal_flutter/onesignal_flutter.dart'; class LoadingScreen extends StatefulWidget { final Function loadedCallback; @@ -27,14 +23,12 @@ class LoadingScreenState extends State { late SessionManager sessionManager; late SharedPreferences sharedPreferences; - bool runningInit = false; bool startedInit = false; - Dio dio = Dio(); double progress = 0.0; String loaderText = "Loading..."; - String baseUrl = FirebaseRemoteConfig.instance.getString("baseUrl"); + String baseUrl = FirebaseRemoteConfig.instance.getString("testUrl"); late AppLocalizations? local; @@ -52,183 +46,49 @@ class LoadingScreenState extends State { } Future init() async { + if (startedInit) return; startedInit = true; sharedPreferences = await SharedPreferences.getInstance(); + String? endpoint = sharedPreferences.getString("customEndpoint"); + if (endpoint != null && endpoint != "") { + baseUrl = endpoint; + } quickstart = sharedPreferences.getBool('quickstart') ?? false; - progress = 0.1; - loaderText = local!.loadCredentials; - dio.interceptors - .add(DioCacheManager(CacheConfig(baseUrl: baseUrl)).interceptor); - setState(() {}); - loadUser(); - } - - Future loadUser() async { if (sharedPreferences.getBool("ice") == true) { sessionManager.set("iCanteenEnabled", true); } - String? failedToken; - if (sharedPreferences.getString("token") != null) { - String? token = sharedPreferences.getString("token"); - progress = 0.2; - loaderText = local!.loadLoggingIn; - setState(() {}); - Response response = await dio - .get( - "$baseUrl/login", - options: buildCacheOptions( - const Duration(days: 5), - maxStale: const Duration(days: 14), - forceRefresh: !quickstart, - options: Options( - headers: { - "Authorization": "Bearer $token", - }, - ), - ), - ) - .catchError((obj) { - sharedPreferences.remove("token"); - failedToken = token; - return Response( - requestOptions: RequestOptions(path: "$baseUrl/login"), - statusCode: 500, - ); - }); - - if (response.statusCode == 200) { - if (response.data.runtimeType == String) { - if (jsonDecode(response.data)["icanteen"] == true) { - await sessionManager.set('iCanteenEnabled', true); - } - } else { - if (response.data["icanteen"] == true) { - await sessionManager.set('iCanteenEnabled', true); - } - } - OneSignal.shared.setExternalUserId(token!); - progress = 0.5; - loaderText = local!.loadLoggedIn; - setState(() {}); - if (response.data.runtimeType == Map) { - sessionManager.set('user', jsonEncode(response.data)); - } else { - sessionManager.set('user', response.data); - } - return loadTimetable(); - } else { - failedToken = token; - } - } else if (sharedPreferences.getString("email") != null && + progress = 0.1; + loaderText = local!.loadCredentials; + setState(() {}); + if (sharedPreferences.getString("email") != null && sharedPreferences.getString("password") != null) { - String? email = sharedPreferences.getString("email"); - String? password = sharedPreferences.getString("password"); - - progress = 0.3; - loaderText = local!.loadAccessToken; - setState(() {}); - - Response response = await dio - .post( - "$baseUrl/token", - data: { - "email": email, - "password": password, - }, - options: buildCacheOptions( - const Duration(days: 5), - forceRefresh: !quickstart, - ), - ) - .catchError((obj) { - return Response( - requestOptions: RequestOptions(path: "$baseUrl/token"), - statusCode: 500, - ); - }); - - if (response.statusCode == 500) { - sharedPreferences.remove("email"); - sharedPreferences.remove("password"); - runningInit = false; - // ignore: use_build_context_synchronously - Navigator.push(context, - MaterialPageRoute(builder: (context) => const LoginPage())) - .then((value) => init()); + if (!await EP2Data.getInstance().init( + onProgressUpdate: (text, prog) { + setState(() { + loaderText = text; + progress = prog; + }); + }, + local: local!)) { + gotoLogin(); } else { - if (response.data["token"] == failedToken) { - sharedPreferences.remove("email"); - sharedPreferences.remove("password"); - runningInit = false; - // ignore: use_build_context_synchronously - Navigator.push(context, - MaterialPageRoute(builder: (context) => const LoginPage())) - .then((value) => init()); - } else { - sessionManager.set("token", response.data["token"]); - loaderText = local!.loadVerify; - setState(() {}); - String token = response.data["token"]; - response = await dio - .get( - "$baseUrl/login", - options: buildCacheOptions( - const Duration(days: 5), - maxStale: const Duration(days: 14), - forceRefresh: !quickstart, - options: Options( - headers: { - "Authorization": "Bearer ${response.data["token"]}", - }, - ), - ), - ) - .catchError((obj) { - sharedPreferences.remove("token"); - failedToken = response.data["token"]; - return Response( - requestOptions: RequestOptions(path: "$baseUrl/login"), - statusCode: 500, - ); - }); - - if (response.statusCode == 200) { - if (response.data.runtimeType == String) { - if (jsonDecode(response.data)["icanteen"] == true) { - await sessionManager.set('iCanteenEnabled', true); - } - } else { - if (response.data["icanteen"] == true) { - await sessionManager.set('iCanteenEnabled', true); - } - } - OneSignal.shared.setExternalUserId(token); - progress = 0.6; - loaderText = local!.loadLoggedIn; - setState(() {}); - if (response.data.runtimeType == Map) { - sessionManager.set('user', jsonEncode(response.data)); - } else { - sessionManager.set('user', response.data); - } - return loadTimetable(); - } else { - runningInit = false; - // ignore: use_build_context_synchronously - Navigator.push(context, - MaterialPageRoute(builder: (context) => const LoginPage())) - .then((value) => init()); - } - } + widget.loadedCallback(); } } else { - runningInit = false; - Navigator.push(context, - MaterialPageRoute(builder: (context) => const LoginPage())) - .then((value) => init()); + gotoLogin(); } } + void gotoLogin([String? err]) async { + Navigator.push(context, + MaterialPageRoute(builder: (context) => LoginPage(err: err ?? ""))) + .then((value) => { + setState(() { + startedInit = false; + }) + }); + } + DateTime getWeekDay() { DateTime now = DateTime.now(); if (now.weekday > 5) { @@ -237,64 +97,6 @@ class LoadingScreenState extends State { return DateTime(now.year, now.month, now.day); } - Future loadTimetable() async { - progress = 0.7; - loaderText = local!.loadDownloadTimetable; - setState(() {}); - final metric = FirebasePerformance.instance.newHttpMetric( - "$baseUrl/timetable/${getWeekDay().toString()}", HttpMethod.Get); - String token = sharedPreferences.getString("token")!; - metric.start(); - Response response = await dio.get( - "$baseUrl/timetable/${getWeekDay().toString()}", - options: buildCacheOptions( - const Duration(days: 5), - maxStale: const Duration(days: 14), - forceRefresh: !quickstart, - options: Options( - headers: { - "Authorization": "Bearer $token", - }, - ), - ), - ); - metric.stop(); - - sessionManager.set("timetable", response.data); - return loadMessages(); - } - - Future loadMessages() async { - progress = 0.9; - loaderText = local!.loadDownloadMessages; - setState(() {}); - final metric = FirebasePerformance.instance - .newHttpMetric("$baseUrl/messages", HttpMethod.Get); - - String token = sharedPreferences.getString("token")!; - metric.start(); - Response response = await dio.get( - "$baseUrl/messages", - options: buildCacheOptions( - const Duration(days: 5), - maxStale: const Duration(days: 14), - forceRefresh: !quickstart, - options: Options( - headers: { - "Authorization": "Bearer $token", - }, - ), - ), - ); - metric.stop(); - sessionManager.set("messages", jsonEncode(response.data)); - - progress = 1.0; - loaderText = local!.loadDone; - setState(() {}); - widget.loadedCallback(); - } - @override Widget build(BuildContext context) { if (!startedInit) { diff --git a/lib/login.dart b/lib/login.dart index 5c8aaaf..ff28990 100644 --- a/lib/login.dart +++ b/lib/login.dart @@ -3,7 +3,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class LoginPage extends StatefulWidget { - const LoginPage({super.key}); + final String err; + const LoginPage({super.key, required this.err}); @override State createState() => LoinPageState(); @@ -14,6 +15,10 @@ class LoinPageState extends State { late SharedPreferences sharedPreferences; String email = ""; String password = ""; + String server = ""; + bool _showServerField = false; + bool _useCustomEndpoint = false; + String _customEndpoint = ''; @override void initState() { @@ -29,6 +34,26 @@ class LoinPageState extends State { Future getPrefs() async { sharedPreferences = await SharedPreferences.getInstance(); + String? sEmail = sharedPreferences.getString("email"); + String? sPassword = sharedPreferences.getString("password"); + String? sServer = sharedPreferences.getString("server"); + String? sEndpoint = sharedPreferences.getString("customEndpoint"); + + if (sEmail != null) { + email = sEmail; + } + if (sPassword != null) { + password = sPassword; + } + if (sServer != null) { + server = sServer; + _showServerField = true; + } + if (sEndpoint != null && sEndpoint != "") { + _customEndpoint = sEndpoint; + _useCustomEndpoint = true; + } + setState(() {}); } @override @@ -48,6 +73,7 @@ class LoinPageState extends State { ), ), Text(local!.loginUseExistingCredentials), + if (widget.err != "") Text(widget.err), TextField( decoration: InputDecoration( icon: const Icon(Icons.email), @@ -65,10 +91,42 @@ class LoinPageState extends State { obscureText: true, keyboardType: TextInputType.visiblePassword, ), + Row( + children: [ + Checkbox( + value: _useCustomEndpoint, + onChanged: (value) { + setState(() { + _useCustomEndpoint = value!; + }); + }, + ), + Text(local!.loginCustomEndpointCheckbox), + ], + ), + if (_useCustomEndpoint) + TextField( + decoration: InputDecoration( + icon: const Icon(Icons.language), + hintText: local!.loginCustomEndpoint, + ), + onChanged: (value) => {_customEndpoint = value}), + if (widget.err != "" || _showServerField) + TextField( + decoration: InputDecoration( + icon: const Icon(Icons.cloud_queue), + hintText: local!.loginServer, + ), + onChanged: (text) => {server = text}, + keyboardType: TextInputType.url, + ), ElevatedButton( onPressed: () => { sharedPreferences.setString("email", email), sharedPreferences.setString("password", password), + sharedPreferences.setString("server", server), + sharedPreferences.setString( + "customEndpoint", _customEndpoint), Navigator.pop(context), }, child: Text(local!.loginLogin), diff --git a/lib/main.dart b/lib/main.dart index aabe0df..869745b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:dynamic_color/dynamic_color.dart'; +import 'package:eduapge2/api.dart'; import 'package:eduapge2/homework.dart'; import 'package:eduapge2/icanteen.dart'; import 'package:eduapge2/load.dart'; @@ -12,17 +11,28 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_session_manager/flutter_session_manager.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:restart_app/restart_app.dart'; +import 'package:shorebird_code_push/shorebird_code_push.dart'; import 'home.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; -import 'package:onesignal_flutter/onesignal_flutter.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); + final remoteConfig = FirebaseRemoteConfig.instance; + await remoteConfig.setConfigSettings(RemoteConfigSettings( + fetchTimeout: const Duration(minutes: 1), + minimumFetchInterval: const Duration(hours: 1), + )); + await remoteConfig.setDefaults(const { + "baseUrl": "https://lobster-app-z6jfk.ondigitalocean.app/api", + "testUrl": "https://edupage2-server.onrender.com" + }); + await remoteConfig.fetchAndActivate(); await SentryFlutter.init( (options) { options.dsn = @@ -31,8 +41,8 @@ Future main() async { }, appRunner: () => runApp(const MyApp()), ); - OneSignal.shared.setAppId("85587dc6-0a3c-4e91-afd6-e0ca82361763"); - OneSignal.shared.promptUserForPushNotificationPermission(); + //OneSignal.shared.setAppId("85587dc6-0a3c-4e91-afd6-e0ca82361763"); + //OneSignal.shared.promptUserForPushNotificationPermission(); } class MyApp extends StatelessWidget { @@ -84,26 +94,24 @@ class PageBase extends StatefulWidget { class PageBaseState extends State { int _selectedIndex = 0; - String baseUrl = "https://lobster-app-z6jfk.ondigitalocean.app"; - late Response response; - Dio dio = Dio(); + String baseUrl = FirebaseRemoteConfig.instance.getString("testUrl"); bool loaded = false; bool error = false; //for error status bool loading = false; //for data featching status String errmsg = ""; //to assing any error message from API/runtime - List apidataMsg = []; + List apidataMsg = []; bool refresh = true; bool iCanteenEnabled = false; + bool _isCheckingForUpdate = false; + final ShorebirdCodePush _shorebirdCodePush = ShorebirdCodePush(); SessionManager sessionManager = SessionManager(); @override void initState() { - dio.interceptors - .add(DioCacheManager(CacheConfig(baseUrl: baseUrl)).interceptor); - getMsgs(); + if (!_isCheckingForUpdate) _checkForUpdate(); // ik that it's not necessary super.initState(); } @@ -119,31 +127,73 @@ class PageBaseState extends State { }); } - initRemoteConfig() async { - final remoteConfig = FirebaseRemoteConfig.instance; - await remoteConfig.setConfigSettings(RemoteConfigSettings( - fetchTimeout: const Duration(minutes: 1), - minimumFetchInterval: const Duration(hours: 1), - )); - await remoteConfig.setDefaults(const { - "baseUrl": "https://lobster-app-z6jfk.ondigitalocean.app", + Future _checkForUpdate() async { + setState(() { + _isCheckingForUpdate = true; + }); + + // Ask the Shorebird servers if there is a new patch available. + final isUpdateAvailable = + await _shorebirdCodePush.isNewPatchAvailableForDownload(); + + if (!mounted) return; + + setState(() { + _isCheckingForUpdate = false; }); - await remoteConfig.fetchAndActivate(); - baseUrl = remoteConfig.getString("baseUrl"); + + if (isUpdateAvailable) { + _downloadUpdate(); + } + } + + void _showDownloadingBanner() { + ScaffoldMessenger.of(context).showMaterialBanner( + const MaterialBanner( + content: Text('Downloading patch...'), + actions: [ + SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + ], + ), + ); + } + + void _showRestartBanner() { + ScaffoldMessenger.of(context).showMaterialBanner( + const MaterialBanner( + content: Text('A new patch is ready!'), + actions: [ + TextButton( + // Restart the app for the new patch to take effect. + onPressed: Restart.restartApp, + child: Text('Restart app'), + ), + ], + ), + ); + } + + Future _downloadUpdate() async { + _showDownloadingBanner(); + await _shorebirdCodePush.downloadUpdateIfAvailable(); + if (!mounted) return; + + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + _showRestartBanner(); } getMsgs() async { - await initRemoteConfig(); - var msgs = await sessionManager.get('messages'); + apidataMsg = EP2Data.getInstance().timeline.items.values.toList(); var ic = await sessionManager.get('iCanteenEnabled'); if (ic == true) { iCanteenEnabled = true; } - if (msgs != Null && msgs != null) { - setState(() { - apidataMsg = msgs; - }); - } } @override @@ -212,6 +262,7 @@ class PageBaseState extends State { label: AppLocalizations.of(context)!.mainICanteen, selectedIcon: const Icon(Icons.lunch_dining_outlined), ), + /* NavigationDestination( icon: Badge( label: Text(apidataMsg @@ -231,6 +282,12 @@ class PageBaseState extends State { child: const Icon(Icons.mail_outline), ), ), + */ + NavigationDestination( + icon: const Icon(Icons.mail), + label: AppLocalizations.of(context)!.mainMessages, + selectedIcon: const Icon(Icons.mail_outline), + ), NavigationDestination( icon: const Icon(Icons.home_work), label: AppLocalizations.of(context)!.mainHomework, diff --git a/lib/message.dart b/lib/message.dart index 1c7a201..9e989dd 100644 --- a/lib/message.dart +++ b/lib/message.dart @@ -1,3 +1,4 @@ +import 'package:eduapge2/api.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cached_pdfview/flutter_cached_pdfview.dart'; @@ -5,11 +6,9 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_session_manager/flutter_session_manager.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:html_unescape/html_unescape.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:webview_flutter/webview_flutter.dart'; class MessagePage extends StatefulWidget { final SessionManager sessionManager; @@ -25,7 +24,7 @@ class MessagePage extends StatefulWidget { class MessagePageState extends State { late SessionManager sessionManager; late SharedPreferences sharedPreferences; - String baseUrl = FirebaseRemoteConfig.instance.getString("baseUrl"); + String baseUrl = FirebaseRemoteConfig.instance.getString("testUrl"); bool loading = true; Dio dio = Dio(); @@ -49,25 +48,28 @@ class MessagePageState extends State { setState(() { loading = true; //make loading true to show progressindicator }); - - sharedPreferences = await SharedPreferences.getInstance(); - String token = sharedPreferences.getString("token")!; Response response = await dio.get( - "$baseUrl/message/${widget.id}", - options: buildCacheOptions( - const Duration(days: 5), - forceRefresh: true, - options: Options( - headers: { - "Authorization": "Bearer $token", - }, - ), + "$baseUrl/api/timelineitem/${widget.id}", + options: Options( + headers: { + "Authorization": "Bearer ${EP2Data.getInstance().user.token}", + }, ), ); HtmlUnescape unescape = HtmlUnescape(); Map data = response.data; + bool isImportantMessage = false; + if (data["data"]["Value"] != null && + data["data"]["Value"]["messageContent"] != null) { + isImportantMessage = true; + } + Iterable attachments = []; + if (data["data"]["Value"] != null && + data["data"]["Value"]["attachements"] is Map) { + attachments = data["data"]["Value"]["attachements"].entries; + } messages = Stack( children: [ Padding( @@ -85,16 +87,14 @@ class MessagePageState extends State { child: Row( children: [ Text( - data["owner"]["firstname"] + - " " + - data["owner"]["lastname"], + data["vlastnik_meno"], style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold), ), const Icon(Icons.arrow_right_rounded, size: 18), Expanded( child: Text( - unescape.convert(data["title"]), + unescape.convert(data["user_meno"]), overflow: TextOverflow.fade, maxLines: 5, softWrap: true, @@ -108,39 +108,147 @@ class MessagePageState extends State { height: 30, ), SelectableLinkify( - text: unescape.convert(data["text"]), + text: unescape.convert(isImportantMessage + ? data["data"]["Value"]["messageContent"] + : data["text"]), onOpen: _onOpen, ), - for (Map att in data["attachments"]!) - if (att["name"]!.endsWith(".jpg") || - att["name"]!.endsWith(".png")) - Image.network(att["src"]!) - else if (att["name"]!.endsWith(".pdf")) - const PDF( - enableSwipe: true, - swipeHorizontal: true, - autoSpacing: false, - pageFling: false, - ).cachedFromUrl( - att["src"], - placeholder: (progress) => - Center(child: Text('$progress %')), - errorWidget: (error) => - Center(child: Text(error.toString())), + for (MapEntry att in attachments) + if (att.value.endsWith(".jpg") || + att.value.endsWith(".png") || + att.value.endsWith(".jpeg") || + att.value.endsWith(".gif")) + Card( + elevation: 5, + child: Padding( + padding: const EdgeInsets.only(left: 15, top: 15), + child: Column( + children: [ + Image.network( + "https://${data["origin_server"]}${att.key}"), + Row( + children: [ + Expanded( + child: Text( + unescape.convert(att.value), + overflow: TextOverflow.fade, + maxLines: 5, + softWrap: true, + style: const TextStyle(fontSize: 14), + ), + ), + IconButton( + icon: + const Icon(Icons.download_rounded), + onPressed: () async { + await dio.download( + "https://${data["origin_server"]}${att.key}", + "/storage/emulated/0/Download/${att.value}", + ); + }, + ), + ], + ), + ], + ), + ), + ) + else if (att.value.endsWith(".pdf")) + Card( + elevation: 5, + child: Padding( + padding: const EdgeInsets.only(left: 15), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Text( + unescape.convert(att.value), + overflow: TextOverflow.fade, + maxLines: 5, + softWrap: true, + style: const TextStyle(fontSize: 14), + ), + ), + IconButton( + icon: const Icon(Icons.download_rounded), + onPressed: () async { + await dio.download( + "https://${data["origin_server"]}${att.key}", + "/storage/emulated/0/Download/${att.value}", + ); + }, + ), + // IconButton to open a new page with the pdf + IconButton( + icon: const Icon(Icons.open_in_new_rounded), + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Scaffold( + appBar: AppBar( + title: Text( + unescape.convert(att.value), + ), + ), + body: const PDF( + enableSwipe: true, + swipeHorizontal: true, + autoSpacing: false, + pageFling: false, + ).cachedFromUrl( + "https://${data["origin_server"]}${att.key}", + placeholder: (progress) => Center( + child: Text('$progress %')), + errorWidget: (error) => Center( + child: + Text(error.toString())), + ), + ), + ), + ); + }, + ), + ], + ), + ), ) else - SizedBox( - width: width, - height: 500, - child: WebViewWidget( - controller: WebViewController() - ..loadRequest(Uri.parse(att["src"]!))), + Card( + elevation: 5, + child: Padding( + padding: const EdgeInsets.only(left: 15), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Text( + unescape.convert(att.value), + overflow: TextOverflow.fade, + maxLines: 5, + softWrap: true, + style: const TextStyle(fontSize: 14), + ), + ), + IconButton( + icon: const Icon(Icons.download_rounded), + onPressed: () async { + await dio.download( + "https://${data["origin_server"]}${att.key}", + "/storage/emulated/0/Download/${att.value}", + ); + }, + ), + ], + ), + ), ), ], ), ), ), - for (Map r in data["replies"]) + for (Map r in data["replies"] ?? []) Row( children: [ const SizedBox(width: 20), diff --git a/lib/messages.dart b/lib/messages.dart index cd8dcc8..54f6e78 100644 --- a/lib/messages.dart +++ b/lib/messages.dart @@ -1,14 +1,9 @@ -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; +import 'package:eduapge2/api.dart'; import 'package:eduapge2/message.dart'; -import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter_session_manager/flutter_session_manager.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:html_unescape/html_unescape.dart'; -import 'package:shared_preferences/shared_preferences.dart'; class MessagesPage extends StatefulWidget { final SessionManager sessionManager; @@ -35,13 +30,17 @@ extension MoveElement on List { class TimeTablePageState extends State { bool loading = true; + bool loaded = false; late List apidataMsg; + AppLocalizations? loc; + bool _isFetching = false; late Widget messages; + final ScrollController _scrollController = ScrollController(); + @override void initState() { - getData(); //fetching data super.initState(); } @@ -51,43 +50,40 @@ class TimeTablePageState extends State { super.setState(fn); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + getData() async { setState(() { loading = true; //make loading true to show progressindicator }); - apidataMsg = await widget.sessionManager.get('messages'); - messages = getMessages(apidataMsg); + _scrollController.addListener(() async { + if (!_isFetching && + _scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + _isFetching = true; + await EP2Data.getInstance().timeline.loadOlderMessages(); + _isFetching = false; + } + }); + messages = + getMessages(EP2Data.getInstance().timeline.items.values.toList()); loading = false; - setState(() {}); //refresh UI - - SharedPreferences sp = await SharedPreferences.getInstance(); - if (sp.getBool('quickstart') ?? false) { - String token = sp.getString("token")!; - String baseUrl = FirebaseRemoteConfig.instance.getString("baseUrl"); - Dio dio = Dio(); - Response response = await dio.get( - "$baseUrl/messages", - options: buildCacheOptions( - const Duration(days: 5), - maxStale: const Duration(days: 14), - forceRefresh: true, - options: Options( - headers: { - "Authorization": "Bearer $token", - }, - ), - ), - ); - widget.sessionManager.set("messages", jsonEncode(response.data)); - messages = getMessages(response.data); - setState(() {}); - } + setState(() {}); } @override Widget build(BuildContext context) { + loc ??= AppLocalizations.of(context); + if (!loaded) { + loaded = true; + getData(); + } return Scaffold( appBar: AppBar( toolbarHeight: 0, @@ -106,49 +102,42 @@ class TimeTablePageState extends State { loading = true; //make loading true to show progressindicator }); - apidataMsg = await widget.sessionManager.get('messages'); - messages = getMessages(apidataMsg); + messages = + getMessages(EP2Data.getInstance().timeline.items.values.toList()); loading = false; setState(() {}); //refresh UI } - Widget getMessages(var apidataMsg) { + Widget getMessages(List apidataMsg) { HtmlUnescape unescape = HtmlUnescape(); List rows = []; - apidataMsg ??= [ - { - "type": "sprava", - "title": "Načítání...", - "text": "Nebude to trvat dlouho", - } - ]; - List msgs = - apidataMsg.where((msg) => msg["type"] == "sprava").toList(); - List msgsWOR = List.from(msgs); + List msgs = + apidataMsg.where((msg) => msg.type == "sprava").toList(); + msgs.sort((a, b) => b.timeAdded.compareTo(a.timeAdded)); + List msgsWOR = List.from(msgs); List> bump = []; - for (Map msg in msgs) { - if (msg["replyOf"] != null) { + for (TimelineItem msg in msgs) { + if (msg.reactionTo != "") { if (!bump.any((element) => - element["id"]!.compareTo(int.parse(msg["replyOf"])) == 0)) { - bump.add( - {"id": int.parse(msg["replyOf"]), "index": msgsWOR.indexOf(msg)}); + element["ineid"]!.compareTo(int.parse(msg.reactionTo)) == 0)) { + bump.add({ + "ineid": int.parse(msg.reactionTo), + "index": msgsWOR.indexOf(msg) + }); msgsWOR.remove(msg); } else { msgsWOR.remove(msg); } } } - for (Map msg in msgsWOR) { - String attText = msg["attachments"].length < 5 - ? msg["attachments"].length > 1 - ? "y" - : "a" - : ""; + for (TimelineItem msg in msgsWOR) { + bool isImportantMessage = false; + if (msg.data["Value"]["messageContent"] != null) { + isImportantMessage = true; + } rows.add(Card( - color: msg["isSeen"] - ? null - : Theme.of(context).colorScheme.tertiaryContainer, + //color: msg["isSeen"] ? null : Theme.of(context).colorScheme.tertiaryContainer, child: InkWell( onTap: () { Navigator.push( @@ -156,7 +145,7 @@ class TimeTablePageState extends State { MaterialPageRoute( builder: (BuildContext buildContext) => MessagePage( sessionManager: widget.sessionManager, - id: int.parse(msg["id"])))); + id: int.parse(msg.id)))); }, child: Padding( padding: const EdgeInsets.all(10), @@ -165,9 +154,14 @@ class TimeTablePageState extends State { children: [ Row( children: [ + if (isImportantMessage) + const Text( + "! ", + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), Text( - '${msg["owner"]["firstname"]?.trim()} ${msg["owner"]["lastname"]?.trim()}' - .replaceAll(RegExp(r'\s+'), ' '), + msg.ownerName.replaceAll(RegExp(r'\s+'), ' '), style: const TextStyle(fontSize: 18), ), const Icon( @@ -176,7 +170,7 @@ class TimeTablePageState extends State { ), Expanded( child: Text( - unescape.convert(msg["title"]), + unescape.convert(msg.userName), overflow: TextOverflow.fade, maxLines: 5, softWrap: false, @@ -189,7 +183,7 @@ class TimeTablePageState extends State { children: [ Expanded( child: Text( - unescape.convert(msg["text"]), + unescape.convert(msg.text), style: const TextStyle(fontSize: 12), overflow: TextOverflow.fade, maxLines: 5, @@ -198,27 +192,32 @@ class TimeTablePageState extends State { ) ], ), - for (Map r in msg["replies"]) - Row( - children: [ - const SizedBox(width: 10), - const Icon(Icons.subdirectory_arrow_right_rounded), - Expanded( - child: Card( - elevation: 10, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - r["owner"] + ": " + unescape.convert(r["text"]), - softWrap: false, - overflow: TextOverflow.ellipsis, + if (msg.reactionTo != "") + for (TimelineItem r in msgs + .where((element) => + element.reactionTo == msg.otherId.toString()) + .toList()) + Row( + children: [ + const SizedBox(width: 10), + const Icon(Icons.subdirectory_arrow_right_rounded), + Expanded( + child: Card( + elevation: 10, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "${r.ownerName}: ${unescape.convert(r.text)}", + softWrap: false, + overflow: TextOverflow.ellipsis, + ), ), ), ), - ), - ], - ), - if (msg["attachments"].length > 0) + ], + ), + if (msg.data["Value"].containsKey("attachements") && + msg.data["Value"]["attachements"].length > 0) Padding( padding: const EdgeInsets.only(top: 5), child: Row( @@ -228,8 +227,9 @@ class TimeTablePageState extends State { Icons.attach_file_rounded, size: 18, ), - Text(msg["attachments"].length.toString()), - Text(" Přípon$attText"), + Text(loc?.messagesAttachments( + msg.data["Value"]["attachements"].length) ?? + ""), ], ), ), @@ -240,10 +240,10 @@ class TimeTablePageState extends State { )); } for (Map b in bump) { - rows.move( - msgsWOR.indexOf(msgsWOR - .firstWhere((element) => int.parse(element["id"]) == b["id"])), - b["index"]!); + if (!msgs.any((element) => element.id == b["ineid"].toString())) continue; + TimelineItem toBump = + msgs.firstWhere((element) => element.id == b["ineid"].toString()); + rows.move(msgsWOR.indexOf(toBump), b["index"]!); } return Card( elevation: 5, @@ -261,8 +261,12 @@ class TimeTablePageState extends State { padding: const EdgeInsets.only(top: 40), child: RefreshIndicator( onRefresh: _pullRefresh, - child: ListView( - children: rows, + child: ListView.builder( + controller: _scrollController, + itemCount: rows.length, + itemBuilder: (BuildContext context, int index) { + return rows[index]; + }, ), )), ], diff --git a/lib/timetable.dart b/lib/timetable.dart index a7c2f3c..e2847c2 100644 --- a/lib/timetable.dart +++ b/lib/timetable.dart @@ -1,12 +1,8 @@ -import 'dart:convert'; - +import 'package:eduapge2/api.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:flutter_session_manager/flutter_session_manager.dart'; import 'package:intl/intl.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class TimeTablePage extends StatefulWidget { @@ -19,34 +15,23 @@ class TimeTablePage extends StatefulWidget { } class TimeTablePageState extends State { - String baseUrl = FirebaseRemoteConfig.instance.getString("baseUrl"); - - TimeTableData tt = TimeTableData(DateTime.now(), [ - TimeTableClass("2", "ZAE", "STJI", "8:55", "9:40", "U32", 0, {}), - TimeTableClass("3", "ANJ", "MAOL", "10:00", "10:45", "U02", 0, {}), - TimeTableClass("4", "CJL", "GAMA", "10:55", "11:40", "U60", 0, {}), - TimeTableClass("5", "MAT", "VAPE", "11:50", "12:35", "U60", 1, {}) - ]); - - late Response response; - Dio dio = Dio(); + String baseUrl = FirebaseRemoteConfig.instance.getString("testUrl"); bool error = false; //for error status bool loading = false; //for data featching status String errmsg = ""; //to assing any error message from API/runtime - late Map apidataTT; + List periods = []; bool refresh = false; bool userInteracted = false; + EP2Data data = EP2Data.getInstance(); + int daydiff = 0; List timetables = []; @override void initState() { - dio.interceptors - .add(DioCacheManager(CacheConfig(baseUrl: baseUrl)).interceptor); - getData(); //fetching data super.initState(); } @@ -64,170 +49,74 @@ class TimeTablePageState extends State { return DateTime(now.year, now.month, now.day); } - getData() async { - setState(() { - loading = true; //make loading true to show progressindicator - }); - - apidataTT = await widget.sessionManager.get('timetable'); - - List ttClasses = []; - List lessons = apidataTT["lessons"]; - for (Map ttLesson in lessons) { - ttClasses.add( - TimeTableClass( - ttLesson["period"]["name"], - ttLesson["subject"]["short"], - ttLesson["teachers"][0]["short"], - ttLesson["period"]["startTime"], - ttLesson["period"]["endTime"], - ttLesson["classrooms"][0]["short"], - 0, - ttLesson, - ), - ); - } - tt = TimeTableData(DateTime.parse(apidataTT["date"]), ttClasses); - timetables.add(tt); - - loading = false; - refresh = false; - setState(() {}); //refresh UI - - SharedPreferences sp = await SharedPreferences.getInstance(); - if (sp.getBool('quickstart') ?? false) { - String token = sp.getString("token")!; - String baseUrl = "https://lobster-app-z6jfk.ondigitalocean.app/api"; - Dio dio = Dio(); - Response response = await dio.get( - "$baseUrl/timetable/${getWeekDay().toString()}", - options: buildCacheOptions( - const Duration(days: 5), - maxStale: const Duration(days: 14), - forceRefresh: true, - options: Options( - headers: { - "Authorization": "Bearer $token", - }, - ), - ), - ); - widget.sessionManager.set("timetable", jsonEncode(response.data)); - - List ttClasses = []; - List lessons = jsonDecode(response.data)["lessons"]; - for (Map ttLesson in lessons) { - ttClasses.add( - TimeTableClass( - ttLesson["period"]["name"], - ttLesson["subject"]["short"], - ttLesson["teachers"][0]["short"], - ttLesson["period"]["startTime"], - ttLesson["period"]["endTime"], - ttLesson["classrooms"][0]["short"], - 0, - ttLesson, - ), - ); - } - tt = TimeTableData(DateTime.parse(apidataTT["date"]), ttClasses); - timetables.clear(); - timetables.add(tt); - - setState(() {}); - } - } - - Future loadTt(DateTime date) async { - if (timetables.any((element) => isSameDay(element.date, date))) { - return timetables.firstWhere((element) => isSameDay(element.date, date)); - } - SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); - String token = sharedPreferences.getString("token")!; - - Response response = await dio.get( - "$baseUrl/timetable/${DateTime(date.year, date.month, date.day).toString()}", - options: buildCacheOptions( - const Duration(days: 4), - maxStale: const Duration(days: 14), - options: Options( - headers: { - "Authorization": "Bearer $token", - }, - ), - ), - ); - - List ttClasses = []; - List lessons = jsonDecode(response.data)["lessons"]; - for (Map ttLesson in lessons) { - ttClasses.add( - TimeTableClass( - ttLesson["period"]["name"], - ttLesson["subject"]["short"], - ttLesson["teachers"].length > 0 - ? ttLesson["teachers"][0]["short"] - : "?", - ttLesson["period"]["startTime"], - ttLesson["period"]["endTime"], - ttLesson["classrooms"].length > 0 - ? ttLesson["classrooms"][0]["short"] - : "?", - 0, - ttLesson, - ), - ); - } - TimeTableData t = TimeTableData( - DateTime.parse(jsonDecode(response.data)["date"]), ttClasses); - timetables.add(t); - return t; - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( toolbarHeight: 0, ), - body: Stack( - children: [ - getTimeTable( - timetables.firstWhere( - (element) => isSameDay( - element.date, - DateTime.now().add( - Duration(days: daydiff), - ), - ), - orElse: () => tt, - ), - daydiff, - (diff) => { - setState( - () { - daydiff = daydiff + diff; - userInteracted = true; - }, - ), - loadTt( - DateTime.now().add( - Duration(days: daydiff), - ), - ).then( - (value) => { - tt = value, - setState( - () {}, + body: loading + ? const CircularProgressIndicator() + : PageView.builder( + controller: PageController(initialPage: 500), + itemBuilder: (context, index) { + return getTimeTable( + data.timetable.timetables.values.firstWhere( + (element) => isSameDay( + element.date, + DateTime.now().add( + Duration(days: daydiff + index - 500), ), + ), + orElse: () { + data.timetable + .loadTt( + DateTime.now().add( + Duration(days: daydiff + index - 500), + ), + ) + .then( + (value) => { + setState( + () {}, + ), + }, + ); + return TimeTableData( + DateTime.now().add( + Duration(days: daydiff + index - 500), + ), + [], + []); }, ), - }, - AppLocalizations.of(context), - userInteracted, - context), - ], - ), + daydiff, + (diff) => { + setState( + () { + daydiff = daydiff + diff; + userInteracted = true; + }, + ), + data.timetable + .loadTt( + DateTime.now().add( + Duration(days: daydiff + index - 500), + ), + ) + .then( + (value) => { + setState( + () {}, + ), + }, + ), + }, + AppLocalizations.of(context), + true, + context); + }, + ), backgroundColor: Theme.of(context).colorScheme.background, ); } @@ -239,43 +128,119 @@ bool isSameDay(DateTime day1, DateTime day2) { day1.year == day2.year; } -class TimeTableData { - TimeTableData(this.date, this.classes); +TimeTableData processTimeTable(TimeTableData tt) { + List classes = tt.classes; + List periods = tt.periods; + + // Go through all classes, and assign them a startPeriod and endPeriod both equal to the their period + for (int i = 0; i < classes.length; i++) { + TimeTableClass currentClass = classes[i]; + TimeTablePeriod currentPeriod = + periods.firstWhere((period) => period.id == currentClass.period, + orElse: () => TimeTablePeriod.fromJson({ + "id": currentClass.period, + "starttime": currentClass.startTime, + "endtime": currentClass.endTime, + "name": currentClass.period, + "short": currentClass.period, + })); + currentClass.startPeriod = currentPeriod; + currentClass.endPeriod = currentPeriod; + } - final DateTime date; - final List classes; -} + // Match class end times to period end times + for (int i = 0; i < classes.length; i++) { + TimeTableClass currentClass = classes[i]; + TimeTablePeriod currentPeriod = + periods.firstWhere((period) => period.id == currentClass.endPeriod!.id, + orElse: () => TimeTablePeriod.fromJson({ + "id": currentClass.endPeriod!.id, + "starttime": currentClass.endTime, + "endtime": currentClass.endTime, + "name": currentClass.endPeriod!.id, + "short": currentClass.endPeriod!.id, + })); + if (currentClass.endTime != currentPeriod.endTime) { + int nextPeriodIndex = periods + .indexWhere((period) => period.endTime == currentClass.endTime); + if (nextPeriodIndex != -1) { + TimeTablePeriod nextPeriod = periods[nextPeriodIndex]; + currentClass.endPeriod = nextPeriod; + } + } + } + + classes.sort((a, b) { + int? sp = int.tryParse(a.startPeriod!.id); + int? ep = int.tryParse(b.endPeriod!.id); + if (sp == null || ep == null) return 0; + return sp.compareTo(ep); + }); + periods.sort((a, b) => a.startTime.compareTo(b.startTime)); + + List newClasses = []; + + // Add empty classes in between existing classes + for (int i = 0; i < classes.length - 1; i++) { + TimeTableClass currentClass = classes[i]; + TimeTableClass nextClass = classes[i + 1]; + int currentPeriodIndex = + periods.indexWhere((period) => period.id == currentClass.endPeriod!.id); + int nextPeriodIndex = + periods.indexWhere((period) => period.id == nextClass.startPeriod!.id); + bool hasClassAfter = + nextPeriodIndex != -1 && nextPeriodIndex - currentPeriodIndex > 1; + if (hasClassAfter) { + for (int j = currentPeriodIndex + 1; j < nextPeriodIndex; j++) { + TimeTablePeriod emptyPeriod = periods[j]; + TimeTableClass emptyClass = TimeTableClass( + period: emptyPeriod.id, + startTime: emptyPeriod.startTime, + endTime: emptyPeriod.endTime, + ); + emptyClass.startPeriod = emptyPeriod; + emptyClass.endPeriod = emptyPeriod; + newClasses.add(emptyClass); + } + } + } -class TimeTableClass { - TimeTableClass(this.period, this.subject, this.teacher, this.startTime, - this.endTime, this.classRoom, this.notifications, this.data); + classes.addAll(newClasses); + classes.sort((a, b) { + int? sp = int.tryParse(a.startPeriod!.id); + int? ep = int.tryParse(b.endPeriod!.id); + if (sp == null || ep == null) return 0; + return sp.compareTo(ep); + }); - final String period; - final String subject; - final String teacher; - final String startTime; - final String endTime; - final String classRoom; - final int notifications; - final dynamic data; + return TimeTableData(tt.date, classes, periods); } Widget getTimeTable(TimeTableData tt, int daydiff, Function(int) modifyDayDiff, AppLocalizations? local, bool userInteracted, BuildContext context) { List rows = []; - /* - if (daydiff == 0 && tt.classes.isNotEmpty) { - String endTime = tt.classes.last.endTime; - DateTime now = DateTime.now(); - DateTime end = DateTime(now.year, now.month, now.day, - int.parse(endTime.split(':')[0]), int.parse(endTime.split(':')[1])); - if (end.compareTo(now) < 0 && !userInteracted) { - modifyDayDiff(1); - } - } - */ + for (TimeTableClass ttclass in tt.classes) { List extrasRow = []; + if (ttclass.teachers.isNotEmpty) { + List teachers = ttclass.teachers; + String names = teachers.length == 1 ? "Teacher: " : "Teachers: "; + names += "${teachers[0].firstName} ${teachers[0].lastName}"; + for (Teacher teacher in teachers.skip(1)) { + names += ", ${teacher.firstName} ${teacher.lastName}"; + } + extrasRow.add( + Expanded( + child: Text( + names, + overflow: TextOverflow.fade, + maxLines: 5, + softWrap: false, + ), + ), + ); + } + /* Not implemented yet if (ttclass.data['curriculum'] != null) { extrasRow.add( Expanded( @@ -301,6 +266,57 @@ Widget getTimeTable(TimeTableData tt, int daydiff, Function(int) modifyDayDiff, ), ); } + */ + List cRows = []; + int? sp = int.tryParse(ttclass.startPeriod!.id); + int? ep = int.tryParse(ttclass.endPeriod!.id); + if (sp == null || ep == null) continue; + for (int i = sp; i <= ep; i++) { + TimeTablePeriod period = tt.periods.firstWhere((e) => e.short == "$i", + orElse: () => + TimeTablePeriod("$i", "00:00", "00:00", "Unknown", "Unknown")); + cRows.add( + Row( + children: [ + Text( + "${period.short}. ", + style: const TextStyle( + fontSize: 14, + ), + ), + if (ttclass.subject != null) + Text( + ttclass.subject!.short, + style: const TextStyle( + fontSize: 22, + ), + ), + const Spacer(), + Text( + "${period.startTime} - ${period.endTime}", + style: const TextStyle( + fontSize: 14, + ), + ), + const Spacer(), + for (Classroom classroom in ttclass.classrooms) + Text( + "${classroom.short} ", + style: const TextStyle( + fontSize: 18, + ), + ), + /* Not implemented yet + Badge( + label: Text(ttclass.notifications.toString()), + isLabelVisible: ttclass.notifications != 0, + child: const Icon(Icons.inbox), + ) + */ + ], + ), + ); + } rows.add(TableRow( children: [ TableCell( @@ -309,41 +325,7 @@ Widget getTimeTable(TimeTableData tt, int daydiff, Function(int) modifyDayDiff, padding: const EdgeInsets.all(10), child: Column( children: [ - Row( - children: [ - Text( - "${ttclass.period}. ", - style: const TextStyle( - fontSize: 14, - ), - ), - Text( - ttclass.subject, - style: const TextStyle( - fontSize: 22, - ), - ), - const Spacer(), - Text( - "${ttclass.startTime} - ${ttclass.endTime}", - style: const TextStyle( - fontSize: 14, - ), - ), - const Spacer(), - Text( - "${ttclass.classRoom} ", - style: const TextStyle( - fontSize: 18, - ), - ), - Badge( - label: Text(ttclass.notifications.toString()), - isLabelVisible: ttclass.notifications != 0, - child: const Icon(Icons.inbox), - ) - ], - ), + ...cRows, Row( children: extrasRow, ), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d26afe9..5c9db36 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,7 +10,6 @@ import dynamic_color import firebase_analytics import firebase_core import firebase_remote_config -import package_info import package_info_plus import path_provider_foundation import sentry_flutter @@ -24,8 +23,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseRemoteConfigPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseRemoteConfigPlugin")) - FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index af78720..f1e1a57 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,30 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: d93b0378aadce9c1388108067946276582c2ae89426c64c17920c74988508fed - url: "https://pub.dev" - source: hosted - version: "22.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: "2d8e8e123ca3675625917f535fcc0d3a50092eef44334168f9b18adc050d4c6e" + sha256: "1a52f1afae8ab7ac4741425114713bdbba802f1ce1e0648e167ffcc6e05e96cf" url: "https://pub.dev" source: hosted - version: "1.3.6" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "581a0281129283e75d4d67d6ac6e391c0515cdce37eb6eb4bc8a52e65d2b16b6" - url: "https://pub.dev" - source: hosted - version: "1.7.2" + version: "1.3.21" args: dependency: transitive description: @@ -49,22 +33,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - build: - dependency: transitive - description: - name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - build_config: - dependency: transitive - description: - name: build_config - sha256: ad77deb6e9c143a3f550fbb4c5c1e0c6aadabe24274898d06b9526c61b9cf4fb - url: "https://pub.dev" - source: hosted - version: "1.0.0" characters: dependency: transitive description: @@ -73,22 +41,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" - url: "https://pub.dev" - source: hosted - version: "0.3.5" clock: dependency: transitive description: @@ -101,18 +53,18 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - sha256: b74247fad72c171381dbe700ca17da24deac637ab6d43c343b42867acb95c991 + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "5.0.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -121,14 +73,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.4" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" crypto: dependency: transitive description: @@ -137,54 +81,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d - url: "https://pub.dev" - source: hosted - version: "1.0.6" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "7f5b48e6a448c4b46250a6113857a00eaa82821ef5a3d7f42e68eb69d1283fa3" - url: "https://pub.dev" - source: hosted - version: "2.1.1" dbus: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" dio: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" - url: "https://pub.dev" - source: hosted - version: "4.0.6" - dio_http_cache: - dependency: "direct main" - description: - name: dio_http_cache - sha256: ced385a6fcf7f9fd238cc9b866a75583ee309d97bf1ceccaa00bb978b1d00db4 + sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "5.4.0" dynamic_color: dependency: "direct main" description: name: dynamic_color - sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d + sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b url: "https://pub.dev" source: hosted - version: "1.6.6" + version: "1.6.9" fake_async: dependency: transitive description: @@ -213,98 +133,106 @@ packages: dependency: "direct main" description: name: firebase_analytics - sha256: "82992b2e93e4752d30296a881f65dde6dfdc09671f9a8cf994fa5d453bd72bde" + sha256: edb9f9eaecf0e6431e5c12b7fabdb68be3e85ce51f941ccbfa6cb71327e8b535 url: "https://pub.dev" source: hosted - version: "10.4.5" + version: "10.8.5" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: b277ab49112ebc4e545c7fc4fdfab99f692f7cd0e35347f8ed6c85d52a87562c + sha256: de4a54353cf58412c6da6b660a0dbad8efacb33b345c0286bc3a2edb869124d8 url: "https://pub.dev" source: hosted - version: "3.6.5" + version: "3.9.5" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "3f05999c06294dbdc05f4afef2b8976e6f57eb449e6aaa07ff751784763a68e0" + sha256: "77e4c02ffd0204ccc7856221193265c807b7e056fa62855f973a7f77435b5d41" url: "https://pub.dev" source: hosted - version: "0.5.4+5" + version: "0.5.5+17" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "675c209c94a1817649137cbd113fc4c9ae85e48d03dd578629abbec6d8a4d93d" + sha256: "7e049e32a9d347616edb39542cf92cd53fdb4a99fb6af0a0bff327c14cd76445" url: "https://pub.dev" source: hosted - version: "2.16.0" + version: "2.25.4" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 + sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "5.0.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: e8c408923cd3a25bd342c576a114f2126769cd1a57106a4edeaa67ea4a84e962 + sha256: "57e61d6010e253b36d38191cefd6199d7849152cdcd234b61ca290cdb278a0ba" url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "2.11.4" firebase_performance: dependency: "direct main" description: name: firebase_performance - sha256: bdbb9435980c305a12cd2a54ef13659c64877bee16d4feeea401da144cf40729 + sha256: ceb4631f50cfefdfb675034001ff590dac8f12a70945b46d5d5fc2744dd0864e url: "https://pub.dev" source: hosted - version: "0.9.2+5" + version: "0.9.3+13" firebase_performance_platform_interface: dependency: transitive description: name: firebase_performance_platform_interface - sha256: "6faa1d2cab7336c9b91b123eb2b70ac3f721a33f90924166e75ac8471b6c6087" + sha256: "65a777eef5e0df7be151551df286c577b272aff6f8e5c585a3251ec5af8b6754" url: "https://pub.dev" source: hosted - version: "0.1.4+5" + version: "0.1.4+21" firebase_performance_web: dependency: transitive description: name: firebase_performance_web - sha256: "55b2d011e29b9b8314d44c2eb0eafe484988692f3ffe9b196bdb021ccce9822f" + sha256: b80ce2bfb30b20db26d9a37d638a4970a19056ea2dfaf7d8db3d8bd53dbd778f url: "https://pub.dev" source: hosted - version: "0.1.4+5" + version: "0.1.4+21" firebase_remote_config: dependency: "direct main" description: name: firebase_remote_config - sha256: "945fbf4afe6a5eb5fe1bebe9e310c019d2896e15b9dc579914a2be1c832a7030" + sha256: "701d563063b44ea39e4d23bb5c3fcc3f6eab8fa9247a9a0b276058ad7325ec86" url: "https://pub.dev" source: hosted - version: "4.2.6" + version: "4.3.13" firebase_remote_config_platform_interface: dependency: transitive description: name: firebase_remote_config_platform_interface - sha256: "9d424edcbfb6ff43f5829c67d22cf834083fc7636dee07b4c3e04936818814f9" + sha256: "270df798d81421bfbb7fdc4f0bebdcb77e594fefdf1e6a65d56584ff5a06ea54" url: "https://pub.dev" source: hosted - version: "1.4.6" + version: "1.4.21" firebase_remote_config_web: dependency: transitive description: name: firebase_remote_config_web - sha256: b4bb823426ef65c7af3e7829c1d8babc167951bec11712ac4ac7d884ad57ab3c + sha256: a78967a208bc7a9bf44f8339c3c411b9d7e9529c644194ea5d4d0ca503c3481b url: "https://pub.dev" source: hosted - version: "1.4.6" + version: "1.4.21" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -343,23 +271,23 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.1" flutter_localizations: dependency: "direct main" description: flutter source: sdk version: "0.0.0" flutter_pdfview: - dependency: "direct overridden" + dependency: transitive description: name: flutter_pdfview - sha256: "1a0e065689f2e0f4a795aef8ddfc3bb40387f24333290b62b4d3f1dfd6bef5fa" + sha256: a9055bf920c7095bf08c2781db431ba23577aa5da5a056a7152dc89a18fbec6f url: "https://pub.dev" source: hosted - version: "1.2.9" + version: "1.3.2" flutter_session_manager: dependency: "direct main" description: @@ -383,14 +311,6 @@ packages: description: flutter source: sdk version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" html_unescape: dependency: "direct main" description: @@ -403,10 +323,10 @@ packages: dependency: transitive description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" http_parser: dependency: transitive description: @@ -436,22 +356,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "0aa7409f6c82acfab96853b8b0c7503de49918cbe705a57cfdeb477756b4521b" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - json_serializable: - dependency: transitive - description: - name: json_serializable - sha256: "86d3edf6914d6562ed4c7d9288239fbf1a9ee3c498ed0089a535c0d3703bb323" - url: "https://pub.dev" - source: hosted - version: "4.1.4" linkify: dependency: transitive description: @@ -464,18 +368,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - logging: - dependency: transitive - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "3.0.0" matcher: dependency: transitive description: @@ -496,10 +392,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" nm: dependency: transitive description: @@ -512,34 +408,18 @@ packages: dependency: "direct main" description: name: onesignal_flutter - sha256: f4e54ad09bbfc2401b5d3e9cda6b31577facf0dd119d282d008df5710f5665d0 - url: "https://pub.dev" - source: hosted - version: "3.5.1" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "5d717e25ed5d72c52ae66f422c11272a54ef0b2fb208bcf27a4b4bc5fe033128" url: "https://pub.dev" source: hosted - version: "2.1.0" - package_info: - dependency: "direct main" - description: - name: package_info - sha256: "6c07d9d82c69e16afeeeeb6866fe43985a20b3b50df243091bfc4a4ad2b03b75" - url: "https://pub.dev" - source: hosted - version: "2.0.2" + version: "5.1.0" package_info_plus: - dependency: transitive + dependency: "direct main" description: name: package_info_plus - sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "5.0.1" package_info_plus_platform_interface: dependency: transitive description: @@ -560,26 +440,26 @@ packages: dependency: transitive description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -592,10 +472,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -604,38 +484,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" - pedantic: - dependency: transitive - description: - name: pedantic - sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" - url: "https://pub.dev" - source: hosted - version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.8" process: dependency: transitive description: @@ -644,30 +516,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "0e01f805457ef610ccaf8d18067596afc34107a27149778b06b2083edbc140c1" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 - url: "https://pub.dev" - source: hosted - version: "3.2.1" restart_app: dependency: "direct main" description: @@ -688,26 +536,26 @@ packages: dependency: transitive description: name: sentry - sha256: "39c23342fc96105da449914f7774139a17a0ca8a4e70d9ad5200171f7e47d6ba" + sha256: a7946f4a90b0feb47214981d881b98149e05f6c576da9f2a2f33945bf561de25 url: "https://pub.dev" source: hosted - version: "7.9.0" + version: "7.16.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: ff68ab31918690da004a42e20204242a3ad9ad57da7e2712da8487060ac9767f + sha256: "6db7fa1b076faf2f5dd77d8cc9ef206171f32a290cc638842d78e5d62b441a27" url: "https://pub.dev" source: hosted - version: "7.9.0" + version: "7.16.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_android: dependency: transitive description: @@ -720,63 +568,55 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shorebird_code_push: dependency: "direct main" description: name: shorebird_code_push - sha256: "751bac1e53928bd398ed5ad407938e356df8416151c572eaba73fb7d844bdf95" + sha256: "872839ac1e9b86f97db99c6456c4886620246f15e1cff9d29d4101e2beb991eb" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.3" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: ffb7124eb6752de71e87a122cc50a8a191044add69fd990d76958bc38ee552fd - url: "https://pub.dev" - source: hosted - version: "1.0.3" source_span: dependency: transitive description: @@ -785,38 +625,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: transitive description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.3" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -837,10 +685,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -853,10 +701,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" typed_data: dependency: transitive description: @@ -869,74 +717,74 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.2.4" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.2.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.3.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.2.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.1" uuid: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.3.3" vector_math: dependency: transitive description: @@ -949,26 +797,18 @@ packages: dependency: transitive description: name: vm_service - sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "11.7.1" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" + version: "11.10.0" web: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" webdriver: dependency: transitive description: @@ -977,70 +817,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: "82f6787d5df55907aa01e49bd9644f4ed1cc82af7a8257dd9947815959d2e755" - url: "https://pub.dev" - source: hosted - version: "4.2.4" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: "0d8f5ac96a155e672129bf94c7abf625de01241d44d269dbaff083f1b4deb1aa" - url: "https://pub.dev" - source: hosted - version: "3.9.5" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "9d32a63a5ee111b37482cb3eac3379b9f0992afd27a52ee30279dbf06f41918b" - url: "https://pub.dev" - source: hosted - version: "2.5.1" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: d2f7241849582da80b79acb03bb936422412ce5c0c79fb5f6a1de5421a5aecc4 - url: "https://pub.dev" - source: hosted - version: "3.7.4" win32: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.2.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" + version: "6.5.0" sdks: - dart: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8ae5e01..27c9665 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,42 +3,37 @@ description: A custom mobile client for EduPage, focusing on design and speed im publish_to: none version: 1.9.0 environment: - sdk: '>=2.19.0 <3.0.0' + sdk: "^3.2.0" dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter - cupertino_icons: ^1.0.2 - dio: ^4.0.6 - dio_http_cache: ^0.3.0 + dio: ^5.4.0 shared_preferences: ^2.0.17 flutter_session_manager: ^1.0.3 - connectivity_plus: ^3.0.2 + connectivity_plus: ^5.0.2 intl: any sentry_flutter: ^7.4.0 firebase_core: ^2.9.0 firebase_analytics: ^10.2.0 - onesignal_flutter: ^3.5.0 + onesignal_flutter: ^5.1.0 firebase_performance: ^0.9.1 html_unescape: ^2.0.0 - webview_flutter: ^4.2.0 flutter_cached_pdfview: ^0.4.1 - package_info: ^2.0.2 + package_info_plus: ^5.0.1 url_launcher: ^6.1.12 shorebird_code_push: ^1.1.0 restart_app: ^1.2.1 flutter_linkify: ^6.0.0 dynamic_color: ^1.4.0 firebase_remote_config: ^4.2.6 -dependency_overrides: - flutter_pdfview: 1.2.9 dev_dependencies: integration_test: sdk: flutter flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^3.0.1 flutter: assets: - shorebird.yaml diff --git a/test/widget_test.dart b/test/widget_test.dart index 853256d..143b1bc 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,44 +5,61 @@ // 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:eduapge2/login.dart'; +import 'package:eduapge2/home.dart'; +import 'package:eduapge2/messages.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:shared_preferences/shared_preferences.dart'; void main() { - testWidgets('Login page', (WidgetTester tester) async { - // Initiates SharedPreferences - SharedPreferences.setMockInitialValues({}); - - // Gets test user's username and password from environment - String? username = const String.fromEnvironment("USERNAME"); - String? password = const String.fromEnvironment("PASSWORD"); - - // Initiates widget - await tester.pumpWidget(const LocalizationsInj(child: LoginPage())); - - // Checks for TextFields and login button - expect(find.text('Username'), findsOneWidget); - expect(find.text('Password'), findsOneWidget); - expect(find.text('Login'), findsOneWidget); + group('TimeOfDay', () { + TimeOfDay time1 = const TimeOfDay(hour: 4, minute: 20); + TimeOfDay time2 = const TimeOfDay(hour: 6, minute: 09); + TimeOfDay time3 = const TimeOfDay(hour: 6, minute: 05); + TimeOfDay time4 = const TimeOfDay(hour: 4, minute: 19); + test('is lesser', () => {expect(time1 < time2, true)}); + test('is lesser or equal (equal)', () => {expect(time1 <= time1, true)}); + test('is lesser or equal (lesser)', () => {expect(time4 <= time1, true)}); + test('is greater', () => {expect(time2 > time3, true)}); + }); - // Types test user's credentails into fields - await tester.enterText(find.byType(TextField).at(0), username); - await tester.enterText(find.byType(TextField).at(1), password); - await tester.tap(find.byType(ElevatedButton)); + group('DateTime', () { + DateTime parsed = DateTimeExtension.parseTime("4:20"); + test( + 'parseTime', () => {expect(parsed.hour, 4), expect(parsed.minute, 20)}); + }); - // Checks that the credentails were stored correctly - SharedPreferences prefs = await SharedPreferences.getInstance(); - expect(prefs.get("email"), equals(username)); - expect(prefs.get("password"), equals(password)); + group('List', () { + List testList = [1, 2, 3, 4, 5]; + test( + 'move forward', + () => { + testList.move(0, 4), + expect(testList[0] == 2, true), + expect(testList[4] == 1, true) + }); + test( + 'move back', + () => { + testList.move(4, 0), + expect(testList[0] == 1, true), + expect(testList[4] == 5, true) + }); + test( + 'move multiple', + () => { + testList.move(1, 3), + testList.move(2, 4), + expect(testList[1] == 3, true), + expect(testList[3] == 5, true), + expect(testList[4] == 4, true), + }); }); } class LocalizationsInj extends StatelessWidget { final Widget child; - const LocalizationsInj({Key? key, required this.child}) : super(key: key); + const LocalizationsInj({super.key, required this.child}); @override Widget build(BuildContext context) {