diff --git a/refilc/lib/api/client.dart b/refilc/lib/api/client.dart index ed02aa1..93bfd7a 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..a693315 100644 --- a/refilc/lib/database/init.dart +++ b/refilc/lib/database/init.dart @@ -56,6 +56,8 @@ const settingsDB = DatabaseStruct("settings", { "uwu_mode": int, "new_popups": int, "unseen_new_features": String, + "cloud_sync_enabled": int, + "cloud_sync_token": 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..f05ff87 100644 --- a/refilc/lib/models/settings.dart +++ b/refilc/lib/models/settings.dart @@ -109,6 +109,8 @@ class SettingsProvider extends ChangeNotifier { bool _uwuMode; bool _newPopups; List _unseenNewFeatures; + bool _cloudSyncEnabled; + String _cloudSyncToken; // quick settings bool _qTimetableLessonNum; bool _qTimetableSubTiles; @@ -184,6 +186,8 @@ class SettingsProvider extends ChangeNotifier { required bool uwuMode, required bool newPopups, required List unseenNewFeatures, + required bool cloudSyncEnabled, + required String cloudSyncToken, required bool qTimetableLessonNum, required bool qTimetableSubTiles, required bool qSubjectsSubTiles, @@ -256,6 +260,8 @@ class SettingsProvider extends ChangeNotifier { _uwuMode = uwuMode, _newPopups = newPopups, _unseenNewFeatures = unseenNewFeatures, + _cloudSyncEnabled = cloudSyncEnabled, + _cloudSyncToken = cloudSyncToken, _qTimetableLessonNum = qTimetableLessonNum, _qTimetableSubTiles = qTimetableSubTiles, _qSubjectsSubTiles = qSubjectsSubTiles; @@ -347,6 +353,8 @@ 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'], qTimetableLessonNum: map['q_timetable_lesson_num'] == 1, qTimetableSubTiles: map['q_timetable_sub_tiles'] == 1, qSubjectsSubTiles: map['q_subjects_sub_tiles'] == 1, @@ -426,6 +434,8 @@ 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, "q_timetable_lesson_num": _qTimetableLessonNum ? 1 : 0, "q_timetable_sub_tiles": _qTimetableSubTiles ? 1 : 0, "q_subjects_sub_tiles": _qSubjectsSubTiles ? 1 : 0, @@ -509,6 +519,8 @@ class SettingsProvider extends ChangeNotifier { uwuMode: false, newPopups: true, unseenNewFeatures: ['grade_exporting'], + cloudSyncEnabled: false, + cloudSyncToken: '', qTimetableLessonNum: true, qTimetableSubTiles: true, qSubjectsSubTiles: true, @@ -583,6 +595,8 @@ class SettingsProvider extends ChangeNotifier { bool get uwuMode => _uwuMode; bool get newPopups => _newPopups; List get unseenNewFeatures => _unseenNewFeatures; + bool get cloudSyncEnabled => _cloudSyncEnabled; + String get cloudSyncToken => _cloudSyncToken; bool get qTimetableLessonNum => _qTimetableLessonNum; bool get qTimetableSubTiles => _qTimetableSubTiles; bool get qSubjectsSubTiles => _qSubjectsSubTiles; @@ -597,6 +611,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 +668,8 @@ class SettingsProvider extends ChangeNotifier { bool? uwuMode, bool? newPopups, List? unseenNewFeatures, + bool? cloudSyncEnabled, + String? cloudSyncToken, bool? qTimetableLessonNum, bool? qTimetableSubTiles, bool? qSubjectsSubTiles, @@ -675,6 +692,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 +868,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; @@ -866,6 +890,95 @@ class SettingsProvider extends ChangeNotifier { notifyListeners(); } + Future updateFromMap({ + required Map map, + bool store = true, + }) async { + await update( + store: store, + language: map["language"], + startPage: Pages.values[map["start_page"]], + rounding: map["rounding"], + theme: ThemeMode.values[map["theme"]], + accentColor: AccentColor.values[map["accent_color"]], + gradeColors: [ + Color(map["grade_color1"]), + Color(map["grade_color2"]), + Color(map["grade_color3"]), + Color(map["grade_color4"]), + Color(map["grade_color5"]), + ], + 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"]], + abWeeks: map["ab_weeks"] == 1, + swapABweeks: map["swap_ab_weeks"] == 1, + updateChannel: UpdateChannel.values[map["update_channel"]], + 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"]), + customBackgroundColor: Color(map["custom_background_color"]), + customHighlightColor: Color(map["custom_highlight_color"]), + customIconColor: Color(map["custom_icon_color"]), + customTextColor: Color(map["custom_text_color"]), + shadowEffect: map["shadow_effect"] == 1, + premiumScopes: jsonDecode(map["premium_scopes"]).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"]), + 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_mobile_ui/lib/screens/login/qwid_login.dart b/refilc_mobile_ui/lib/screens/login/qwid_login.dart new file mode 100644 index 0000000..cc1428e --- /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=c3b871fb-d922-4e23-b94d-b31f294c9253&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..222ade5 --- /dev/null +++ b/refilc_mobile_ui/lib/screens/settings/submenu/cloud_sync_screen.dart @@ -0,0 +1,250 @@ +// import 'package:refilc/models/settings.dart'; +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": 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", }, };