diff --git a/refilc/lib/api/client.dart b/refilc/lib/api/client.dart index ed02aa1..73f079f 100644 --- a/refilc/lib/api/client.dart +++ b/refilc/lib/api/client.dart @@ -54,6 +54,9 @@ class FilcAPI { static const payment = "$baseUrl/v4/payment"; static const stripeSheet = "$payment/stripe-sheet"; + // Cloud Sync + static const cloudSyncApi = "$baseUrl/v4/me/cloud-sync"; + static Future checkConnectivity() async => (await Connectivity().checkConnectivity())[0] != ConnectivityResult.none; @@ -390,6 +393,32 @@ class FilcAPI { return null; } + + // cloud sync + static Future cloudSync(Map data, String token) async { + try { + var client = http.Client(); + + http.Response res = await client.post( + Uri.parse(cloudSyncApi), + body: data, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Bearer $token', + }, + ); + + if (res.statusCode != 200) { + throw "HTTP ${res.statusCode}: ${res.body}"; + } + + return jsonDecode(res.body); + } on Exception catch (error, stacktrace) { + log("ERROR: FilcAPI.cloudSync: $error $stacktrace"); + } + + return null; + } } class ErrorReport { diff --git a/refilc/lib/database/init.dart b/refilc/lib/database/init.dart index 2850814..75826e6 100644 --- a/refilc/lib/database/init.dart +++ b/refilc/lib/database/init.dart @@ -56,6 +56,9 @@ const settingsDB = DatabaseStruct("settings", { "uwu_mode": int, "new_popups": int, "unseen_new_features": String, + "cloud_sync_enabled": int, + "cloud_sync_token": String, + "local_updated_at": String, // quick settings "q_timetable_lesson_num": int, "q_timetable_sub_tiles": int, "q_subjects_sub_tiles": int, diff --git a/refilc/lib/models/cloud_sync_data.dart b/refilc/lib/models/cloud_sync_data.dart new file mode 100644 index 0000000..71ab4d9 --- /dev/null +++ b/refilc/lib/models/cloud_sync_data.dart @@ -0,0 +1,22 @@ +class CloudSyncData { + Map settings; + List deviceIds; + String reFilcPlusId; + Map json; + + CloudSyncData({ + this.settings = const {}, + this.deviceIds = const [], + this.reFilcPlusId = "", + required this.json, + }); + + factory CloudSyncData.fromJson(Map json) { + return CloudSyncData( + settings: json['settings'] ?? {}, + deviceIds: List.from(json['device_ids'] ?? []), + reFilcPlusId: json['refilc_plus_id'] ?? "", + json: json, + ); + } +} diff --git a/refilc/lib/models/settings.dart b/refilc/lib/models/settings.dart index 1863941..bc75a50 100644 --- a/refilc/lib/models/settings.dart +++ b/refilc/lib/models/settings.dart @@ -109,6 +109,9 @@ class SettingsProvider extends ChangeNotifier { bool _uwuMode; bool _newPopups; List _unseenNewFeatures; + bool _cloudSyncEnabled; + String _cloudSyncToken; + DateTime _updatedAt; // quick settings bool _qTimetableLessonNum; bool _qTimetableSubTiles; @@ -184,6 +187,9 @@ class SettingsProvider extends ChangeNotifier { required bool uwuMode, required bool newPopups, required List unseenNewFeatures, + required bool cloudSyncEnabled, + required String cloudSyncToken, + required DateTime updatedAt, required bool qTimetableLessonNum, required bool qTimetableSubTiles, required bool qSubjectsSubTiles, @@ -256,6 +262,9 @@ class SettingsProvider extends ChangeNotifier { _uwuMode = uwuMode, _newPopups = newPopups, _unseenNewFeatures = unseenNewFeatures, + _cloudSyncEnabled = cloudSyncEnabled, + _cloudSyncToken = cloudSyncToken, + _updatedAt = updatedAt, _qTimetableLessonNum = qTimetableLessonNum, _qTimetableSubTiles = qTimetableSubTiles, _qSubjectsSubTiles = qSubjectsSubTiles; @@ -347,6 +356,9 @@ class SettingsProvider extends ChangeNotifier { uwuMode: map['uwu_mode'] == 1, newPopups: map['new_popups'] == 1, unseenNewFeatures: jsonDecode(map["unseen_new_features"]).cast(), + cloudSyncEnabled: map['cloud_sync_enabled'] == 1, + cloudSyncToken: map['cloud_sync_token'], + updatedAt: DateTime.tryParse(map['local_updated_at']) ?? DateTime.now(), qTimetableLessonNum: map['q_timetable_lesson_num'] == 1, qTimetableSubTiles: map['q_timetable_sub_tiles'] == 1, qSubjectsSubTiles: map['q_subjects_sub_tiles'] == 1, @@ -426,6 +438,9 @@ class SettingsProvider extends ChangeNotifier { "uwu_mode": _uwuMode ? 1 : 0, "new_popups": _newPopups ? 1 : 0, "unseen_new_features": jsonEncode(_unseenNewFeatures), + "cloud_sync_enabled": _cloudSyncEnabled ? 1 : 0, + "cloud_sync_token": _cloudSyncToken, + "local_updated_at": _updatedAt.toIso8601String(), "q_timetable_lesson_num": _qTimetableLessonNum ? 1 : 0, "q_timetable_sub_tiles": _qTimetableSubTiles ? 1 : 0, "q_subjects_sub_tiles": _qSubjectsSubTiles ? 1 : 0, @@ -509,6 +524,9 @@ class SettingsProvider extends ChangeNotifier { uwuMode: false, newPopups: true, unseenNewFeatures: ['grade_exporting'], + cloudSyncEnabled: false, + cloudSyncToken: '', + updatedAt: DateTime.now(), qTimetableLessonNum: true, qTimetableSubTiles: true, qSubjectsSubTiles: true, @@ -583,6 +601,9 @@ class SettingsProvider extends ChangeNotifier { bool get uwuMode => _uwuMode; bool get newPopups => _newPopups; List get unseenNewFeatures => _unseenNewFeatures; + bool get cloudSyncEnabled => _cloudSyncEnabled; + String get cloudSyncToken => _cloudSyncToken; + DateTime get updatedAt => _updatedAt; bool get qTimetableLessonNum => _qTimetableLessonNum; bool get qTimetableSubTiles => _qTimetableSubTiles; bool get qSubjectsSubTiles => _qSubjectsSubTiles; @@ -597,6 +618,7 @@ class SettingsProvider extends ChangeNotifier { List? gradeColors, bool? newsEnabled, String? seenNewsId, + String? seenNews, // only for restoring from map bool? notificationsEnabled, bool? notificationsGradesEnabled, bool? notificationsAbsencesEnabled, @@ -653,6 +675,8 @@ class SettingsProvider extends ChangeNotifier { bool? uwuMode, bool? newPopups, List? unseenNewFeatures, + bool? cloudSyncEnabled, + String? cloudSyncToken, bool? qTimetableLessonNum, bool? qTimetableSubTiles, bool? qSubjectsSubTiles, @@ -675,6 +699,7 @@ class SettingsProvider extends ChangeNotifier { tempList.add(seenNewsId); _seenNews = tempList.join(','); } + if (seenNews != null && seenNews != _seenNews) _seenNews = seenNews; if (notificationsEnabled != null && notificationsEnabled != _notificationsEnabled) { _notificationsEnabled = notificationsEnabled; @@ -850,6 +875,12 @@ class SettingsProvider extends ChangeNotifier { if (unseenNewFeatures != null && unseenNewFeatures != _unseenNewFeatures) { _unseenNewFeatures = unseenNewFeatures; } + if (cloudSyncEnabled != null && cloudSyncEnabled != _cloudSyncEnabled) { + _cloudSyncEnabled = cloudSyncEnabled; + } + if (cloudSyncToken != null && cloudSyncToken != _cloudSyncToken) { + _cloudSyncToken = cloudSyncToken; + } if (qTimetableLessonNum != null && qTimetableLessonNum != _qTimetableLessonNum) { _qTimetableLessonNum = qTimetableLessonNum; @@ -861,11 +892,115 @@ class SettingsProvider extends ChangeNotifier { if (qSubjectsSubTiles != null && qSubjectsSubTiles != _qSubjectsSubTiles) { _qSubjectsSubTiles = qSubjectsSubTiles; } + // change updated at time + _updatedAt = DateTime.now(); // store or not if (store) await _database?.store.storeSettings(this); notifyListeners(); } + Future updateFromMap({ + required Map map, + bool store = true, + }) async { + print(map); + + await update( + store: store, + language: map["language"], + startPage: Pages.values[map["start_page"] ?? _startPage.index], + rounding: map["rounding"], + theme: ThemeMode.values[map["theme"] ?? _theme.index], + accentColor: + AccentColor.values[map["accent_color"] ?? _accentColor.index], + gradeColors: [ + Color(map["grade_color1"] ?? _gradeColors[0].value), + Color(map["grade_color2"] ?? _gradeColors[1].value), + Color(map["grade_color3"] ?? _gradeColors[2].value), + Color(map["grade_color4"] ?? _gradeColors[3].value), + Color(map["grade_color5"] ?? _gradeColors[4].value), + ], + newsEnabled: map["news"] == 1, + seenNews: map["seen_news"], + notificationsEnabled: map["notifications"] == 1, + notificationsGradesEnabled: map["notifications_grades"] == 1, + notificationsAbsencesEnabled: map["notifications_absences"] == 1, + notificationsMessagesEnabled: map["notifications_messages"] == 1, + notificationsLessonsEnabled: map["notifications_lessons"] == 1, + notificationsBitfield: map["notifications_bitfield"], + notificationPollInterval: map["notification_poll_interval"], + developerMode: map["developer_mode"] == 1, + vibrate: + VibrationStrength.values[map["vibration_strength"] ?? _vibrate.index], + abWeeks: map["ab_weeks"] == 1, + swapABweeks: map["swap_ab_weeks"] == 1, + updateChannel: + UpdateChannel.values[map["update_channel"] ?? _updateChannel.index], + config: Config.fromJson(jsonDecode(map["config"] ?? "{}")), + xFilcId: map["x_filc_id"], + analyticsEnabled: map["analytics_enabled"] == 1, + graphClassAvg: map["graph_class_avg"] == 1, + goodStudent: false, + presentationMode: map["presentation_mode"] == 1, + bellDelayEnabled: map["bell_delay_enabled"] == 1, + bellDelay: map["bell_delay"], + gradeOpeningFun: map["grade_opening_fun"] == 1, + iconPack: Map.fromEntries( + IconPack.values.map((e) => MapEntry(e.name, e)))[map["icon_pack"]]!, + customAccentColor: + Color(map["custom_accent_color"] ?? _customAccentColor.value), + customBackgroundColor: + Color(map["custom_background_color"] ?? _customBackgroundColor.value), + customHighlightColor: + Color(map["custom_highlight_color"] ?? _customHighlightColor.value), + customIconColor: + Color(map["custom_icon_color"] ?? _customIconColor.value), + customTextColor: + Color(map["custom_text_color"] ?? _customTextColor.value), + shadowEffect: map["shadow_effect"] == 1, + premiumScopes: + jsonDecode(map["premium_scopes"] ?? _premiumScopes).cast(), + premiumAccessToken: map["premium_token"], + premiumLogin: map["premium_login"], + lastAccountId: map["last_account_id"], + renamedSubjectsEnabled: map["renamed_subjects_enabled"] == 1, + renamedSubjectsItalics: map["renamed_subjects_italics"] == 1, + renamedTeachersEnabled: map["renamed_teachers_enabled"] == 1, + renamedTeachersItalics: map["renamed_teachers_italics"] == 1, + liveActivityColor: + Color(map["live_activity_color"] ?? _liveActivityColor), + welcomeMessage: map["welcome_message"], + appIcon: map["app_icon"], + currentThemeId: map['current_theme_id'], + currentThemeDisplayName: map['current_theme_display_name'], + currentThemeCreator: map['current_theme_creator'], + showBreaks: map['show_breaks'] == 1, + // pinSetGeneral: map['general_s_pin'], + // pinSetPersonalize: map['personalize_s_pin'], + // pinSetNotify: map['notify_s_pin'], + // pinSetExtras: map['extras_s_pin'], + fontFamily: map['font_family'], + titleOnlyFont: map['title_only_font'] == 1, + plusSessionId: map['plus_session_id'], + calSyncRoomLocation: map['cal_sync_room_location'], + calSyncShowExams: map['cal_sync_show_exams'] == 1, + calSyncShowTeacher: map['cal_sync_show_teacher'] == 1, + calSyncRenamed: map['cal_sync_renamed'] == 1, + calendarId: map['calendar_id'], + navShadow: map['nav_shadow'] == 1, + newColors: map['new_colors'] == 1, + uwuMode: map['uwu_mode'] == 1, + newPopups: map['new_popups'] == 1, + unseenNewFeatures: + jsonDecode(map["unseen_new_features"] ?? "[]").cast(), + cloudSyncEnabled: map['cloud_sync_enabled'] == 1, + cloudSyncToken: map['cloud_sync_token'], + qTimetableLessonNum: map['q_timetable_lesson_num'] == 1, + qTimetableSubTiles: map['q_timetable_sub_tiles'] == 1, + qSubjectsSubTiles: map['q_subjects_sub_tiles'] == 1, + ); + } + void exportJson() { String sets = json.encode(toMap()); Clipboard.setData(ClipboardData(text: sets)); diff --git a/refilc/lib/models/user.dart b/refilc/lib/models/user.dart index e4fca5d..33b9e73 100644 --- a/refilc/lib/models/user.dart +++ b/refilc/lib/models/user.dart @@ -21,6 +21,10 @@ class User { String accessToken; DateTime accessTokenExpire; String refreshToken; + // cloud sync + // String qwidAccessToken; + // DateTime? qwidAccessTokenExpire; + // String qwidRefreshToken; String get displayName => nickname != '' ? nickname : name; bool get hasStreak => gradeStreak > 0; @@ -39,6 +43,9 @@ class User { required this.accessToken, required this.accessTokenExpire, required this.refreshToken, + // this.qwidAccessToken = "", + // this.qwidAccessTokenExpire, + // this.qwidRefreshToken = "", }) { if (id != null) { this.id = id; @@ -74,6 +81,11 @@ class User { ? map["access_token_expire"] : DateTime.now().toIso8601String()), refreshToken: map["refresh_token"] ?? "", + // qwidAccessToken: map["qwid_access_token"] ?? "", + // qwidAccessTokenExpire: map["qwid_access_token_expire"] != "" + // ? DateTime.parse(map["qwid_access_token_expire"]) + // : null, + // qwidRefreshToken: map["qwid_refresh_token"] ?? "", ); } @@ -92,6 +104,11 @@ class User { "access_token": accessToken, "access_token_expire": accessTokenExpire.toIso8601String(), "refresh_token": refreshToken, + // "qwid_access_token": qwidAccessToken, + // "qwid_access_token_expire": qwidAccessTokenExpire != null + // ? qwidAccessTokenExpire!.toIso8601String() + // : "", + // "qwid_refresh_token": qwidRefreshToken, }; } diff --git a/refilc/pubspec.yaml b/refilc/pubspec.yaml index f3e4921..0797927 100644 --- a/refilc/pubspec.yaml +++ b/refilc/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: crypto: ^3.0.2 elegant_notification: ^2.2.0 flutter_feather_icons: ^2.0.0+1 - live_activities: ^1.7.4 + # live_activities: ^1.7.4 animated_flip_counter: ^0.3.4 lottie: ^3.1.0 rive: ^0.12.4 @@ -62,10 +62,10 @@ dependencies: flutter_expandable_fab: ^2.0.0 uni_links: ^0.5.1 url_launcher: ^6.1.6 - workmanager: - git: - url: https://github.com/refilc/flutter_workmanager.git - ref: v0.5.1 + # workmanager: + # git: + # url: https://github.com/refilc/flutter_workmanager.git + # ref: v0.5.1 flutter_svg: ^2.0.10+1 image_picker: ^1.0.7 animations: ^2.0.1 diff --git a/refilc_kreta_api/lib/providers/grade_provider.dart b/refilc_kreta_api/lib/providers/grade_provider.dart index 210be49..804b1e9 100644 --- a/refilc_kreta_api/lib/providers/grade_provider.dart +++ b/refilc_kreta_api/lib/providers/grade_provider.dart @@ -153,7 +153,7 @@ class GradeProvider with ChangeNotifier { for (Grade grade in grs) { if (grade.value.value == 5) { gradeStreak++; - } else { + } else if (grade.value.value !=0) { break; } } diff --git a/refilc_mobile_ui/lib/pages/timetable/fs_timetable.dart b/refilc_mobile_ui/lib/pages/timetable/fs_timetable.dart index 64e3be0..ad2f61a 100644 --- a/refilc_mobile_ui/lib/pages/timetable/fs_timetable.dart +++ b/refilc_mobile_ui/lib/pages/timetable/fs_timetable.dart @@ -75,7 +75,7 @@ class _FSTimetableState extends State { body: ListView.builder( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 24.0), - itemCount: maxLessonCount + 1, + itemCount: maxLessonCount + 2, itemBuilder: (context, index) { List columns = []; for (int dayIndex = -1; dayIndex < days.length; dayIndex++) { @@ -119,10 +119,8 @@ class _FSTimetableState extends State { if (lessons.isEmpty) continue; - int lsnIndx = int.tryParse(lessons.first.lessonIndex) ?? 1; - final dayOffset = lsnIndx == 0 ? 1 : lsnIndx; - if (index == 0 && dayIndex >= 0) { + // if (index == 0 || dayIndex >=0) { columns.add( SizedBox( width: colw, @@ -141,16 +139,10 @@ class _FSTimetableState extends State { continue; } - final lessonIndex = index - dayOffset; - Lesson? lsn = lessons.firstWhereOrNull( (e) => e.lessonIndex == (index - 1).toString()); - if (lessonIndex < 0 || - lessonIndex > lessons.length || - (index == 1 && lsnIndx != 0) || - (lsnIndx != 0 && lessonIndex - 1 == -1) || - lsn == null) { + if (lsn == null) { columns.add(SizedBox(width: colw)); continue; } @@ -259,4 +251,4 @@ class _FSTimetableState extends State { ), ); } -} +} \ No newline at end of file diff --git a/refilc_mobile_ui/lib/plus/components/plan_card.dart b/refilc_mobile_ui/lib/plus/components/plan_card.dart index f8aba19..a672de0 100644 --- a/refilc_mobile_ui/lib/plus/components/plan_card.dart +++ b/refilc_mobile_ui/lib/plus/components/plan_card.dart @@ -98,11 +98,48 @@ class PlusPlanCard extends StatelessWidget { onTap: () { // pop dialog Navigator.of(context).pop(); - // start payment process - Navigator.of(context) - .push(MaterialPageRoute(builder: (context) { - return PremiumActivationView(product: id); - })); + // show payment option selector + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0)), + title: Text('payment_method'.i18n), + content: Text('select_payment_method'.i18n), + actions: [ + ActionButton( + label: "stripe".i18n, + onTap: () { + // pop dialog + Navigator.of(context).pop(); + // start payment process + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) { + return PremiumActivationView( + product: id, + paymentProvider: "stripe", + ); + })); + }, + ), + ActionButton( + label: "paypal".i18n, + onTap: () { + // pop dialog + Navigator.of(context).pop(); + // start payment process + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) { + return PremiumActivationView( + product: id, + paymentProvider: "paypal", + ); + })); + }, + ), + ], + ), + ); }, ), ], diff --git a/refilc_mobile_ui/lib/plus/plus_screen.i18n.dart b/refilc_mobile_ui/lib/plus/plus_screen.i18n.dart index 902ba64..ac0db01 100644 --- a/refilc_mobile_ui/lib/plus/plus_screen.i18n.dart +++ b/refilc_mobile_ui/lib/plus/plus_screen.i18n.dart @@ -47,11 +47,16 @@ extension SettingsLocalization on String { "rfp_16": "Private leaks and informations about upcoming features", "rfp_17": "Grade exporting", "rfp_18": "Viewing exported grades", - // docs popup + // docs and payment method popup "docs": "Documents", "docs_acceptance": "By pressing the \"Next\" button, you accept reFilc's Terms and Conditions for subscriptions (available at the following link: filc.one/pay-terms) and our Privacy Policy (available at the following link: filc.one/pay-privacy).", "next": "Next", + "payment_method": "Payment Method", + "select_payment_method": + "Please select a preferred payment method! Credit card payments are handled by Stripe, which also supports Apple Pay, Google Pay and Revolut Pay.", + "stripe": "Credit Card", + "paypal": "PayPal", // other "and": " and ", "every": "Every ", @@ -106,11 +111,16 @@ extension SettingsLocalization on String { "rfp_16": "Privát betekintések és információk közelgő újításokról", "rfp_17": "Jegy exportálás", "rfp_18": "Exportált jegyek megtekintése", - // docs popup + // docs and payment method popup "docs": "Dokumentumok", "docs_acceptance": "A \"Tovább\" gombra kattintva elfogadod a reFilc előfizetésekkel kapcsolatos Általános Szerződési Feltételeit (elérhető az alábbi link-en: filc.one/pay-terms), valamint Adatkezelési Tájékoztatónkat (elérhető az alábbi link-en: filc.one/pay-privacy).", "next": "Tovább", + "payment_method": "Fizetési mód", + "select_payment_method": + "Kérlek válassz egy fizetési módot! A bankkártyás fizetést a Stripe biztosítja, mely támogat Apple Pay-t, Google Pay-t és Revolut Pay-t is.", + "stripe": "Bankkártya", + "paypal": "PayPal", // other "and": " és ", "every": "Minden ", @@ -167,11 +177,16 @@ extension SettingsLocalization on String { "rfp_16": "Private Leaks und Informationen über kommende Funktionen", "rfp_17": "Notenexport", "rfp_18": "Anzeigen exportierter Noten", - // docs popup + // docs and payment method popup "docs": "Dokumente", "docs_acceptance": "Durch Drücken der Schaltfläche \"Weiter\" akzeptieren Sie die Allgemeinen Geschäftsbedingungen von reFilc für Abonnements (verfügbar unter folgendem Link: filc.one/pay-terms) und unsere Datenschutzrichtlinie (verfügbar unter folgendem Link: filc.one/pay-privacy).", "next": "Weiter", + "payment_method": "Zahlungsmethode", + "select_payment_method": + "Bitte wählen Sie eine bevorzugte Zahlungsmethode aus! Kreditkartenzahlungen werden von Stripe abgewickelt, der auch Apple Pay, Google Pay und Revolut Pay unterstützt.", + "stripe": "Kreditkarte", + "paypal": "PayPal", // other "and": " und ", "every": "Jeder ", diff --git a/refilc_mobile_ui/lib/screens/login/qwid_login.dart b/refilc_mobile_ui/lib/screens/login/qwid_login.dart new file mode 100644 index 0000000..a19269e --- /dev/null +++ b/refilc_mobile_ui/lib/screens/login/qwid_login.dart @@ -0,0 +1,170 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class QwIDLoginWidget extends StatefulWidget { + const QwIDLoginWidget({super.key, required this.onLogin}); + + // final String selectedSchool; + final void Function(String code) onLogin; + + @override + State createState() => _QwIDLoginWidgetState(); +} + +class _QwIDLoginWidgetState extends State + with TickerProviderStateMixin { + late final WebViewController controller; + late AnimationController _animationController; + var loadingPercentage = 0; + var currentUrl = ''; + bool _hasFadedIn = false; + + @override + void initState() { + super.initState(); + + _animationController = AnimationController( + vsync: this, // Use the TickerProviderStateMixin + duration: const Duration(milliseconds: 350), + ); + + controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onNavigationRequest: (n) async { + if (n.url.startsWith('refilc://oauth2-callback/qwid')) { + setState(() { + loadingPercentage = 0; + currentUrl = n.url; + }); + + // final String instituteCode = widget.selectedSchool; + // if (!n.url.startsWith( + // 'https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect?code=')) { + // return; + // } + + String longLivedToken = n.url + .replaceAll('refilc://oauth2-callback/qwid?access_token=', ''); + + widget.onLogin(longLivedToken); + // Future.delayed(const Duration(milliseconds: 500), () { + // Navigator.of(context).pop(); + // }); + // Navigator.of(context).pop(); + + return NavigationDecision.prevent; + } else { + return NavigationDecision.navigate; + } + }, + onPageStarted: (url) async { + // setState(() { + // loadingPercentage = 0; + // currentUrl = url; + // }); + + // // final String instituteCode = widget.selectedSchool; + // if (!url.startsWith( + // 'https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect?code=')) { + // return; + // } + + // List requiredThings = url + // .replaceAll( + // 'https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect?code=', + // '') + // .replaceAll( + // '&scope=openid%20email%20offline_access%20kreta-ellenorzo-webapi.public%20kreta-eugyintezes-webapi.public%20kreta-fileservice-webapi.public%20kreta-mobile-global-webapi.public%20kreta-dkt-webapi.public%20kreta-ier-webapi.public&state=refilc_student_mobile&session_state=', + // ':') + // .split(':'); + + // String code = requiredThings[0]; + // // String sessionState = requiredThings[1]; + + // widget.onLogin(code); + // // Future.delayed(const Duration(milliseconds: 500), () { + // // Navigator.of(context).pop(); + // // }); + // // Navigator.of(context).pop(); + }, + onProgress: (progress) { + setState(() { + loadingPercentage = progress; + }); + }, + onPageFinished: (url) { + setState(() { + loadingPercentage = 100; + }); + }, + )) + ..loadRequest( + Uri.parse( + 'https://qwid.qwit.dev/oauth2/authorize?client_id=99aa103a-0bd7-43e0-8421-3bb0b2f6adb1&scope=*&redirect_uri=https://api.refilc.hu/v4/oauth2/callback/app/qwid&response_type=code'), // &institute_code=${widget.selectedSchool} + ); + } + + // Future loadLoginUrl() async { + // String nonceStr = await Provider.of(context, listen: false) + // .getAPI(KretaAPI.nonce, json: false); + + // Nonce nonce = getNonce(nonceStr, ); + // } + + @override + void dispose() { + // Step 3: Dispose of the animation controller + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // Trigger the fade-in animation only once when loading reaches 100% + if (loadingPercentage == 100 && !_hasFadedIn) { + _animationController.forward(); // Play the animation + _hasFadedIn = + true; // Set the flag to true, so the animation is not replayed + } + + return Stack( + children: [ + // Webview that will be displayed only when the loading is 100% + if (loadingPercentage == 100) + FadeTransition( + opacity: Tween(begin: 0, end: 1).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeIn, + ), + ), + child: WebViewWidget( + controller: controller, + ), + ), + + // Show the CircularProgressIndicator while loading is not 100% + if (loadingPercentage < 100) + Center( + child: TweenAnimationBuilder( + tween: Tween(begin: 0, end: loadingPercentage / 100.0), + duration: const Duration(milliseconds: 300), + builder: (context, double value, child) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + value: value, // Smoothly animates the progress + ), + ], + ); + }, + ), + ), + ], + ); + } +} diff --git a/refilc_mobile_ui/lib/screens/settings/settings_screen.dart b/refilc_mobile_ui/lib/screens/settings/settings_screen.dart index 6868474..c312771 100644 --- a/refilc_mobile_ui/lib/screens/settings/settings_screen.dart +++ b/refilc_mobile_ui/lib/screens/settings/settings_screen.dart @@ -68,6 +68,7 @@ import 'package:refilc_mobile_ui/screens/settings/user/profile_pic.dart'; // import 'package:refilc_plus/ui/mobile/settings/welcome_message.dart'; // import 'package:refilc_mobile_ui/screens/error_screen.dart'; import 'package:refilc_mobile_ui/screens/error_report_screen.dart'; +import 'submenu/cloud_sync_screen.dart'; import 'submenu/general_screen.dart'; import 'package:refilc_plus/ui/mobile/plus/settings_inline.dart'; @@ -428,6 +429,13 @@ class SettingsScreenState extends State borderRadius: const BorderRadius.vertical( top: Radius.circular(12.0), bottom: Radius.circular(4.0)), ), + // cloud-sync + const MenuCloudSyncSettings( + borderRadius: BorderRadius.vertical( + top: Radius.circular(4.0), + bottom: Radius.circular(4.0), + ), + ), // open dcs (digital collaboration space) PanelButton( onPressed: () => _openDKT(user.user!), diff --git a/refilc_mobile_ui/lib/screens/settings/submenu/cloud_sync_screen.dart b/refilc_mobile_ui/lib/screens/settings/submenu/cloud_sync_screen.dart new file mode 100644 index 0000000..0d9ad67 --- /dev/null +++ b/refilc_mobile_ui/lib/screens/settings/submenu/cloud_sync_screen.dart @@ -0,0 +1,252 @@ +// import 'package:refilc/models/settings.dart'; +import 'dart:convert'; + +import 'package:refilc/api/client.dart'; +import 'package:refilc/api/providers/database_provider.dart'; +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc/models/settings.dart'; +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; +import 'package:refilc_mobile_ui/common/splitted_panel/splitted_panel.dart'; +import 'package:refilc_mobile_ui/screens/login/qwid_login.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; +import 'package:refilc/models/cloud_sync_data.dart'; +// import 'package:provider/provider.dart'; +import 'submenu_screen.i18n.dart'; + +class MenuCloudSyncSettings extends StatelessWidget { + const MenuCloudSyncSettings({ + super.key, + this.borderRadius = const BorderRadius.vertical( + top: Radius.circular(4.0), bottom: Radius.circular(4.0)), + }); + + final BorderRadius borderRadius; + + @override + Widget build(BuildContext context) { + return PanelButton( + onPressed: () => Navigator.of(context, rootNavigator: true).push( + CupertinoPageRoute( + builder: (context) => const CloudSyncSettingsScreen()), + ), + title: Text("cloud_sync".i18n), + leading: Icon( + FeatherIcons.uploadCloud, + size: 22.0, + color: AppColors.of(context).text.withOpacity(0.95), + ), + borderRadius: borderRadius, + ); + } +} + +class CloudSyncSettingsScreen extends StatefulWidget { + const CloudSyncSettingsScreen({super.key}); + + @override + CloudSyncSettingsScreenState createState() => CloudSyncSettingsScreenState(); +} + +class CloudSyncSettingsScreenState extends State { + late SettingsProvider settingsProvider; + late UserProvider user; + + String longLivedToken = ''; + + @override + Widget build(BuildContext context) { + SettingsProvider settingsProvider = Provider.of(context); + // UserProvider user = Provider.of(context); + + return Scaffold( + appBar: AppBar( + surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, + leading: BackButton(color: AppColors.of(context).text), + title: Text( + "cloud_sync".i18n, + style: TextStyle(color: AppColors.of(context).text), + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), + child: Column( + children: [ + SplittedPanel( + padding: const EdgeInsets.only(top: 8.0), + cardPadding: const EdgeInsets.all(4.0), + isSeparated: true, + children: [ + PanelButton( + padding: const EdgeInsets.only(left: 14.0, right: 6.0), + onPressed: () async { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + isScrollControlled: + true, // This ensures the modal accommodates input fields properly + builder: (BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height * 0.9 + + MediaQuery.of(context).viewInsets.bottom, + decoration: const BoxDecoration( + color: Color(0xFFDAE4F7), + borderRadius: BorderRadius.only( + topRight: Radius.circular(24.0), + topLeft: Radius.circular(24.0), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 18), + child: Container( + decoration: const BoxDecoration( + color: Color(0xFFB9C8E5), + borderRadius: BorderRadius.only( + topRight: Radius.circular(2.0), + topLeft: Radius.circular(2.0), + ), + ), + width: 40, + height: 4, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + right: 14, left: 14, bottom: 24), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(16), + ), + child: QwIDLoginWidget( + onLogin: (String token) { + setState(() { + longLivedToken = token; + }); + Navigator.of(context).pop(); + }, + ), + ), + ), + ), + ) + ], + ), + ); + }, + ).then((value) { + // After closing the modal bottom sheet, check if the code is set + if (longLivedToken.isNotEmpty) { + // Call your API after retrieving the code + settingsProvider.update( + cloudSyncToken: longLivedToken, + store: true, + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('login_successful'.i18n))); + } + }); + }, + trailingDivider: true, + title: Text( + "qwit_sign_in".i18n, + style: TextStyle( + color: AppColors.of(context).text.withOpacity( + settingsProvider.gradeOpeningFun ? .95 : .25), + ), + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), + ), + ), + SwitchListTile( + value: settingsProvider.cloudSyncEnabled, + onChanged: (value) { + settingsProvider.update( + cloudSyncEnabled: value, + store: true, + ); + }, + title: Text("cloud_sync_enabled".i18n), + ), + PanelButton( + padding: const EdgeInsets.only(left: 14.0, right: 6.0), + onPressed: () async { + if (settingsProvider.cloudSyncToken.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('sign_in_first'.i18n), + ), + ); + return; + } else { + FilcAPI.cloudSync( + { + "settings": jsonEncode(settingsProvider.toMap()), + // "device_ids": [ + // settingsProvider.xFilcId, + // ], + // "refilc_plus_id": settingsProvider.plusSessionId, + }, + settingsProvider.cloudSyncToken, + ).then((response) { + if (response == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('sync_failed'.i18n), + ), + ); + return; + } + + CloudSyncData cloudSyncData = CloudSyncData.fromJson( + response['data']['cloud_sync_data']); + + settingsProvider.updateFromMap( + map: cloudSyncData.settings, + store: true, + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('sync_successful'.i18n), + ), + ); + }); + } + }, + trailingDivider: true, + title: Text( + "sync_now".i18n, + style: TextStyle( + color: AppColors.of(context).text.withOpacity( + settingsProvider.gradeOpeningFun ? .95 : .25), + ), + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/refilc_mobile_ui/lib/screens/settings/submenu/submenu_screen.i18n.dart b/refilc_mobile_ui/lib/screens/settings/submenu/submenu_screen.i18n.dart index 35c63b1..c6904b2 100644 --- a/refilc_mobile_ui/lib/screens/settings/submenu/submenu_screen.i18n.dart +++ b/refilc_mobile_ui/lib/screens/settings/submenu/submenu_screen.i18n.dart @@ -32,6 +32,8 @@ extension SettingsLocalization on String { "understand": "I understand", "theme_share_failed": "An error occurred while sharing the theme.", "theme_share_ratelimit": "You can only share 1 theme per minute.", + // cloud sync + "cloud_sync": "Cloud Sync", }, "hu_hu": { "general": "Általános", @@ -62,6 +64,8 @@ extension SettingsLocalization on String { "understand": "Értem", "theme_share_failed": "Hiba történt a téma megosztása közben.", "theme_share_ratelimit": "Csak 1 témát oszthatsz meg percenként.", + // cloud sync + "cloud_sync": "Felhő szinkronizálás", }, "de_de": { "general": "Allgemeine", @@ -93,6 +97,8 @@ extension SettingsLocalization on String { "theme_share_failed": "Beim Teilen des Themas ist ein Fehler aufgetreten.", "theme_share_ratelimit": "Sie können nur 1 Thema pro Minute teilen.", + // cloud sync + "cloud_sync": "Cloud-Synchronisierung", }, }; diff --git a/refilc_mobile_ui/pubspec.yaml b/refilc_mobile_ui/pubspec.yaml index a1facbc..e180352 100644 --- a/refilc_mobile_ui/pubspec.yaml +++ b/refilc_mobile_ui/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: animations: ^2.0.11 animated_list_plus: ^0.5.0 confetti: ^0.7.0 - live_activities: ^1.9.1+1 + # live_activities: ^1.9.1+1 animated_flip_counter: ^0.3.4 lottie: ^3.1.0 rive: ^0.12.4 @@ -51,7 +51,7 @@ dependencies: rounded_expansion_tile: git: url: https://github.com/kimaah/rounded_expansion_tile.git - go_router: ^14.2.0 + # go_router: ^14.2.0 flutter_expandable_fab: ^2.0.0 intl: ^0.19.0 i18n_extension: ^12.0.1 @@ -67,7 +67,7 @@ dependencies: uuid: ^4.3.3 maps_launcher: ^2.2.0 google_fonts: ^6.1.0 - flutter_any_logo: ^1.1.1 + # flutter_any_logo: ^1.1.1 custom_sliding_segmented_control: ^1.8.1 get_it: ^7.6.7 xml: ^6.5.0