Merge branch 'dev' into master

This commit is contained in:
Márton Kiss
2025-01-03 15:26:35 +01:00
committed by GitHub
15 changed files with 715 additions and 29 deletions

View File

@@ -75,7 +75,7 @@ class _FSTimetableState extends State<FSTimetable> {
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<Widget> columns = [];
for (int dayIndex = -1; dayIndex < days.length; dayIndex++) {
@@ -119,10 +119,8 @@ class _FSTimetableState extends State<FSTimetable> {
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<FSTimetable> {
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<FSTimetable> {
),
);
}
}
}

View File

@@ -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",
);
}));
},
),
],
),
);
},
),
],

View File

@@ -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 ",

View File

@@ -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<QwIDLoginWidget> createState() => _QwIDLoginWidgetState();
}
class _QwIDLoginWidgetState extends State<QwIDLoginWidget>
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<String> 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<void> loadLoginUrl() async {
// String nonceStr = await Provider.of<KretaClient>(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<double>(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<double>(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
),
],
);
},
),
),
],
);
}
}

View File

@@ -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<SettingsScreen>
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!),

View File

@@ -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<CloudSyncSettingsScreen> {
late SettingsProvider settingsProvider;
late UserProvider user;
String longLivedToken = '';
@override
Widget build(BuildContext context) {
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
// UserProvider user = Provider.of<UserProvider>(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),
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -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",
},
};

View File

@@ -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