diff --git a/refilc/assets/images/apple_fire_emoji.png b/refilc/assets/images/apple_fire_emoji.png new file mode 100644 index 0000000..7ea6be8 Binary files /dev/null and b/refilc/assets/images/apple_fire_emoji.png differ diff --git a/refilc/lib/api/client.dart b/refilc/lib/api/client.dart index 0387e84..ed02aa1 100644 --- a/refilc/lib/api/client.dart +++ b/refilc/lib/api/client.dart @@ -18,11 +18,11 @@ import 'package:connectivity_plus/connectivity_plus.dart'; class FilcAPI { // API base - static const baseUrl = "https://api.refilc.hu"; + static const baseUrl = "https://api.refilcapp.hu"; // Public API static const schoolList = "$baseUrl/v3/public/school-list"; - static const news = "$baseUrl/v3/public/news"; + static const news = "$baseUrl/v4/public/news"; static const supporters = "$baseUrl/v3/public/supporters"; // Private API @@ -51,7 +51,7 @@ class FilcAPI { static const gradeColorsByID = "$gradeColorsGet/"; // Payment API - static const payment = "$baseUrl/v3/payment"; + static const payment = "$baseUrl/v4/payment"; static const stripeSheet = "$payment/stripe-sheet"; static Future checkConnectivity() async => @@ -93,10 +93,14 @@ class FilcAPI { "x-filc-id": settings.xFilcId, "user-agent": userAgent, // platform things - "rf-platform": Platform.operatingSystem, - "rf-platform-version": Platform.operatingSystemVersion, - "rf-app-version": - const String.fromEnvironment("APPVER", defaultValue: "?"), + "rf-platform": + settings.analyticsEnabled ? Platform.operatingSystem : "unknown", + "rf-platform-version": settings.analyticsEnabled + ? Platform.operatingSystemVersion + : "unknown", + "rf-app-version": settings.analyticsEnabled + ? const String.fromEnvironment("APPVER", defaultValue: "?") + : "unknown", "rf-uinid": settings.xFilcId, }; @@ -231,7 +235,7 @@ class FilcAPI { } // sharing - static Future addSharedTheme(SharedTheme theme) async { + static Future addSharedTheme(SharedTheme theme) async { try { theme.json.remove('json'); theme.json['is_public'] = theme.isPublic.toString(); @@ -263,13 +267,19 @@ class FilcAPI { headers: {'Content-Type': 'application/x-www-form-urlencoded'}, ); - if (res.statusCode != 201) { - throw "HTTP ${res.statusCode}: ${res.body}"; + // if (res.statusCode != 201) { + // throw "HTTP ${res.statusCode}: ${res.body}"; + // } + + if (res.statusCode == 201) { + log('Shared theme successfully with ID: ${theme.id}'); } - log('Shared theme successfully with ID: ${theme.id}'); + return res.statusCode; } on Exception catch (error, stacktrace) { log("ERROR: FilcAPI.addSharedTheme: $error $stacktrace"); + + return 696; } } @@ -303,8 +313,7 @@ class FilcAPI { return null; } - static Future addSharedGradeColors( - SharedGradeColors gradeColors) async { + static Future addSharedGradeColors(SharedGradeColors gradeColors) async { try { gradeColors.json.remove('json'); gradeColors.json['is_public'] = gradeColors.isPublic.toString(); @@ -320,13 +329,19 @@ class FilcAPI { headers: {'Content-Type': 'application/x-www-form-urlencoded'}, ); - if (res.statusCode != 201) { - throw "HTTP ${res.statusCode}: ${res.body}"; + // if (res.statusCode != 201) { + // throw "HTTP ${res.statusCode}: ${res.body}"; + // } + + if (res.statusCode == 201) { + log('Shared grade colors successfully with ID: ${gradeColors.id}'); } - log('Shared grade colors successfully with ID: ${gradeColors.id}'); + return res.statusCode; } on Exception catch (error, stacktrace) { log("ERROR: FilcAPI.addSharedGradeColors: $error $stacktrace"); + + return 696; } } diff --git a/refilc/lib/api/login.dart b/refilc/lib/api/login.dart index 6db0229..3895afc 100644 --- a/refilc/lib/api/login.dart +++ b/refilc/lib/api/login.dart @@ -65,8 +65,11 @@ Future loginAPI({ parents: ['Teszt András', 'Teszt Linda'], json: {"a": "b"}, address: '1117 Budapest, Gábor Dénes utca 4.', + gradeDelay: 0, ), role: Role.parent, + accessToken: '', + accessTokenExpire: DateTime.now(), refreshToken: '', ); @@ -153,6 +156,8 @@ Future loginAPI({ name: student.name, student: student, role: JwtUtils.getRoleFromJWT(res["access_token"])!, + accessToken: res["access_token"], + accessTokenExpire: DateTime.now(), refreshToken: '', ); @@ -234,6 +239,15 @@ Future newLoginAPI({ if (res != null) { if (kDebugMode) { print(res); + + // const splitSize = 1000; + // RegExp exp = RegExp(r"\w{" "$splitSize" "}"); + // // String str = "0102031522"; + // Iterable matches = exp.allMatches(res.toString()); + // var list = matches.map((m) => m.group(0)); + // list.forEach((e) { + // print(e); + // }); } if (res.containsKey("error")) { @@ -266,6 +280,9 @@ Future newLoginAPI({ name: student.name, student: student, role: role, + accessToken: res["access_token"], + accessTokenExpire: + DateTime.now().add(Duration(seconds: (res["expires_in"] - 30))), refreshToken: res["refresh_token"], ); diff --git a/refilc/lib/api/providers/sync.dart b/refilc/lib/api/providers/sync.dart index d6be0b1..81fca47 100644 --- a/refilc/lib/api/providers/sync.dart +++ b/refilc/lib/api/providers/sync.dart @@ -2,6 +2,7 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:refilc/api/providers/database_provider.dart'; import 'package:refilc/api/providers/status_provider.dart'; import 'package:refilc/api/providers/user_provider.dart'; @@ -40,6 +41,12 @@ Future syncAll(BuildContext context) { StatusProvider statusProvider = Provider.of(context, listen: false); + // check if access token isn't expired + // if (user.user?.accessToken == null) { + // lock = false; + // return Future.value(); + // } + List> tasks = []; int taski = 0; @@ -50,6 +57,28 @@ Future syncAll(BuildContext context) { } tasks = [ + // refresh login + syncStatus(() async { + // print(user.user?.accessTokenExpire); + // print('${user.user?.accessToken ?? "no token"} - ACCESS TOKEN'); + + if (user.user == null) return; + if (user.user!.accessTokenExpire.isBefore(DateTime.now())) { + String authRes = await Provider.of(context, listen: false) + .refreshLogin() ?? + ''; + if (authRes != 'success') { + if (kDebugMode) print('ERROR: failed to refresh login'); + lock = false; + return Future.value(); + } else { + if (kDebugMode) print('INFO: access token refreshed'); + } + } else { + if (kDebugMode) print('INFO: access token is not expired'); + } + }()), + syncStatus(Provider.of(context, listen: false).fetch()), syncStatus(Provider.of(context, listen: false) .fetch(week: Week.current())), @@ -71,6 +100,8 @@ Future syncAll(BuildContext context) { if (studentJson == null) return; Student student = Student.fromJson(studentJson); + // print(studentJson); + user.user?.name = student.name; // Store user @@ -89,13 +120,11 @@ Future syncAll(BuildContext context) { return false; } - - return Future.wait(tasks).then((value) { // Unlock lock = false; - if(Platform.isIOS && LiveCardProvider.hasActivityStarted == true){ + if (Platform.isIOS && LiveCardProvider.hasActivityStarted == true) { PlatformChannel.endLiveActivity(); LiveCardProvider.hasActivityStarted = false; } diff --git a/refilc/lib/database/init.dart b/refilc/lib/database/init.dart index cde1c14..2850814 100644 --- a/refilc/lib/database/init.dart +++ b/refilc/lib/database/init.dart @@ -27,7 +27,8 @@ const settingsDB = DatabaseStruct("settings", { "notifications_absences": int, "notifications_messages": int, "notifications_lessons": int, // notifications - "x_filc_id": String, "graph_class_avg": int, "presentation_mode": int, + "x_filc_id": String, "graph_class_avg": int, + "analytics_enabled": int, "presentation_mode": int, "bell_delay": int, "bell_delay_enabled": int, "grade_opening_fun": int, "icon_pack": String, "premium_scopes": String, "premium_token": String, "premium_login": String, @@ -66,6 +67,7 @@ const usersDB = DatabaseStruct("users", { "institute_code": String, "student": String, "role": int, "nickname": String, "picture": String, // premium only (it's now plus btw) "grade_streak": int, + "access_token": String, "access_token_expire": String, "refresh_token": String, }); const userDataDB = DatabaseStruct("user_data", { @@ -140,6 +142,8 @@ Future initDB(DatabaseProvider database) async { "nickname": "", "picture": "", "grade_streak": 0, + "access_token": "", + "access_token_expire": "", "refresh_token": "", }, ); diff --git a/refilc/lib/models/news.dart b/refilc/lib/models/news.dart index 05431d7..c2dd928 100644 --- a/refilc/lib/models/news.dart +++ b/refilc/lib/models/news.dart @@ -7,6 +7,8 @@ class News { String platform; bool emergency; DateTime expireDate; + List? appVersions; + String? specificAppId; Map? json; News({ @@ -18,6 +20,8 @@ class News { required this.platform, required this.emergency, required this.expireDate, + this.appVersions, + this.specificAppId, this.json, }); @@ -31,6 +35,10 @@ class News { platform: json["platform"] ?? "", emergency: json["emergency"] ?? false, expireDate: DateTime.parse(json["expire_date"] ?? ''), + appVersions: json["app_versions"] != null + ? List.from(json["app_versions"]) + : null, + specificAppId: json["specific_app_id"], json: json, ); } diff --git a/refilc/lib/models/settings.dart b/refilc/lib/models/settings.dart index 6bde6b2..1863941 100644 --- a/refilc/lib/models/settings.dart +++ b/refilc/lib/models/settings.dart @@ -60,6 +60,7 @@ class SettingsProvider extends ChangeNotifier { UpdateChannel _updateChannel; Config _config; String _xFilcId; + bool _analyticsEnabled; bool _graphClassAvg; bool _goodStudent; bool _presentationMode; @@ -137,6 +138,7 @@ class SettingsProvider extends ChangeNotifier { required UpdateChannel updateChannel, required Config config, required String xFilcId, + required bool analyticsEnabled, required bool graphClassAvg, required bool goodStudent, required bool presentationMode, @@ -208,6 +210,7 @@ class SettingsProvider extends ChangeNotifier { _updateChannel = updateChannel, _config = config, _xFilcId = xFilcId, + _analyticsEnabled = analyticsEnabled, _graphClassAvg = graphClassAvg, _goodStudent = goodStudent, _presentationMode = presentationMode, @@ -297,6 +300,7 @@ class SettingsProvider extends ChangeNotifier { updateChannel: UpdateChannel.values[map["update_channel"]], config: Config.fromJson(configMap ?? {}), xFilcId: map["x_filc_id"], + analyticsEnabled: map["analytics_enabled"] == 1, graphClassAvg: map["graph_class_avg"] == 1, goodStudent: false, presentationMode: map["presentation_mode"] == 1, @@ -377,6 +381,7 @@ class SettingsProvider extends ChangeNotifier { "notification_poll_interval": _notificationPollInterval, "config": jsonEncode(config.json), "x_filc_id": _xFilcId, + "analytics_enabled": _analyticsEnabled ? 1 : 0, "graph_class_avg": _graphClassAvg ? 1 : 0, "presentation_mode": _presentationMode ? 1 : 0, "bell_delay_enabled": _bellDelayEnabled ? 1 : 0, @@ -458,6 +463,7 @@ class SettingsProvider extends ChangeNotifier { updateChannel: UpdateChannel.stable, config: Config.fromJson({}), xFilcId: const Uuid().v4(), + analyticsEnabled: true, graphClassAvg: false, goodStudent: false, presentationMode: false, @@ -532,6 +538,7 @@ class SettingsProvider extends ChangeNotifier { UpdateChannel get updateChannel => _updateChannel; Config get config => _config; String get xFilcId => _xFilcId; + bool get analyticsEnabled => _analyticsEnabled; bool get graphClassAvg => _graphClassAvg; bool get goodStudent => _goodStudent; bool get presentationMode => _presentationMode; @@ -604,6 +611,7 @@ class SettingsProvider extends ChangeNotifier { UpdateChannel? updateChannel, Config? config, String? xFilcId, + bool? analyticsEnabled, bool? graphClassAvg, bool? goodStudent, bool? presentationMode, @@ -708,6 +716,9 @@ class SettingsProvider extends ChangeNotifier { } if (config != null && config != _config) _config = config; if (xFilcId != null && xFilcId != _xFilcId) _xFilcId = xFilcId; + if (analyticsEnabled != null && analyticsEnabled != _analyticsEnabled) { + _analyticsEnabled = analyticsEnabled; + } if (graphClassAvg != null && graphClassAvg != _graphClassAvg) { _graphClassAvg = graphClassAvg; } diff --git a/refilc/lib/models/user.dart b/refilc/lib/models/user.dart index 1b76ec2..eab3624 100644 --- a/refilc/lib/models/user.dart +++ b/refilc/lib/models/user.dart @@ -18,6 +18,8 @@ class User { String picture; int gradeStreak; // new login method + String accessToken; + DateTime accessTokenExpire; String refreshToken; String get displayName => nickname != '' ? nickname : name; @@ -34,6 +36,8 @@ class User { this.nickname = "", this.picture = "", this.gradeStreak = 0, + required this.accessToken, + required this.accessTokenExpire, required this.refreshToken, }) { if (id != null) { @@ -59,11 +63,15 @@ class User { birth: DateTime.now(), yearId: '1', parents: [], + gradeDelay: 0, ), role: Role.values[map["role"] ?? 0], nickname: map["nickname"] ?? "", picture: map["picture"] ?? "", gradeStreak: map["grade_streak"] ?? 0, + accessToken: map["access_token"] ?? "", + accessTokenExpire: DateTime.parse( + map["access_token_expire"] ?? DateTime.now().toIso8601String()), refreshToken: map["refresh_token"] ?? "", ); } @@ -80,6 +88,8 @@ class User { "nickname": nickname, "picture": picture, "grade_streak": gradeStreak, + "access_token": accessToken, + "access_token_expire": accessTokenExpire.toIso8601String(), "refresh_token": refreshToken, }; } diff --git a/refilc/lib/ui/filter/widgets.dart b/refilc/lib/ui/filter/widgets.dart index 5142a88..b9bcec1 100644 --- a/refilc/lib/ui/filter/widgets.dart +++ b/refilc/lib/ui/filter/widgets.dart @@ -178,7 +178,8 @@ Future> getFilterWidgets(FilterType activeData, // Ads case FilterType.ads: if (adProvider.available) { - items = ad_filter.getWidgets(adProvider.ads); + items = ad_filter.getWidgets( + adProvider.ads, context); } break; } diff --git a/refilc/lib/ui/filter/widgets/ads.dart b/refilc/lib/ui/filter/widgets/ads.dart index 6be93f1..c4a1660 100644 --- a/refilc/lib/ui/filter/widgets/ads.dart +++ b/refilc/lib/ui/filter/widgets/ads.dart @@ -1,23 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:refilc/models/ad.dart'; import 'package:refilc/ui/date_widget.dart'; +import 'package:refilc_mobile_ui/common/widgets/ad/ad_tile.dart'; import 'package:refilc_mobile_ui/common/widgets/ad/ad_viewable.dart' as mobile; +import 'package:refilc_mobile_ui/plus/plus_screen.dart'; +import 'package:refilc_plus/providers/plus_provider.dart'; +import 'package:uuid/uuid.dart'; -List getWidgets(List providerAds) { +List getWidgets(List providerAds, BuildContext context) { List items = []; + bool hasPlus = Provider.of(context).hasPremium; + + DateWidget plusWidget = DateWidget( + key: const Uuid().v4(), + date: DateTime.now(), + widget: AdTile( + Ad( + title: 'reFilc+', + description: + 'Fizess elő reFilc+-ra, rejtsd el a hirdetéseket és támogasd az app működését!', + author: '', + logoUrl: Uri.parse('https://refilc.hu/image/brand/logo.png'), + overridePremium: false, + date: DateTime(2007, 6, 29, 9, 41), + expireDate: DateTime.now().add(const Duration(days: 11)), + launchUrl: Uri.parse('https://refilc.hu/plus'), + ), + onTap: () => Navigator.of(context, rootNavigator: true) + .push(MaterialPageRoute(builder: (context) { + return const PlusScreen(); + })), + padding: const EdgeInsets.symmetric(horizontal: 5.0), + showExternalIcon: false, + ), + ); + if (providerAds.isNotEmpty) { for (var ad in providerAds) { if (ad.date.isBefore(DateTime.now()) && - ad.expireDate.isAfter(DateTime.now())) { - providerAds.sort((a, b) => -a.date.compareTo(b.date)); + ad.expireDate.isAfter(DateTime.now()) && + DateTime.now().hour.isOdd) { + if (!hasPlus || ad.overridePremium) { + providerAds.sort((a, b) => -a.date.compareTo(b.date)); - items.add(DateWidget( - key: ad.description, - date: ad.date, - widget: mobile.AdViewable(ad), - )); + items.add(DateWidget( + key: ad.description, + date: ad.date, + widget: mobile.AdViewable(ad), + )); + } + } else { + if (DateTime.now().weekday == DateTime.saturday && + items.isEmpty && + !hasPlus) { + items.add(plusWidget); + } } } + } else { + if (DateTime.now().weekday == DateTime.saturday && + items.isEmpty && + !hasPlus) { + items.add(plusWidget); + } } return items; diff --git a/refilc/pubspec.yaml b/refilc/pubspec.yaml index a7fa3f0..9482d1f 100644 --- a/refilc/pubspec.yaml +++ b/refilc/pubspec.yaml @@ -3,7 +3,7 @@ description: "Egy nem hivatalos e-KRÉTA kliens, diákoktól diákoknak." homepage: https://refilc.hu publish_to: "none" -version: 5.0.4+274 +version: 5.0.6+276 environment: sdk: ">=3.3.2 <=3.4.3" @@ -39,7 +39,7 @@ dependencies: # ref: master path_provider: ^2.0.2 permission_handler: ^11.0.1 - share_plus: ^9.0.0 + share_plus: ^10.0.3 connectivity_plus: ^6.0.3 flutter_displaymode: ^0.6.0 quick_actions: ^1.0.1 diff --git a/refilc_kreta_api/lib/client/client.dart b/refilc_kreta_api/lib/client/client.dart index e9ff255..e383ac1 100644 --- a/refilc_kreta_api/lib/client/client.dart +++ b/refilc_kreta_api/lib/client/client.dart @@ -28,7 +28,7 @@ class KretaClient { late final DatabaseProvider _database; late final StatusProvider _status; - bool _loginRefreshing = false; + // bool _loginRefreshing = false; KretaClient({ this.accessToken, @@ -67,10 +67,14 @@ class KretaClient { headerMap = {}; } + if (accessToken == null || accessToken == '') { + accessToken = _user.user?.accessToken; + } + try { http.Response? res; - for (int i = 0; i < 3; i++) { + for (int i = 0; i < 2; i++) { if (autoHeader) { if (!headerMap.containsKey("authorization") && accessToken != null) { headerMap["authorization"] = "Bearer $accessToken"; @@ -85,13 +89,15 @@ class KretaClient { if (res.statusCode == 401) { headerMap.remove("authorization"); - await refreshLogin(); + print("DEBUG: 401 error, refreshing login"); + print("DEBUG: 401 error, URL: $url"); + // await refreshLogin(); } else { break; } // Wait before retrying - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 1500)); } if (res == null) throw "Login error"; @@ -129,10 +135,14 @@ class KretaClient { headerMap = {}; } + if (accessToken == null || accessToken == '') { + accessToken = _user.user?.accessToken; + } + try { http.Response? res; - for (int i = 0; i < 3; i++) { + for (int i = 0; i < 2; i++) { if (autoHeader) { if (!headerMap.containsKey("authorization") && accessToken != null) { headerMap["authorization"] = "Bearer $accessToken"; @@ -150,11 +160,14 @@ class KretaClient { res = await client.post(Uri.parse(url), headers: headerMap, body: body); if (res.statusCode == 401) { - await refreshLogin(); + // await refreshLogin(); headerMap.remove("authorization"); } else { break; } + + // Wait before retrying + await Future.delayed(const Duration(milliseconds: 1500)); } if (res == null) throw "Login error"; @@ -187,6 +200,10 @@ class KretaClient { headerMap = {}; } + if (accessToken == null || accessToken == '') { + accessToken = _user.user?.accessToken; + } + try { http.StreamedResponse? res; @@ -217,7 +234,7 @@ class KretaClient { if (res.statusCode == 401) { headerMap.remove("authorization"); - await refreshLogin(); + // await refreshLogin(); } else { break; } @@ -237,8 +254,8 @@ class KretaClient { } Future refreshLogin() async { - if (_loginRefreshing) return null; - _loginRefreshing = true; + // if (_loginRefreshing) return null; + // _loginRefreshing = true; User? loginUser = _user.user; if (loginUser == null) return null; @@ -257,8 +274,8 @@ class KretaClient { refreshToken ??= loginUser.refreshToken; - // print("REFRESH TOKEN BELOW"); - // print(refreshToken); + print("REFRESH TOKEN BELOW"); + print(refreshToken); if (refreshToken != null) { // print("REFRESHING LOGIN"); @@ -268,8 +285,8 @@ class KretaClient { refreshToken: loginUser.refreshToken, instituteCode: loginUser.instituteCode, )); - // print("REFRESH RESPONSE BELOW"); - // print(res); + print("REFRESH RESPONSE BELOW"); + print(res); if (res != null) { if (res.containsKey("error")) { // remove user if refresh token expired @@ -287,6 +304,11 @@ class KretaClient { if (res.containsKey("access_token")) { accessToken = res["access_token"]; + loginUser.accessToken = res["access_token"]; + loginUser.accessTokenExpire = + DateTime.now().add(Duration(seconds: (res["expires_in"] - 30))); + _database.store.storeUser(loginUser); + _user.refresh(); } if (res.containsKey("refresh_token")) { refreshToken = res["refresh_token"]; @@ -297,15 +319,20 @@ class KretaClient { if (res.containsKey("id_token")) { idToken = res["id_token"]; } - _loginRefreshing = false; + // _loginRefreshing = false; + print('successful refresh'); + + return 'success'; } else { - _loginRefreshing = false; + // _loginRefreshing = false; + return null; } } else { - _loginRefreshing = false; + // _loginRefreshing = false; + return null; } - return null; + // return null; } Future logout() async { diff --git a/refilc_kreta_api/lib/models/student.dart b/refilc_kreta_api/lib/models/student.dart index 31aa35e..e4b46bf 100644 --- a/refilc_kreta_api/lib/models/student.dart +++ b/refilc_kreta_api/lib/models/student.dart @@ -11,6 +11,8 @@ class Student { String? address; String? groupId; List parents; + int gradeDelay; + String? bankAccount; // List parentsPhone; String? className; @@ -22,6 +24,8 @@ class Student { required this.yearId, this.address, required this.parents, + required this.gradeDelay, + this.bankAccount, // required this.parentsPhone, this.json, }); @@ -57,6 +61,10 @@ class Student { : null : null, parents: parents, + gradeDelay: json["Intezmeny"]["TestreszabasBeallitasok"] + ["ErtekelesekMegjelenitesenekKesleltetesenekMerteke"] ?? + 0, + bankAccount: json["Bankszamla"]["BankszamlaSzam"], json: json, ); } diff --git a/refilc_kreta_api/lib/providers/share_provider.dart b/refilc_kreta_api/lib/providers/share_provider.dart index 1df105c..c45c228 100644 --- a/refilc_kreta_api/lib/providers/share_provider.dart +++ b/refilc_kreta_api/lib/providers/share_provider.dart @@ -19,7 +19,7 @@ class ShareProvider extends ChangeNotifier { // } // themes - Future shareCurrentTheme( + Future<(SharedTheme?, int)> shareCurrentTheme( BuildContext context, { bool isPublic = false, bool shareNick = true, @@ -56,9 +56,13 @@ class ShareProvider extends ChangeNotifier { }; SharedTheme theme = SharedTheme.fromJson(themeJson, gradeColors); - FilcAPI.addSharedTheme(theme); + int shareResult = await FilcAPI.addSharedTheme(theme); - return theme; + if (shareResult == 201) { + return (theme, 201); + } else { + return (null, shareResult); + } } Future getThemeById(BuildContext context, @@ -142,7 +146,7 @@ class ShareProvider extends ChangeNotifier { } // grade colors - Future shareCurrentGradeColors( + Future<(SharedGradeColors?, int)> shareCurrentGradeColors( BuildContext context, { bool isPublic = false, bool shareNick = true, @@ -162,9 +166,13 @@ class ShareProvider extends ChangeNotifier { }; SharedGradeColors gradeColors = SharedGradeColors.fromJson(gradeColorsJson); - FilcAPI.addSharedGradeColors(gradeColors); + int shareResult = await FilcAPI.addSharedGradeColors(gradeColors); - return gradeColors; + if (shareResult == 201) { + return (gradeColors, 201); + } else { + return (null, shareResult); + } } Future getGradeColorsById(BuildContext context, diff --git a/refilc_mobile_ui/lib/common/profile_image/profile_image.dart b/refilc_mobile_ui/lib/common/profile_image/profile_image.dart index 6024a7f..69abe4b 100644 --- a/refilc_mobile_ui/lib/common/profile_image/profile_image.dart +++ b/refilc_mobile_ui/lib/common/profile_image/profile_image.dart @@ -266,12 +266,16 @@ class _ProfileImageState extends State { child: Transform.translate( offset: Offset(-widget.radius / 4, -widget.radius / 4), child: Container( - alignment: Alignment.topLeft, - child: Text( - '🔥', - style: TextStyle(fontSize: widget.radius * 0.8), - ), - ), + alignment: Alignment.topLeft, + child: Image.asset( + 'assets/images/apple_fire_emoji.png', + width: widget.radius, + ) + // Text( + // '🔥', + // style: TextStyle(fontSize: widget.radius * 0.8), + // ), + ), ), ), ), diff --git a/refilc_mobile_ui/lib/common/widgets/ad/ad_tile.dart b/refilc_mobile_ui/lib/common/widgets/ad/ad_tile.dart index ddde59a..2ae7e44 100644 --- a/refilc_mobile_ui/lib/common/widgets/ad/ad_tile.dart +++ b/refilc_mobile_ui/lib/common/widgets/ad/ad_tile.dart @@ -5,11 +5,13 @@ import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; class AdTile extends StatelessWidget { - const AdTile(this.ad, {super.key, this.onTap, this.padding}); + const AdTile(this.ad, + {super.key, this.onTap, this.padding, this.showExternalIcon = true}); final Ad ad; final Function()? onTap; final EdgeInsetsGeometry? padding; + final bool showExternalIcon; @override Widget build(BuildContext context) { @@ -28,6 +30,7 @@ class AdTile extends StatelessWidget { Text( ad.description, style: TextStyle( + fontSize: 14.5, fontWeight: FontWeight.w500, color: AppColors.of(context).text.withOpacity(0.7), ), @@ -38,6 +41,8 @@ class AdTile extends StatelessWidget { ? ClipRRect( borderRadius: BorderRadius.circular(50.0), child: Image.network( + width: 42.0, + height: 42.0, ad.logoUrl.toString(), errorBuilder: (context, error, stackTrace) { ad.logoUrl = null; @@ -46,7 +51,12 @@ class AdTile extends StatelessWidget { ), ) : null, - trailing: const Icon(FeatherIcons.externalLink), + trailing: showExternalIcon + ? const Icon( + FeatherIcons.externalLink, + size: 20.0, + ) + : null, ), ); } diff --git a/refilc_mobile_ui/lib/common/widgets/ad/ad_viewable.dart b/refilc_mobile_ui/lib/common/widgets/ad/ad_viewable.dart index 1e1d103..ccff405 100644 --- a/refilc_mobile_ui/lib/common/widgets/ad/ad_viewable.dart +++ b/refilc_mobile_ui/lib/common/widgets/ad/ad_viewable.dart @@ -12,6 +12,7 @@ class AdViewable extends StatelessWidget { @override Widget build(BuildContext context) { return AdTile( + padding: const EdgeInsets.symmetric(horizontal: 5.0), ad, onTap: () => launchUrl( ad.launchUrl, diff --git a/refilc_mobile_ui/lib/plus/components/plan_card.dart b/refilc_mobile_ui/lib/plus/components/plan_card.dart index 18985b7..f8aba19 100644 --- a/refilc_mobile_ui/lib/plus/components/plan_card.dart +++ b/refilc_mobile_ui/lib/plus/components/plan_card.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:refilc/models/settings.dart'; +import 'package:refilc_mobile_ui/common/action_button.dart'; import 'package:refilc_plus/providers/plus_provider.dart'; import 'package:refilc_plus/ui/mobile/plus/activation_view/activation_view.dart'; import 'package:refilc_mobile_ui/plus/plus_screen.i18n.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:uuid/uuid.dart'; class PlusPlanCard extends StatelessWidget { const PlusPlanCard({ @@ -38,33 +40,39 @@ class PlusPlanCard extends StatelessWidget { Widget build(BuildContext context) { return GestureDetector( onTap: () { - if (!docsAccepted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text( - "El kell fogadnod az ÁSZF-et és az Adatkezelési Tájékoztatót!", - style: - TextStyle(color: Colors.black, fontWeight: FontWeight.bold), - ), - backgroundColor: Colors.white, - )); + // if (!docsAccepted) { + // ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + // content: Text( + // "El kell fogadnod az ÁSZF-et és az Adatkezelési Tájékoztatót!", + // style: + // TextStyle(color: Colors.black, fontWeight: FontWeight.bold), + // ), + // backgroundColor: Colors.white, + // )); - return; - } + // return; + // } if (Provider.of(context, listen: false).xFilcId == "none") { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text( - "Be kell kapcsolnod a Névtelen Analitikát a beállítások főoldalán, mielőtt reFilc+ előfizetést vásárolnál!", - style: - TextStyle(color: Colors.black, fontWeight: FontWeight.bold), - ), - backgroundColor: Colors.white, - )); - - return; + Provider.of(context, listen: false) + .update(xFilcId: const Uuid().v4(), store: true); } + // if (Provider.of(context, listen: false).xFilcId == + // "none") { + // ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + // content: Text( + // "Be kell kapcsolnod a Névtelen Analitikát a beállítások főoldalán, mielőtt reFilc+ előfizetést vásárolnál!", + // style: + // TextStyle(color: Colors.black, fontWeight: FontWeight.bold), + // ), + // backgroundColor: Colors.white, + // )); + + // return; + // } + if (Provider.of(context, listen: false).hasPremium) { if (!active) { launchUrl( @@ -77,9 +85,29 @@ class PlusPlanCard extends StatelessWidget { return; } - Navigator.of(context).push(MaterialPageRoute(builder: (context) { - return PremiumActivationView(product: id); - })); + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0)), + title: Text('docs'.i18n), + content: Text('docs_acceptance'.i18n), + actions: [ + ActionButton( + label: "next".i18n, + onTap: () { + // pop dialog + Navigator.of(context).pop(); + // start payment process + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) { + return PremiumActivationView(product: id); + })); + }, + ), + ], + ), + ); }, child: Container( decoration: BoxDecoration( diff --git a/refilc_mobile_ui/lib/plus/plus_screen.dart b/refilc_mobile_ui/lib/plus/plus_screen.dart index d6d1bd3..e2db8b1 100644 --- a/refilc_mobile_ui/lib/plus/plus_screen.dart +++ b/refilc_mobile_ui/lib/plus/plus_screen.dart @@ -1,5 +1,6 @@ // ignore_for_file: use_build_context_synchronously +import 'package:flutter/services.dart'; import 'package:refilc_mobile_ui/plus/plus_screen.i18n.dart'; import 'package:refilc_mobile_ui/plus/components/plan_card.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; @@ -335,6 +336,46 @@ class PlusScreenState extends State { contentPadding: const EdgeInsets.only(left: 15.0, right: 10.0), onTap: () async { + // try clipboard re-activation + final data = await Clipboard.getData("text/plain"); + if (data != null && + data.text != null && + data.text != "") { + // activate using clipboard data + final result = await context + .read() + .auth + .finishAuth(data.text!); + + if (!result && mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar( + content: Text( + "Sikertelen aktiválás. Kérlek próbáld újra később!", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold), + ), + backgroundColor: Colors.red, + )); + } else { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar( + content: Text( + "Sikeres aktiválás!", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold), + ), + backgroundColor: Colors.green, + )); + + Future.delayed(const Duration(seconds: 2), + () => Navigator.of(context).pop()); + } + } + + // try re-activation using refresh final result = await context .read() .auth @@ -379,41 +420,41 @@ class PlusScreenState extends State { ), ), // aszf warning - const SizedBox( - height: 18.0, - ), - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.0), - border: Border.all( - color: Colors.black.withOpacity(0.2), - ), - ), - child: CheckboxListTile( - side: - const BorderSide(color: Colors.black, width: 2.0), - contentPadding: - const EdgeInsets.only(left: 15.0, right: 10.0), - value: docsAccepted, - onChanged: (value) { - setState(() { - docsAccepted = !docsAccepted; - }); - }, - // title: Text( - // 'show_lifetime'.i18n, - // style: const TextStyle( - // color: Colors.black, - // fontWeight: FontWeight.w500, - // ), - // ), - subtitle: const Text( - '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)?', - textAlign: TextAlign.start, - style: TextStyle(color: Colors.black), - ), - ), - ), + // const SizedBox( + // height: 18.0, + // ), + // Container( + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(16.0), + // border: Border.all( + // color: Colors.black.withOpacity(0.2), + // ), + // ), + // child: CheckboxListTile( + // side: + // const BorderSide(color: Colors.black, width: 2.0), + // contentPadding: + // const EdgeInsets.only(left: 15.0, right: 10.0), + // value: docsAccepted, + // onChanged: (value) { + // setState(() { + // docsAccepted = !docsAccepted; + // }); + // }, + // // title: Text( + // // 'show_lifetime'.i18n, + // // style: const TextStyle( + // // color: Colors.black, + // // fontWeight: FontWeight.w500, + // // ), + // // ), + // subtitle: const Text( + // '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)?', + // textAlign: TextAlign.start, + // style: TextStyle(color: Colors.black), + // ), + // ), + // ), // CheckboxListTile(value: false, onChanged: onChanged) // Padding( // padding: const EdgeInsets.symmetric(horizontal: 12.0), diff --git a/refilc_mobile_ui/lib/plus/plus_screen.i18n.dart b/refilc_mobile_ui/lib/plus/plus_screen.i18n.dart index 5c9b29b..902ba64 100644 --- a/refilc_mobile_ui/lib/plus/plus_screen.i18n.dart +++ b/refilc_mobile_ui/lib/plus/plus_screen.i18n.dart @@ -47,13 +47,19 @@ 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": "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", // other "and": " and ", "every": "Every ", "benefit": " benefit", "show_lifetime": "Show Lifetime Plans", "more_soon": "More coming soon...", - "faq_dc": "To redeem your benefits, contact us on Discord in DMs!", + "faq_dc": + "To redeem your Discord-related benefits, contact us on Discord in DMs!", "reactivate": "Reactivate Existing Subscription", }, "hu_hu": { @@ -100,6 +106,11 @@ 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": "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", // other "and": " és ", "every": "Minden ", @@ -107,7 +118,7 @@ extension SettingsLocalization on String { "show_lifetime": "Örökre szóló csomagok", "more_soon": "Hamarosan mégtöbb finomság...", "faq_dc": - "Az előnyök beváltásához írj nekünk Discord-on privát üzenetet!", + "A Discord-al kapcsolatos előnyök beváltásához írj nekünk Discord-on privát üzenetet!", "reactivate": "Meglévő előfizetés újraaktiválása", }, "de_de": { @@ -156,6 +167,11 @@ extension SettingsLocalization on String { "rfp_16": "Private Leaks und Informationen über kommende Funktionen", "rfp_17": "Notenexport", "rfp_18": "Anzeigen exportierter Noten", + // docs 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", // other "and": " und ", "every": "Jeder ", diff --git a/refilc_mobile_ui/lib/screens/login/login_screen.dart b/refilc_mobile_ui/lib/screens/login/login_screen.dart index 9de9d59..9d20c7e 100644 --- a/refilc_mobile_ui/lib/screens/login/login_screen.dart +++ b/refilc_mobile_ui/lib/screens/login/login_screen.dart @@ -1,21 +1,17 @@ // import 'dart:async'; import 'package:refilc/api/client.dart'; +import 'dart:io' show Platform; import 'package:refilc/api/login.dart'; import 'package:refilc/theme/colors/colors.dart'; -import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart'; import 'package:refilc_mobile_ui/common/custom_snack_bar.dart'; import 'package:refilc_mobile_ui/common/system_chrome.dart'; -import 'package:refilc_mobile_ui/common/widgets/absence/absence_display.dart'; -import 'package:refilc_mobile_ui/screens/login/login_button.dart'; -import 'package:refilc_mobile_ui/screens/login/login_input.dart'; import 'package:refilc_mobile_ui/screens/login/school_input/school_input.dart'; import 'package:refilc_mobile_ui/screens/settings/privacy_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'login_screen.i18n.dart'; import 'package:carousel_slider/carousel_slider.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:refilc_mobile_ui/screens/login/kreten_login.dart'; //new library for new web login class LoginScreen extends StatefulWidget { @@ -79,13 +75,19 @@ class LoginScreenState extends State { }); } + double paddingTop = 0; @override Widget build(BuildContext context) { precacheImage(const AssetImage('assets/images/showcase1.png'), context); precacheImage(const AssetImage('assets/images/showcase2.png'), context); precacheImage(const AssetImage('assets/images/showcase3.png'), context); precacheImage(const AssetImage('assets/images/showcase4.png'), context); - bool selected = false; + + if (Platform.isIOS) { + paddingTop = 0; + } else if (Platform.isAndroid) { + paddingTop = 20; + } return Scaffold( body: Container( @@ -102,7 +104,7 @@ class LoginScreenState extends State { children: [ // app icon Padding( - padding: const EdgeInsets.only(left: 24, top: 20), + padding: EdgeInsets.only(left: 24, top: paddingTop), child: Row( children: [ Image.asset( @@ -447,6 +449,7 @@ class LoginScreenState extends State { // }), // ); // } + // ignore: non_constant_identifier_names void _NewLoginAPI({required BuildContext context}) { String code = codeController.text; diff --git a/refilc_mobile_ui/lib/screens/settings/accounts/account_view.dart b/refilc_mobile_ui/lib/screens/settings/accounts/account_view.dart index 7f4eadd..598b2ca 100644 --- a/refilc_mobile_ui/lib/screens/settings/accounts/account_view.dart +++ b/refilc_mobile_ui/lib/screens/settings/accounts/account_view.dart @@ -56,6 +56,16 @@ class AccountView extends StatelessWidget { Detail( title: "parents".plural(user.student.parents.length), description: user.student.parents.join(", ")), + if (user.student.gradeDelay > 0) + Detail( + title: "grade_delay".i18n, + description: "hrs".i18n.fill([user.student.gradeDelay]), + ), + // if ((user.student.bankAccount ?? "").isNotEmpty) + // Detail( + // title: "bank_account".i18n, + // description: (user.student.bankAccount ?? "not_provided".i18n), + // ), const SizedBox( height: 10.0, ), diff --git a/refilc_mobile_ui/lib/screens/settings/accounts/account_view.i18n.dart b/refilc_mobile_ui/lib/screens/settings/accounts/account_view.i18n.dart index 5e38fae..1e8c115 100644 --- a/refilc_mobile_ui/lib/screens/settings/accounts/account_view.i18n.dart +++ b/refilc_mobile_ui/lib/screens/settings/accounts/account_view.i18n.dart @@ -10,6 +10,8 @@ extension Localization on String { "address": "Home address", "parents": "Parent(s)", "parents_phone": "Parents' phone number: ", + "grade_delay": "Grade visibility delay", + "hrs": "%s hour(s)", }, "hu_hu": { "birthdate": "Születési dátum", @@ -17,6 +19,8 @@ extension Localization on String { "class": "Osztály", "address": "Lakcím", "parents": "Szülő(k)", + "grade_delay": "Jegy megjelenítési késleltetés", + "hrs": "%s óra", }, "de_de": { "birthdate": "Geburtsdatum", @@ -24,6 +28,8 @@ extension Localization on String { "class": "Klasse", "address": "Wohnanschrift", "parents": "Elter(n)", + "grade_delay": "Notenverzögerung", + "hrs": "%s Stunde(n)", }, }; diff --git a/refilc_mobile_ui/lib/screens/settings/settings_screen.dart b/refilc_mobile_ui/lib/screens/settings/settings_screen.dart index cb6b2bb..6868474 100644 --- a/refilc_mobile_ui/lib/screens/settings/settings_screen.dart +++ b/refilc_mobile_ui/lib/screens/settings/settings_screen.dart @@ -42,6 +42,7 @@ import 'package:refilc_mobile_ui/screens/settings/accounts/account_view.dart'; import 'package:refilc_mobile_ui/screens/settings/notifications_screen.dart'; import 'package:refilc_mobile_ui/screens/settings/privacy_view.dart'; import 'package:refilc_mobile_ui/screens/settings/settings_helper.dart'; +import 'package:refilc_mobile_ui/screens/settings/submenu/code_scanner.dart'; import 'package:refilc_mobile_ui/screens/settings/submenu/extras_screen.dart'; import 'package:refilc_mobile_ui/screens/settings/submenu/personalize_screen.dart'; import 'package:flutter/foundation.dart'; @@ -782,10 +783,14 @@ class SettingsScreenState extends State color: AppColors.of(context).text.withOpacity(0.75), ), ), - leading: const Text( - "🔥", - style: TextStyle(fontSize: 22.0), + leading: Image.asset( + 'assets/images/apple_fire_emoji.png', + width: 24.0, ), + // leading: const Text( + // "🔥", + // style: TextStyle(fontSize: 22.0), + // ), trailing: Text( "${user.gradeStreak}", style: TextStyle( @@ -1008,14 +1013,15 @@ class SettingsScreenState extends State children: [ PanelButton( leading: Icon( - FeatherIcons.map, + Icons.qr_code, size: 22.0, color: AppColors.of(context).text.withOpacity(0.95), ), - title: Text("stickermap".i18n), - onPressed: () => launchUrl( - Uri.parse("https://stickermap.refilc.hu"), - mode: LaunchMode.inAppBrowserView, + title: Text("qr_scanner".i18n), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const CodeScannerScreen(), + ), ), borderRadius: const BorderRadius.vertical( top: Radius.circular(12.0), @@ -1030,6 +1036,22 @@ class SettingsScreenState extends State ), title: Text("news".i18n), onPressed: () => _openNews(context), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(4.0), + bottom: Radius.circular(4.0), + ), + ), + PanelButton( + leading: Icon( + FeatherIcons.map, + size: 22.0, + color: AppColors.of(context).text.withOpacity(0.95), + ), + title: Text("stickermap".i18n), + onPressed: () => launchUrl( + Uri.parse("https://stickermap.refilc.hu"), + mode: LaunchMode.inAppBrowserView, + ), borderRadius: const BorderRadius.vertical( top: Radius.circular(4.0), bottom: Radius.circular(12.0), @@ -1173,7 +1195,7 @@ class SettingsScreenState extends State secondary: Icon( FeatherIcons.barChart2, size: 22.0, - color: settings.xFilcId != "none" + color: settings.analyticsEnabled ? AppColors.of(context).text.withOpacity(0.95) : AppColors.of(context).text.withOpacity(.25), ), @@ -1183,28 +1205,29 @@ class SettingsScreenState extends State fontWeight: FontWeight.w600, fontSize: 16.0, color: AppColors.of(context).text.withOpacity( - settings.xFilcId != "none" ? 1.0 : .5), + settings.analyticsEnabled ? 1.0 : .5), ), ), subtitle: Text( "Anonymous Usage Analytics".i18n, style: TextStyle( - color: AppColors.of(context).text.withOpacity( - settings.xFilcId != "none" ? .5 : .2), + color: AppColors.of(context) + .text + .withOpacity(settings.analyticsEnabled ? .5 : .2), ), ), onChanged: (v) { - String newId; - if (v == false) { - newId = "none"; - } else if (settings.xFilcId == "none") { - newId = SettingsProvider.defaultSettings().xFilcId; - } else { - newId = settings.xFilcId; - } - settings.update(xFilcId: newId); + // String newId; + // if (v == false) { + // newId = "none"; + // } else if (settings.xFilcId == "none") { + // newId = SettingsProvider.defaultSettings().xFilcId; + // } else { + // newId = settings.xFilcId; + // } + settings.update(analyticsEnabled: v); }, - value: settings.xFilcId != "none", + value: settings.analyticsEnabled, activeColor: Theme.of(context).colorScheme.secondary, ), ), diff --git a/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart b/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart index 2c4387c..928d576 100644 --- a/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart +++ b/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart @@ -131,6 +131,11 @@ extension SettingsLocalization on String { "feedback": "Feedback", "other": "Other", "stickermap": "Sticker Map", + "qr_scanner": "QR Scanner", + "camera_perm_error": + "Camera permission is required to scan QR codes.", + "invalid_qr_code": "Invalid QR code!", + "success": "Success!", }, "hu_hu": { "heads_up": "Figyelem!", @@ -260,6 +265,11 @@ extension SettingsLocalization on String { "feedback": "Visszajelzés", "other": "Egyéb", "stickermap": "Matrica térkép", + "qr_scanner": "QR Kódolvasó", + "camera_perm_error": + "A kamera engedély szükséges a QR kódok beolvasásához.", + "invalid_qr_code": "Érvénytelen QR kód!", + "success": "Siker!", }, "de_de": { "heads_up": "Achtung!", @@ -389,6 +399,11 @@ extension SettingsLocalization on String { "feedback": "Feedback", "other": "Sonstiges", "stickermap": "Sticker Map", + "qr_scanner": "QR-Scanner", + "camera_perm_error": + "Kameraberechtigung ist erforderlich, um QR-Codes zu scannen.", + "invalid_qr_code": "Ungültiger QR-Code!", + "success": "Erfolg!", }, }; diff --git a/refilc_mobile_ui/lib/screens/settings/submenu/code_scanner.dart b/refilc_mobile_ui/lib/screens/settings/submenu/code_scanner.dart new file mode 100644 index 0000000..ebae250 --- /dev/null +++ b/refilc_mobile_ui/lib/screens/settings/submenu/code_scanner.dart @@ -0,0 +1,172 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_mobile_ui/common/custom_snack_bar.dart'; +import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class CodeScannerScreen extends StatefulWidget { + const CodeScannerScreen({super.key}); + + @override + State createState() => _CodeScannerScreenState(); +} + +class _CodeScannerScreenState extends State { + Barcode? result; + QRViewController? controller; + final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + controller!.pauseCamera(); + } + controller!.resumeCamera(); + } + + // @override + // void initState() { + // super.initState(); + + // controller!.resumeCamera(); + // } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + title: Text('qr_scanner'.i18n), + leading: const BackButton(), + actions: [ + IconButton( + icon: FutureBuilder( + future: controller?.getFlashStatus(), + builder: (context, snapshot) { + return Icon( + snapshot.data == true + ? FeatherIcons.zapOff + : FeatherIcons.zap, + ); + }, + ), + onPressed: () async { + await controller?.toggleFlash(); + setState(() {}); + }, + ), + ], + ), + body: _buildQrView(context), + ); + } + + Widget _buildQrView(BuildContext context) { + var scanArea = (MediaQuery.of(context).size.width < 400 || + MediaQuery.of(context).size.height < 400) + ? 150.0 + : 280.0; + + return QRView( + key: qrKey, + onQRViewCreated: _onQRViewCreated, + overlay: QrScannerOverlayShape( + borderColor: Theme.of(context).primaryColor, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: scanArea, + ), + onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p), + ); + } + + void _onQRViewCreated(QRViewController controller) { + setState(() { + this.controller = controller; + }); + controller.scannedDataStream.listen((scanData) { + // controller.pauseCamera(); + if (result?.code == scanData.code) return; + + setState(() { + result = scanData; + }); + + if (scanData.code != null) { + if (scanData.code!.startsWith('qw://')) { + // String data = scanData.code!.replaceFirst('qw://', ''); + // check the qr id from api + // TODO: this qr shit + } else if (scanData.code!.startsWith('https://') || + scanData.code!.startsWith('http://')) { + Uri uri = + Uri.parse(scanData.code!.replaceFirst('http://', 'https://')); + + // print(uri); + + if (uri.host.contains('refilc.hu') || + uri.host.contains('refilcapp.hu') || + uri.host.contains('filc.one')) { + ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( + content: Text("success".i18n, + style: const TextStyle(color: Colors.white)), + backgroundColor: const Color(0xFF00A900), + context: context, + )); + + // launch refilc url + Future.delayed(const Duration(seconds: 1), () { + Navigator.of(context).pop(); + launchUrl(uri, mode: LaunchMode.inAppBrowserView); + }); + } else { + // show invalid code error + // Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( + content: Text("invalid_qr_code".i18n, + style: const TextStyle(color: Colors.white)), + backgroundColor: AppColors.of(context).red, + context: context, + )); + + controller.resumeCamera(); + } + } else { + // show invalid code error + // Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( + content: Text("invalid_qr_code".i18n, + style: const TextStyle(color: Colors.white)), + backgroundColor: AppColors.of(context).red, + context: context, + )); + + controller.resumeCamera(); + } + } + }); + } + + void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) { + if (!p) { + ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( + content: Text("camera_perm_error".i18n, + style: const TextStyle(color: Colors.white)), + backgroundColor: AppColors.of(context).red, + context: context, + )); + } + } + + @override + void dispose() { + controller?.dispose(); + super.dispose(); + } +} diff --git a/refilc_mobile_ui/lib/screens/settings/submenu/share_theme_popup.dart b/refilc_mobile_ui/lib/screens/settings/submenu/share_theme_popup.dart index 92da234..ac34212 100644 --- a/refilc_mobile_ui/lib/screens/settings/submenu/share_theme_popup.dart +++ b/refilc_mobile_ui/lib/screens/settings/submenu/share_theme_popup.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; // import 'package:refilc/models/settings.dart'; -import 'package:refilc/models/shared_theme.dart'; +import 'package:refilc/theme/colors/colors.dart'; import 'package:refilc_kreta_api/providers/share_provider.dart'; import 'package:refilc_mobile_ui/common/action_button.dart'; +import 'package:refilc_mobile_ui/common/custom_snack_bar.dart'; import 'package:refilc_mobile_ui/common/splitted_panel/splitted_panel.dart'; import 'package:share_plus/share_plus.dart'; import 'submenu_screen.i18n.dart'; @@ -131,15 +132,59 @@ class ShareThemeDialogState extends State { ), onPressed: () async { // share the fucking theme - SharedGradeColors gradeColors = + var (gradeColors, gradeColorsStatus) = await shareProvider.shareCurrentGradeColors(context); - SharedTheme theme = await shareProvider.shareCurrentTheme( + + if (gradeColorsStatus == 429) { + ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( + content: Text("theme_share_ratelimit".i18n, + style: const TextStyle(color: Colors.white)), + backgroundColor: AppColors.of(context).red, + context: context, + )); + + return; + } else if (gradeColorsStatus != 201) { + ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( + content: Text("theme_share_failed".i18n, + style: const TextStyle(color: Colors.white)), + backgroundColor: AppColors.of(context).red, + context: context, + )); + + return; + } + + var (theme, themeStatus) = await shareProvider.shareCurrentTheme( context, - gradeColors: gradeColors, + gradeColors: gradeColors!, isPublic: isPublic, displayName: _title.text, ); + if (themeStatus == 429) { + ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( + content: Text("theme_share_ratelimit".i18n, + style: const TextStyle(color: Colors.white)), + backgroundColor: AppColors.of(context).red, + context: context, + )); + + return; + } else if (themeStatus != 201) { + ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( + content: Text("theme_share_failed".i18n, + style: const TextStyle(color: Colors.white)), + backgroundColor: AppColors.of(context).red, + context: context, + )); + + return; + } + + // print(theme); + // print(themeStatus); + // save theme id in settings // Provider.of(context, listen: false) // .update(currentThemeId: theme.id); @@ -149,7 +194,7 @@ class ShareThemeDialogState extends State { // show the share popup Share.share( - theme.id, + theme!.id, subject: 'share_subj_theme'.i18n, ); }, 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 ce76ee7..35c63b1 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 @@ -30,6 +30,8 @@ extension SettingsLocalization on String { "share_disclaimer": "By sharing the theme, you agree that the nickname you set and all settings of the theme will be shared publicly.", "understand": "I understand", + "theme_share_failed": "An error occurred while sharing the theme.", + "theme_share_ratelimit": "You can only share 1 theme per minute.", }, "hu_hu": { "general": "Általános", @@ -58,6 +60,8 @@ extension SettingsLocalization on String { "share_disclaimer": "A téma megosztásával elfogadod, hogy az általad beállított becenév és a téma minden beállítása nyilvánosan megosztásra kerüljön.", "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.", }, "de_de": { "general": "Allgemeine", @@ -86,6 +90,9 @@ extension SettingsLocalization on String { "share_disclaimer": "Durch das Teilen des Themes erklären Sie sich damit einverstanden, dass der von Ihnen festgelegte Spitzname und alle Einstellungen des Themes öffentlich geteilt werden.", "understand": "Ich verstehe", + "theme_share_failed": + "Beim Teilen des Themas ist ein Fehler aufgetreten.", + "theme_share_ratelimit": "Sie können nur 1 Thema pro Minute teilen.", }, }; diff --git a/refilc_mobile_ui/pubspec.yaml b/refilc_mobile_ui/pubspec.yaml index 53f2629..7c050a9 100644 --- a/refilc_mobile_ui/pubspec.yaml +++ b/refilc_mobile_ui/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: auto_size_text: ^3.0.0 connectivity_plus: ^6.0.3 collection: ^1.18.0 - share_plus: ^9.0.0 + share_plus: ^10.0.3 image_picker: ^1.0.7 path_provider: ^2.1.2 image_crop: @@ -77,6 +77,7 @@ dependencies: webview_flutter: ^4.8.0 file_picker: ^8.0.5 shake_flutter: ^17.0.0 + qr_code_scanner_plus: ^2.0.6 dev_dependencies: flutter_lints: ^4.0.0 diff --git a/refilc_plus b/refilc_plus index 26cd3fc..6abc4ed 160000 --- a/refilc_plus +++ b/refilc_plus @@ -1 +1 @@ -Subproject commit 26cd3fc163d72ddb849edfeb7fdb7b64c7df44bc +Subproject commit 6abc4edf70deeaffea8b8a7dd95acebecc5a520b