changed everything from filcnaplo to refilc finally

This commit is contained in:
Kima
2024-02-24 20:12:25 +01:00
parent 0d1c7b7143
commit 1171e3aaaf
655 changed files with 38728 additions and 44967 deletions

View File

@@ -0,0 +1,217 @@
import 'dart:io';
import 'dart:math';
import 'package:refilc/api/client.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'error_report_screen.i18n.dart';
class ErrorReportScreen extends StatelessWidget {
final FlutterErrorDetails details;
const ErrorReportScreen(this.details, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.red,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
const Align(
alignment: Alignment.topLeft,
child: BackButton(),
),
const Spacer(),
const Icon(
FeatherIcons.alertTriangle,
size: 100,
),
const Spacer(),
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
"uhoh".i18n,
style: const TextStyle(
color: Colors.white,
fontSize: 32.0,
fontWeight: FontWeight.w900,
),
),
),
Text(
"description".i18n,
style: TextStyle(
color: Colors.white.withOpacity(.95),
fontSize: 24.0,
fontWeight: FontWeight.w700,
),
),
const Spacer(),
Stack(
alignment: Alignment.topRight,
children: [
Container(
height: 110.0,
width: double.infinity,
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: Colors.black.withOpacity(.2)),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Text(
details.exceptionAsString(),
style: const TextStyle(fontFamily: 'SpaceMono'),
),
),
),
IconButton(
icon: const Icon(FeatherIcons.info),
onPressed: () {
showDialog(
context: context,
builder: (context) => StacktracePopup(details));
},
)
],
),
const Spacer(),
SizedBox(
width: double.infinity,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(vertical: 14.0)),
backgroundColor: MaterialStateProperty.all(Colors.white),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
),
),
child: Text(
"submit".i18n,
style: const TextStyle(
color: Colors.black,
fontSize: 17.0,
fontWeight: FontWeight.bold,
),
),
onPressed: () => reportProblem(context),
),
),
const SizedBox(height: 32.0)
],
),
),
),
);
}
Future reportProblem(BuildContext context) async {
final report = ErrorReport(
os: "${Platform.operatingSystem} ${Platform.operatingSystemVersion}",
error: details.exceptionAsString(),
version: const String.fromEnvironment("APPVER", defaultValue: "?"),
stack: details.stack.toString(),
);
FilcAPI.sendReport(report);
Navigator.pop(context);
}
}
class StacktracePopup extends StatelessWidget {
final FlutterErrorDetails details;
const StacktracePopup(this.details, {super.key});
@override
Widget build(BuildContext context) {
String stack = details.stack.toString();
return Container(
margin: const EdgeInsets.all(32.0),
child: Scaffold(
backgroundColor: Colors.transparent,
body: Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(4.0),
),
padding: const EdgeInsets.only(top: 15.0, right: 15.0, left: 15.0),
child: Column(
children: [
Expanded(
child: ListView(children: [
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
"details".i18n,
style: const TextStyle(fontSize: 20.0),
),
),
ErrorDetail(
"error".i18n,
details.exceptionAsString(),
),
ErrorDetail("os".i18n,
"${Platform.operatingSystem} ${Platform.operatingSystemVersion}"),
ErrorDetail(
"version".i18n,
const String.fromEnvironment("APPVER",
defaultValue: "?")),
ErrorDetail(
"stack".i18n, stack.substring(0, min(stack.length, 5000)))
]),
),
TextButton(
child: Text("done".i18n,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary)),
onPressed: () {
Navigator.of(context).pop();
})
],
),
),
),
);
}
}
class ErrorDetail extends StatelessWidget {
final String title;
final String content;
const ErrorDetail(this.title, this.content, {super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 6.5, vertical: 4.0),
margin: const EdgeInsets.only(top: 4.0),
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(4.0)),
child: Text(
content,
style: const TextStyle(
fontFamily: 'SpaceMono', color: Colors.white),
))
],
),
);
}
}

View File

@@ -0,0 +1,45 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"uhoh": "Uh Oh!",
"description": "An error occurred!",
"submit": "Submit",
"details": "Details",
"error": "Error",
"os": "Operating System",
"version": "App Version",
"stack": "Stack Trace",
"done": "Done",
},
"hu_hu": {
"uhoh": "Ajajj!",
"description": "Hiba történt!",
"submit": "Probléma Jelentése",
"details": "Részletek",
"error": "Hiba",
"os": "Operációs Rendszer",
"version": "App Verzió",
"stack": "Stacktrace",
"done": "Kész",
},
"de_de": {
"uhoh": "Uh Oh!",
"description": "Ein Fehler ist aufgetreten!",
"submit": "Abschicken",
"details": "Details",
"error": "Fehler",
"os": "Betriebssystem",
"version": "App Version",
"stack": "Stack Trace",
"done": "Fertig",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,65 @@
import 'package:refilc/theme/colors/colors.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
class ErrorScreen extends StatelessWidget {
const ErrorScreen(this.details, {super.key});
final FlutterErrorDetails details;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
shadowColor: Colors.transparent,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Icon(FeatherIcons.alertTriangle,
size: 48.0, color: AppColors.of(context).red),
),
const Padding(
padding: EdgeInsets.all(12.0),
child: Text(
"An error occurred...",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16.0),
),
),
Expanded(
child: Container(
padding: const EdgeInsets.all(12.0),
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14.0),
color: Theme.of(context).colorScheme.background,
),
child: CupertinoScrollbar(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: SelectableText(
('${details.exceptionAsString()}\n'),
style: const TextStyle(fontFamily: "monospace"),
),
),
),
),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:refilc/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class LoginButton extends StatelessWidget {
const LoginButton({super.key, required this.onPressed, required this.child});
final void Function()? onPressed;
final Widget? child;
@override
Widget build(BuildContext context) {
return MaterialButton(
elevation: 0,
focusElevation: 0,
hoverElevation: 0,
highlightElevation: 0,
minWidth: double.infinity,
onPressed: onPressed,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
color: AppColors.of(context).buttonBackground,
textColor: Colors.white,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 6.0,
),
child: child,
),
);
}
}

View File

@@ -0,0 +1,129 @@
import 'package:refilc/theme/colors/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
enum LoginInputStyle { username, password, school }
class LoginInput extends StatefulWidget {
const LoginInput(
{super.key,
required this.style,
this.controller,
this.focusNode,
this.onClear});
final Function()? onClear;
final LoginInputStyle style;
final TextEditingController? controller;
final FocusNode? focusNode;
@override
State<LoginInput> createState() => _LoginInputState();
}
class _LoginInputState extends State<LoginInput> {
late bool obscure;
@override
void initState() {
super.initState();
obscure = widget.style == LoginInputStyle.password;
}
@override
Widget build(BuildContext context) {
String autofill;
switch (widget.style) {
case LoginInputStyle.username:
autofill = AutofillHints.username;
break;
case LoginInputStyle.password:
autofill = AutofillHints.password;
break;
case LoginInputStyle.school:
autofill = AutofillHints.organizationName;
break;
}
return TextField(
focusNode: widget.focusNode,
controller: widget.controller,
cursorColor: AppColors.of(context).filc,
textInputAction: TextInputAction.next,
autofillHints: [autofill],
obscureText: obscure,
scrollPhysics: const BouncingScrollPhysics(),
decoration: InputDecoration(
// fillColor: Colors.black.withOpacity(0.15),
filled: false,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: BorderSide(
width: 1,
color: AppColors.of(context).inputBorder,
),
),
// focusedBorder: UnderlineInputBorder(
// borderRadius: BorderRadius.circular(12.0),
// borderSide: const BorderSide(width: 0, color: Colors.transparent),
// ),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: BorderSide(
width: 1,
color: AppColors.of(context).filc,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(
width: 1,
color: Color(0xFFFF0000),
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0),
suffixIconConstraints:
const BoxConstraints(maxHeight: 42.0, maxWidth: 48.0),
suffixIcon: widget.style == LoginInputStyle.password ||
widget.style == LoginInputStyle.school
? ClipOval(
child: Material(
type: MaterialType.transparency,
child: IconButton(
splashRadius: 20.0,
padding: EdgeInsets.zero,
onPressed: () {
if (widget.style == LoginInputStyle.password) {
setState(() => obscure = !obscure);
} else {
widget.controller?.clear();
if (widget.onClear != null) widget.onClear!();
}
},
icon: widget.style == LoginInputStyle.password
? Icon(
obscure ? FeatherIcons.eye : FeatherIcons.eyeOff,
color: AppColors.of(context).text.withOpacity(0.8),
weight: 0.1,
size: 18.0,
)
: Icon(
FeatherIcons.x,
color: AppColors.of(context).text.withOpacity(0.8),
weight: 0.1,
size: 20.0,
),
),
),
)
: null,
),
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: AppColors.of(context).text.withOpacity(0.8),
),
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
Route loginRoute(Widget widget) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => widget,
transitionDuration: const Duration(milliseconds: 650),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var curve = Curves.easeInOut;
var curveTween = CurveTween(curve: curve);
var begin = const Offset(1.0, 0.0);
var end = Offset.zero;
var tween = Tween(begin: begin, end: end).chain(curveTween);
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
);
}

View File

@@ -0,0 +1,390 @@
// import 'dart:async';
import 'package:refilc/api/client.dart';
import 'package:refilc/api/login.dart';
import 'package:refilc/theme/colors/colors.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/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';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key, this.back = false});
final bool back;
@override
LoginScreenState createState() => LoginScreenState();
}
class LoginScreenState extends State<LoginScreen> {
final usernameController = TextEditingController();
final passwordController = TextEditingController();
final schoolController = SchoolInputController();
final _scrollController = ScrollController();
LoginState _loginState = LoginState.normal;
bool showBack = false;
// Scaffold Gradient background
// final LinearGradient _backgroundGradient = const LinearGradient(
// colors: [
// Color.fromARGB(255, 61, 122, 244),
// Color.fromARGB(255, 23, 77, 185),
// Color.fromARGB(255, 7, 42, 112),
// ],
// begin: Alignment(-0.8, -1.0),
// end: Alignment(0.8, 1.0),
// stops: [-1.0, 0.0, 1.0],
// );
late String tempUsername = '';
@override
void initState() {
super.initState();
showBack = widget.back;
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: Colors.white,
systemNavigationBarIconBrightness: Brightness.dark,
));
FilcAPI.getSchools().then((schools) {
if (schools != null) {
schoolController.update(() {
schoolController.schools = schools;
});
} else {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
content: Text("schools_error".i18n,
style: const TextStyle(color: Colors.white)),
backgroundColor: AppColors.of(context).red,
context: context,
));
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(color: AppColors.of(context).loginBackground),
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
controller: _scrollController,
child: Container(
decoration:
BoxDecoration(color: AppColors.of(context).loginBackground),
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.only(left: 16.0, top: 12.0),
child: ClipOval(
child: Material(
type: MaterialType.transparency,
child: showBack
? BackButton(
color: AppColors.of(context).loginPrimary)
: const SizedBox(height: 48.0),
),
),
),
const SizedBox(
height: 50.0,
),
// app icon
Padding(
padding: EdgeInsets.zero,
child: Image.asset(
'assets/icons/ic_rounded.png',
width: 50.0,
),
),
// texts
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 12.0,
),
child: Text(
'reFilc',
style: TextStyle(
color: AppColors.of(context).loginPrimary,
fontSize: 28.0,
fontWeight: FontWeight.bold,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10.0,
),
child: Text(
'login_w_kreten'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.of(context).loginPrimary,
fontSize: 18.0,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
),
// inputs
Padding(
padding: const EdgeInsets.only(
left: 22.0,
right: 22.0,
top: 150.0,
),
child: AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// username
Padding(
padding: const EdgeInsets.only(bottom: 6.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"username".i18n,
maxLines: 1,
style: TextStyle(
color: AppColors.of(context).loginPrimary,
fontWeight: FontWeight.w500,
fontSize: 12.0,
),
),
),
Expanded(
child: Text(
"usernameHint".i18n,
maxLines: 1,
textAlign: TextAlign.right,
style: TextStyle(
color:
AppColors.of(context).loginSecondary,
fontWeight: FontWeight.w500,
fontSize: 12.0,
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: LoginInput(
style: LoginInputStyle.username,
controller: usernameController,
),
),
// password
Padding(
padding: const EdgeInsets.only(bottom: 6.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"password".i18n,
maxLines: 1,
style: TextStyle(
color: AppColors.of(context).loginPrimary,
fontWeight: FontWeight.w500,
fontSize: 12.0,
),
),
),
Expanded(
child: Text(
"passwordHint".i18n,
maxLines: 1,
textAlign: TextAlign.right,
style: TextStyle(
color:
AppColors.of(context).loginSecondary,
fontWeight: FontWeight.w500,
fontSize: 12.0,
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: LoginInput(
style: LoginInputStyle.password,
controller: passwordController,
),
),
// school
Padding(
padding: const EdgeInsets.only(bottom: 6.0),
child: Text(
"school".i18n,
maxLines: 1,
style: TextStyle(
color: AppColors.of(context).loginPrimary,
fontWeight: FontWeight.w500,
fontSize: 12.0,
),
),
),
SchoolInput(
scroll: _scrollController,
controller: schoolController,
),
],
),
),
),
// login button
Padding(
padding: const EdgeInsets.only(
top: 35.0,
left: 22.0,
right: 22.0,
),
child: Visibility(
visible: _loginState != LoginState.inProgress,
replacement: const Padding(
padding: EdgeInsets.symmetric(vertical: 6.0),
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
),
child: LoginButton(
child: Text("login".i18n,
maxLines: 1,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0,
)),
onPressed: () => _loginAPI(context: context),
),
),
),
// error messages
if (_loginState == LoginState.missingFields ||
_loginState == LoginState.invalidGrant ||
_loginState == LoginState.failed)
Padding(
padding: const EdgeInsets.only(
top: 8.0, left: 12.0, right: 12.0),
child: Text(
[
"missing_fields",
"invalid_grant",
"error"
][_loginState.index]
.i18n,
style: const TextStyle(
color: Colors.red,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 22.0),
// privacy policy
GestureDetector(
onTap: () => PrivacyView.show(context),
child: Text(
'privacy'.i18n,
style: TextStyle(
color: AppColors.of(context).loginSecondary,
fontWeight: FontWeight.w500,
fontSize: 14.0,
),
),
),
const Spacer(),
],
),
),
),
),
),
);
}
void _loginAPI({required BuildContext context}) {
String username = usernameController.text;
String password = passwordController.text;
tempUsername = username;
if (username == "" ||
password == "" ||
schoolController.selectedSchool == null) {
return setState(() => _loginState = LoginState.missingFields);
}
// ignore: no_leading_underscores_for_local_identifiers
void _callAPI() {
loginAPI(
username: username,
password: password,
instituteCode: schoolController.selectedSchool!.instituteCode,
context: context,
onLogin: (user) {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
context: context,
brightness: Brightness.light,
content: Text("welcome".i18n.fill([user.name]),
overflow: TextOverflow.ellipsis),
));
},
onSuccess: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
setSystemChrome(context);
Navigator.of(context).pushReplacementNamed("login_to_navigation");
}).then(
(res) => setState(() {
// if (res == LoginState.invalidGrant &&
// tempUsername.replaceAll(username, '').length <= 3) {
// tempUsername = username + ' ';
// Timer(
// const Duration(milliseconds: 500),
// () => _loginAPI(context: context),
// );
// // _loginAPI(context: context);
// } else {
_loginState = res;
// }
}),
);
}
setState(() => _loginState = LoginState.inProgress);
_callAPI();
}
}

View File

@@ -0,0 +1,63 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"username": "Username",
"usernameHint": "Student ID number",
"password": "Password",
"passwordHint": "Date of birth",
"school": "School",
"login": "Log in",
"welcome": "Welcome, %s!",
"missing_fields": "Missing Fields!",
"invalid_grant":
// "Invalid Username/Password! (Try adding spaces after Username)",
"Invalid Username/Password!",
"error": "Failed to log in.",
"schools_error": "Failed to get schools.",
"login_w_kreten": "Log in with your e-KRÉTA account to continue!",
"privacy": "Privacy Policy",
},
"hu_hu": {
"username": "Felhasználónév",
"usernameHint": "Oktatási azonosító",
"password": "Jelszó",
"passwordHint": "Születési dátum",
"school": "Iskola",
"login": "Belépés",
"welcome": "Üdv, %s!",
"missing_fields": "Hiányzó adatok!",
"invalid_grant":
// "Helytelen Felhasználónév/Jelszó! (Próbálj szóközöket írni a Felhasználónév után)",
"Helytelen Felhasználónév/Jelszó!",
"error": "Sikertelen bejelentkezés.",
"schools_error": "Nem sikerült lekérni az iskolákat.",
"login_w_kreten":
"Jelentkezz be az e-KRÉTA fiókoddal a folytatáshoz!",
"privacy": "Adatkezelési tájékoztató",
},
"de_de": {
"username": "Benutzername",
"usernameHint": "Ausbildung ID",
"password": "Passwort",
"passwordHint": "Geburtsdatum",
"school": "Schule",
"login": "Einloggen",
"welcome": "Wilkommen, %s!",
"missing_fields": "Fehlende Felder!",
"invalid_grant": "Ungültiger Benutzername/Passwort!",
"error": "Anmeldung fehlgeschlagen.",
"schools_error": "Keine Schulen gefunden.",
"login_w_kreten":
"Melden Sie sich mit Ihrem e-KRÉTA-Konto an, um fortzufahren!",
"privacy": "Datenschutzrichtlinie",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,122 @@
import 'package:refilc_mobile_ui/screens/login/login_input.dart';
import 'package:refilc_mobile_ui/screens/login/school_input/school_input_overlay.dart';
import 'package:refilc_mobile_ui/screens/login/school_input/school_input_tile.dart';
import 'package:refilc_mobile_ui/screens/login/school_input/school_search.dart';
import 'package:flutter/material.dart';
import 'package:refilc_kreta_api/models/school.dart';
class SchoolInput extends StatefulWidget {
const SchoolInput(
{super.key, required this.controller, required this.scroll});
final SchoolInputController controller;
final ScrollController scroll;
@override
SchoolInputState createState() => SchoolInputState();
}
class SchoolInputState extends State<SchoolInput> {
final _focusNode = FocusNode();
final _layerLink = LayerLink();
late SchoolInputOverlay overlay;
@override
void initState() {
super.initState();
widget.controller.update = (fn) {
if (mounted) setState(fn);
};
overlay = SchoolInputOverlay(layerLink: _layerLink);
// Show school list when focused
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
WidgetsBinding.instance
.addPostFrameCallback((_) => overlay.createOverlayEntry(context));
Future.delayed(const Duration(milliseconds: 100)).then((value) {
if (mounted && widget.scroll.hasClients) {
widget.scroll.animateTo(widget.scroll.offset + 500,
duration: const Duration(milliseconds: 500),
curve: Curves.ease);
}
});
} else {
overlay.entry?.remove();
}
});
// LoginInput TextField listener
widget.controller.textController.addListener(() {
String text = widget.controller.textController.text;
if (text.isEmpty) {
overlay.children = null;
return;
}
List<School> results =
searchSchools(widget.controller.schools ?? [], text);
setState(() {
overlay.children = results
.map((School e) => SchoolInputTile(
school: e,
onTap: () => _selectSchool(e),
))
.toList();
});
Overlay.of(context).setState(() {});
});
}
void _selectSchool(School school) {
FocusScope.of(context).requestFocus(FocusNode());
setState(() {
widget.controller.selectedSchool = school;
widget.controller.textController.text = school.name;
});
}
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: widget.controller.schools == null
? Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10.0),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.15),
borderRadius: BorderRadius.circular(12.0),
),
child: const Center(
child: SizedBox(
height: 28.0,
width: 28.0,
child: CircularProgressIndicator(
color: Colors.white,
),
),
),
)
: LoginInput(
style: LoginInputStyle.school,
focusNode: _focusNode,
onClear: () {
widget.controller.selectedSchool = null;
FocusScope.of(context).requestFocus(_focusNode);
},
controller: widget.controller.textController,
),
);
}
}
class SchoolInputController {
final textController = TextEditingController();
School? selectedSchool;
List<School>? schools;
late void Function(void Function()) update;
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'school_input_overlay.i18n.dart';
class SchoolInputOverlay {
OverlayEntry? entry;
final LayerLink layerLink;
List<Widget>? children;
SchoolInputOverlay({required this.layerLink});
void createOverlayEntry(BuildContext context) {
entry = OverlayEntry(builder: (_) => buildOverlayEntry(context));
Overlay.of(context).insert(entry!);
}
Widget buildOverlayEntry(BuildContext context) {
RenderBox renderBox = context.findRenderObject()! as RenderBox;
var size = renderBox.size;
return SchoolInputOverlayWidget(
size: size,
layerLink: layerLink,
children: children,
);
}
}
class SchoolInputOverlayWidget extends StatelessWidget {
const SchoolInputOverlayWidget({
super.key,
required this.children,
required this.size,
required this.layerLink,
});
final Size size;
final List<Widget>? children;
final LayerLink layerLink;
@override
Widget build(BuildContext context) {
return children != null
? Positioned(
width: size.width,
height: (children?.length ?? 0) > 0 ? 150.0 : 50.0,
child: CompositedTransformFollower(
link: layerLink,
showWhenUnlinked: false,
offset: Offset(0.0, size.height + 5.0),
child: Material(
color: Theme.of(context).colorScheme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0)),
elevation: 4.0,
shadowColor: Colors.black,
child: (children?.length ?? 0) > 0
? ListView.builder(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: children?.length ?? 0,
itemBuilder: (context, index) {
return children?[index] ?? Container();
},
)
: Center(
child: Text("noresults".i18n),
),
),
),
)
: Container();
}
}

View File

@@ -0,0 +1,21 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"noresults": "No results!",
},
"hu_hu": {
"noresults": "Nincs találat!",
},
"de_de": {
"noresults": "Keine Treffer!",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,61 @@
import 'package:refilc_kreta_api/models/school.dart';
import 'package:flutter/material.dart';
class SchoolInputTile extends StatelessWidget {
const SchoolInputTile({super.key, required this.school, this.onTap});
final School school;
final Function()? onTap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: InkWell(
onTap: () {
onTap!();
},
borderRadius: BorderRadius.circular(6.0),
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// School name
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
school.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
Row(
children: [
// School id
Expanded(
child: Text(
school.instituteCode,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// School city
Expanded(
child: Text(
school.city,
textAlign: TextAlign.right,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:refilc_kreta_api/models/school.dart';
import 'package:refilc/utils/format.dart';
List<School> searchSchools(List<School> all, String pattern) {
pattern = pattern.toLowerCase().specialChars();
if (pattern == "") return all;
List<School> results = [];
for (var item in all) {
int contains = 0;
pattern.split(" ").forEach((variation) {
if (item.name.toLowerCase().specialChars().contains(variation)) {
contains++;
}
});
if (contains == pattern.split(" ").length) results.add(item);
if (item.instituteCode.toLowerCase().specialChars().contains(pattern))
results.add(item);
}
results.sort((a, b) => a.name.compareTo(b.name));
return results;
}

View File

@@ -0,0 +1,31 @@
import 'package:refilc_mobile_ui/screens/navigation/navbar_item.dart';
import 'package:flutter/material.dart';
class Navbar extends StatelessWidget {
const Navbar(
{super.key,
required this.selectedIndex,
required this.onSelected,
required this.items});
final int selectedIndex;
final void Function(int index) onSelected;
final List<NavItem> items;
@override
Widget build(BuildContext context) {
final List<Widget> buttons = List.generate(
items.length,
(index) => NavbarItem(
item: items[index],
active: index == selectedIndex,
onTap: () => onSelected(index),
),
);
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: buttons,
);
}
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
class NavItem {
final String title;
final Widget icon;
final Widget activeIcon;
const NavItem(
{required this.title, required this.icon, required this.activeIcon});
}
class NavbarItem extends StatelessWidget {
const NavbarItem({
super.key,
required this.item,
required this.active,
required this.onTap,
});
final NavItem item;
final bool active;
final void Function() onTap;
@override
Widget build(BuildContext context) {
final Widget icon = active ? item.activeIcon : item.icon;
return SafeArea(
child: GestureDetector(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 6.0),
child: Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: active
? Theme.of(context).colorScheme.secondary.withOpacity(.4)
: null,
borderRadius: BorderRadius.circular(14.0),
),
child: Stack(
children: [
IconTheme(
data: IconThemeData(
color: Theme.of(context).colorScheme.secondary,
),
child: icon,
),
IconTheme(
data: IconThemeData(
color: Theme.of(context).brightness == Brightness.light
? Colors.black.withOpacity(.5)
: Colors.white.withOpacity(.3),
),
child: icon,
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,25 @@
class NavigationRoute {
late String _name;
late int _index;
final List<String> _internalPageMap = [
"home",
"grades",
"timetable",
"messages",
"absences",
];
String get name => _name;
int get index => _index;
set name(String n) {
_name = n;
_index = _internalPageMap.indexOf(n);
}
set index(int i) {
_index = i;
_name = _internalPageMap.elementAt(i);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:refilc_mobile_ui/pages/absences/absences_page.dart';
import 'package:refilc_mobile_ui/pages/grades/grades_page.dart';
import 'package:refilc_mobile_ui/pages/home/home_page.dart';
import 'package:refilc_mobile_ui/pages/messages/messages_page.dart';
import 'package:refilc_mobile_ui/pages/timetable/timetable_page.dart';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
Route navigationRouteHandler(RouteSettings settings) {
switch (settings.name) {
case "home":
return navigationPageRoute((context) => const HomePage());
case "grades":
return navigationPageRoute((context) => const GradesPage());
case "timetable":
return navigationPageRoute((context) => const TimetablePage());
case "messages":
return navigationPageRoute((context) => const MessagesPage());
case "absences":
return navigationPageRoute((context) => const AbsencesPage());
default:
return navigationPageRoute((context) => const HomePage());
}
}
Route navigationPageRoute(Widget Function(BuildContext) builder) {
return PageRouteBuilder(
pageBuilder: (context, _, __) => builder(context),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
fillColor: Theme.of(context).scaffoldBackgroundColor,
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
);
}

View File

@@ -0,0 +1,342 @@
import 'package:refilc/api/providers/update_provider.dart';
import 'package:refilc/helpers/quick_actions.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/theme/observer.dart';
import 'package:refilc_kreta_api/client/client.dart';
import 'package:refilc_kreta_api/providers/grade_provider.dart';
import 'package:refilc_mobile_ui/common/system_chrome.dart';
import 'package:refilc_mobile_ui/screens/navigation/nabar.dart';
import 'package:refilc_mobile_ui/screens/navigation/navbar_item.dart';
import 'package:refilc_mobile_ui/screens/navigation/navigation_route.dart';
import 'package:refilc_mobile_ui/screens/navigation/navigation_route_handler.dart';
import 'package:refilc/icons/filc_icons.dart';
import 'package:refilc_mobile_ui/screens/navigation/status_bar.dart';
import 'package:refilc_mobile_ui/screens/news/news_view.dart';
import 'package:refilc_mobile_ui/screens/settings/settings_screen.dart';
import 'package:refilc_plus/ui/mobile/goal_planner/goal_complete_modal.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'package:refilc_mobile_ui/common/screens.i18n.dart';
import 'package:refilc/api/providers/news_provider.dart';
import 'package:refilc/api/providers/sync.dart';
import 'package:home_widget/home_widget.dart';
import 'package:wtf_sliding_sheet/wtf_sliding_sheet.dart';
import 'package:background_fetch/background_fetch.dart';
import 'package:refilc_plus/providers/goal_provider.dart';
import 'package:refilc/api/providers/ad_provider.dart';
class NavigationScreen extends StatefulWidget {
const NavigationScreen({super.key});
static NavigationScreenState? of(BuildContext context) =>
context.findAncestorStateOfType<NavigationScreenState>();
@override
NavigationScreenState createState() => NavigationScreenState();
}
class NavigationScreenState extends State<NavigationScreen>
with WidgetsBindingObserver {
late NavigationRoute selected;
List<String> initializers = [];
final _navigatorState = GlobalKey<NavigatorState>();
late SettingsProvider settings;
late NewsProvider newsProvider;
late GoalProvider goalProvider;
late UpdateProvider updateProvider;
late GradeProvider gradeProvicer;
late AdProvider adProvider;
NavigatorState? get navigator => _navigatorState.currentState;
void customRoute(Route route) => navigator?.pushReplacement(route);
bool init(String id) {
if (initializers.contains(id)) return false;
initializers.add(id);
return true;
}
void _checkForWidgetLaunch() {
HomeWidget.initiallyLaunchedFromHomeWidget().then(_launchedFromWidget);
}
void _launchedFromWidget(Uri? uri) async {
if (uri == null) return;
if (uri.scheme == "timetable" && uri.authority == "refresh") {
Navigator.of(context).popUntil((route) => route.isFirst);
setPage("timetable");
_navigatorState.currentState
?.pushNamedAndRemoveUntil("timetable", (_) => false);
} else if (uri.scheme == "settings" && uri.authority == "premium") {
Navigator.of(context).popUntil((route) => route.isFirst);
showSlidingBottomSheet(
context,
useRootNavigator: true,
builder: (context) => SlidingSheetDialog(
color: Theme.of(context).scaffoldBackgroundColor,
duration: const Duration(milliseconds: 400),
scrollSpec: const ScrollSpec.bouncingScroll(),
snapSpec: const SnapSpec(
snap: true,
snappings: [1.0],
initialSnap: 1.0,
positioning: SnapPositioning.relativeToSheetHeight,
),
cornerRadius: 16,
cornerRadiusOnFullscreen: 0,
builder: (context, state) => Material(
color: Theme.of(context).scaffoldBackgroundColor,
child: const SettingsScreen(),
),
),
);
}
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
// Configure BackgroundFetch.
int status = await BackgroundFetch.configure(
BackgroundFetchConfig(
minimumFetchInterval: 15,
stopOnTerminate: false,
enableHeadless: true,
requiresBatteryNotLow: false,
requiresCharging: false,
requiresStorageNotLow: false,
requiresDeviceIdle: false,
requiredNetworkType: NetworkType.ANY), (String taskId) async {
// <-- Event handler
// This is the fetch-event callback.
if (kDebugMode) {
print("[BackgroundFetch] Event received $taskId");
}
// IMPORTANT: You must signal completion of your task or the OS can punish your app
// for taking too long in the background.
BackgroundFetch.finish(taskId);
}, (String taskId) async {
// <-- Task timeout handler.
// This task has exceeded its allowed running-time. You must stop what you're doing and immediately .finish(taskId)
if (kDebugMode) {
print("[BackgroundFetch] TASK TIMEOUT taskId: $taskId");
}
BackgroundFetch.finish(taskId);
});
if (kDebugMode) {
print('[BackgroundFetch] configure success: $status');
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
}
@override
void initState() {
super.initState();
HomeWidget.setAppGroupId('hu.refilc.naplo.group');
_checkForWidgetLaunch();
HomeWidget.widgetClicked.listen(_launchedFromWidget);
settings = Provider.of<SettingsProvider>(context, listen: false);
selected = NavigationRoute();
selected.index = settings.startPage.index; // set page index to start page
// add brightness observer
WidgetsBinding.instance.addObserver(this);
// set client User-Agent
Provider.of<KretaClient>(context, listen: false).userAgent =
settings.config.userAgent;
// get news
newsProvider = Provider.of<NewsProvider>(context, listen: false);
newsProvider.restore().then((value) => newsProvider.fetch());
// init grade provider (for goals)
gradeProvicer = Provider.of<GradeProvider>(context, listen: false);
// get goals
goalProvider = Provider.of<GoalProvider>(context, listen: false);
goalProvider.fetchDone(gradeProvider: gradeProvicer);
// get releases
updateProvider = Provider.of<UpdateProvider>(context, listen: false);
updateProvider.fetch();
// get advertisements
adProvider = Provider.of<AdProvider>(context, listen: false);
adProvider.fetch();
// initial sync
syncAll(context);
setupQuickActions();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangePlatformBrightness() {
if (settings.theme == ThemeMode.system) {
// ignore: deprecated_member_use
Brightness? brightness =
// ignore: deprecated_member_use
WidgetsBinding.instance.window.platformBrightness;
Provider.of<ThemeModeObserver>(context, listen: false).changeTheme(
brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark);
}
super.didChangePlatformBrightness();
}
void setPage(String page) => setState(() => selected.name = page);
@override
Widget build(BuildContext context) {
setSystemChrome(context);
settings = Provider.of<SettingsProvider>(context);
newsProvider = Provider.of<NewsProvider>(context);
goalProvider = Provider.of<GoalProvider>(context);
// show news and complete goals
WidgetsBinding.instance.addPostFrameCallback((_) {
if (newsProvider.show) {
NewsView.show(newsProvider.news[0], context: context)
.then((value) => newsProvider.release());
newsProvider.lock();
}
if (goalProvider.hasDoneGoals) {
GoalCompleteModal.show(goalProvider.doneSubject!, context: context);
goalProvider.lock();
}
});
handleQuickActions(context, (page) {
setPage(page);
_navigatorState.currentState?.pushReplacementNamed(page);
});
// ignore: deprecated_member_use
return WillPopScope(
onWillPop: () async {
if (_navigatorState.currentState?.canPop() ?? false) {
_navigatorState.currentState?.pop();
if (!kDebugMode) {
return true;
}
return false;
}
if (selected.index != 0) {
setState(() => selected.index = 0);
_navigatorState.currentState?.pushReplacementNamed(selected.name);
}
return false;
},
child: Scaffold(
body: Column(
children: [
Expanded(
child: Stack(
alignment: Alignment.bottomCenter,
children: [
Navigator(
key: _navigatorState,
initialRoute: selected.name,
onGenerateRoute: (settings) =>
navigationRouteHandler(settings),
),
],
),
),
// Status bar
Material(
color: Theme.of(context).colorScheme.background,
child: const StatusBar(),
),
// Bottom Navigaton Bar
Material(
color: Theme.of(context).scaffoldBackgroundColor,
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: Navbar(
selectedIndex: selected.index,
onSelected: onPageSelected,
items: [
NavItem(
title: "home".i18n,
icon: const Icon(FilcIcons.home),
activeIcon: const Icon(FilcIcons.homefill),
),
NavItem(
title: "grades".i18n,
icon: const Icon(FeatherIcons.bookmark),
activeIcon: const Icon(FilcIcons.gradesfill),
),
NavItem(
title: "timetable".i18n,
icon: const Icon(FeatherIcons.calendar),
activeIcon: const Icon(FilcIcons.timetablefill),
),
NavItem(
title: "messages".i18n,
icon: const Icon(FeatherIcons.messageSquare),
activeIcon: const Icon(FilcIcons.messagesfill),
),
NavItem(
title: "absences".i18n,
icon: const Icon(FeatherIcons.clock),
activeIcon: const Icon(FilcIcons.absencesfill),
),
],
),
),
),
],
),
),
);
}
void onPageSelected(int index) {
// Vibrate, then set the active screen
if (selected.index != index) {
switch (settings.vibrate) {
case VibrationStrength.light:
HapticFeedback.lightImpact();
break;
case VibrationStrength.medium:
HapticFeedback.mediumImpact();
break;
case VibrationStrength.strong:
HapticFeedback.heavyImpact();
break;
default:
}
setState(() => selected.index = index);
_navigatorState.currentState?.pushReplacementNamed(selected.name);
}
}
}

View File

@@ -0,0 +1,120 @@
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc/utils/color.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:refilc/api/providers/status_provider.dart';
import 'status_bar.i18n.dart';
class StatusBar extends StatefulWidget {
const StatusBar({super.key});
@override
StatusBarState createState() => StatusBarState();
}
class StatusBarState extends State<StatusBar> {
late StatusProvider statusProvider;
@override
Widget build(BuildContext context) {
statusProvider = Provider.of<StatusProvider>(context);
Status? currentStatus = statusProvider.getStatus();
Color backgroundColor = _statusColor(currentStatus);
Color color = ColorUtils.foregroundColor(backgroundColor);
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
height: currentStatus != null ? 32.0 : 0,
width: double.infinity,
color: Theme.of(context).scaffoldBackgroundColor,
child: Stack(
children: [
// Background
AnimatedContainer(
margin: const EdgeInsets.only(left: 6.0, right: 6.0, top: 8.0),
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
height: currentStatus != null ? 28.0 : 0,
decoration: BoxDecoration(
color: backgroundColor,
boxShadow: [
BoxShadow(color: Theme.of(context).shadowColor, blurRadius: 8.0)
],
borderRadius: BorderRadius.circular(45.0),
),
),
// Progress bar
if (currentStatus == Status.syncing)
Container(
margin: const EdgeInsets.only(left: 6.0, right: 6.0, top: 8.0),
alignment: Alignment.bottomLeft,
child: AnimatedContainer(
height: currentStatus != null ? 28.0 : 0,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
width: MediaQuery.of(context).size.width *
statusProvider.progress -
16.0,
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.secondary.withOpacity(0.8),
borderRadius: BorderRadius.circular(45.0),
),
),
),
// Text
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Center(
child: Text(
_statusString(currentStatus),
style: TextStyle(color: color, fontWeight: FontWeight.w500),
),
),
),
],
),
);
}
String _statusString(Status? status) {
switch (status) {
case Status.syncing:
return "Syncing data".i18n;
case Status.maintenance:
return "KRETA Maintenance".i18n;
case Status.apiError:
return "KRETA API error".i18n;
case Status.network:
return "No connection".i18n;
default:
return "";
}
}
Color _statusColor(Status? status) {
switch (status) {
case Status.maintenance:
return AppColors.of(context).red;
case Status.apiError:
return AppColors.of(context).red;
case Status.network:
case Status.syncing:
default:
HSLColor color =
HSLColor.fromColor(Theme.of(context).scaffoldBackgroundColor);
if (color.lightness >= 0.5) {
color = color.withSaturation(0.3);
color = color.withLightness(color.lightness - 0.1);
} else {
color = color.withSaturation(0);
color = color.withLightness(color.lightness + 0.2);
}
return color.toColor();
}
}
}

View File

@@ -0,0 +1,30 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Syncing data": "Syncing data",
"KRETA Maintenance": "KRETA Maintenance",
"KRETA API error": "KRETA API Error",
"No connection": "No connection",
},
"hu_hu": {
"Syncing data": "Adatok frissítése",
"KRETA Maintenance": "KRÉTA Karbantartás",
"KRETA API error": "KRÉTA API Hiba",
"No connection": "Nincs kapcsolat",
},
"de_de": {
"Syncing data": "Daten aktualisieren",
"KRETA Maintenance": "KRETA Wartung",
"KRETA API error": "KRETA API Fehler",
"No connection": "Keine Verbindung",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,66 @@
import 'dart:math';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc_mobile_ui/common/empty.dart';
import 'package:refilc_mobile_ui/common/panel/panel.dart';
import 'package:refilc_mobile_ui/screens/news/news_tile.dart';
import 'package:refilc/models/news.dart';
import 'package:refilc_mobile_ui/screens/news/news_view.dart';
import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:refilc/api/providers/news_provider.dart';
class NewsScreen extends StatelessWidget {
const NewsScreen({super.key});
@override
Widget build(BuildContext context) {
var newsProvider = Provider.of<NewsProvider>(context);
List<News> news = [];
news = newsProvider.news.where((e) => e.title != "").toList();
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text("news".i18n,
style: TextStyle(color: AppColors.of(context).text)),
),
body: SafeArea(
child: RefreshIndicator(
onRefresh: () => newsProvider.fetch(),
child: ListView.builder(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
itemCount: max(news.length, 1),
itemBuilder: (context, index) {
if (news.isNotEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Panel(
child: Material(
type: MaterialType.transparency,
child: NewsTile(
news[index],
onTap: () => NewsView.show(news[index],
context: context, force: true),
),
),
),
);
} else {
return const Padding(
padding: EdgeInsets.only(top: 24.0),
child: Empty(subtitle: "Nothing to see here"),
);
}
},
),
),
),
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:refilc/models/news.dart';
import 'package:refilc/utils/format.dart';
class NewsTile extends StatelessWidget {
const NewsTile(this.news, {super.key, this.onTap});
final News news;
final Function()? onTap;
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
news.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
news.content.escapeHtml().replaceAll("\n", " "),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onTap: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
);
}
}

View File

@@ -0,0 +1,137 @@
import 'dart:io';
import 'package:refilc/models/settings.dart';
import 'package:refilc_mobile_ui/common/dialog_button.dart';
import 'package:flutter/material.dart';
import 'package:refilc/models/news.dart';
import 'package:refilc/utils/format.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:provider/provider.dart';
import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart';
class NewsView extends StatelessWidget {
const NewsView(this.news, {super.key});
final News news;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 100.0, horizontal: 32.0),
child: Material(
borderRadius: BorderRadius.circular(12.0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
child: Column(
children: [
// Content
Expanded(
child: ListView(
physics: const BouncingScrollPhysics(),
children: [
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 6.0, top: 14.0, bottom: 8.0),
child: Text(
news.title,
maxLines: 3,
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 18.0),
),
),
SelectableLinkify(
text: news.content.escapeHtml(),
options:
const LinkifyOptions(looseUrl: true, removeWww: true),
onOpen: (link) {
launch(
link.url,
customTabsOption: CustomTabsOption(
showPageTitle: true,
toolbarColor:
Theme.of(context).scaffoldBackgroundColor),
);
},
style: const TextStyle(
fontWeight: FontWeight.w500, fontSize: 14.0),
),
],
),
),
// Actions
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (news.link != "")
DialogButton(
label: news.openLabel != ""
? news.openLabel
: "open".i18n.toUpperCase(),
onTap: () => launch(
news.link,
customTabsOption: CustomTabsOption(
showPageTitle: true,
toolbarColor:
Theme.of(context).scaffoldBackgroundColor),
),
),
DialogButton(
label: "done".i18n,
onTap: () => Navigator.of(context).maybePop(),
),
],
),
],
),
),
),
);
}
static Future<T?> show<T>(News news,
{required BuildContext context, bool force = false}) {
if (news.title == "") return Future<T?>.value(null);
bool popup = news.platform == 'all' || force;
if (Provider.of<SettingsProvider>(context, listen: false).newsEnabled ||
news.emergency ||
force) {
switch (news.platform.trim().toLowerCase()) {
case "android":
if (Platform.isAndroid) popup = true;
break;
case "ios":
if (Platform.isIOS) popup = true;
break;
case "linux":
if (Platform.isLinux) popup = true;
break;
case "windows":
if (Platform.isWindows) popup = true;
break;
case "macos":
if (Platform.isMacOS) popup = true;
break;
case "all":
popup = true;
break;
default:
popup = true;
}
} else {
popup = false;
}
if (popup) {
return showDialog<T?>(
context: context,
builder: (context) => NewsView(news),
barrierDismissible: true);
} else {
return Future<T?>.value(null);
}
}
}

View File

@@ -0,0 +1,186 @@
// ignore_for_file: use_build_context_synchronously
import 'package:refilc/api/providers/database_provider.dart';
import 'package:refilc/api/providers/self_note_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/models/self_note.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc_kreta_api/providers/homework_provider.dart';
import 'package:refilc_mobile_ui/screens/notes/notes_screen.i18n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
class AddNoteScreen extends StatefulWidget {
const AddNoteScreen({super.key, this.initialNote});
final SelfNote? initialNote;
@override
AddNoteScreenState createState() => AddNoteScreenState();
}
class AddNoteScreenState extends State<AddNoteScreen> {
late UserProvider user;
late HomeworkProvider homeworkProvider;
late DatabaseProvider databaseProvider;
late SelfNoteProvider selfNoteProvider;
final _contentController = TextEditingController();
final _titleController = TextEditingController();
@override
void initState() {
_contentController.text = widget.initialNote?.content ?? '';
_titleController.text = widget.initialNote?.title ?? '';
super.initState();
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
homeworkProvider = Provider.of<HomeworkProvider>(context);
databaseProvider = Provider.of<DatabaseProvider>(context);
selfNoteProvider = Provider.of<SelfNoteProvider>(context);
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
widget.initialNote == null ? 'new_note'.i18n : 'edit_note'.i18n,
style: TextStyle(
color: AppColors.of(context).text,
fontSize: 26.0,
fontWeight: FontWeight.bold,
),
),
actions: [
ClipRRect(
borderRadius: BorderRadius.circular(10.1),
child: GestureDetector(
onTap: () async {
// handle tap
if (_contentController.text.replaceAll(' ', '') == '') {
return;
}
var notes = selfNoteProvider.notes;
if (widget.initialNote == null) {
notes.add(SelfNote.fromJson({
'id': const Uuid().v4(),
'title': _titleController.text.replaceAll(' ', '') == ''
? null
: _titleController.text,
'content': _contentController.text
}));
} else {
var i =
notes.indexWhere((e) => e.id == widget.initialNote!.id);
notes[i] = SelfNote.fromJson({
'id': notes[i].id,
'title': _titleController.text.replaceAll(' ', '') == ''
? null
: _titleController.text,
'content': _contentController.text,
});
}
await selfNoteProvider.store(notes);
Navigator.of(context).pop();
if (widget.initialNote != null) {
Navigator.of(context).pop();
}
},
child: Container(
color: Theme.of(context).colorScheme.primary.withOpacity(0.4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Stack(
children: [
IconTheme(
data: IconThemeData(
color: Theme.of(context).colorScheme.secondary,
),
child: const Icon(
FeatherIcons.check,
size: 20.0,
),
),
IconTheme(
data: IconThemeData(
color:
Theme.of(context).brightness == Brightness.light
? Colors.black.withOpacity(.5)
: Colors.white.withOpacity(.3),
),
child: const Icon(
FeatherIcons.check,
size: 20.0,
),
),
],
),
),
),
),
),
const SizedBox(
width: 20,
),
],
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: Column(
children: [
TextField(
controller: _titleController,
expands: false,
maxLines: 1,
decoration: InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: "hint_t".i18n,
hintStyle: const TextStyle(
fontSize: 22.0,
fontWeight: FontWeight.w600,
),
),
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 22.0,
fontWeight: FontWeight.w600,
),
),
Expanded(
child: TextField(
controller: _contentController,
expands: true,
minLines: null,
maxLines: null,
decoration: InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: "hint".i18n,
hintStyle: const TextStyle(fontSize: 16.0),
),
textAlign: TextAlign.start,
style: const TextStyle(fontSize: 16.0),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,153 @@
import 'package:refilc/api/providers/self_note_provider.dart';
import 'package:refilc/models/self_note.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc_mobile_ui/screens/notes/add_note_screen.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';
class NoteViewScreen extends StatefulWidget {
const NoteViewScreen({super.key, required this.note});
final SelfNote note;
@override
NoteViewScreenState createState() => NoteViewScreenState();
}
class NoteViewScreenState extends State<NoteViewScreen> {
late SelfNoteProvider selfNoteProvider;
@override
Widget build(BuildContext context) {
selfNoteProvider = Provider.of<SelfNoteProvider>(context);
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
widget.note.title ?? '${widget.note.content.split(' ')[0]}...',
style: TextStyle(
color: AppColors.of(context).text,
fontSize: 26.0,
fontWeight: FontWeight.bold,
),
),
actions: [
ClipRRect(
borderRadius: BorderRadius.circular(10.1),
child: GestureDetector(
onTap: () {
// handle tap
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) =>
AddNoteScreen(initialNote: widget.note)));
},
child: Container(
color: Theme.of(context).colorScheme.primary.withOpacity(0.4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Stack(
children: [
IconTheme(
data: IconThemeData(
color: Theme.of(context).colorScheme.secondary,
),
child: const Icon(
FeatherIcons.edit,
size: 20.0,
),
),
IconTheme(
data: IconThemeData(
color:
Theme.of(context).brightness == Brightness.light
? Colors.black.withOpacity(.5)
: Colors.white.withOpacity(.3),
),
child: const Icon(
FeatherIcons.edit,
size: 20.0,
),
),
],
),
),
),
),
),
const SizedBox(
width: 10,
),
ClipRRect(
borderRadius: BorderRadius.circular(10.1),
child: GestureDetector(
onTap: () async {
// handle tap
var notes = selfNoteProvider.notes;
notes.removeWhere((e) => e.id == widget.note.id);
await selfNoteProvider.store(notes);
// ignore: use_build_context_synchronously
Navigator.of(context).pop();
},
child: Container(
color: Theme.of(context).colorScheme.primary.withOpacity(0.4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Stack(
children: [
IconTheme(
data: IconThemeData(
color: Theme.of(context).colorScheme.secondary,
),
child: const Icon(
FeatherIcons.trash2,
size: 20.0,
),
),
IconTheme(
data: IconThemeData(
color:
Theme.of(context).brightness == Brightness.light
? Colors.black.withOpacity(.5)
: Colors.white.withOpacity(.3),
),
child: const Icon(
FeatherIcons.trash2,
size: 20.0,
),
),
],
),
),
),
),
),
const SizedBox(
width: 20,
),
],
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
child: Column(
children: [
Expanded(
child: Text(
widget.note.content,
textAlign: TextAlign.justify,
style: const TextStyle(fontSize: 18.0),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,282 @@
import 'dart:math';
import 'package:refilc/api/providers/database_provider.dart';
import 'package:refilc/api/providers/self_note_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc/utils/format.dart';
import 'package:refilc_kreta_api/models/homework.dart';
import 'package:refilc_kreta_api/providers/homework_provider.dart';
import 'package:refilc_mobile_ui/common/empty.dart';
import 'package:refilc_mobile_ui/common/panel/panel.dart';
import 'package:refilc_mobile_ui/common/soon_alert/soon_alert.dart';
import 'package:refilc_mobile_ui/common/widgets/tick_tile.dart';
import 'package:refilc_mobile_ui/screens/notes/add_note_screen.dart';
import 'package:refilc_mobile_ui/screens/notes/note_view_screen.dart';
import 'package:refilc_mobile_ui/screens/notes/notes_screen.i18n.dart';
import 'package:refilc_mobile_ui/screens/notes/self_note_tile.dart';
import 'package:refilc_plus/providers/premium_provider.dart';
import 'package:refilc_plus/ui/mobile/premium/premium_inline.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';
class NotesScreen extends StatefulWidget {
const NotesScreen({super.key, required this.doneItems});
final Map<String, bool> doneItems;
@override
NotesScreenState createState() => NotesScreenState();
}
class NotesScreenState extends State<NotesScreen> {
late UserProvider user;
late HomeworkProvider homeworkProvider;
late DatabaseProvider databaseProvider;
late SelfNoteProvider selfNoteProvider;
List<Widget> noteTiles = [];
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
homeworkProvider = Provider.of<HomeworkProvider>(context);
databaseProvider = Provider.of<DatabaseProvider>(context);
selfNoteProvider = Provider.of<SelfNoteProvider>(context);
void generateTiles() {
List<Widget> tiles = [];
List<Homework> hw = homeworkProvider.homework
.where((e) => e.deadline.isAfter(DateTime.now()))
// e.deadline.isBefore(DateTime(DateTime.now().year,
// DateTime.now().month, DateTime.now().day + 3)))
.toList();
// todo tiles
List<Widget> toDoTiles = [];
if (hw.isNotEmpty) {
toDoTiles.addAll(hw.map((e) => TickTile(
padding: EdgeInsets.zero,
title: 'homework'.i18n,
description:
'${(e.subject.isRenamed ? e.subject.renamedTo : e.subject.name) ?? ''}, ${e.content.escapeHtml()}',
isTicked: widget.doneItems[e.id] ?? false,
onTap: (p0) async {
if (!widget.doneItems.containsKey(e.id)) {
widget.doneItems.addAll({e.id: p0});
} else {
widget.doneItems[e.id] = p0;
}
await databaseProvider.userStore
.storeToDoItem(widget.doneItems, userId: user.id!);
},
)));
}
if (toDoTiles.isNotEmpty) {
tiles.add(Panel(
title: Text('todo'.i18n),
child: Column(
children: toDoTiles,
),
));
}
// self notes
List<Widget> selfNoteTiles = [];
if (selfNoteProvider.notes.isNotEmpty) {
selfNoteTiles.addAll(selfNoteProvider.notes.map(
(e) => SelfNoteTile(
title: e.title ?? e.content.split(' ')[0],
content: e.content,
onTap: () => Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) => NoteViewScreen(note: e))),
),
));
}
if (selfNoteTiles.isNotEmpty) {
// padding
tiles.add(const SizedBox(
height: 28.0,
));
// actual thing
tiles.add(Panel(
title: Text('your_notes'.i18n),
padding: EdgeInsets.zero,
isTransparent: true,
child: Wrap(
spacing: 18.0,
runSpacing: 18.0,
children: selfNoteTiles,
),
));
}
// insert empty tile
if (tiles.isEmpty) {
tiles.insert(
0,
Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Empty(subtitle: "empty".i18n),
),
);
}
tiles.add(Provider.of<PremiumProvider>(context, listen: false).hasPremium
? const SizedBox()
: const Padding(
padding: EdgeInsets.only(top: 24.0),
child: PremiumInline(features: [
PremiumInlineFeature.stats,
]),
));
// padding
tiles.add(const SizedBox(height: 32.0));
noteTiles = List.castFrom(tiles);
}
generateTiles();
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"notes".i18n,
style: TextStyle(
color: AppColors.of(context).text,
fontSize: 26.0,
fontWeight: FontWeight.bold,
),
),
actions: [
ClipRRect(
borderRadius: BorderRadius.circular(10.1),
child: GestureDetector(
onTap: () {
// handle tap
SoonAlert.show(context: context);
},
child: Container(
color: Theme.of(context).colorScheme.primary.withOpacity(0.4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Stack(
children: [
IconTheme(
data: IconThemeData(
color: Theme.of(context).colorScheme.secondary,
),
child: const Icon(
FeatherIcons.search,
size: 20.0,
),
),
IconTheme(
data: IconThemeData(
color:
Theme.of(context).brightness == Brightness.light
? Colors.black.withOpacity(.5)
: Colors.white.withOpacity(.3),
),
child: const Icon(
FeatherIcons.search,
size: 20.0,
),
),
],
),
),
),
),
),
const SizedBox(
width: 10,
),
ClipRRect(
borderRadius: BorderRadius.circular(10.1),
child: GestureDetector(
onTap: () {
// handle tap
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) => const AddNoteScreen()));
},
child: Container(
color: Theme.of(context).colorScheme.primary.withOpacity(0.4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Stack(
children: [
IconTheme(
data: IconThemeData(
color: Theme.of(context).colorScheme.secondary,
),
child: const Icon(
FeatherIcons.plus,
size: 20.0,
),
),
IconTheme(
data: IconThemeData(
color:
Theme.of(context).brightness == Brightness.light
? Colors.black.withOpacity(.5)
: Colors.white.withOpacity(.3),
),
child: const Icon(
FeatherIcons.plus,
size: 20.0,
),
),
],
),
),
),
),
),
const SizedBox(
width: 20,
),
],
),
body: SafeArea(
child: RefreshIndicator(
onRefresh: () {
Provider.of<HomeworkProvider>(context, listen: false)
.fetch(from: DateTime.now().subtract(const Duration(days: 30)));
Provider.of<SelfNoteProvider>(context, listen: false).restore();
return Future(() => null);
},
child: ListView.builder(
padding: EdgeInsets.zero,
physics: const BouncingScrollPhysics(),
itemCount: max(noteTiles.length, 1),
itemBuilder: (context, index) {
if (noteTiles.isNotEmpty) {
EdgeInsetsGeometry panelPadding =
const EdgeInsets.symmetric(horizontal: 24.0);
return Padding(padding: panelPadding, child: noteTiles[index]);
} else {
return Container();
}
},
),
),
),
);
}
}

View File

@@ -0,0 +1,45 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"notes": "Notes",
"empty": "You don't have any notes",
"todo": "Tasks",
"homework": "Homework",
"new_note": "New Note",
"edit_note": "Edit Note",
"hint": "Note content...",
"hint_t": "Note title...",
"your_notes": "Your Notes",
},
"hu_hu": {
"notes": "Füzet",
"empty": "Nincsenek jegyzeteid",
"todo": "Feladatok",
"homework": "Házi feladat",
"new_note": "Új jegyzet",
"edit_note": "Jegyzet szerkesztése",
"hint": "Jegyzet tartalma...",
"hint_t": "Jegyzet címe...",
"your_notes": "Jegyzeteid",
},
"de_de": {
"notes": "Broschüre",
"empty": "Sie haben keine Notizen",
"todo": "Aufgaben",
"homework": "Hausaufgaben",
"new_note": "Neue Notiz",
"edit_note": "Notiz bearbeiten",
"hint": "Inhalt beachten...",
"hint_t": "Titel notieren...",
"your_notes": "Deine Noten",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,64 @@
import 'package:refilc/models/settings.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SelfNoteTile extends StatelessWidget {
const SelfNoteTile(
{super.key, required this.title, required this.content, this.onTap});
final String title;
final String content;
final Function()? onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
height: 172.0,
width: 172.0,
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
color: Theme.of(context).colorScheme.background,
boxShadow: [
if (Provider.of<SettingsProvider>(context, listen: false)
.shadowEffect)
BoxShadow(
offset: const Offset(0, 21),
blurRadius: 23.0,
color: Theme.of(context).shadowColor,
),
],
),
child: Text(
content.replaceAll('\n', ' '),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
maxLines: 6,
style: const TextStyle(fontSize: 17.0),
),
),
const SizedBox(
height: 5.0,
),
SizedBox(
width: 152.0,
child: Text(
title,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.0,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,47 @@
import 'package:refilc/theme/colors/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
class AccountTile extends StatelessWidget {
const AccountTile(
{super.key,
this.onTap,
this.onTapMenu,
this.profileImage,
this.name,
this.username});
final void Function()? onTap;
final void Function()? onTapMenu;
final Widget? profileImage;
final Widget? name;
final Widget? username;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
onTap: onTap,
onLongPress: onTapMenu,
leading: profileImage,
title: name,
subtitle: username,
trailing: onTapMenu != null
? Material(
color: Colors.transparent,
child: IconButton(
splashRadius: 24.0,
onPressed: onTapMenu,
icon: Icon(FeatherIcons.moreVertical,
color: AppColors.of(context).text.withOpacity(0.8)),
),
)
: null,
),
);
}
}

View File

@@ -0,0 +1,63 @@
// ignore_for_file: no_leading_underscores_for_local_identifiers
import 'package:refilc/models/user.dart';
import 'package:refilc_mobile_ui/common/bottom_card.dart';
import 'package:refilc_mobile_ui/common/detail.dart';
import 'package:refilc_mobile_ui/common/profile_image/profile_image.dart';
import 'package:refilc_mobile_ui/screens/settings/accounts/account_tile.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'account_view.i18n.dart';
class AccountView extends StatelessWidget {
const AccountView(this.user, {super.key});
final User user;
static void show(User user, {required BuildContext context}) =>
showBottomCard(context: context, child: AccountView(user));
@override
Widget build(BuildContext context) {
List<String> _nameParts = user.name.split(" ");
String _firstName = _nameParts.length > 1 ? _nameParts[1] : _nameParts[0];
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountTile(
profileImage: ProfileImage(
name: _firstName,
backgroundColor: Theme.of(context).colorScheme.primary,
role: user.role,
),
name: SelectableText(
user.name,
style: const TextStyle(fontWeight: FontWeight.w500),
maxLines: 2,
minLines: 1,
),
username: SelectableText(user.username),
),
// User details
Detail(
title: "birthdate".i18n,
description:
DateFormat("yyyy. MM. dd.").format(user.student.birth)),
Detail(title: "school".i18n, description: user.student.school.name),
if (user.student.className != null)
Detail(title: "class".i18n, description: user.student.className!),
if (user.student.address != null)
Detail(title: "address".i18n, description: user.student.address!),
if (user.student.parents.isNotEmpty)
Detail(
title: "parents".plural(user.student.parents.length),
description: user.student.parents.join(", ")),
],
),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"birthdate": "Birth date",
"school": "School",
"class": "Class",
"address": "Home address",
"parents": "Parents".one("Parent"),
},
"hu_hu": {
"birthdate": "Születési dátum",
"school": "Iskola",
"class": "Osztály",
"address": "Lakcím",
"parents": "Szülők".one("Szülő"),
},
"de_de": {
"birthdate": "Geburtsdatum",
"school": "Schule",
"class": "Klasse",
"address": "Wohnanschrift",
"parents": "Eltern",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,84 @@
import 'package:refilc/helpers/subject.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class SubjectIconGallery extends StatelessWidget {
const SubjectIconGallery({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"Subject Icon Gallery",
style: TextStyle(color: AppColors.of(context).text),
),
),
body: ListView(
children: const [
SubjectIconItem("Matematika"),
SubjectIconItem("Magyar Nyelv"),
SubjectIconItem("Nyelvtan"),
SubjectIconItem("Irodalom"),
SubjectIconItem("Történelem"),
SubjectIconItem("Földrajz"),
SubjectIconItem("Rajz"),
SubjectIconItem("Vizuális kultúra"),
SubjectIconItem("Fizika"),
SubjectIconItem("Ének"),
SubjectIconItem("Testnevelés"),
SubjectIconItem("Kémia"),
SubjectIconItem("Biológia"),
SubjectIconItem("Természetismeret"),
SubjectIconItem("Erkölcstan"),
SubjectIconItem("Pénzügy"),
SubjectIconItem("Informatika"),
SubjectIconItem("Digitális kultúra"),
SubjectIconItem("Programozás"),
SubjectIconItem("Hálózat"),
SubjectIconItem("Színház technika"),
SubjectIconItem("Média"),
SubjectIconItem("Elektronika"),
SubjectIconItem("Gépészet"),
SubjectIconItem("Technika"),
SubjectIconItem("Tánc"),
SubjectIconItem("Filozófia"),
SubjectIconItem("Osztályfőnöki"),
SubjectIconItem("Gazdaság"),
SubjectIconItem("Szorgalom"),
SubjectIconItem("Magatartás"),
SubjectIconItem("Angol nyelv"),
SubjectIconItem("Linux"),
SubjectIconItem("Adatbázis"),
SubjectIconItem("Asztali alkalmazások"),
SubjectIconItem("Projekt"),
],
),
);
}
}
class SubjectIconItem extends StatelessWidget {
const SubjectIconItem(this.name, {super.key});
final String name;
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(
SubjectIcon.resolveVariant(subjectName: name, context: context),
color: AppColors.of(context).text,
),
title: Text(
name,
style: TextStyle(
color: AppColors.of(context).text,
fontWeight: FontWeight.w500,
),
),
);
}
}

View File

@@ -0,0 +1,94 @@
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/utils/format.dart';
// import 'package:refilc_kreta_api/models/teacher.dart';
import 'package:refilc_mobile_ui/common/panel/panel_button.dart';
// import 'package:refilc_plus/models/premium_scopes.dart';
// import 'package:refilc_plus/providers/premium_provider.dart';
// import 'package:refilc_plus/ui/mobile/premium/upsell.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_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:refilc_mobile_ui/common/beta_chip.dart';
class MenuDesktopSettings extends StatelessWidget {
const MenuDesktopSettings({super.key, required this.settings});
final SettingsProvider settings;
@override
Widget build(BuildContext context) {
return PanelButton(
padding: const EdgeInsets.only(left: 14.0),
onPressed: () {
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) => const ModifyDesktopSettings()),
);
},
title: Row(
children: [
Text(
"desktop_settings".i18n,
style: TextStyle(color: AppColors.of(context).text),
),
const Spacer(),
const BetaChip(),
const Spacer()
],
),
leading: Icon(FeatherIcons.monitor,
color: Theme.of(context).colorScheme.secondary),
);
}
}
class ModifyDesktopSettings extends StatefulWidget {
const ModifyDesktopSettings({super.key});
@override
State<ModifyDesktopSettings> createState() => _ModifyDesktopSettingsState();
}
class _ModifyDesktopSettingsState extends State<ModifyDesktopSettings> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
late UserProvider user;
late DatabaseProvider dbProvider;
late SettingsProvider settings;
@override
void initState() {
super.initState();
user = Provider.of<UserProvider>(context, listen: false);
dbProvider = Provider.of<DatabaseProvider>(context, listen: false);
}
@override
Widget build(BuildContext context) {
settings = Provider.of<SettingsProvider>(context);
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"desktop_settings".i18n,
style: TextStyle(color: AppColors.of(context).text),
),
),
body: const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [],
),
),
));
}
}

View File

@@ -0,0 +1,72 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
// subject rename
"renamed_subjects": "Renamed Subjects",
"rename_subjects": "Rename Subjects",
"rename_subject": "Rename Subject",
"select_subject": "Select Subject",
"modified_name": "Modified Name",
"modify_subjects": "Modify Subjects",
"cancel": "Cancel",
"done": "Done",
"rename_new_subject": "Rename New Subject",
"italics_toggle": "Italic Font",
// teacher rename
"renamed_teachers": "Renamed Teachers",
"rename_teachers": "Rename Teachers",
"rename_teacher": "Rename Teacher",
"select_teacher": "Select Teacher",
"modify_teachers": "Modify Teachers",
"rename_new_teacher": "Rename New Teacher",
},
"hu_hu": {
// subject rename
"renamed_subjects": "Átnevezett Tantárgyaid",
"rename_subjects": "Tantárgyak átnevezése",
"rename_subject": "Tantárgy átnevezése",
"select_subject": "Válassz tantárgyat",
"modified_name": "Módosított név",
"modify_subjects": "Tantárgyak módosítása",
"cancel": "Mégse",
"done": "Kész",
"rename_new_subject": "Új tantárgy átnevezése",
"italics_toggle": "Dőlt betűs megjelenítés",
// teacher rename
"renamed_teachers": "Átnevezett tanáraid",
"rename_teachers": "Tanárok átnevezése",
"rename_teacher": "Tanár átnevezése",
"select_teacher": "Válassz tanárt",
"modify_teachers": "Tanárok módosítása",
"rename_new_teacher": "Új tanár átnevezése",
},
"de_de": {
// subject rename
"renamed_subjects": "Umbenannte Fächer",
"rename_subjects": "Fächer umbenennen",
"rename_subject": "Fach umbenennen",
"select_subject": "Fach auswählen",
"modified_name": "Geänderter Name",
"modify_subjects": "Fächer ändern",
"cancel": "Abbrechen",
"done": "Erledigt",
"rename_new_subject": "Neues Fach umbenennen",
"italics_toggle": "Kursivschrift umschalten",
// teacher rename
"renamed_teachers": "Renamed Teachers",
"rename_teachers": "Rename Teachers",
"rename_teacher": "Rename Teacher",
"select_teacher": "Select Teacher",
"modify_teachers": "Modify Teachers",
"rename_new_teacher": "Rename New Teacher",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,439 @@
// ignore_for_file: use_build_context_synchronously
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:refilc/api/providers/database_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/helpers/subject.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc/utils/format.dart';
import 'package:refilc_kreta_api/models/subject.dart';
import 'package:refilc_kreta_api/providers/absence_provider.dart';
import 'package:refilc_kreta_api/providers/grade_provider.dart';
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
import 'package:refilc_mobile_ui/common/panel/panel.dart';
import 'package:refilc_mobile_ui/common/panel/panel_button.dart';
// import 'package:refilc_plus/models/premium_scopes.dart';
// import 'package:refilc_plus/providers/premium_provider.dart';
// import 'package:refilc_plus/ui/mobile/premium/upsell.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 'modify_names.i18n.dart';
class MenuRenamedSubjects extends StatelessWidget {
const MenuRenamedSubjects({super.key, required this.settings});
final SettingsProvider settings;
@override
Widget build(BuildContext context) {
return PanelButton(
padding: const EdgeInsets.only(left: 14.0),
onPressed: () {
// if (!Provider.of<PremiumProvider>(context, listen: false)
// .hasScope(PremiumScopes.renameSubjects)) {
// PremiumLockedFeatureUpsell.show(
// context: context, feature: PremiumFeature.subjectrename);
// return;
// }
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(builder: (context) => const ModifySubjectNames()),
);
},
title: Text(
"rename_subjects".i18n,
style: TextStyle(
color: AppColors.of(context)
.text
.withOpacity(settings.renamedSubjectsEnabled ? 1.0 : .5)),
),
leading: settings.renamedSubjectsEnabled
? const Icon(FeatherIcons.penTool)
: Icon(FeatherIcons.penTool,
color: AppColors.of(context).text.withOpacity(.25)),
trailingDivider: true,
trailing: Switch(
onChanged: (v) async {
// if (!Provider.of<PremiumProvider>(context, listen: false)
// .hasScope(PremiumScopes.renameSubjects)) {
// PremiumLockedFeatureUpsell.show(
// context: context, feature: PremiumFeature.subjectrename);
// return;
// }
settings.update(renamedSubjectsEnabled: v);
await Provider.of<GradeProvider>(context, listen: false)
.convertBySettings();
await Provider.of<TimetableProvider>(context, listen: false)
.convertBySettings();
await Provider.of<AbsenceProvider>(context, listen: false)
.convertBySettings();
},
value: settings.renamedSubjectsEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
);
}
}
class ModifySubjectNames extends StatefulWidget {
const ModifySubjectNames({super.key});
@override
State<ModifySubjectNames> createState() => _ModifySubjectNamesState();
}
class _ModifySubjectNamesState extends State<ModifySubjectNames> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final _subjectName = TextEditingController();
String? selectedSubjectId;
late List<GradeSubject> subjects;
late UserProvider user;
late DatabaseProvider dbProvider;
late SettingsProvider settings;
@override
void initState() {
super.initState();
subjects = Provider.of<GradeProvider>(context, listen: false)
.grades
.map((e) => e.subject)
.toSet()
.toList()
..sort((a, b) => a.name.compareTo(b.name));
user = Provider.of<UserProvider>(context, listen: false);
dbProvider = Provider.of<DatabaseProvider>(context, listen: false);
}
Future<Map<String, String>> fetchRenamedSubjects() async {
return await dbProvider.userQuery.renamedSubjects(userId: user.id!);
}
void showRenameDialog() {
showDialog(
context: context,
builder: (context) => StatefulBuilder(builder: (context, setS) {
return AlertDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14.0))),
title: Text("rename_subject".i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButton2(
items: subjects
.map((item) => DropdownMenuItem<String>(
value: item.id,
child: Text(
item.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColors.of(context).text,
),
overflow: TextOverflow.ellipsis,
),
))
.toList(),
onChanged: (String? v) async {
final renamedSubs = await fetchRenamedSubjects();
setS(() {
selectedSubjectId = v;
if (renamedSubs.containsKey(selectedSubjectId)) {
_subjectName.text = renamedSubs[selectedSubjectId]!;
} else {
_subjectName.text = "";
}
});
},
iconSize: 14,
iconEnabledColor: AppColors.of(context).text,
iconDisabledColor: AppColors.of(context).text,
underline: const SizedBox(),
itemHeight: 40,
itemPadding: const EdgeInsets.only(left: 14, right: 14),
buttonWidth: 50,
dropdownWidth: 300,
dropdownPadding: null,
buttonDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
),
dropdownElevation: 8,
scrollbarRadius: const Radius.circular(40),
scrollbarThickness: 6,
scrollbarAlwaysShow: true,
offset: const Offset(-10, -10),
buttonSplashColor: Colors.transparent,
customButton: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 2),
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 8.0),
child: Text(
selectedSubjectId == null
? "select_subject".i18n
: subjects
.firstWhere(
(element) => element.id == selectedSubjectId)
.name,
style: Theme.of(context).textTheme.titleSmall!.copyWith(
fontWeight: FontWeight.w700,
color: AppColors.of(context).text.withOpacity(0.75)),
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: TextAlign.center,
),
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Icon(FeatherIcons.arrowDown, size: 32),
),
TextField(
controller: _subjectName,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
focusedBorder: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
hintText: "modified_name".i18n,
suffixIcon: IconButton(
icon: const Icon(
FeatherIcons.x,
color: Colors.grey,
),
onPressed: () {
setState(() {
_subjectName.text = "";
});
},
),
),
),
],
),
actions: [
TextButton(
child: Text(
"cancel".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
"done".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () async {
if (selectedSubjectId != null) {
final renamedSubs = await fetchRenamedSubjects();
renamedSubs[selectedSubjectId!] = _subjectName.text;
await dbProvider.userStore
.storeRenamedSubjects(renamedSubs, userId: user.id!);
await Provider.of<GradeProvider>(context, listen: false)
.convertBySettings();
await Provider.of<TimetableProvider>(context, listen: false)
.convertBySettings();
await Provider.of<AbsenceProvider>(context, listen: false)
.convertBySettings();
}
Navigator.of(context).pop(true);
setState(() {});
},
),
],
);
}),
).then((val) {
_subjectName.text = "";
selectedSubjectId = null;
});
}
@override
Widget build(BuildContext context) {
settings = Provider.of<SettingsProvider>(context);
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"modify_subjects".i18n,
style: TextStyle(color: AppColors.of(context).text),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Panel(
child: SwitchListTile(
title: Text("italics_toggle".i18n),
onChanged: (value) =>
settings.update(renamedSubjectsItalics: value),
value: settings.renamedSubjectsItalics,
),
),
const SizedBox(
height: 20,
),
InkWell(
onTap: showRenameDialog,
borderRadius: BorderRadius.circular(12.0),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 2),
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.symmetric(
vertical: 18.0, horizontal: 12.0),
child: Center(
child: Text(
"rename_new_subject".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: AppColors.of(context).text.withOpacity(.85),
),
),
),
),
),
const SizedBox(
height: 30,
),
FutureBuilder<Map<String, String>>(
future: fetchRenamedSubjects(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Container();
}
return Panel(
title: Text("renamed_subjects".i18n),
child: Column(
children: snapshot.data!.keys.map(
(key) {
GradeSubject? subject = subjects
.firstWhere((element) => key == element.id);
String renameTo = snapshot.data![key]!;
return RenamedSubjectItem(
subject: subject,
renamedTo: renameTo,
modifyCallback: () {
setState(() {
selectedSubjectId = subject.id;
_subjectName.text = renameTo;
});
showRenameDialog();
},
removeCallback: () {
setState(() {
Map<String, String> subs =
Map.from(snapshot.data!);
subs.remove(key);
dbProvider.userStore.storeRenamedSubjects(
subs,
userId: user.id!);
});
},
);
},
).toList(),
),
);
},
),
],
),
),
));
}
}
class RenamedSubjectItem extends StatelessWidget {
const RenamedSubjectItem({
super.key,
required this.subject,
required this.renamedTo,
required this.modifyCallback,
required this.removeCallback,
});
final GradeSubject subject;
final String renamedTo;
final void Function() modifyCallback;
final void Function() removeCallback;
@override
Widget build(BuildContext context) {
return ListTile(
minLeadingWidth: 32.0,
dense: true,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
visualDensity: VisualDensity.compact,
onTap: () {},
leading: Icon(
SubjectIcon.resolveVariant(subject: subject, context: context),
color: AppColors.of(context).text.withOpacity(.75)),
title: InkWell(
onTap: modifyCallback,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
subject.name.capital(),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: AppColors.of(context).text.withOpacity(.75)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
renamedTo,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
trailing: InkWell(
onTap: removeCallback,
child: Icon(FeatherIcons.trash,
color: AppColors.of(context).red.withOpacity(.75)),
),
);
}
}

View File

@@ -0,0 +1,222 @@
import 'package:refilc/models/settings.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc_mobile_ui/common/beta_chip.dart';
import 'package:refilc_mobile_ui/common/panel/panel.dart';
import 'package:refilc_mobile_ui/common/panel/panel_button.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 'notifications_screen.i18n.dart';
class MenuNotifications extends StatelessWidget {
const MenuNotifications({
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 NotificationsScreen()),
),
title: Row(
children: [
Text(
"notifications_screen".i18n,
),
const SizedBox(width: 5.0),
const BetaChip(
disabled: false,
),
],
),
leading: Icon(
FeatherIcons.messageCircle,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
trailing: Icon(
FeatherIcons.chevronRight,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
borderRadius: borderRadius,
);
}
}
class NotificationsScreen extends StatelessWidget {
const NotificationsScreen({super.key});
@override
Widget build(BuildContext context) {
SettingsProvider settings = Provider.of<SettingsProvider>(context);
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"notifications_screen".i18n,
style: TextStyle(color: AppColors.of(context).text),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Panel(
child: Column(
children: [
Material(
type: MaterialType.transparency,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsGradesEnabled,
onChanged: (v) =>
settings.update(notificationsGradesEnabled: v),
title: Row(
children: [
Icon(
FeatherIcons.bookmark,
color: settings.notificationsGradesEnabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 14.0),
Expanded(
child: Text(
"grades".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsGradesEnabled
? 1.0
: .5,
),
),
),
),
],
),
),
),
Material(
type: MaterialType.transparency,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsAbsencesEnabled,
onChanged: (v) =>
settings.update(notificationsAbsencesEnabled: v),
title: Row(
children: [
Icon(
FeatherIcons.clock,
color: settings.notificationsAbsencesEnabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 14.0),
Expanded(
child: Text(
"absences".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsAbsencesEnabled
? 1.0
: .5,
),
),
),
),
],
),
),
),
Material(
type: MaterialType.transparency,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsMessagesEnabled,
onChanged: (v) =>
settings.update(notificationsMessagesEnabled: v),
title: Row(
children: [
Icon(
FeatherIcons.messageSquare,
color: settings.notificationsMessagesEnabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 14.0),
Expanded(
child: Text(
"messages".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsMessagesEnabled
? 1.0
: .5,
),
),
),
),
],
),
),
),
Material(
type: MaterialType.transparency,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsLessonsEnabled,
onChanged: (v) =>
settings.update(notificationsLessonsEnabled: v),
title: Row(
children: [
Icon(
FeatherIcons.calendar,
color: settings.notificationsLessonsEnabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 14.0),
Expanded(
child: Text(
"lessons".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsLessonsEnabled
? 1.0
: .5,
),
),
),
),
],
),
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,34 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"notifications_screen": "Notifications",
"grades": "Grades",
"absences": "Absences",
"messages": "Messages",
"lessons": "Lessons"
},
"hu_hu": {
"notifications_screen": "Értesítések",
"grades": "Jegyek",
"absences": "Hiányzások",
"messages": "Üzenetek",
"lessons": "Órák"
},
"de_de": {
"notifications_screen": "Mitteilung",
"grades": "Noten",
"absences": "Fehlen",
"messages": "Nachrichten",
"lessons": "Unterricht"
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'settings_screen.i18n.dart';
class PrivacyView extends StatelessWidget {
const PrivacyView({super.key});
static void show(BuildContext context) => showDialog(
context: context,
builder: (context) => const PrivacyView(),
barrierDismissible: true);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 100.0, horizontal: 32.0),
child: Material(
borderRadius: BorderRadius.circular(12.0),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: ListView(
physics: const BouncingScrollPhysics(),
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("privacy".i18n),
),
SelectableLinkify(
text: """
• A reFilc (továbbiakban alkalmazás) egy mobilos, asztali és webes kliensalkalmazás, segítségével az e-Kréta rendszeréből letöltheted és felhasználóbarát módon megjelenítheted az adataidat. Tanulmányi adataid csak közvetlenül az alkalmazás és a Kréta-szerverek között közlekednek, titkosított kapcsolaton keresztül.
• A reFilc fejlesztői és/vagy üzemeltetői, valamint az alkalmazás a tanulmányi és személyes adataidat semmilyen célból és semmilyen körülmények között nem másolják, nem tárolják és harmadik félnek nem továbbítják. Ezeket így az Educational Development Informatikai Zrt. kezeli, az Ő adatkezeléssel kapcsolatos tájékoztatójukat itt találod: https://tudasbazis.ekreta.hu/pages/viewpage.action?pageId=4065038
• Azok törlésével vagy módosítával kapcsolatban keresd az osztályfőnöködet vagy az iskolád rendszergazdáját.
• Az alkalmazás névtelen használati statisztikákat gyűjt, ezek alapján tudjuk meghatározni a felhasználók és a telepítések számát, valamint az eszközük platformját. Ezt a beállításokban kikapcsolhatod. Kérünk, hogy ha csak teheted, hagyd ezt a funkciót bekapcsolva, hogy pontosabb információnk legyen a felhasználóink platform-megoszlásáról.
• Amikor az alkalmazás hibába ütközik, lehetőség van hibajelentés küldésére. Ez személyes- és/vagy tanulmányi adatokat nem tartalmaz, viszont részletes információval szolgál a hibáról, annak okáról és eszközödről. A küldés előtt megjelenő képernyőn a te felelősséged átnézni a továbbításra kerülő adatsort. A hibajelentéseket a reFilc fejlesztői felületén és egy privát Discord szobában tároljuk, ezekhez csak az app fejlesztői férnek hozzá.
• Az alkalmazás (az alábbi platformokon: Android, Linux, Windows) minden egyes indításakor a reFilc API, valamint a Github API segítségével ellenőrzi, hogy elérhető-e új verzió, és kérésre innen letölti és telepíti a frissítést.
• Amennyiben az adataiddal kapcsolatban bármilyen kérdésed van (megtekintés, törlés, módosítás, adathordozás), keress minket a social@refilc.hu e-mail címen, vagy Discord szerverünkön!
• A kliensalkalmazás bármely eszközön és platformon történő használatával tudomásul vetted és elfogadod a jelen adatkezelési tájékoztatót. A reFilc csapata fenntartja a jogot a tájékoztató módosítására és a módosításokról nem köteles értesíteni a felhasználóit!
""",
onOpen: (link) => launch(link.url,
customTabsOption: CustomTabsOption(
toolbarColor: Theme.of(context).scaffoldBackgroundColor,
showPageTitle: true,
)),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,983 @@
// ignore_for_file: prefer_function_declarations_over_variables, library_private_types_in_public_api, use_build_context_synchronously
import 'dart:io';
import 'package:refilc/api/providers/database_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/helpers/quick_actions.dart';
import 'package:refilc/icons/filc_icons.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc/theme/observer.dart';
import 'package:refilc_kreta_api/models/grade.dart';
import 'package:refilc_kreta_api/models/subject.dart';
import 'package:refilc_kreta_api/models/week.dart';
import 'package:refilc_kreta_api/providers/absence_provider.dart';
import 'package:refilc_kreta_api/providers/grade_provider.dart';
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
import 'package:refilc_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu.dart';
import 'package:refilc_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart';
import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart';
import 'package:refilc_mobile_ui/common/filter_bar.dart';
import 'package:refilc_mobile_ui/common/material_action_button.dart';
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
import 'package:refilc_mobile_ui/common/panel/panel_button.dart';
import 'package:refilc_mobile_ui/common/system_chrome.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:provider/provider.dart';
import 'package:refilc_mobile_ui/common/screens.i18n.dart';
import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:refilc/models/icon_pack.dart';
import 'package:refilc/utils/format.dart';
import 'package:refilc_mobile_ui/screens/settings/theme_screen.dart';
import 'package:refilc_plus/models/premium_scopes.dart';
import 'package:refilc_plus/providers/premium_provider.dart';
import 'package:refilc_plus/ui/mobile/premium/upsell.dart';
class SettingsHelper {
static const Map<String, String> langMap = {
"en": "🇬🇧 English",
"hu": "🇭🇺 Magyar",
"de": "🇩🇪 Deutsch"
};
static const List<String> fontList = [
"Montserrat",
"Merienda",
"M PLUS Code Latin",
"Figtree",
"Fira Code",
"Vollkorn",
];
static const Map<Pages, String> pageTitle = {
Pages.home: "home",
Pages.grades: "grades",
Pages.timetable: "timetable",
Pages.messages: "messages",
Pages.absences: "absences",
};
static Map<VibrationStrength, String> vibrationTitle = {
VibrationStrength.off: "voff",
VibrationStrength.light: "vlight",
VibrationStrength.medium: "vmedium",
VibrationStrength.strong: "vstrong",
};
static Map<Pages, String> localizedPageTitles() => pageTitle
.map((key, value) => MapEntry(key, ScreensLocalization(value).i18n));
static Map<VibrationStrength, String> localizedVibrationTitles() =>
vibrationTitle
.map((key, value) => MapEntry(key, SettingsLocalization(value).i18n));
static void language(BuildContext context) {
showBottomSheetMenu(
context,
items: List.generate(langMap.length, (index) {
String lang = langMap.keys.toList()[index];
return BottomSheetMenuItem(
onPressed: () {
Provider.of<SettingsProvider>(context, listen: false)
.update(language: lang);
I18n.of(context).locale = Locale(lang, lang.toUpperCase());
Navigator.of(context).maybePop();
if (Platform.isAndroid || Platform.isIOS) {
setupQuickActions();
}
},
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(langMap.values.toList()[index]),
if (lang == I18n.of(context).locale.languageCode)
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.secondary,
),
],
),
);
}),
);
}
// static void uwuMode(BuildContext context, value) {
// final settings = Provider.of<SettingsProvider>(context, listen: false);
// if (value) {
// I18n.of(context).locale = const Locale('uw', 'UW');
// } else {
// I18n.of(context).locale =
// Locale(settings.language, settings.language.toUpperCase());
// }
// if (Platform.isAndroid || Platform.isIOS) {
// setupQuickActions();
// }
// }
static void fontFamily(BuildContext context) {
SettingsProvider settings =
Provider.of<SettingsProvider>(context, listen: false);
showBottomSheetMenu(
context,
items: List.generate(fontList.length, (index) {
String font = fontList[index];
return BottomSheetMenuItem(
onPressed: () {
settings.update(fontFamily: font == 'Montserrat' ? '' : font);
Provider.of<ThemeModeObserver>(context, listen: false)
.changeTheme(settings.theme, updateNavbarColor: false);
Navigator.of(context).maybePop();
},
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
font,
style: GoogleFonts.getFont(font),
),
if (font == settings.fontFamily ||
font.replaceAll('Montserrat', '') == settings.fontFamily)
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.secondary,
),
],
),
);
}),
);
}
static void iconPack(BuildContext context) {
final settings = Provider.of<SettingsProvider>(context, listen: false);
showBottomSheetMenu(
context,
items: List.generate(IconPack.values.length, (index) {
IconPack current = IconPack.values[index];
return BottomSheetMenuItem(
onPressed: () {
settings.update(iconPack: current);
Navigator.of(context).maybePop();
},
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(current.name.capital()),
if (current == settings.iconPack)
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.secondary,
),
],
),
);
}),
);
}
static void startPage(BuildContext context) {
Map<Pages, IconData> pageIcons = {
Pages.home: FilcIcons.home,
Pages.grades: FeatherIcons.bookmark,
Pages.timetable: FeatherIcons.calendar,
Pages.messages: FeatherIcons.messageSquare,
Pages.absences: FeatherIcons.clock,
};
showBottomSheetMenu(
context,
items: List.generate(Pages.values.length, (index) {
return BottomSheetMenuItem(
onPressed: () {
Provider.of<SettingsProvider>(context, listen: false)
.update(startPage: Pages.values[index]);
Navigator.of(context).maybePop();
},
title: Row(
children: [
Icon(pageIcons[Pages.values[index]],
size: 20.0, color: Theme.of(context).colorScheme.secondary),
const SizedBox(width: 16.0),
Text(localizedPageTitles()[Pages.values[index]] ?? ""),
const Spacer(),
if (Pages.values[index] ==
Provider.of<SettingsProvider>(context, listen: false)
.startPage)
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.secondary,
),
],
),
);
}),
);
}
static void rounding(BuildContext context) {
showRoundedModalBottomSheet(
context,
child: const RoundingSetting(),
);
}
// new v5 roundings
static void newRoundings(BuildContext context, GradeSubject subject) {
showRoundedModalBottomSheet(
context,
child: RoundingSetting(
rounding: subject.customRounding,
subjectId: subject.id,
),
);
}
// end
static void theme(BuildContext context) {
var settings = Provider.of<SettingsProvider>(context, listen: false);
void Function(ThemeMode) setTheme = (mode) {
settings.update(theme: mode);
Provider.of<ThemeModeObserver>(context, listen: false).changeTheme(mode);
Navigator.of(context).maybePop();
};
showBottomSheetMenu(context, items: [
BottomSheetMenuItem(
onPressed: () => setTheme(ThemeMode.system),
title: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Icon(FeatherIcons.smartphone,
size: 20.0, color: Theme.of(context).colorScheme.secondary),
),
Text(SettingsLocalization("system").i18n),
const Spacer(),
if (settings.theme == ThemeMode.system)
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.secondary,
),
],
),
),
BottomSheetMenuItem(
onPressed: () => setTheme(ThemeMode.light),
title: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Icon(FeatherIcons.sun,
size: 20.0, color: Theme.of(context).colorScheme.secondary),
),
Text(SettingsLocalization("light").i18n),
const Spacer(),
if (settings.theme == ThemeMode.light)
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.secondary,
),
],
),
),
BottomSheetMenuItem(
onPressed: () => setTheme(ThemeMode.dark),
title: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Icon(FeatherIcons.moon,
size: 20.0, color: Theme.of(context).colorScheme.secondary),
),
Text(SettingsLocalization("dark").i18n),
const Spacer(),
if (settings.theme == ThemeMode.dark)
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.secondary,
),
],
),
)
]);
}
static void accentColor(BuildContext context) {
Navigator.of(context, rootNavigator: true).push(
PageRouteBuilder(
pageBuilder: (context, _, __) =>
const PremiumCustomAccentColorSetting(),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
);
}
static void gradeColors(BuildContext context) {
showRoundedModalBottomSheet(
context,
child: const GradeColorsSetting(),
);
}
static void liveActivityColor(BuildContext context) {
showRoundedModalBottomSheet(
context,
child: const LiveActivityColorSetting(),
);
}
static void vibrate(BuildContext context) {
showBottomSheetMenu(
context,
items: List.generate(VibrationStrength.values.length, (index) {
VibrationStrength value = VibrationStrength.values[index];
return BottomSheetMenuItem(
onPressed: () {
Provider.of<SettingsProvider>(context, listen: false)
.update(vibrate: value);
Navigator.of(context).maybePop();
},
title: Row(
children: [
Container(
width: 12.0,
height: 12.0,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.secondary
.withOpacity((index + 1) / (vibrationTitle.length + 1)),
shape: BoxShape.circle,
),
),
const SizedBox(width: 16.0),
Text(localizedVibrationTitles()[value] ?? "?"),
const Spacer(),
if (value ==
Provider.of<SettingsProvider>(context, listen: false).vibrate)
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.secondary,
),
],
),
);
}),
);
}
static void bellDelay(BuildContext context) {
showRoundedModalBottomSheet(
context,
child: const BellDelaySetting(),
);
}
// v5 user changer
static void changeCurrentUser(BuildContext context, List<Widget> accountTiles,
int len, String addUsrLocTxt) {
showBottomSheetMenu(
context,
items: List.generate(len, (index) {
if (index == accountTiles.length) {
return const SizedBox(
height: 10.0,
);
// return Center(
// child: Container(
// margin: const EdgeInsets.only(top: 12.0, bottom: 4.0),
// height: 3.0,
// width: 175.0,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(12.0),
// color: AppColors.of(context).text.withOpacity(.25),
// ),
// ),
// );
} else if (index == accountTiles.length + 1) {
return PanelButton(
onPressed: () {
if (!Provider.of<PremiumProvider>(context, listen: false)
.hasScope(PremiumScopes.maxTwoAccounts)) {
PremiumLockedFeatureUpsell.show(
context: context, feature: PremiumFeature.moreAccounts);
return;
}
Navigator.of(context).pushNamed("login_back").then((value) {
setSystemChrome(context);
});
},
title: Text(addUsrLocTxt),
leading: const Padding(
padding: EdgeInsets.only(left: 8.22, right: 6.9),
child: Icon(FeatherIcons.userPlus),
),
);
} else {
return accountTiles[index];
}
}),
);
}
// v5 grade rarity texts
static void surpriseGradeRarityText(
BuildContext context, {
required String title,
required String cancel,
required String done,
required List<String> rarities,
}) {
showRoundedModalBottomSheet(
context,
child: GradeRarityTextSetting(
title: title,
cancel: cancel,
done: done,
defaultRarities: rarities,
),
);
}
}
// Rounding modal
class RoundingSetting extends StatefulWidget {
const RoundingSetting({super.key, this.rounding, this.subjectId});
final double? rounding;
final String? subjectId;
@override
_RoundingSettingState createState() => _RoundingSettingState();
}
class _RoundingSettingState extends State<RoundingSetting> {
late double rounding;
@override
void initState() {
super.initState();
rounding = (widget.rounding ??
Provider.of<SettingsProvider>(context, listen: false).rounding) /
10;
}
@override
Widget build(BuildContext context) {
UserProvider userProvider =
Provider.of<UserProvider>(context, listen: false);
DatabaseProvider databaseProvider =
Provider.of<DatabaseProvider>(context, listen: false);
int roundingResult;
if (4.5 >= 4.5.floor() + rounding) {
roundingResult = 5;
} else {
roundingResult = 4;
}
return Column(children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Slider(
value: rounding,
min: 0.1,
max: 0.9,
divisions: 8,
label: rounding.toStringAsFixed(1),
activeColor: Theme.of(context).colorScheme.secondary,
thumbColor: Theme.of(context).colorScheme.secondary,
onChanged: (v) => setState(() => rounding = v),
),
),
Container(
width: 50.0,
padding: const EdgeInsets.only(right: 16.0),
child: Center(
child: Text(rounding.toStringAsFixed(1),
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18.0,
)),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("4.5",
style: TextStyle(fontSize: 26.0, fontWeight: FontWeight.w500)),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Icon(FeatherIcons.arrowRight, color: Colors.grey),
),
GradeValueWidget(GradeValue(roundingResult, "", "", 100),
fill: true, size: 32.0),
],
),
Padding(
padding: const EdgeInsets.only(bottom: 12.0, top: 6.0),
child: MaterialActionButton(
child: Text(SettingsLocalization("done").i18n),
onPressed: () async {
if (widget.rounding == null) {
Provider.of<SettingsProvider>(context, listen: false)
.update(rounding: (rounding * 10).toInt());
} else {
Map<String, String> roundings = await databaseProvider.userQuery
.getRoundings(userId: userProvider.id!);
roundings[widget.subjectId!] = (rounding * 10).toStringAsFixed(2);
await databaseProvider.userStore
.storeRoundings(roundings, userId: userProvider.id!);
await Provider.of<GradeProvider>(context, listen: false)
.convertBySettings();
await Provider.of<TimetableProvider>(context, listen: false)
.convertBySettings();
await Provider.of<AbsenceProvider>(context, listen: false)
.convertBySettings();
}
// ik i'm like a kreta dev, but setstate isn't working, so please don't kill me bye :3
// actually it also looks good and it's kinda useful
Navigator.of(context).pop();
Navigator.of(context).pop();
// setState(() {});
},
),
),
]);
}
}
// Bell Delay Modal
class BellDelaySetting extends StatefulWidget {
const BellDelaySetting({super.key});
@override
State<BellDelaySetting> createState() => _BellDelaySettingState();
}
class _BellDelaySettingState extends State<BellDelaySetting>
with SingleTickerProviderStateMixin {
late TabController _tabController;
late Duration currentDelay;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 2,
vsync: this,
initialIndex:
Provider.of<SettingsProvider>(context, listen: false).bellDelay > 0
? 1
: 0);
currentDelay = Duration(
seconds:
Provider.of<SettingsProvider>(context, listen: false).bellDelay);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
FilterBar(
scrollable: true,
tabAlignment: TabAlignment.center,
items: [
Tab(text: SettingsLocalization("delay").i18n),
Tab(text: SettingsLocalization("hurry").i18n),
],
controller: _tabController,
onTap: (i) async {
// swap current page with target page
setState(() {
currentDelay = i == 0 ? -currentDelay.abs() : currentDelay.abs();
});
},
),
SizedBox(
height: 200,
child: CupertinoTheme(
data: CupertinoThemeData(
brightness: Theme.of(context).brightness,
),
child: CupertinoTimerPicker(
key: UniqueKey(),
mode: CupertinoTimerPickerMode.ms,
initialTimerDuration: currentDelay.abs(),
onTimerDurationChanged: (Duration d) {
HapticFeedback.selectionClick();
currentDelay = _tabController.index == 0 ? -d : d;
},
),
),
),
Text(SettingsLocalization("sync_help").i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w500,
color: AppColors.of(context).text.withOpacity(.75))),
Padding(
padding: const EdgeInsets.only(bottom: 12.0, top: 6.0),
child: Column(
children: [
MaterialActionButton(
backgroundColor: Theme.of(context).colorScheme.primary,
child: Text(SettingsLocalization("sync").i18n),
onPressed: () {
final lessonProvider =
Provider.of<TimetableProvider>(context, listen: false);
Duration? closest;
DateTime now = DateTime.now();
for (var lesson
in lessonProvider.getWeek(Week.current()) ?? []) {
Duration sdiff = lesson.start.difference(now);
Duration ediff = lesson.end.difference(now);
if (closest == null || sdiff.abs() < closest.abs()) {
closest = sdiff;
}
if (ediff.abs() < closest.abs()) closest = ediff;
}
if (closest != null) {
if (closest.inHours.abs() >= 1) return;
currentDelay = closest;
Provider.of<SettingsProvider>(context, listen: false)
.update(bellDelay: currentDelay.inSeconds);
_tabController.index = currentDelay.inSeconds > 0 ? 1 : 0;
setState(() {});
}
},
),
MaterialActionButton(
child: Text(SettingsLocalization("done").i18n),
onPressed: () {
//Provider.of<SettingsProvider>(context, listen: false).update(context, rounding: (r * 10).toInt());
Provider.of<SettingsProvider>(context, listen: false)
.update(bellDelay: currentDelay.inSeconds);
Navigator.of(context).maybePop();
},
),
],
),
),
],
);
}
}
class GradeColorsSetting extends StatefulWidget {
const GradeColorsSetting({super.key});
@override
_GradeColorsSettingState createState() => _GradeColorsSettingState();
}
class _GradeColorsSettingState extends State<GradeColorsSetting> {
Color currentColor = const Color(0x00000000);
late SettingsProvider settings;
@override
void initState() {
super.initState();
settings = Provider.of<SettingsProvider>(context, listen: false);
}
@override
Widget build(BuildContext context) {
return Column(children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(5, (index) {
return ClipOval(
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () {
currentColor = settings.gradeColors[index];
showRoundedModalBottomSheet(
context,
child: Column(children: [
MaterialColorPicker(
selectedColor: settings.gradeColors[index],
onColorChange: (v) {
setState(() {
currentColor = v;
});
},
allowShades: true,
elevation: 0,
physics: const NeverScrollableScrollPhysics(),
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
MaterialActionButton(
onPressed: () {
List<Color> colors =
List.castFrom(settings.gradeColors);
var defaultColors =
SettingsProvider.defaultSettings()
.gradeColors;
colors[index] = defaultColors[index];
settings.update(gradeColors: colors);
Navigator.of(context).maybePop();
},
child: Text(SettingsLocalization("reset").i18n),
),
MaterialActionButton(
onPressed: () {
List<Color> colors =
List.castFrom(settings.gradeColors);
colors[index] = currentColor.withAlpha(255);
settings.update(
gradeColors: settings.gradeColors);
Navigator.of(context).maybePop();
},
child: Text(SettingsLocalization("done").i18n),
),
],
),
),
]),
).then((value) => setState(() {}));
},
child: GradeValueWidget(GradeValue(index + 1, "", "", 0),
fill: true, size: 36.0),
),
),
);
}),
),
),
]);
}
}
class GradeRarityTextSetting extends StatefulWidget {
const GradeRarityTextSetting({
super.key,
required this.title,
required this.cancel,
required this.done,
required this.defaultRarities,
});
final String title;
final String cancel;
final String done;
final List<String> defaultRarities;
@override
_GradeRarityTextSettingState createState() => _GradeRarityTextSettingState();
}
class _GradeRarityTextSettingState extends State<GradeRarityTextSetting> {
late SettingsProvider settings;
late DatabaseProvider db;
late UserProvider user;
final _rarityText = TextEditingController();
@override
void initState() {
super.initState();
settings = Provider.of<SettingsProvider>(context, listen: false);
db = Provider.of<DatabaseProvider>(context, listen: false);
user = Provider.of<UserProvider>(context, listen: false);
}
@override
Widget build(BuildContext context) {
return Column(children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(5, (index) {
return ClipOval(
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () async {
showRenameDialog(
title: widget.title,
cancel: widget.cancel,
done: widget.done,
rarities:
await db.userQuery.getGradeRarities(userId: user.id!),
gradeIndex: (index + 1).toString(),
defaultRarities: widget.defaultRarities,
);
},
child: GradeValueWidget(GradeValue(index + 1, "", "", 0),
fill: true, size: 36.0),
),
),
);
}),
),
),
]);
}
void showRenameDialog(
{required String title,
required String cancel,
required String done,
required Map<String, String> rarities,
required String gradeIndex,
required List<String> defaultRarities,
required}) {
showDialog(
context: context,
builder: (context) => StatefulBuilder(builder: (context, setS) {
String? rr = rarities[gradeIndex];
rr ??= '';
_rarityText.text = rr;
return AlertDialog(
title: Text(title),
content: TextField(
controller: _rarityText,
autofocus: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
label: Text(defaultRarities[int.parse(gradeIndex) - 1]),
suffixIcon: IconButton(
icon: const Icon(FeatherIcons.x),
onPressed: () {
setState(() {
_rarityText.clear();
});
},
),
),
),
actions: [
TextButton(
child: Text(
cancel,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
done,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
rarities[gradeIndex] = _rarityText.text;
Provider.of<DatabaseProvider>(context, listen: false)
.userStore
.storeGradeRarities(rarities, userId: user.id!);
Navigator.of(context).pop(true);
},
),
],
);
}),
).then((val) {
_rarityText.clear();
});
}
}
class LiveActivityColorSetting extends StatefulWidget {
const LiveActivityColorSetting({super.key});
@override
_LiveActivityColorSettingState createState() =>
_LiveActivityColorSettingState();
}
class _LiveActivityColorSettingState extends State<LiveActivityColorSetting> {
late SettingsProvider settings;
Color currentColor = const Color(0x00000000);
@override
void initState() {
super.initState();
settings = Provider.of<SettingsProvider>(context, listen: false);
}
@override
Widget build(BuildContext context) {
return Column(children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Material(
type: MaterialType.transparency,
child: Column(children: [
MaterialColorPicker(
allowShades: false,
selectedColor: settings.liveActivityColor,
onMainColorChange: (k) {
setState(() {
currentColor = k as Color;
settings.update(
liveActivityColor: currentColor.withAlpha(255));
Navigator.of(context).maybePop();
});
},
elevation: 0,
physics: const NeverScrollableScrollPhysics(),
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0, top: 40.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
MaterialActionButton(
onPressed: () {
var defaultColors =
SettingsProvider.defaultSettings().liveActivityColor;
settings.update(liveActivityColor: defaultColors);
Navigator.of(context).maybePop();
},
child: Text(SettingsLocalization("reset").i18n),
),
],
),
),
]),
),
),
]);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
Route settingsRoute(Widget widget) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => widget,
transitionDuration: const Duration(milliseconds: 500),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var curve = Curves.ease;
var curveTween = CurveTween(curve: curve);
var begin = const Offset(0.0, 1.0);
var end = Offset.zero;
var tween = Tween(begin: begin, end: end).chain(curveTween);
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,303 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"personal_details": "Personal Details",
"open_dkt": "Open DCS",
"edit_nickname": "Edit Nickname",
"edit_profile_picture": "Edit Profile Picture",
"remove_profile_picture": "Remove Profile Picture",
"select_profile_picture": "to select a picture",
"click_here": "Click here",
"light": "Light",
"dark": "Dark",
"system": "System",
"add_user": "Add User",
"log_out": "Log Out",
"update_available": "Update Available",
"general": "General",
"language": "Language",
"startpage": "Startpage",
"rounding": "Rounding",
"appearance": "Appearance",
"theme": "Theme",
"color": "Color",
"grade_colors": "Grade Colors",
"live_activity_color": "Live Activity Color",
"notifications": "Notifications",
"popups": "Pop-up Alerts",
"news": "News",
"extras": "Extras",
"about": "About",
"supporters": "Supporters",
"privacy": "Privacy Policy",
"licenses": "Licenses",
"vibrate": "Vibration",
"voff": "Off",
"vlight": "Light",
"vmedium": "Medium",
"vstrong": "Strong",
"cancel": "Cancel",
"done": "Done",
"reset": "Reset",
"open": "Open",
"data_collected":
"Data collected: Platform (eg. iOS), App version (eg. 5.0.0), Unique Install Identifier",
"Analytics": "Analytics",
"Anonymous Usage Analytics": "Anonymous Usage Analytics",
"graph_class_avg": "Class average on graph",
"goodstudent": "Good student mode",
"attention": "Attention!",
"goodstudent_disclaimer":
"reFilc can not be held liable for the usage of this feature.\n\n(if your mother beats you up because you showed her fake grades, you can only blame yourself for it)",
"understand": "I understand",
"secret": "Secret Settings",
"bell_delay": "Bell Delay",
"delay": "Delay",
"hurry": "Hurry",
"sync": "Synchronize",
"sync_help": "Press the Synchronize button when the bell rings.",
"surprise_grades": "Surprise Grades",
"icon_pack": "Icon Pack",
"change_username": "Set a nickname",
"Accent Color": "Accent Color",
"Background Color": "Background Color",
"Highlight Color": "Highlight Color",
"Adaptive Theme": "Adaptive Theme",
"presentation": "Presentation mode",
"uwufymode": "UwU-fied mode (hungarian)",
"devmoretaps": "You are %s taps away from Developer Mode.",
"devactivated": "Developer Mode successfully activated.",
"devsettings": "Developer Settings",
"devmode": "Developer Mode",
"copy_jwt": "Copy JWT",
"welcome_msg": "Welcome Message",
"default": "Dynamic",
"edit_welcome_msg": "Edit welcome message",
"shadow_effect": "Shadow Effect",
"app_icon": "App Icon",
"settings": "Settings",
"personalization": "Personalization",
"edit": "Edit",
"switch_account": "Switch Account",
"subjects": "Subjects",
"select_subject": "Select Subject",
"own_paints": "Own Paints",
"dl_paint": "Redeemed",
"public_paint": "Public Paints",
"no_pub_paint": "No Public Paints",
"enter_id": "Enter Paint ID",
"paint_id": "Paint ID...",
"set_as_current": "Set as current",
"share_subj_theme": "Share Theme",
"no_name": "Untitled Paint",
"current_paint": "Current Paint",
"rename_subjects": "Rename Subjects",
"rename_teachers": "Rename Teachers",
"show_breaks": "Show Breaks",
"fonts": "Fonts",
"font_family": "Font Family",
},
"hu_hu": {
"personal_details": "Személyes információk",
"open_dkt": "DKT megnyitása",
"edit_nickname": "Becenév szerkesztése",
"edit_profile_picture": "Profil-kép szerkesztése",
"remove_profile_picture": "Profil-kép törlése",
"select_profile_picture": "a kép kiválasztásához",
"click_here": "Kattints ide",
"light": "Világos",
"dark": "Sötét",
"system": "Rendszer",
"add_user": "Felhasználó hozzáadása",
"log_out": "Kijelentkezés",
"update_available": "Frissítés elérhető",
"general": "Általános",
"language": "Nyelv",
"startpage": "Kezdőlap",
"rounding": "Kerekítés",
"appearance": "Kinézet",
"theme": "Téma",
"color": "Színek",
"grade_colors": "Jegyek színei",
"live_activity_color": "Live Activity színe",
"notifications": "Értesítések",
"popups": "Pop-up értesítések",
"news": "Hírek",
"extras": "Extrák",
"about": "Névjegy",
"supporters": "Támogatók",
"privacy": "Adatvédelmi irányelvek",
"licenses": "Licencek",
"vibrate": "Rezgés",
"voff": "Kikapcsolás",
"vlight": "Alacsony",
"vmedium": "Közepes",
"vstrong": "Erős",
"cancel": "Mégsem",
"done": "Kész",
"reset": "Visszaállítás",
"open": "Megnyitás",
"data_collected":
"Gyűjtött adat: platform (pl.: iOS), app verzió (pl.: 5.0.0), egyedi telepítési azonosító",
"Analytics": "Analitika",
"Anonymous Usage Analytics": "Névtelen használati analitika",
"graph_class_avg": "Osztályátlag a grafikonon",
"goodstudent": "Jó tanuló mód",
"attention": "Figyelem!",
"goodstudent_disclaimer":
"A reFilc minden felelősséget elhárít a funkció használatával kapcsolatban.\n\n(Értsd: ha az anyád megver, mert megtévesztő ábrákat mutattál neki, azért csakis magadat hibáztathatod.)",
"understand": "Értem",
"secret": "Titkos Beállítások",
"bell_delay": "Csengő eltolódása",
"delay": "Késleltetés",
"hurry": "Siettetés",
"sync": "Szinkronizálás",
"sync_help": "Csengetéskor nyomd meg a Szinkronizálás gombot.",
"surprise_grades": "Meglepetés jegyek",
"icon_pack": "Ikon séma",
"change_username": "Becenév beállítása",
"Accent Color": "Egyedi szín",
"Background Color": "Háttér színe",
"Highlight Color": "Panelek színe",
"Adaptive Theme": "Adaptív téma",
"presentation": "Bemutató mód",
"uwufymode": "UwU mód (magyar)",
"devmoretaps": "Még %s koppintásra vagy a Fejlesztői módtól.",
"devactivated": "Fejlesztői mód sikeresen aktiválva.",
"devsettings": "Fejlesztői Beállítások",
"devmode": "Fejlesztői mód",
"copy_jwt": "JWT másolása",
"welcome_msg": "Üdvözlő üzenet",
"default": "Dinamikus",
"edit_welcome_msg": "Üdvözlő üzenet szerkesztése",
"shadow_effect": "Árnyékhatás",
"app_icon": "Alkalmazásikon",
"settings": "Beállítások",
"personalization": "Személyre szabás",
"edit": "Szerkesztés",
"switch_account": "Fiókváltás",
"subjects": "Tantárgyak",
"select_subject": "Válassz tantárgyat",
"own_paints": "Saját témák",
"dl_paint": "Beszerzett",
"public_paint": "Nyilvános témák",
"no_pub_paint": "Nincsenek nyilvános festékek",
"enter_id": "Azonosító megadása",
"paint_id": "Téma azonosító...",
"set_as_current": "Beállítás jelenleginek",
"share_subj_theme": "Téma Megosztás",
"no_name": "Névtelen téma",
"current_paint": "Jelenlegi téma",
"rename_subjects": "Tantárgyak átnevezése",
"rename_teachers": "Tanárok átnevezése",
"show_breaks": "Szünetek megjelenítése",
"fonts": "Betűk",
"font_family": "Betűtípus",
},
"de_de": {
"personal_details": "Persönliche Angaben",
"open_dkt": "Öffnen RDZ",
"edit_nickname": "Spitznamen bearbeiten",
"edit_profile_picture": "Profilbild bearbeiten",
"remove_profile_picture": "Profilbild entfernen",
"select_profile_picture": "um ein Bild auszuwählen",
"click_here": "Klick hier",
"light": "Licht",
"dark": "Dunkel",
"system": "System",
"add_user": "Benutzer hinzufügen",
"log_out": "Abmelden",
"update_available": "Update verfügbar",
"general": "Allgemein",
"language": "Sprache",
"startpage": "Startseite",
"rounding": "Rundung",
"appearance": "Erscheinungsbild",
"theme": "Thema",
"color": "Farbe",
"grade_colors": "Grad Farben",
"live_activity_color": "Live Activity Farben",
"notifications": "Mitteilung",
"popups": "Popup-Nachrichten",
"news": "Nachrichten",
"extras": "Extras",
"about": "Informationen",
"supporters": "Unterstützer",
"privacy": "Datenschutzbestimmungen",
"licenses": "Lizenzen",
"vibrate": "Vibration",
"voff": "Aus",
"vlight": "Leicht",
"vmedium": "Mittel",
"vstrong": "Stark",
"cancel": "Abbrechen",
"done": "Fertig",
"reset": "Zurücksetzen",
"open": "Öffnen",
"data_collected":
"Erhobene Daten: Plattform (z.B. iOS), App version (z.B. 5.0.0), Eindeutige Installationskennung",
"Analytics": "Analytik",
"Anonymous Usage Analytics": "Anonyme Nutzungsanalyse",
"graph_class_avg": "Klassendurchschnitt in der Grafik",
"goodstudent": "Guter Student Modus",
"attention": "Achtung!",
"goodstudent_disclaimer":
"reFilc kann nicht für die Nutzung dieser Funktion haftbar gemacht werden.\n\n(Wenn deine Mutter dich verprügelt, weil du ihr falsche Noten gezeigt hast, kannst du dir nur die Schuld dafür geben)",
"understand": "Ich verstehe",
"secret": "Geheime Einstellungen",
"bell_delay": "Klingelverzögerung",
"delay": "Verzögern",
"hurry": "Eile",
"sync": "Synchronisieren",
"sync_help": "Drücken Sie die Sync-Taste, wenn die Glocke läutet.",
"surprise_grades": "Überraschungsnoten",
"icon_pack": "Icon-Pack",
"change_username": "Einen Spitznamen festlegen",
"Accent Color": "Accent Color",
"Background Color": "Background Color",
"Highlight Color": "Highlight Color",
"Adaptive Theme": "Adaptive Theme",
"presentation": "Präsentationsmodus",
"uwufymode": "UwU-Modus (ungarisch)",
"devmoretaps": "Sie sind %s Taps vom Entwicklermodus entfernt.",
"devactivated": "Entwicklermodus erfolgreich aktiviert.",
"devsettings": "Entwickleroptionen",
"devmode": "Entwicklermodus",
"copy_jwt": "JWT kopieren",
"welcome_msg": "Willkommensnachricht",
"default": "Dynamisch",
"edit_welcome_msg": "Begrüßungsnachricht bearbeiten",
"shadow_effect": "Schatteneffekt",
"app_icon": "App-Symbol",
"settings": "Einstellungen",
"personalization": "Personalisierung",
"edit": "Bearbeiten",
"switch_account": "Benutzer wechseln",
"subjects": "Themen",
"select_subject": "Fach auswählen",
"own_paints": "Meine Themen",
"dl_paint": "Eingelöst",
"public_paint": "Öffentliche Themen",
"no_pub_paint": "Keine öffentlichen Anstriche",
"enter_id": "Themen-ID eingeben",
"paint_id": "Themen-ID...",
"set_as_current": "Als aktuell einstellen",
"share_subj_theme": "Thema Teilen",
"no_name": "Anonymes Thema",
"current_paint": "Aktuelles Thema",
"rename_subjects": "Fächer umbenennen",
"rename_teachers": "Lehrer umbenennen",
"show_breaks": "Pausen anzeigen",
"fonts": "Schriftarten",
"font_family": "Schriftfamilie",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,398 @@
// ignore_for_file: use_build_context_synchronously
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/utils/format.dart';
import 'package:refilc_kreta_api/models/subject.dart';
import 'package:refilc_kreta_api/models/teacher.dart';
import 'package:refilc_kreta_api/providers/absence_provider.dart';
import 'package:refilc_kreta_api/providers/grade_provider.dart';
import 'package:refilc_kreta_api/providers/timetable_provider.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/settings/settings_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'edit_subject.i18n.dart';
class EditSubjectScreen extends StatefulWidget {
const EditSubjectScreen({
super.key,
required this.subject,
required this.teacher,
});
final GradeSubject subject;
final Teacher teacher;
@override
EditSubjectScreenState createState() => EditSubjectScreenState();
}
class EditSubjectScreenState extends State<EditSubjectScreen> {
late SettingsProvider settingsProvider;
late DatabaseProvider databaseProvider;
late UserProvider user;
final _subjectName = TextEditingController();
final _teacherName = TextEditingController();
@override
Widget build(BuildContext context) {
settingsProvider = Provider.of<SettingsProvider>(context);
databaseProvider = Provider.of<DatabaseProvider>(context);
user = Provider.of<UserProvider>(context);
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
(widget.subject.isRenamed && settingsProvider.renamedSubjectsEnabled
? widget.subject.renamedTo
: widget.subject.name.capital()) ??
'',
style: TextStyle(
color: AppColors.of(context).text,
fontStyle: settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: FontStyle.normal,
),
),
actions: [
IconButton(
onPressed: () async {
Map<String, String> subs = await databaseProvider.userQuery
.renamedSubjects(userId: user.id!);
subs.remove(widget.subject.id);
await databaseProvider.userStore
.storeRenamedSubjects(subs, userId: user.id!);
Map<String, String> teach = await databaseProvider.userQuery
.renamedTeachers(userId: user.id!);
teach.remove(widget.teacher.id);
await databaseProvider.userStore
.storeRenamedTeachers(teach, userId: user.id!);
updateProviders();
// im crying rn
Navigator.of(context).pop();
Navigator.of(context).pop();
setState(() {});
},
icon: const Icon(FeatherIcons.trash2),
),
const SizedBox(
width: 8.0,
),
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Column(
children: [
// rename subject
SplittedPanel(
padding: const EdgeInsets.only(top: 8.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
showSubjectRenameDialog();
},
title: Text("rename_it".i18n),
leading: Icon(
FeatherIcons.penTool,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
// rename teacher
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
showTeacherRenameDialog();
},
title: Text("rename_te".i18n),
leading: Icon(
FeatherIcons.user,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
// edit rounding
// SplittedPanel(
// padding: const EdgeInsets.only(top: 9.0),
// cardPadding: const EdgeInsets.all(4.0),
// isSeparated: true,
// children: [
// PanelButton(
// onPressed: () {
// SettingsHelper.newRoundings(context, widget.subject);
// setState(() {});
// },
// title: Text(
// "rounding".i18n,
// style: TextStyle(
// color: AppColors.of(context).text.withOpacity(.95),
// ),
// ),
// leading: Icon(
// FeatherIcons.gitCommit,
// size: 22.0,
// color: AppColors.of(context).text.withOpacity(.95),
// ),
// trailing: Text(
// ((widget.subject.customRounding ??
// settingsProvider.rounding) /
// 10)
// .toStringAsFixed(1),
// style: const TextStyle(fontSize: 14.0),
// ),
// borderRadius: const BorderRadius.vertical(
// top: Radius.circular(12.0),
// bottom: Radius.circular(12.0),
// ),
// ),
// ],
// ),
],
),
),
),
);
}
// rename dialogs
void showSubjectRenameDialog() {
_subjectName.text = widget.subject.renamedTo ?? '';
showDialog(
context: context,
builder: (context) => StatefulBuilder(builder: (context, setS) {
return AlertDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14.0))),
title: Text("rename_it".i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius:
const BorderRadius.all(Radius.circular(12.0))),
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Center(
child: Text(
widget.subject.name,
style: const TextStyle(
fontWeight: FontWeight.w500, fontSize: 16.0),
),
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Icon(FeatherIcons.arrowDown, size: 32),
),
TextField(
controller: _subjectName,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
focusedBorder: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
hintText: "modified_name".i18n,
suffixIcon: IconButton(
icon: const Icon(
FeatherIcons.x,
color: Colors.grey,
),
onPressed: () {
setState(() {
_subjectName.text = "";
});
},
),
),
),
],
),
actions: [
TextButton(
child: Text(
"cancel".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
"done".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () async {
Map<String, String> renamedSubjs = await databaseProvider
.userQuery
.renamedSubjects(userId: user.id!);
renamedSubjs[widget.subject.id] = _subjectName.text;
await databaseProvider.userStore
.storeRenamedSubjects(renamedSubjs, userId: user.id!);
updateProviders();
Navigator.of(context).pop();
setState(() {});
},
),
],
);
}),
).then((val) {
_subjectName.text = "";
});
}
void showTeacherRenameDialog() {
_teacherName.text = widget.teacher.renamedTo ?? '';
showDialog(
context: context,
builder: (context) => StatefulBuilder(builder: (context, setS) {
return AlertDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14.0))),
title: Text("rename_te".i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius:
const BorderRadius.all(Radius.circular(12.0))),
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Center(
child: Text(
widget.teacher.name,
style: const TextStyle(
fontWeight: FontWeight.w500, fontSize: 16.0),
),
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Icon(FeatherIcons.arrowDown, size: 32),
),
TextField(
controller: _teacherName,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
focusedBorder: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
hintText: "modified_name".i18n,
suffixIcon: IconButton(
icon: const Icon(
FeatherIcons.x,
color: Colors.grey,
),
onPressed: () {
setState(() {
_teacherName.text = "";
});
},
),
),
),
],
),
actions: [
TextButton(
child: Text(
"cancel".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
"done".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () async {
Map<String, String> renamedTeach = await databaseProvider
.userQuery
.renamedTeachers(userId: user.id!);
renamedTeach[widget.teacher.id] = _teacherName.text;
await databaseProvider.userStore
.storeRenamedTeachers(renamedTeach, userId: user.id!);
updateProviders();
Navigator.of(context).pop();
setState(() {});
},
),
],
);
}),
).then((val) {
_teacherName.text = "";
});
}
void updateProviders() async {
await Provider.of<GradeProvider>(context, listen: false)
.convertBySettings();
await Provider.of<TimetableProvider>(context, listen: false)
.convertBySettings();
await Provider.of<AbsenceProvider>(context, listen: false)
.convertBySettings();
}
}

View File

@@ -0,0 +1,42 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"rename_it": "Rename Subject",
"rename_te": "Rename Teacher",
"rounding": "Rounding",
"gs_mode": "Good Student Mode",
"rename_subject": "Rename Subject",
"modified_name": "Modified Name",
"cancel": "Cancel",
"done": "Done",
},
"hu_hu": {
"rename_it": "Tantárgy átnevezése",
"rename_te": "Tanár átnevezése",
"rounding": "Kerekítés",
"gs_mode": "Jó tanuló mód",
"rename_subject": "Tantárgy átnevezése",
"modified_name": "Módosított név",
"cancel": "Mégse",
"done": "Kész",
},
"de_de": {
"rename_it": "Betreff umbenennen",
"rename_te": "Lehrer umbenennen",
"rounding": "Rundung",
"gs_mode": "Guter Student Modus",
"rename_subject": "Fach umbenennen",
"modified_name": "Geänderter Name",
"cancel": "Abbrechen",
"done": "Erledigt",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,156 @@
// import 'package:refilc/models/settings.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/settings/settings_helper.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_plus/models/premium_scopes.dart';
import 'package:refilc_plus/providers/premium_provider.dart';
import 'package:refilc_plus/ui/mobile/premium/upsell.dart';
import 'package:refilc_plus/ui/mobile/settings/welcome_message.dart';
// import 'package:provider/provider.dart';
import 'submenu_screen.i18n.dart';
class MenuExtrasSettings extends StatelessWidget {
const MenuExtrasSettings({
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 ExtrasSettingsScreen()),
),
title: Text("extras".i18n),
leading: Icon(
FeatherIcons.edit,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
trailing: Icon(
FeatherIcons.chevronRight,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
borderRadius: borderRadius,
);
}
}
class ExtrasSettingsScreen extends StatefulWidget {
const ExtrasSettingsScreen({super.key});
@override
ExtrasSettingsScreenState createState() => ExtrasSettingsScreenState();
}
class ExtrasSettingsScreenState extends State<ExtrasSettingsScreen> {
late SettingsProvider settingsProvider;
late UserProvider user;
@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(
"extras".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 {
if (!Provider.of<PremiumProvider>(context, listen: false)
.hasScope(PremiumScopes.customGradeRarities)) {
return PremiumLockedFeatureUpsell.show(
context: context,
feature: PremiumFeature.gradeRarities);
}
// settingsProvider.update(
// gradeOpeningFun: !settingsProvider.gradeOpeningFun);
SettingsHelper.surpriseGradeRarityText(
context,
title: 'rarity_title'.i18n,
cancel: 'cancel'.i18n,
done: 'done'.i18n,
rarities: [
"common".i18n,
"uncommon".i18n,
"rare".i18n,
"epic".i18n,
"legendary".i18n,
],
);
setState(() {});
},
trailingDivider: true,
title: Text(
"surprise_grades".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(
settingsProvider.gradeOpeningFun ? .95 : .25),
),
),
leading: Icon(
FeatherIcons.gift,
size: 22.0,
color: AppColors.of(context).text.withOpacity(
settingsProvider.gradeOpeningFun ? .95 : .25),
),
trailing: Switch(
onChanged: (v) async {
settingsProvider.update(gradeOpeningFun: v);
setState(() {});
},
value: settingsProvider.gradeOpeningFun,
activeColor: Theme.of(context).colorScheme.secondary,
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
WelcomeMessagePanelButton(settingsProvider, user),
],
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,371 @@
import 'package:refilc/models/settings.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc/utils/format.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/settings/settings_helper.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_mobile_ui/screens/settings/settings_screen.i18n.dart';
class MenuGeneralSettings extends StatelessWidget {
const MenuGeneralSettings({
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 GeneralSettingsScreen()),
),
title: Text("general".i18n),
leading: Icon(
FeatherIcons.settings,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
trailing: Icon(
FeatherIcons.chevronRight,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
borderRadius: borderRadius,
);
}
}
class GeneralSettingsScreen extends StatefulWidget {
const GeneralSettingsScreen({super.key});
@override
GeneralSettingsScreenState createState() => GeneralSettingsScreenState();
}
class GeneralSettingsScreenState extends State<GeneralSettingsScreen> {
@override
Widget build(BuildContext context) {
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
String startPageTitle =
SettingsHelper.localizedPageTitles()[settingsProvider.startPage] ?? "?";
String languageText =
SettingsHelper.langMap[settingsProvider.language] ?? "?";
String vibrateTitle = {
VibrationStrength.off: "voff".i18n,
VibrationStrength.light: "vlight".i18n,
VibrationStrength.medium: "vmedium".i18n,
VibrationStrength.strong: "vstrong".i18n,
}[settingsProvider.vibrate] ??
"?";
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"general".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: () {
SettingsHelper.bellDelay(context);
setState(() {});
},
title: Text(
"bell_delay".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(
settingsProvider.bellDelayEnabled ? .95 : .25),
),
),
leading: Icon(
settingsProvider.bellDelayEnabled
? FeatherIcons.bell
: FeatherIcons.bellOff,
size: 22.0,
color: AppColors.of(context).text.withOpacity(
settingsProvider.bellDelayEnabled ? .95 : .25),
),
trailingDivider: true,
trailing: Switch(
onChanged: (v) =>
settingsProvider.update(bellDelayEnabled: v),
value: settingsProvider.bellDelayEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
SettingsHelper.rounding(context);
setState(() {});
},
title: Text(
"rounding".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.gitCommit,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Text(
(settingsProvider.rounding / 10).toStringAsFixed(1),
style: const TextStyle(fontSize: 14.0),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
padding: const EdgeInsets.only(left: 14.0, right: 6.0),
onPressed: () {
settingsProvider.update(
graphClassAvg: !settingsProvider.graphClassAvg);
setState(() {});
},
title: Text(
"graph_class_avg".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(
settingsProvider.graphClassAvg ? .95 : .25),
),
),
leading: Icon(
FeatherIcons.barChart,
size: 22.0,
color: AppColors.of(context).text.withOpacity(
settingsProvider.graphClassAvg ? .95 : .25),
),
trailing: Switch(
onChanged: (v) =>
settingsProvider.update(graphClassAvg: v),
value: settingsProvider.graphClassAvg,
activeColor: Theme.of(context).colorScheme.secondary,
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
SettingsHelper.startPage(context);
setState(() {});
},
title: Text(
"startpage".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.play,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Text(
startPageTitle.capital(),
style: const TextStyle(fontSize: 14.0),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
SettingsHelper.language(context);
setState(() {});
},
title: Text(
"language".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.globe,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Text(
languageText,
style: const TextStyle(fontSize: 14.0),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
SettingsHelper.vibrate(context);
setState(() {});
},
title: Text(
"vibrate".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.radio,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Text(
vibrateTitle,
style: const TextStyle(fontSize: 14.0),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
padding: const EdgeInsets.only(left: 14.0, right: 6.0),
onPressed: () {
settingsProvider.update(
showBreaks: !settingsProvider.showBreaks);
setState(() {});
},
title: Text(
"show_breaks".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(
settingsProvider.showBreaks ? .95 : .25),
),
),
leading: Icon(
settingsProvider.showBreaks
? FeatherIcons.eye
: FeatherIcons.eyeOff,
size: 22.0,
color: AppColors.of(context)
.text
.withOpacity(settingsProvider.showBreaks ? .95 : .25),
),
trailing: Switch(
onChanged: (v) => settingsProvider.update(showBreaks: v),
value: settingsProvider.showBreaks,
activeColor: Theme.of(context).colorScheme.secondary,
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
padding: const EdgeInsets.only(left: 14.0, right: 6.0),
onPressed: () {
settingsProvider.update(
newsEnabled: !settingsProvider.newsEnabled);
setState(() {});
},
title: Text(
"news".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(
settingsProvider.newsEnabled ? .95 : .25),
),
),
leading: Icon(
Icons.newspaper_outlined,
size: 22.0,
color: AppColors.of(context).text.withOpacity(
settingsProvider.newsEnabled ? .95 : .25),
),
trailing: Switch(
onChanged: (v) => settingsProvider.update(newsEnabled: v),
value: settingsProvider.newsEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,572 @@
// ignore_for_file: use_build_context_synchronously
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/models/shared_theme.dart';
import 'package:refilc/theme/colors/accent.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc/theme/observer.dart';
import 'package:refilc_kreta_api/providers/share_provider.dart';
import 'package:refilc_mobile_ui/common/custom_snack_bar.dart';
import 'package:refilc_mobile_ui/common/empty.dart';
import 'package:refilc_mobile_ui/common/panel/panel_button.dart';
import 'package:refilc_mobile_ui/common/splitted_panel/splitted_panel.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_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:share_plus/share_plus.dart';
class MenuPaintList extends StatelessWidget {
const MenuPaintList({
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: () async {
List<SharedTheme> publicThemes =
await Provider.of<ShareProvider>(context, listen: false)
.getAllPublicThemes(context);
Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute(
builder: (context) => PaintListScreen(publicThemes: publicThemes)));
},
title: Text(
"own_paints".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.list,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Icon(
FeatherIcons.chevronRight,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
borderRadius: borderRadius,
);
}
}
class PaintListScreen extends StatefulWidget {
const PaintListScreen({super.key, required this.publicThemes});
final List<SharedTheme> publicThemes;
@override
PaintListScreenState createState() => PaintListScreenState();
}
class PaintListScreenState extends State<PaintListScreen>
with SingleTickerProviderStateMixin {
late SettingsProvider settingsProvider;
late UserProvider user;
late ShareProvider shareProvider;
late AnimationController _hideContainersController;
late List<Widget> tiles;
final _paintId = TextEditingController();
SharedTheme? newThemeByID;
@override
void initState() {
super.initState();
shareProvider = Provider.of<ShareProvider>(context, listen: false);
_hideContainersController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 200));
}
void buildPublicPaintTiles() async {
List<Widget> subjectTiles = [];
var added = [];
var i = 0;
for (var t in widget.publicThemes) {
if (added.contains(t.id)) continue;
Widget w = PanelButton(
onPressed: () => {
// TODO: set theme
},
title: Column(
children: [
Text(
t.displayName,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
Text(
t.nickname,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.75),
),
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
margin: const EdgeInsets.only(left: 2.0),
width: 12.0,
height: 12.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: t.backgroundColor,
),
),
Container(
margin: const EdgeInsets.only(left: 2.0),
width: 12.0,
height: 12.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: t.panelsColor,
),
),
Container(
margin: const EdgeInsets.only(left: 2.0),
width: 12.0,
height: 12.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: t.accentColor,
),
),
],
),
borderRadius: BorderRadius.vertical(
top: Radius.circular(i == 0 ? 12.0 : 4.0),
bottom:
Radius.circular(i + 1 == widget.publicThemes.length ? 12.0 : 4.0),
),
);
i += 1;
subjectTiles.add(w);
added.add(t.id);
}
if (widget.publicThemes.isEmpty) {
subjectTiles.add(Empty(
subtitle: 'no_pub_paint'.i18n,
));
}
tiles = subjectTiles;
}
@override
Widget build(BuildContext context) {
settingsProvider = Provider.of<SettingsProvider>(context);
user = Provider.of<UserProvider>(context);
buildPublicPaintTiles();
return AnimatedBuilder(
animation: _hideContainersController,
builder: (context, child) => Opacity(
opacity: 1 - _hideContainersController.value,
child: Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"own_paints".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: [
// enter id
SplittedPanel(
padding: EdgeInsets.zero,
cardPadding: const EdgeInsets.all(3.0),
hasBorder: true,
isTransparent: true,
children: [
PanelButton(
onPressed: () => showEnterIDDialog(),
title: Text(
"enter_id".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.plus,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
const SizedBox(
height: 18.0,
),
// current paint
SplittedPanel(
title: Text('current_paint'.i18n),
padding: EdgeInsets.zero,
cardPadding: const EdgeInsets.all(4.0),
children: [
PanelButton(
onPressed: () async {
if (settingsProvider.currentThemeId != '') {
Share.share(
settingsProvider.currentThemeId,
subject: 'share_subj_theme'.i18n,
);
} else {
SharedGradeColors gradeColors = await shareProvider
.shareCurrentGradeColors(context);
SharedTheme theme =
await shareProvider.shareCurrentTheme(
context,
gradeColors: gradeColors,
);
Share.share(
theme.id,
subject: 'share_subj_theme'.i18n,
);
}
},
longPressInstead: true,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
settingsProvider.currentThemeDisplayName != ''
? settingsProvider.currentThemeDisplayName
: 'no_name'.i18n,
style: TextStyle(
color:
AppColors.of(context).text.withOpacity(.95),
),
),
Text(
settingsProvider.currentThemeCreator != ''
? settingsProvider.currentThemeCreator
: 'Anonymous',
style: TextStyle(
color:
AppColors.of(context).text.withOpacity(.65),
fontSize: 15.0,
fontWeight: FontWeight.w500,
),
),
],
),
trailing: Transform.translate(
offset: const Offset(8.0, 0.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
margin: const EdgeInsets.only(left: 2.0),
width: 14.0,
height: 14.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color:
(settingsProvider.customBackgroundColor ??
SettingsProvider.defaultSettings()
.customBackgroundColor),
boxShadow: [
BoxShadow(
color: AppColors.of(context)
.text
.withOpacity(0.15),
offset: const Offset(1, 2),
blurRadius: 3,
),
],
),
),
Transform.translate(
offset: const Offset(-4.0, 0.0),
child: Container(
margin: const EdgeInsets.only(left: 2.0),
width: 14.0,
height: 14.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: (settingsProvider
.customHighlightColor ??
SettingsProvider.defaultSettings()
.customHighlightColor),
boxShadow: [
BoxShadow(
color: AppColors.of(context)
.text
.withOpacity(0.15),
offset: const Offset(1, 2),
blurRadius: 3,
),
],
),
),
),
Transform.translate(
offset: const Offset(-8.0, 0.0),
child: Container(
margin: const EdgeInsets.only(left: 2.0),
width: 14.0,
height: 14.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: settingsProvider.customAccentColor ??
accentColorMap[
settingsProvider.accentColor],
boxShadow: [
BoxShadow(
color: AppColors.of(context)
.text
.withOpacity(0.15),
offset: const Offset(1, 2),
blurRadius: 3,
),
],
),
),
),
],
),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
bottom: Radius.circular(12),
),
),
],
),
const SizedBox(
height: 18.0,
),
// own paints
SplittedPanel(
title: Text('public_paint'.i18n),
padding: EdgeInsets.zero,
cardPadding: const EdgeInsets.all(4.0),
children: tiles,
),
],
),
),
),
),
),
);
}
// enter id dialog
void showEnterIDDialog() {
_paintId.text = '';
showDialog(
context: context,
builder: (context) => StatefulBuilder(builder: (context, setS) {
return AlertDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14.0))),
title: Text("enter_id".i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _paintId,
onEditingComplete: () async {
// SharedTheme? theme = await shareProvider.getThemeById(
// context,
// id: _paintId.text.replaceAll(' ', ''),
// );
// if (theme != null) {
// // set theme variable
// newThemeByID = theme;
// _paintId.clear();
// } else {
// ScaffoldMessenger.of(context).showSnackBar(
// CustomSnackBar(
// content: Text("theme_not_found".i18n,
// style: const TextStyle(color: Colors.white)),
// backgroundColor: AppColors.of(context).red,
// context: context,
// ),
// );
// }
},
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
focusedBorder: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
hintText: 'paint_id'.i18n,
suffixIcon: IconButton(
icon: const Icon(
FeatherIcons.x,
color: Colors.grey,
),
onPressed: () {
setState(() {
_paintId.text = '';
});
},
),
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Icon(FeatherIcons.arrowDown, size: 32),
),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius:
const BorderRadius.all(Radius.circular(12.0))),
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Center(
child: Text(
'set_as_current'.i18n,
style: const TextStyle(
fontWeight: FontWeight.w500, fontSize: 16.0),
),
),
),
],
),
actions: [
TextButton(
child: Text(
"cancel".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
"done".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () async {
// get sex
SharedTheme? theme = await shareProvider.getThemeById(
context,
id: _paintId.text.replaceAll(' ', ''),
);
if (theme != null) {
// set theme variable
newThemeByID = theme;
_paintId.clear();
} else {
ScaffoldMessenger.of(context).showSnackBar(
CustomSnackBar(
content: Text("theme_not_found".i18n,
style: const TextStyle(color: Colors.white)),
backgroundColor: AppColors.of(context).red,
context: context,
),
);
}
// slay
setPaint();
setState(() {});
Navigator.of(context).pop();
},
),
],
);
}),
).then((val) {
_paintId.clear();
});
}
void setPaint() async {
if (newThemeByID == null) return;
// changing grade colors
List<Color> colors = [
newThemeByID!.gradeColors.oneColor,
newThemeByID!.gradeColors.twoColor,
newThemeByID!.gradeColors.threeColor,
newThemeByID!.gradeColors.fourColor,
newThemeByID!.gradeColors.fiveColor,
];
settingsProvider.update(gradeColors: colors, store: true);
// changing shadow effect
settingsProvider.update(
shadowEffect: newThemeByID!.shadowEffect, store: true);
// changing theme mode
settingsProvider.update(theme: newThemeByID!.themeMode, store: true);
// changing theme
settingsProvider.update(
customBackgroundColor: newThemeByID!.backgroundColor,
customHighlightColor: newThemeByID!.panelsColor,
customAccentColor: newThemeByID!.accentColor,
customIconColor: newThemeByID!.iconColor,
// new things
currentThemeId: newThemeByID!.id,
currentThemeDisplayName: newThemeByID!.displayName,
currentThemeCreator: newThemeByID!.nickname,
// we should store it
store: true,
);
// changing font family
settingsProvider.update(fontFamily: newThemeByID!.fontFamily, store: true);
// seems weird but it works, trust me (idk why)
// await settingsProvider.update(theme: settingsProvider.theme, store: true);
Provider.of<ThemeModeObserver>(context, listen: false)
.changeTheme(settingsProvider.theme, updateNavbarColor: true);
}
}

View File

@@ -0,0 +1,834 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/helpers/subject.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc/utils/format.dart';
import 'package:refilc_kreta_api/models/grade.dart';
import 'package:refilc_kreta_api/providers/absence_provider.dart';
import 'package:refilc_kreta_api/providers/grade_provider.dart';
import 'package:refilc_kreta_api/providers/timetable_provider.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/settings/settings_helper.dart';
import 'package:refilc_mobile_ui/screens/settings/submenu/edit_subject.dart';
import 'package:refilc_mobile_ui/screens/settings/submenu/paint_list.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_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:refilc_plus/models/premium_scopes.dart';
import 'package:refilc_plus/providers/premium_provider.dart';
import 'package:refilc_plus/ui/mobile/premium/upsell.dart';
import 'package:google_fonts/google_fonts.dart';
class MenuPersonalizeSettings extends StatelessWidget {
const MenuPersonalizeSettings({
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 PersonalizeSettingsScreen()),
),
title: Text("personalization".i18n),
leading: Icon(
Icons.palette_outlined,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
trailing: Icon(
FeatherIcons.chevronRight,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
borderRadius: borderRadius,
);
}
}
class PersonalizeSettingsScreen extends StatefulWidget {
const PersonalizeSettingsScreen({super.key});
@override
PersonalizeSettingsScreenState createState() =>
PersonalizeSettingsScreenState();
}
class PersonalizeSettingsScreenState extends State<PersonalizeSettingsScreen>
with SingleTickerProviderStateMixin {
late SettingsProvider settingsProvider;
late UserProvider user;
late AnimationController _hideContainersController;
late List<Grade> editedShit;
late List<Grade> otherShit;
late List<Widget> tiles;
// late List<Widget> fontTiles;
@override
void initState() {
super.initState();
// editedShit = Provider.of<GradeProvider>(context, listen: false)
// .grades
// .where((e) => e.teacher.isRenamed || e.subject.isRenamed)
// // .map((e) => e.subject)
// .toSet()
// .toList()
// ..sort((a, b) => a.subject.name.compareTo(b.subject.name));
List<Grade> other = Provider.of<GradeProvider>(context, listen: false)
.grades
.where((e) => !e.teacher.isRenamed && !e.subject.isRenamed)
.toSet()
.toList()
..sort((a, b) => a.subject.name.compareTo(b.subject.name));
otherShit = [];
var addedOthers = [];
for (var e in other) {
if (addedOthers.contains(e.subject.id)) continue;
addedOthers.add(e.subject.id);
otherShit.add(e);
}
otherShit = otherShit
..sort((a, b) =>
a.subject.name.compareTo(b.subject.name)); // just cuz why not
// editedTeachers = Provider.of<GradeProvider>(context, listen: false)
// .grades
// .where((e) => e.teacher.isRenamed || e.subject.isRenamed)
// .map((e) => e.teacher)
// .toSet()
// .toList();
// // ..sort((a, b) => a.name.compareTo(b.name));
// otherTeachers = Provider.of<GradeProvider>(context, listen: false)
// .grades
// .where((e) => !e.teacher.isRenamed && !e.subject.isRenamed)
// .map((e) => e.teacher)
// .toSet()
// .toList();
// ..sort((a, b) => a.name.compareTo(b.name));
_hideContainersController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 200));
}
void buildSubjectTiles() {
List<Widget> subjectTiles = [];
var added = [];
var i = 0;
List<Grade> need = [];
for (var s in editedShit) {
if (added.contains(s.subject.id)) continue;
need.add(s);
added.add(s.subject.id);
}
for (var s in need) {
Widget widget = PanelButton(
onPressed: () async {
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) => EditSubjectScreen(
subject: s.subject,
teacher: s.teacher, // not sure why, but it works tho
),
),
);
setState(() {});
},
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
(s.subject.isRenamed && settingsProvider.renamedSubjectsEnabled
? s.subject.renamedTo
: s.subject.name.capital()) ??
'',
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
fontStyle: settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: FontStyle.normal,
),
),
Text(
(s.teacher.isRenamed && settingsProvider.renamedTeachersEnabled
? s.teacher.renamedTo
: s.teacher.name.capital()) ??
'',
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.85),
fontWeight: FontWeight.w400,
fontSize: 15.0,
height: 1.2,
),
),
],
),
leading: Icon(
SubjectIcon.resolveVariant(context: context, subject: s.subject),
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Icon(
FeatherIcons.chevronRight,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
borderRadius: BorderRadius.vertical(
top: Radius.circular(i == 0 ? 12.0 : 4.0),
bottom: Radius.circular(i + 1 == need.length ? 12.0 : 4.0),
),
);
i += 1;
subjectTiles.add(widget);
}
tiles = subjectTiles;
}
// void buildFontTiles() {
// List<String> fonts = [
// "Merienda",
// "M PLUS Code Latin",
// "Figtree",
// "Fira Code",
// "Vollkorn",
// ];
// List<Widget> fTiles = [];
// var added = [];
// for (var f in fonts) {
// if (added.contains(f)) continue;
// Widget widget = PanelButton(
// onPressed: () async {
// settingsProvider.update(fontFamily: f);
// setState(() {});
// },
// title: Text(
// f,
// style: GoogleFonts.getFont(
// f,
// color: AppColors.of(context).text.withOpacity(.95),
// fontStyle: settingsProvider.renamedSubjectsItalics
// ? FontStyle.italic
// : FontStyle.normal,
// ),
// ),
// trailing: settingsProvider.fontFamily == f
// ? Icon(
// FeatherIcons.chevronRight,
// size: 22.0,
// color: AppColors.of(context).text.withOpacity(0.95),
// )
// : null,
// borderRadius: BorderRadius.circular(12.0),
// );
// fTiles.add(widget);
// added.add(f);
// }
// fontTiles = fTiles;
// }
@override
Widget build(BuildContext context) {
settingsProvider = Provider.of<SettingsProvider>(context);
user = Provider.of<UserProvider>(context);
// get edited shit
editedShit = Provider.of<GradeProvider>(context, listen: false)
.grades
.where((e) => e.teacher.isRenamed || e.subject.isRenamed)
// .map((e) => e.subject)
.toSet()
.toList()
..sort((a, b) => a.subject.name.compareTo(b.subject.name));
String themeModeText = {
ThemeMode.light: "light".i18n,
ThemeMode.dark: "dark".i18n,
ThemeMode.system: "system".i18n
}[settingsProvider.theme] ??
"?";
// build da tilés
buildSubjectTiles();
// buildFontTiles();
return AnimatedBuilder(
animation: _hideContainersController,
builder: (context, child) => Opacity(
opacity: 1 - _hideContainersController.value,
child: Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"personalization".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: [
// app theme
SplittedPanel(
padding: const EdgeInsets.only(top: 8.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
SettingsHelper.theme(context);
setState(() {});
},
title: Text("theme".i18n),
leading: Icon(
FeatherIcons.sun,
size: 22.0,
color: AppColors.of(context).text.withOpacity(0.95),
),
trailing: Text(
themeModeText,
style: const TextStyle(fontSize: 14.0),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
// color magic shit
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: false,
children: [
PanelButton(
padding: const EdgeInsets.only(left: 14.0, right: 14.0),
onPressed: () async {
await _hideContainersController.forward();
SettingsHelper.accentColor(context);
setState(() {});
_hideContainersController.reset();
},
title: Text(
"color".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.droplet,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Container(
width: 12.0,
height: 12.0,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
shape: BoxShape.circle,
),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(4.0),
),
),
const MenuPaintList(
borderRadius: BorderRadius.vertical(
top: Radius.circular(4.0),
bottom: Radius.circular(12.0),
),
),
],
),
// shadow toggle
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
padding: const EdgeInsets.only(left: 14.0, right: 6.0),
onPressed: () async {
settingsProvider.update(
shadowEffect: !settingsProvider.shadowEffect);
setState(() {});
},
title: Text(
"shadow_effect".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(
settingsProvider.shadowEffect ? .95 : .25),
),
),
leading: Icon(
FeatherIcons.moon,
size: 22.0,
color: AppColors.of(context).text.withOpacity(
settingsProvider.shadowEffect ? .95 : .25),
),
trailing: Switch(
onChanged: (v) async {
settingsProvider.update(shadowEffect: v);
setState(() {});
},
value: settingsProvider.shadowEffect,
activeColor: Theme.of(context).colorScheme.secondary,
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
// change subject icons
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
SettingsHelper.iconPack(context);
},
title: Text(
"icon_pack".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.grid,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Text(
settingsProvider.iconPack.name.capital(),
style: const TextStyle(fontSize: 14.0),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
// grade colors
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
SettingsHelper.gradeColors(context);
setState(() {});
},
title: Text(
"grade_colors".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.star,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
5,
(i) => Container(
margin: const EdgeInsets.only(left: 2.0),
width: 12.0,
height: 12.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: settingsProvider.gradeColors[i],
),
),
),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
// rename things
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: false,
children: [
// rename subjects
PanelButton(
padding: const EdgeInsets.only(left: 14.0, right: 6.0),
onPressed: () async {
settingsProvider.update(
renamedSubjectsEnabled:
!settingsProvider.renamedSubjectsEnabled);
await Provider.of<GradeProvider>(context,
listen: false)
.convertBySettings();
await Provider.of<TimetableProvider>(context,
listen: false)
.convertBySettings();
await Provider.of<AbsenceProvider>(context,
listen: false)
.convertBySettings();
setState(() {});
},
title: Text(
"rename_subjects".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(
settingsProvider.renamedSubjectsEnabled
? .95
: .25),
),
),
leading: Icon(
Icons.school_outlined,
size: 22.0,
color: AppColors.of(context).text.withOpacity(
settingsProvider.renamedSubjectsEnabled
? .95
: .25),
),
trailing: Switch(
onChanged: (v) async {
settingsProvider.update(renamedSubjectsEnabled: v);
await Provider.of<GradeProvider>(context,
listen: false)
.convertBySettings();
await Provider.of<TimetableProvider>(context,
listen: false)
.convertBySettings();
await Provider.of<AbsenceProvider>(context,
listen: false)
.convertBySettings();
setState(() {});
},
value: settingsProvider.renamedSubjectsEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(4.0),
),
),
// rename teachers
PanelButton(
padding: const EdgeInsets.only(left: 14.0, right: 6.0),
onPressed: () async {
settingsProvider.update(
renamedTeachersEnabled:
!settingsProvider.renamedTeachersEnabled);
await Provider.of<GradeProvider>(context,
listen: false)
.convertBySettings();
await Provider.of<TimetableProvider>(context,
listen: false)
.convertBySettings();
await Provider.of<AbsenceProvider>(context,
listen: false)
.convertBySettings();
setState(() {});
},
title: Text(
"rename_teachers".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(
settingsProvider.renamedTeachersEnabled
? .95
: .25),
),
),
leading: Icon(
FeatherIcons.user,
size: 22.0,
color: AppColors.of(context).text.withOpacity(
settingsProvider.renamedTeachersEnabled
? .95
: .25),
),
trailing: Switch(
onChanged: (v) async {
settingsProvider.update(renamedTeachersEnabled: v);
await Provider.of<GradeProvider>(context,
listen: false)
.convertBySettings();
await Provider.of<TimetableProvider>(context,
listen: false)
.convertBySettings();
await Provider.of<AbsenceProvider>(context,
listen: false)
.convertBySettings();
setState(() {});
},
value: settingsProvider.renamedTeachersEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(4.0),
bottom: Radius.circular(12.0),
),
),
],
),
// live activity color
if (Platform.isIOS)
SplittedPanel(
padding: const EdgeInsets.only(top: 9.0),
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
if (!Provider.of<PremiumProvider>(context,
listen: false)
.hasScope(PremiumScopes.liveActivityColor)) {
PremiumLockedFeatureUpsell.show(
context: context,
feature: PremiumFeature.liveActivity,
);
return;
}
SettingsHelper.liveActivityColor(context);
setState(() {});
},
title: Text(
"live_activity_color".i18n,
style: TextStyle(
color:
AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.activity,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Container(
margin: const EdgeInsets.only(left: 2.0),
width: 12.0,
height: 12.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: settingsProvider.liveActivityColor,
),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
// SplittedPanel(
// padding: const EdgeInsets.only(top: 9.0),
// cardPadding: const EdgeInsets.all(4.0),
// isSeparated: true,
// children: [],
// ),
if (settingsProvider.renamedSubjectsEnabled ||
settingsProvider.renamedTeachersEnabled)
Column(
children: [
const SizedBox(
height: 18.0,
),
SplittedPanel(
title: Text('subjects'.i18n),
padding: EdgeInsets.zero,
cardPadding: const EdgeInsets.all(4.0),
children: tiles,
),
const SizedBox(
height: 9.0,
),
SplittedPanel(
padding: EdgeInsets.zero,
cardPadding: const EdgeInsets.all(3.0),
hasBorder: true,
isTransparent: true,
children: [
DropdownButton2(
items: otherShit
.map((item) => DropdownMenuItem<String>(
value: item.subject.id,
child: Text(
item.subject.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColors.of(context).text,
),
overflow: TextOverflow.ellipsis,
),
))
.toList(),
onChanged: (String? v) async {
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) => EditSubjectScreen(
subject: otherShit
.firstWhere((e) => e.subject.id == v)
.subject,
teacher: otherShit
.firstWhere((e) => e.subject.id == v)
.teacher,
),
),
);
setState(() {});
// _subjectName.text = "";
},
iconSize: 14,
iconEnabledColor: AppColors.of(context).text,
iconDisabledColor: AppColors.of(context).text,
underline: const SizedBox(),
itemHeight: 40,
itemPadding:
const EdgeInsets.only(left: 14, right: 14),
buttonWidth: 50,
dropdownWidth: 300,
dropdownPadding: null,
buttonDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
),
dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
),
dropdownElevation: 8,
scrollbarRadius: const Radius.circular(40),
scrollbarThickness: 6,
scrollbarAlwaysShow: true,
offset: const Offset(-10, -10),
buttonSplashColor: Colors.transparent,
customButton: PanelButton(
title: Text(
"select_subject".i18n,
style: TextStyle(
color: AppColors.of(context)
.text
.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.plus,
size: 22.0,
color: AppColors.of(context)
.text
.withOpacity(.95),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
),
],
),
],
),
// custom fonts
const SizedBox(
height: 18.0,
),
SplittedPanel(
title: Text('fonts'.i18n),
padding: EdgeInsets.zero,
cardPadding: const EdgeInsets.all(4.0),
isSeparated: true,
children: [
PanelButton(
onPressed: () {
SettingsHelper.fontFamily(context);
setState(() {});
},
title: Text(
"font_family".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.95),
),
),
leading: Icon(
FeatherIcons.type,
size: 22.0,
color: AppColors.of(context).text.withOpacity(.95),
),
trailing: Text(
settingsProvider.fontFamily != ''
? settingsProvider.fontFamily
: 'Montserrat',
style: GoogleFonts.getFont(
settingsProvider.fontFamily != ''
? settingsProvider.fontFamily
: 'Montserrat',
fontSize: 14.0),
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12.0),
bottom: Radius.circular(12.0),
),
),
],
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"general": "General",
"personalization": "Personalization",
"extras": "Extras",
"surprise_grades": "Surprise Grades",
"cancel": "Cancel",
"done": "Done",
"rarity_title": "Rarity Text",
// default rarities
"common": "Common",
"uncommon": "Uncommon",
"rare": "Rare",
"epic": "Epic",
"legendary": "Legendary",
},
"hu_hu": {
"general": "Általános",
"personalization": "Személyre szabás",
"extras": "Extrák",
"surprise_grades": "Meglepetés jegyek",
"cancel": "Mégse",
"done": "Kész",
"rarity_title": "Ritkaság szövege",
// default rarities
"common": "Gyakori",
"uncommon": "Nem gyakori",
"rare": "Ritka",
"epic": "Epikus",
"legendary": "Legendás",
},
"de_de": {
"general": "Allgemeine",
"personalization": "Personalisierung",
"extras": "Extras",
"surprise_grades": "Überraschende Noten",
"cancel": "Abbrechen",
"done": "Bereit",
"rarity_title": "Text zur Seltenheit",
// default rarities
"common": "Gemeinsam",
"uncommon": "Gelegentlich",
"rare": "Selten",
"epic": "Episch",
"legendary": "Legendär",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,996 @@
// ignore_for_file: use_build_context_synchronously, deprecated_member_use
import 'package:refilc/models/settings.dart';
import 'package:refilc/models/shared_theme.dart';
import 'package:refilc/theme/colors/accent.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc/theme/observer.dart';
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
import 'package:refilc/ui/widgets/message/message_tile.dart';
import 'package:refilc_kreta_api/models/grade.dart';
import 'package:refilc_kreta_api/models/homework.dart';
import 'package:refilc_kreta_api/models/message.dart';
import 'package:refilc_mobile_ui/common/action_button.dart';
import 'package:refilc_mobile_ui/common/filter_bar.dart';
import 'package:refilc_mobile_ui/common/panel/panel.dart';
import 'package:refilc_mobile_ui/common/widgets/grade/new_grades.dart';
import 'package:refilc_mobile_ui/common/widgets/homework/homework_tile.dart';
// import 'package:refilc_plus/models/premium_scopes.dart';
// import 'package:refilc_plus/providers/premium_provider.dart';
import 'package:refilc_kreta_api/providers/share_provider.dart';
import 'package:refilc/ui/flutter_colorpicker/colorpicker.dart';
// import 'package:refilc_plus/ui/mobile/premium/upsell.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'theme_screen.i18n.dart';
import 'package:share_plus/share_plus.dart';
class PremiumCustomAccentColorSetting extends StatefulWidget {
const PremiumCustomAccentColorSetting({super.key});
@override
State<PremiumCustomAccentColorSetting> createState() =>
_PremiumCustomAccentColorSettingState();
}
enum CustomColorMode {
theme,
saved,
accent,
background,
highlight,
icon,
enterId,
}
class _PremiumCustomAccentColorSettingState
extends State<PremiumCustomAccentColorSetting>
with TickerProviderStateMixin {
late final SettingsProvider settings;
late final ShareProvider shareProvider;
bool colorSelection = false;
bool customColorMenu = false;
CustomColorMode colorMode = CustomColorMode.theme;
final customColorInput = TextEditingController();
final unknownColor = Colors.black;
late TabController _testTabController;
late TabController _colorsTabController;
late AnimationController _openAnimController;
late final Animation<double> backgroundAnimation =
Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.2, 1.0, curve: Curves.easeInOut),
),
);
late final Animation<double> fullPageAnimation =
Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.0, 0.6, curve: Curves.easeInOut),
),
);
late final Animation<double> backContainerAnimation =
Tween<double>(begin: 100, end: 0).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.0, 0.9, curve: Curves.easeInOut),
),
);
late final Animation<double> backContentAnimation =
Tween<double>(begin: 100, end: 0).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.2, 1.0, curve: Curves.easeInOut),
),
);
late final Animation<double> backContentScaleAnimation =
Tween<double>(begin: 0.8, end: 0.9).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.45, 1.0, curve: Curves.easeInOut),
),
);
late final Animation<double> pickerContainerAnimation =
Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.25, 0.8, curve: Curves.easeInOut),
),
);
@override
void initState() {
super.initState();
_colorsTabController = TabController(length: 4, vsync: this);
_testTabController = TabController(length: 4, vsync: this);
settings = Provider.of<SettingsProvider>(context, listen: false);
shareProvider = Provider.of<ShareProvider>(context, listen: false);
_openAnimController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 750));
_openAnimController.forward();
}
@override
void dispose() {
_openAnimController.dispose();
super.dispose();
}
void setTheme(ThemeMode mode, bool store) async {
await settings.update(theme: mode, store: store);
Provider.of<ThemeModeObserver>(context, listen: false)
.changeTheme(mode, updateNavbarColor: false);
}
dynamic getCustomColor() {
switch (colorMode) {
case CustomColorMode.theme:
return accentColorMap[settings.accentColor];
case CustomColorMode.saved:
return [
settings.customBackgroundColor,
settings.customHighlightColor,
settings.customAccentColor
];
case CustomColorMode.background:
return settings.customBackgroundColor;
case CustomColorMode.highlight:
return settings.customHighlightColor;
case CustomColorMode.accent:
return settings.customAccentColor;
case CustomColorMode.icon:
return settings.customIconColor;
case CustomColorMode.enterId:
// do nothing here lol
break;
}
}
void updateCustomColor(dynamic v, bool store,
{Color? accent, Color? background, Color? panels, Color? icon}) {
// reset custom theme id
settings.update(
currentThemeId: '',
currentThemeDisplayName: '',
currentThemeCreator: '',
store: store,
);
if (colorMode != CustomColorMode.theme) {
settings.update(accentColor: AccentColor.custom, store: store);
}
switch (colorMode) {
case CustomColorMode.theme:
settings.update(
accentColor: accentColorMap.keys.firstWhere(
(element) => accentColorMap[element] == v,
orElse: () => AccentColor.filc),
store: store);
settings.update(
customBackgroundColor: AppColors.of(context).background,
store: store);
settings.update(
customHighlightColor: AppColors.of(context).highlight,
store: store);
settings.update(customAccentColor: v, store: store);
break;
case CustomColorMode.saved:
settings.update(customBackgroundColor: v[0], store: store);
settings.update(customHighlightColor: v[1], store: store);
settings.update(customAccentColor: v[3], store: store);
break;
case CustomColorMode.background:
settings.update(customBackgroundColor: v, store: store);
break;
case CustomColorMode.highlight:
settings.update(customHighlightColor: v, store: store);
break;
case CustomColorMode.accent:
settings.update(customAccentColor: v, store: store);
break;
case CustomColorMode.icon:
settings.update(customIconColor: v, store: store);
break;
case CustomColorMode.enterId:
settings.update(customBackgroundColor: background, store: store);
settings.update(customHighlightColor: panels, store: store);
settings.update(customAccentColor: accent, store: store);
settings.update(customIconColor: icon, store: store);
break;
}
}
@override
Widget build(BuildContext context) {
// bool hasAccess = Provider.of<PremiumProvider>(context)
// .hasScope(PremiumScopes.customColors);
bool hasAccess = true;
bool isBackgroundDifferent = Theme.of(context).colorScheme.background !=
AppColors.of(context).background;
ThemeMode currentTheme = Theme.of(context).brightness == Brightness.light
? ThemeMode.light
: ThemeMode.dark;
return WillPopScope(
onWillPop: () async {
Provider.of<ThemeModeObserver>(context, listen: false)
.changeTheme(settings.theme, updateNavbarColor: true);
return true;
},
child: AnimatedBuilder(
animation: _openAnimController,
builder: (context, child) {
final backgroundGradientBottomColor = isBackgroundDifferent
? Theme.of(context).colorScheme.background
: HSVColor.fromColor(Theme.of(context).colorScheme.background)
.withValue(currentTheme == ThemeMode.dark
? 0.1 * _openAnimController.value
: 1.0 - (0.1 * _openAnimController.value))
.withAlpha(1.0)
.toColor();
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: backgroundGradientBottomColor,
));
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.0, 0.75],
colors: isBackgroundDifferent
? [
Theme.of(context).colorScheme.background.withOpacity(1 -
((currentTheme == ThemeMode.dark ? 0.65 : 0.25) *
backgroundAnimation.value)),
backgroundGradientBottomColor,
]
: [
backgroundGradientBottomColor,
backgroundGradientBottomColor
],
),
),
child: Opacity(
opacity: fullPageAnimation.value,
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
actions: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: IconButton(
focusColor: Colors.transparent,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
onPressed: () async {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// duration: Duration(milliseconds: 1000),
// content: Text(
// "Hamarosan...",
// ),
// ),
// );
showDialog(
context: context,
builder: (context) => WillPopScope(
onWillPop: () async => false,
child: AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
title: Text("attention".i18n),
content: Text("share_disclaimer".i18n),
actions: [
ActionButton(
label: "understand".i18n,
onTap: () async {
Navigator.of(context).pop();
SharedGradeColors gradeColors =
await shareProvider
.shareCurrentGradeColors(context);
SharedTheme theme =
await shareProvider.shareCurrentTheme(
context,
gradeColors: gradeColors,
);
Share.share(
theme.id,
subject: 'share_subj_theme'.i18n,
);
},
),
],
),
),
);
},
icon: const Icon(
FeatherIcons.share2,
size: 22.0,
),
),
),
],
title: Text(
"theme_prev".i18n,
style: TextStyle(color: AppColors.of(context).text),
),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Stack(
children: [
Opacity(
opacity: 1 - backContainerAnimation.value * (1 / 100),
child: Transform.translate(
offset: Offset(0, backContainerAnimation.value),
child: Container(
height: double.infinity,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
// https://discord.com/channels/1111649116020285532/1153619667848548452
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [
0.35,
0.75
],
colors: [
settings.customBackgroundColor ??
Theme.of(context).colorScheme.background,
isBackgroundDifferent
? HSVColor.fromColor(Theme.of(context)
.colorScheme
.background)
.withSaturation((HSVColor.fromColor(
Theme.of(context)
.colorScheme
.background)
.saturation -
0.15)
.clamp(0.0, 1.0))
.toColor()
: backgroundGradientBottomColor,
]),
),
margin: const EdgeInsets.symmetric(
vertical: 30, horizontal: 20),
),
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Opacity(
opacity: 1 - backContentAnimation.value * (1 / 100),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Transform.translate(
offset:
Offset(0, -24 + backContentAnimation.value),
child: Transform.scale(
scale: backContentScaleAnimation.value,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 32.0,
right: 32.0,
top: 16.0,
bottom: 6.0,
),
child: FilterBar(
items: const [
Tab(text: "All"),
Tab(text: "Grades"),
Tab(text: "Messages"),
Tab(text: "Absences"),
],
controller: _testTabController,
padding: EdgeInsets.zero,
censored: true,
disableFading: true,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 18.0, vertical: 8.0),
child: NewGradesSurprise(
[
Grade.fromJson(
{
"Uid": "0,Ertekeles",
"RogzitesDatuma":
"2022-01-01T23:00:00Z",
"KeszitesDatuma":
"2022-01-01T23:00:00Z",
"LattamozasDatuma": null,
"Tantargy": {
"Uid": "0",
"Nev": "reFilc szakirodalom",
"Kategoria": {
"Uid": "0,_",
"Nev": "_",
"Leiras": "Nem mondom meg"
},
"SortIndex": 2
},
"Tema":
"Kupak csomag vásárlás vizsga",
"Tipus": {
"Uid": "0,_",
"Nev": "_",
"Leiras":
"Évközi jegy/értékelés",
},
"Mod": {
"Uid": "0,_",
"Nev": "_",
"Leiras": "_ feladat",
},
"ErtekFajta": {
"Uid": "1,Osztalyzat",
"Nev": "Osztalyzat",
"Leiras":
"Elégtelen (1) és Jeles (5) között az öt alapértelmezett érték"
},
"ErtekeloTanarNeve": "Premium",
"Jelleg": "Ertekeles",
"SzamErtek": 5,
"SzovegesErtek": "Jeles(5)",
"SulySzazalekErteke": 100,
"SzovegesErtekelesRovidNev":
null,
"OsztalyCsoport": {"Uid": "0"},
"SortIndex": 2
},
),
],
censored: true,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 6.0),
child: Panel(
child: GradeTile(
Grade.fromJson(
{
"Uid": "0,Ertekeles",
"RogzitesDatuma":
"2022-01-01T23:00:00Z",
"KeszitesDatuma":
"2022-01-01T23:00:00Z",
"LattamozasDatuma": null,
"Tantargy": {
"Uid": "0",
"Nev": "reFilc szakosztály",
"Kategoria": {
"Uid": "0,_",
"Nev": "_",
"Leiras": "Nem mondom meg"
},
"SortIndex": 2
},
"Tema":
"Kupak csomag vásárlás vizsga",
"Tipus": {
"Uid": "0,_",
"Nev": "_",
"Leiras":
"Évközi jegy/értékelés",
},
"Mod": {
"Uid": "0,_",
"Nev": "_",
"Leiras": "_ feladat",
},
"ErtekFajta": {
"Uid": "1,Osztalyzat",
"Nev": "Osztalyzat",
"Leiras":
"Elégtelen (1) és Jeles (5) között az öt alapértelmezett érték"
},
"ErtekeloTanarNeve": "Premium",
"Jelleg": "Ertekeles",
"SzamErtek": 5,
"SzovegesErtek": "Jeles(5)",
"SulySzazalekErteke": 100,
"SzovegesErtekelesRovidNev":
null,
"OsztalyCsoport": {"Uid": "0"},
"SortIndex": 2
},
),
padding: EdgeInsets.zero,
censored: true,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 6.0),
child: Panel(
child: HomeworkTile(
Homework.fromJson(
{
"Uid": "0",
"Tantargy": {
"Uid": "0",
"Nev":
"reFilc premium előnyei",
"Kategoria": {
"Uid": "0,_",
"Nev": "_",
"Leiras":
"reFilc premium előnyei",
},
"SortIndex": 0
},
"TantargyNeve":
"reFilc premium előnyei",
"RogzitoTanarNeve":
"Kupak János",
"Szoveg":
"45 perc filctollal való rajzolás",
"FeladasDatuma":
"2022-01-01T23:00:00Z",
"HataridoDatuma":
"2022-01-01T23:00:00Z",
"RogzitesIdopontja":
"2022-01-01T23:00:00Z",
"IsTanarRogzitette": true,
"IsTanuloHaziFeladatEnabled":
false,
"IsMegoldva": false,
"IsBeadhato": false,
"OsztalyCsoport": {"Uid": "0"},
"IsCsatolasEngedelyezes": false
},
),
padding: EdgeInsets.zero,
censored: true,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 6.0),
child: Panel(
child: MessageTile(
Message.fromJson(
{
"azonosito": 0,
"isElolvasva": true,
"isToroltElem": false,
"tipus": {
"azonosito": 1,
"kod": "BEERKEZETT",
"rovidNev":
"Beérkezett üzenet",
"nev": "Beérkezett üzenet",
"leiras": "Beérkezett üzenet"
},
"uzenet": {
"azonosito": 0,
"kuldesDatum":
"2022-01-01T23:00:00",
"feladoNev": "reFilc",
"feladoTitulus":
"Nagyon magas szintű személy",
"szoveg":
"<p>Kedves Felhasználó!</p><p><br></p><p>A prémium vásárlásakor kapott filctollal 90%-al több esély van jó jegyek szerzésére.</p>",
"targy":
"Filctoll használati útmutató",
"statusz": {
"azonosito": 2,
"kod": "KIKULDVE",
"rovidNev": "Kiküldve",
"nev": "Kiküldve",
"leiras": "Kiküldve"
},
"cimzettLista": [
{
"azonosito": 0,
"kretaAzonosito": 0,
"nev": "Tinta Józsi",
"tipus": {
"azonosito": 0,
"kod": "TANULO",
"rovidNev": "Tanuló",
"nev": "Tanuló",
"leiras": "Tanuló"
}
},
],
"csatolmanyok": [
{
"azonosito": 0,
"fajlNev": "Filctoll.doc"
}
]
}
},
),
censored: true,
padding: const EdgeInsets.only(
right: 6.0,
),
),
),
),
],
),
),
),
),
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Wrap(
children: [
Opacity(
opacity: pickerContainerAnimation.value,
child: SizedBox(
width: double.infinity,
child: Container(
padding: const EdgeInsets.only(bottom: 12.0),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: backgroundGradientBottomColor,
offset: const Offset(0, -8),
blurRadius: 16,
spreadRadius: 18,
),
],
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [
0.0,
0.175
],
colors: [
backgroundGradientBottomColor,
backgroundGradientBottomColor,
]),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0),
child: FilterBar(
items: [
ColorTab(
color: accentColorMap[
settings.accentColor] ??
unknownColor,
tab: Tab(
text: "colorpicker_presets"
.i18n)),
// ColorTab(
// color: unknownColor,
// tab: Tab(text: "enter_id".i18n)),
/*ColorTab(
color:
settings.customAccentColor ??
unknownColor,
tab: Tab(
text: "colorpicker_saved"
.i18n)),*/
ColorTab(
unlocked: hasAccess,
color: settings
.customBackgroundColor ??
unknownColor,
tab: Tab(
text: "colorpicker_background"
.i18n)),
ColorTab(
unlocked: hasAccess,
color: settings
.customHighlightColor ??
unknownColor,
tab: Tab(
text: "colorpicker_panels"
.i18n)),
ColorTab(
unlocked: hasAccess,
color:
settings.customAccentColor ??
unknownColor,
tab: Tab(
text: "colorpicker_accent"
.i18n)),
// ColorTab(
// unlocked: hasAccess,
// color: settings.customIconColor ??
// unknownColor,
// tab: Tab(
// text:
// "colorpicker_icon".i18n)),
],
onTap: (index) {
// if (!hasAccess) {
// index = 0;
// _colorsTabController.animateTo(0,
// duration: Duration.zero);
// PremiumLockedFeatureUpsell.show(
// context: context,
// feature: PremiumFeature
// .customcolors);
// }
switch (index) {
case 0:
setState(() {
colorMode =
CustomColorMode.theme;
});
break;
// case 1:
// setState(() {
// colorMode =
// CustomColorMode.enterId;
// });
// break;
/*case 1:
setState(() {
colorMode =
CustomColorMode.saved;
});
break;*/
case 1:
setState(() {
colorMode =
CustomColorMode.background;
});
break;
case 2:
setState(() {
colorMode =
CustomColorMode.highlight;
});
break;
case 3:
setState(() {
colorMode =
CustomColorMode.accent;
});
break;
case 4:
setState(() {
colorMode =
CustomColorMode.icon;
});
break;
}
},
controller: _colorsTabController,
padding: EdgeInsets.zero,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0),
child: SafeArea(
child: FilcColorPicker(
colorMode: colorMode,
pickerColor: colorMode ==
CustomColorMode.accent
? settings.customAccentColor ??
unknownColor
: colorMode ==
CustomColorMode.background
? settings
.customBackgroundColor ??
unknownColor
: colorMode ==
CustomColorMode.theme
? (accentColorMap[settings
.accentColor] ??
AppColors.of(context)
.text) // idk what else
: colorMode ==
CustomColorMode
.highlight
? settings
.customHighlightColor ??
unknownColor
: settings
.customIconColor ??
unknownColor,
onColorChanged: (c) {
setState(() {
updateCustomColor(c, false);
});
setTheme(settings.theme, false);
},
onColorChangeEnd: (c, {adaptive}) {
setState(() {
if (adaptive == true) {
settings.update(
accentColor:
AccentColor.adaptive);
settings.update(
customBackgroundColor:
AppColors.of(context)
.background,
store: true);
settings.update(
customHighlightColor:
AppColors.of(context)
.highlight,
store: true);
settings.update(
customIconColor:
const Color(0x00000000),
store: true);
} else {
updateCustomColor(c, true);
}
});
setTheme(settings.theme, true);
},
onThemeIdProvided: (theme) {
// changing grade colors
List<Color> colors = [
theme.gradeColors.oneColor,
theme.gradeColors.twoColor,
theme.gradeColors.threeColor,
theme.gradeColors.fourColor,
theme.gradeColors.fiveColor,
];
settings.update(
gradeColors: colors);
// changing shadow effect
settings.update(
shadowEffect:
theme.shadowEffect);
// changing theme
setState(() {
updateCustomColor(
null,
true,
accent: theme.accentColor,
background:
theme.backgroundColor,
panels: theme.panelsColor,
icon: theme.iconColor,
);
});
setTheme(settings.theme, true);
},
),
),
),
],
),
),
),
),
],
),
),
],
),
),
),
);
},
),
);
}
}
class ColorTab extends StatelessWidget {
const ColorTab(
{super.key,
required this.tab,
required this.color,
this.unlocked = true});
final Tab tab;
final Color color;
final bool unlocked;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Transform.translate(
offset: const Offset(-3, 1),
child: unlocked
? Container(
width: 15,
height: 15,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
border: Border.all(color: Colors.black, width: 2.0),
),
)
: const Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
child: Icon(Icons.lock,
color: Color.fromARGB(255, 82, 82, 82), size: 18),
),
),
tab
],
);
}
}
class PremiumColorPickerItem extends StatelessWidget {
const PremiumColorPickerItem(
{super.key, required this.label, this.onTap, required this.color});
final String label;
final void Function()? onTap;
final Color color;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
label,
style: TextStyle(
color: AppColors.of(context).text,
fontWeight: FontWeight.w500),
),
),
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: color, shape: BoxShape.circle, border: Border.all()),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,67 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"theme_prev": "Preview",
"colorpicker_presets": "Presets",
"colorpicker_background": "Background",
"colorpicker_panels": "Panels",
"colorpicker_accent": "Accent",
"colorpicker_icon": "Icon",
"need_sub": "You need Kupak subscription to use modify this.",
"advanced": "Advanced",
"enter_id": "Enter ID",
"theme_id": "Theme ID...",
"theme_not_found": "Theme not found!",
"attention": "Attention!",
"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",
"share_subj_theme": "Share Theme",
},
"hu_hu": {
"theme_prev": "Előnézet",
"colorpicker_presets": "Téma",
"colorpicker_background": "Háttér",
"colorpicker_panels": "Panelek",
"colorpicker_accent": "Színtónus",
"colorpicker_icon": "Ikon",
"need_sub": "A módosításhoz Kupak szintű támogatás szükséges.",
"advanced": "Haladó",
"enter_id": "ID megadása",
"theme_id": "Téma azonosító...",
"theme_not_found": "A téma nem található!",
"attention": "Figyelem!",
"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",
"share_subj_theme": "Téma Megosztás",
},
"de_de": {
"theme_prev": "Vorschau",
"colorpicker_presets": "Farben",
"colorpicker_background": "Hintergrund",
"colorpicker_panels": "Tafeln",
"colorpicker_accent": "Akzent",
"colorpicker_icon": "Ikone",
"need_sub":
"Sie benötigen ein Kupak-Abonnement, um diese Funktion zu ändern.",
"advanced": "Fortschrittlich",
"enter_id": "Geben Sie die ID ein",
"theme_id": "Themen-ID...",
"theme_not_found": "Thema nicht gefunden!",
"attention": "Achtung!",
"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",
"share_subj_theme": "Thema Teilen",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,105 @@
import 'package:refilc/api/providers/database_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/models/user.dart';
import 'package:refilc_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart';
// import 'package:refilc_plus/models/premium_scopes.dart';
// import 'package:refilc_plus/providers/premium_provider.dart';
// import 'package:refilc_plus/ui/mobile/premium/upsell.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:provider/provider.dart';
// ignore: must_be_immutable
class UserMenuNickname extends StatelessWidget {
late User u;
UserMenuNickname(this.u, {super.key});
@override
Widget build(BuildContext context) {
return BottomSheetMenuItem(
onPressed: () {
// if (!Provider.of<PremiumProvider>(context, listen: false)
// .hasScope(PremiumScopes.nickname)) {
// PremiumLockedFeatureUpsell.show(
// context: context, feature: PremiumFeature.profile);
// return;
// }
showDialog(
context: context, builder: (context) => UserNicknameEditor(u));
},
icon: const Icon(FeatherIcons.edit2),
title: Text("edit_nickname".i18n),
);
}
}
// ignore: must_be_immutable
class UserNicknameEditor extends StatefulWidget {
late User u;
UserNicknameEditor(this.u, {super.key});
@override
State<UserNicknameEditor> createState() => _UserNicknameEditorState();
}
class _UserNicknameEditorState extends State<UserNicknameEditor> {
final _userName = TextEditingController();
late final UserProvider user;
@override
void initState() {
super.initState();
user = Provider.of<UserProvider>(context, listen: false);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("change_username".i18n),
content: TextField(
controller: _userName,
autofocus: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
label: Text(widget.u.name),
suffixIcon: IconButton(
icon: const Icon(FeatherIcons.x),
onPressed: () {
setState(() {
_userName.text = "";
});
},
),
),
),
actions: [
TextButton(
child: Text(
"cancel".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
"done".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
widget.u.nickname = _userName.text.trim();
Provider.of<DatabaseProvider>(context, listen: false)
.store
.storeUser(widget.u);
Provider.of<UserProvider>(context, listen: false).refresh();
Navigator.of(context).pop(true);
},
),
],
);
}
}

View File

@@ -0,0 +1,236 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:refilc/api/providers/database_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/models/user.dart';
import 'package:refilc_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart';
// import 'package:refilc_plus/models/premium_scopes.dart';
// import 'package:refilc_plus/providers/premium_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_crop/image_crop.dart';
// ignore: must_be_immutable
class UserMenuProfilePic extends StatelessWidget {
late User u;
UserMenuProfilePic(this.u, {super.key});
@override
Widget build(BuildContext context) {
// if (!Provider.of<PremiumProvider>(context)
// .hasScope(PremiumScopes.nickname)) {
// return const SizedBox();
// }
return BottomSheetMenuItem(
onPressed: () {
showDialog(
context: context, builder: (context) => UserProfilePicEditor(u));
},
icon: const Icon(FeatherIcons.camera),
title: Text("edit_profile_picture".i18n),
);
}
}
// ignore: must_be_immutable
class UserProfilePicEditor extends StatefulWidget {
late User u;
UserProfilePicEditor(this.u, {super.key});
@override
State<UserProfilePicEditor> createState() => _UserProfilePicEditorState();
}
class _UserProfilePicEditorState extends State<UserProfilePicEditor> {
late final UserProvider user;
final cropKey = GlobalKey<CropState>();
File? _file;
File? _sample;
File? _lastCropped;
File? image;
Future pickImage() async {
try {
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
if (image == null) return;
File imageFile = File(image.path);
final sample = await ImageCrop.sampleImage(
file: imageFile,
preferredSize: context.size!.longestSide.ceil(),
);
_sample?.delete();
_file?.delete();
setState(() {
_sample = sample;
_file = imageFile;
});
} on PlatformException catch (e) {
log('Failed to pick image: $e');
}
}
Widget cropImageWidget() {
return SizedBox(
height: 300,
child: Crop.file(
_sample!,
key: cropKey,
aspectRatio: 1.0,
),
);
}
Widget openImageWidget() {
return InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14.0),
),
onTap: () => pickImage(),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(14.0),
),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 8.0),
child: Column(
children: [
Text(
"click_here".i18n,
style: const TextStyle(
fontSize: 22.0,
fontWeight: FontWeight.w600,
),
),
Text(
"select_profile_picture".i18n,
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
),
)
],
),
),
);
}
Future<void> _cropImage() async {
final scale = cropKey.currentState!.scale;
final area = cropKey.currentState!.area;
if (area == null || _file == null) {
return;
}
final sample = await ImageCrop.sampleImage(
file: _file!,
preferredSize: (2000 / scale).round(),
);
final file = await ImageCrop.cropImage(
file: sample,
area: area,
);
sample.delete();
_lastCropped?.delete();
_lastCropped = file;
List<int> imageBytes = await _lastCropped!.readAsBytes();
String base64Image = base64Encode(imageBytes);
widget.u.picture = base64Image;
Provider.of<DatabaseProvider>(context, listen: false)
.store
.storeUser(widget.u);
Provider.of<UserProvider>(context, listen: false).refresh();
debugPrint('$file');
}
@override
void initState() {
super.initState();
user = Provider.of<UserProvider>(context, listen: false);
}
@override
void dispose() {
super.dispose();
_file?.delete();
_sample?.delete();
_lastCropped?.delete();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14.0))),
contentPadding: const EdgeInsets.only(top: 10.0),
title: Text("edit_profile_picture".i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: _sample == null ? openImageWidget() : cropImageWidget(),
),
if (widget.u.picture != "")
TextButton(
child: Text(
"remove_profile_picture".i18n,
style: const TextStyle(
fontWeight: FontWeight.w500, color: Colors.red),
),
onPressed: () {
widget.u.picture = "";
Provider.of<DatabaseProvider>(context, listen: false)
.store
.storeUser(widget.u);
Provider.of<UserProvider>(context, listen: false).refresh();
Navigator.of(context).pop(true);
},
),
],
),
actions: [
TextButton(
child: Text(
"cancel".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
"done".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () async {
await _cropImage();
Navigator.of(context).pop(true);
},
),
],
);
}
}

View File

@@ -0,0 +1,231 @@
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc_kreta_api/models/absence.dart';
import 'package:refilc_kreta_api/models/grade.dart';
import 'package:refilc_kreta_api/models/subject.dart';
import 'package:refilc_kreta_api/providers/absence_provider.dart';
import 'package:refilc_kreta_api/providers/grade_provider.dart';
import 'package:refilc_kreta_api/providers/homework_provider.dart';
import 'package:refilc_mobile_ui/screens/summary/summary_screen.i18n.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AllSumBody extends StatefulWidget {
const AllSumBody({super.key});
@override
AllSumBodyState createState() => AllSumBodyState();
}
class AllSumBodyState extends State<AllSumBody> {
late UserProvider user;
late GradeProvider gradeProvider;
late HomeworkProvider homeworkProvider;
late AbsenceProvider absenceProvider;
//late TimetableProvider timetableProvider;
late Map<String, Map<String, dynamic>> things = {};
late List<Widget> firstSixTiles = [];
late List<Widget> lastSixTiles = [];
int avgDropValue = 0;
bool animation = false;
List<Grade> getSubjectGrades(GradeSubject subject, {int days = 0}) =>
gradeProvider.grades
.where((e) =>
e.subject == subject &&
e.type == GradeType.midYear &&
(days == 0 ||
e.date
.isBefore(DateTime.now().subtract(Duration(days: days)))))
.toList();
@override
void initState() {
super.initState();
gradeProvider = Provider.of<GradeProvider>(context, listen: false);
homeworkProvider = Provider.of<HomeworkProvider>(context, listen: false);
absenceProvider = Provider.of<AbsenceProvider>(context, listen: false);
//timetableProvider = Provider.of<TimetableProvider>(context, listen: false);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
animation = true;
});
});
}
void getGrades() {
var allGrades = gradeProvider.grades;
var testsGrades = gradeProvider.grades.where((a) => a.value.weight == 100);
var closingTestsGrades =
gradeProvider.grades.where((a) => a.value.weight >= 200);
things.addAll({
'tests': {'name': 'test'.i18n, 'value': testsGrades.length},
'closingTests': {
'name': 'closingtest'.i18n,
'value': closingTestsGrades.length
},
'grades': {'name': 'grade'.i18n, 'value': allGrades.length}
});
}
void getHomework() {
var allHomework = homeworkProvider.homework;
things.addAll({
'homework': {'name': 'hw'.i18n, 'value': allHomework.length}
});
}
void getSubjects() {
var allSubjects = gradeProvider.grades
.map((e) => e.subject)
.toSet()
.toList()
..sort((a, b) => a.name.compareTo(b.name));
//var totalLessons;
var totalLessons = 0;
things.addAll({
'subjects': {'name': 'subject'.i18n, 'value': allSubjects.length},
'lessons': {'name': 'lesson'.i18n, 'value': totalLessons}
});
}
void getAbsences() {
var allAbsences = absenceProvider.absences.where((a) => a.delay == 0);
var excusedAbsences = absenceProvider.absences
.where((a) => a.state == Justification.excused && a.delay == 0);
var unexcusedAbsences = absenceProvider.absences.where((a) =>
(a.state == Justification.unexcused ||
a.state == Justification.pending) &&
a.delay == 0);
things.addAll({
'absences': {'name': 'absence_sum'.i18n, 'value': allAbsences.length},
'excusedAbsences': {
'name': 'excused'.i18n,
'value': excusedAbsences.length
},
'unexcusedAbsences': {
'name': 'unexcused'.i18n,
'value': unexcusedAbsences.length
}
});
}
void getDelays() {
var allDelays = absenceProvider.absences.where((a) => a.delay > 0);
var delayTimeList = (allDelays.map((a) {
return a.delay;
}).toList());
var totalDelayTime = 0;
if (delayTimeList.isNotEmpty) {
totalDelayTime = delayTimeList.reduce((a, b) => a + b);
}
var unexcusedDelays = absenceProvider.absences
.where((a) => a.state == Justification.unexcused && a.delay > 0);
things.addAll({
'delays': {'name': 'delay_sum'.i18n, 'value': allDelays.length},
'totalDelay': {'name': 'min'.i18n, 'value': totalDelayTime},
'unexcusedDelays': {
'name': 'unexcused'.i18n,
'value': unexcusedDelays.length
}
});
}
void getEverything() {
getGrades();
getHomework();
getSubjects();
getAbsences();
getDelays();
}
void generateTiles() {
for (var i in things.values) {
Widget w = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
i.values.toList()[1].toString(),
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 36.0,
color: Colors.white,
),
),
Text(
i.values.toList()[0],
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18.0,
color: Colors.white,
),
),
],
);
// TO-DO: az orakat es a hazikat szarul keri le, de majd meg lesz csinalva
if (firstSixTiles.length < 6) {
firstSixTiles.add(w);
} else if (lastSixTiles.length < 6) {
lastSixTiles.add(w);
} else {
break;
}
}
}
@override
Widget build(BuildContext context) {
getEverything();
generateTiles();
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 45,
),
AnimatedContainer(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 420),
transform: Matrix4.translationValues(
animation ? 0 : MediaQuery.of(context).size.width, 0, 0),
height: 250,
child: GridView.count(
crossAxisCount: 3,
mainAxisSpacing: 0,
crossAxisSpacing: 5,
children: firstSixTiles,
),
),
const SizedBox(
height: 30,
),
AnimatedContainer(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 420),
transform: Matrix4.translationValues(
animation ? 0 : -MediaQuery.of(context).size.width, 0, 0),
height: 250,
child: GridView.count(
crossAxisCount: 3,
mainAxisSpacing: 0,
crossAxisSpacing: 5,
children: lastSixTiles,
),
),
],
);
}
}

View File

@@ -0,0 +1,363 @@
import 'dart:math';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/helpers/average_helper.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
import 'package:refilc/utils/format.dart';
import 'package:refilc_kreta_api/models/grade.dart';
import 'package:refilc_kreta_api/models/subject.dart';
import 'package:refilc_kreta_api/providers/grade_provider.dart';
import 'package:refilc_mobile_ui/screens/summary/summary_screen.i18n.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:i18n_extension/i18n_widget.dart';
List<String> faces = [
"(·.·)",
"(≥o≤)",
"(·_·)",
"(˚Δ˚)b",
"(^-^*)",
"(='X'=)",
"(>_<)",
"(;-;)",
"\\(^Д^)/",
"\\(o_o)/",
];
class GradesBody extends StatefulWidget {
const GradesBody({super.key});
@override
GradesBodyState createState() => GradesBodyState();
}
class GradesBodyState extends State<GradesBody> {
late UserProvider user;
late GradeProvider gradeProvider;
late SettingsProvider settings;
late double subjectAvg;
late double endYearAvg;
late String endYearAvgText;
List<Widget> subjectTiles5 = [];
List<Widget> subjectTiles3 = [];
List<Widget> subjectTiles1 = [];
int avgDropValue = 0;
bool animation = false;
List<Grade> getSubjectGrades(GradeSubject subject, {int days = 0}) =>
gradeProvider.grades
.where((e) =>
e.subject == subject &&
e.type == GradeType.midYear &&
(days == 0 ||
e.date
.isBefore(DateTime.now().subtract(Duration(days: days)))))
.toList();
@override
void initState() {
super.initState();
gradeProvider = Provider.of<GradeProvider>(context, listen: false);
settings = Provider.of<SettingsProvider>(context, listen: false);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
animation = true;
});
});
}
void generateTiles({required int filter}) {
List<GradeSubject> subjects = gradeProvider.grades
.map((e) => e.subject)
.toSet()
.toList()
..sort((a, b) => a.name.compareTo(b.name));
List<Widget> tiles = [];
Map<GradeSubject, double> subjectAvgs = {};
var count = 1;
for (GradeSubject subject in subjects) {
List<Grade> subjectGrades = getSubjectGrades(subject);
double avg = AverageHelper.averageEvals(subjectGrades);
if (avg != 0) subjectAvgs[subject] = avg;
Widget widget = AnimatedContainer(
curve: Curves.easeInOut,
duration: Duration(milliseconds: 300 + (count * 120)),
transform: Matrix4.translationValues(
animation ? 0 : MediaQuery.of(context).size.width, 0, 0),
child: Row(
children: [
GradeValueWidget(
GradeValue(avg.round(), '', '', 100),
fill: true,
size: 28.0,
),
const SizedBox(width: 8),
Text(
subject.renamedTo ?? subject.name.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0,
color: Colors.white.withOpacity(0.98),
fontStyle: settings.renamedSubjectsItalics && subject.isRenamed
? FontStyle.italic
: null,
),
)
],
),
);
if (avg.round() == filter) {
tiles.add(widget);
count++;
}
}
if (tiles.isEmpty) {
int index = Random(DateTime.now().minute).nextInt(faces.length);
Widget faceWidget = Center(
child: Text.rich(
TextSpan(
text: faces[index],
style: const TextStyle(
fontSize: 32.0,
fontWeight: FontWeight.w500,
color: Colors.white,
),
children: [
TextSpan(
text: "\n${'no_grades'.i18n}",
style: TextStyle(
fontSize: 18.0,
height: 2.0,
color: Colors.white.withOpacity(0.5)),
),
],
),
textAlign: TextAlign.center,
),
);
tiles.insert(0, faceWidget);
}
subjectAvg = subjectAvgs.isNotEmpty
? subjectAvgs.values.fold(0.0, (double a, double b) => a + b) /
subjectAvgs.length
: 0.0;
List<Grade> endYearGrades = gradeProvider.grades
.where((grade) => grade.type == GradeType.endYear)
.toList();
endYearAvg = AverageHelper.averageEvals(endYearGrades, finalAvg: true);
endYearAvgText = endYearAvg.toStringAsFixed(1);
if (I18n.of(context).locale.languageCode != "en") {
endYearAvgText = endYearAvgText.replaceAll(".", ",");
}
if (filter == 5) {
subjectTiles5 = List.castFrom(tiles);
if (subjectTiles5.length > 4) {
subjectTiles5.length = 4;
}
} else if (filter == 3) {
subjectTiles3 = List.castFrom(tiles);
if (subjectTiles3.length > 3) {
subjectTiles3.length = 3;
}
} else if (filter == 1) {
subjectTiles1 = List.castFrom(tiles);
if (subjectTiles1.length > 2) {
subjectTiles1.length = 2;
}
}
}
void getGrades() {
generateTiles(filter: 5);
generateTiles(filter: 3);
generateTiles(filter: 1);
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
settings = Provider.of<SettingsProvider>(context);
getGrades();
return Expanded(
child: ListView(
children: [
SizedBox(
height: ((100 * subjectTiles5.length) /
(subjectTiles5[0].runtimeType == AnimatedContainer
? 1.95
: 1.2))
.toDouble(),
child: ListView.builder(
padding: const EdgeInsets.only(left: 5),
physics: const BouncingScrollPhysics(),
itemCount: max(subjectTiles5.length, 1),
itemBuilder: (context, index) {
if (subjectTiles5.isNotEmpty) {
EdgeInsetsGeometry panelPadding =
const EdgeInsets.symmetric(horizontal: 24.0);
if (subjectTiles5[index].runtimeType == AnimatedContainer) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: subjectTiles5[index]);
} else {
return Padding(
padding: panelPadding, child: subjectTiles5[index]);
}
} else {
return Container();
}
},
),
),
const SizedBox(height: 12.0),
Text(
'tryagain'.i18n,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22.0,
color: Colors.white,
),
),
const SizedBox(height: 12.0),
SizedBox(
height: ((100 * subjectTiles3.length) /
(subjectTiles3[0].runtimeType == AnimatedContainer
? 1.95
: 1.2))
.toDouble(),
child: ListView.builder(
padding: const EdgeInsets.only(left: 5),
physics: const BouncingScrollPhysics(),
itemCount: max(subjectTiles3.length, 1),
itemBuilder: (context, index) {
if (subjectTiles3.isNotEmpty) {
EdgeInsetsGeometry panelPadding =
const EdgeInsets.symmetric(horizontal: 24.0);
if (subjectTiles3[index].runtimeType == AnimatedContainer) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: subjectTiles3[index]);
} else {
return Padding(
padding: panelPadding, child: subjectTiles3[index]);
}
} else {
return Container();
}
},
),
),
const SizedBox(height: 12.0),
Text(
'oops'.i18n,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22.0,
color: Colors.white,
),
),
const SizedBox(height: 12.0),
SizedBox(
height: ((100 * subjectTiles1.length) /
(subjectTiles1[0].runtimeType == AnimatedContainer
? 1.95
: 1.2))
.toDouble(),
child: ListView.builder(
padding: const EdgeInsets.only(left: 5),
physics: const BouncingScrollPhysics(),
itemCount: max(subjectTiles1.length, 1),
itemBuilder: (context, index) {
if (subjectTiles1.isNotEmpty) {
EdgeInsetsGeometry panelPadding =
const EdgeInsets.symmetric(horizontal: 24.0);
if (subjectTiles1[index].runtimeType == AnimatedContainer) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: subjectTiles1[index]);
} else {
return Padding(
padding: panelPadding, child: subjectTiles1[index]);
}
} else {
return Container();
}
},
),
),
const SizedBox(height: 30.0),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'endyear_avg'.i18n,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22.0,
color: Colors.white,
),
),
Container(
margin: const EdgeInsets.only(top: 10.0),
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 4.0),
decoration: BoxDecoration(
color: gradeColor(context: context, value: endYearAvg)
.withOpacity(.2),
border: Border.all(
color: (gradeColor(context: context, value: endYearAvg))
.withOpacity(0.0),
width: 2.0,
),
borderRadius: BorderRadius.circular(45.0),
),
child: AutoSizeText.rich(
TextSpan(
text: endYearAvgText,
),
maxLines: 1,
minFontSize: 5,
textAlign: TextAlign.center,
style: TextStyle(
color: gradeColor(context: context, value: endYearAvg),
fontWeight: FontWeight.w800,
fontSize: 32.0,
),
),
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,321 @@
// ignore_for_file: no_leading_underscores_for_local_identifiers
import 'dart:math';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/helpers/subject.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/utils/format.dart';
import 'package:refilc_kreta_api/models/absence.dart';
import 'package:refilc_kreta_api/models/lesson.dart';
import 'package:refilc_kreta_api/models/subject.dart';
import 'package:refilc_kreta_api/models/week.dart';
import 'package:refilc_kreta_api/providers/absence_provider.dart';
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
import 'package:refilc_mobile_ui/screens/summary/summary_screen.i18n.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
List<String> faces = [
"(·.·)",
"(≥o≤)",
"(·_·)",
"(˚Δ˚)b",
"(^-^*)",
"(='X'=)",
"(>_<)",
"(;-;)",
"\\(^Д^)/",
"\\(o_o)/",
];
class SubjectAbsence {
GradeSubject subject;
List<Absence> absences;
double percentage;
SubjectAbsence(
{required this.subject, this.absences = const [], this.percentage = 0.0});
}
class LessonsBody extends StatefulWidget {
const LessonsBody({super.key});
@override
LessonsBodyState createState() => LessonsBodyState();
}
class LessonsBodyState extends State<LessonsBody> {
late UserProvider user;
late AbsenceProvider absenceProvider;
late SettingsProvider settingsProvider;
late TimetableProvider timetableProvider;
late List<SubjectAbsence> absences = [];
late List<Widget> lessons = [];
late List<Absence> delays = [];
final Map<GradeSubject, Lesson> _lessonCount = {};
@override
void initState() {
super.initState();
absenceProvider = Provider.of<AbsenceProvider>(context, listen: false);
settingsProvider = Provider.of<SettingsProvider>(context, listen: false);
timetableProvider = Provider.of<TimetableProvider>(context, listen: false);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
for (final lesson in timetableProvider.getWeek(Week.current()) ?? []) {
if (!lesson.isEmpty &&
lesson.subject.id != '' &&
lesson.lessonYearIndex != null) {
_lessonCount.update(
lesson.subject,
(value) {
if (lesson.lessonYearIndex! > value.lessonYearIndex!) {
return lesson;
} else {
return value;
}
},
ifAbsent: () => lesson,
);
}
}
setState(() {});
});
}
void buildSubjectAbsences() {
Map<GradeSubject, SubjectAbsence> _absences = {};
for (final absence in absenceProvider.absences) {
if (absence.delay != 0) continue;
if (!_absences.containsKey(absence.subject)) {
_absences[absence.subject] =
SubjectAbsence(subject: absence.subject, absences: [absence]);
} else {
_absences[absence.subject]?.absences.add(absence);
}
}
_absences.forEach((subject, absence) {
final absentLessonsOfSubject = absenceProvider.absences
.where((e) => e.subject == subject && e.delay == 0)
.length;
final totalLessonsOfSubject = _lessonCount[subject]?.lessonYearIndex ?? 0;
double absentLessonsOfSubjectPercentage;
if (absentLessonsOfSubject <= totalLessonsOfSubject) {
absentLessonsOfSubjectPercentage =
absentLessonsOfSubject / totalLessonsOfSubject * 100;
} else {
absentLessonsOfSubjectPercentage = -1;
}
_absences[subject]?.percentage =
absentLessonsOfSubjectPercentage.clamp(-1, 100.0);
});
absences = _absences.values.toList();
absences.sort((a, b) => -a.percentage.compareTo(b.percentage));
}
void getAndSortDelays() {
delays = absenceProvider.absences;
delays.sort((a, b) => -a.delay.compareTo(b.delay));
}
void generateTiles() {
Widget leastAbsent = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
SubjectIcon.resolveVariant(
subject: absences.last.subject, context: context),
color: Colors.white,
size: 64,
),
Text(
absences.last.subject.renamedTo ??
absences.last.subject.name.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 36.0,
fontStyle: absences.last.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null,
color: Colors.white,
),
),
Text(
'absence'.i18n.fill([absences.last.absences.length]),
style: const TextStyle(
fontSize: 18.0,
color: Colors.white,
),
)
],
),
);
if (absences.last.absences.isNotEmpty) {
lessons.add(leastAbsent);
} else {
lessons.add(buildFaceWidget());
}
Widget mostAbsent = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
SubjectIcon.resolveVariant(
subject: absences.first.subject, context: context),
color: Colors.white,
size: 64,
),
Text(
absences.first.subject.renamedTo ??
absences.first.subject.name.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 36.0,
fontStyle: absences.first.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null,
color: Colors.white,
),
),
Text(
'absence'.i18n.fill([absences.first.absences.length]),
style: const TextStyle(
fontSize: 18.0,
color: Colors.white,
),
)
],
),
);
if (absences.first.absences.isNotEmpty) {
lessons.add(mostAbsent);
} else {
lessons.add(buildFaceWidget());
}
Widget mostDelays = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
SubjectIcon.resolveVariant(
subject: delays.first.subject, context: context),
color: Colors.white,
size: 64,
),
Text(
delays.first.subject.renamedTo ??
delays.first.subject.name.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 36.0,
fontStyle: delays.first.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null,
color: Colors.white,
),
),
Text(
'delay'.i18n.fill([delays.first.delay]),
style: const TextStyle(
fontSize: 18.0,
color: Colors.white,
),
)
],
),
);
if (delays.first.delay != 0) {
lessons.add(mostDelays);
} else {
lessons.add(buildFaceWidget());
}
}
@override
Widget build(BuildContext context) {
buildSubjectAbsences();
getAndSortDelays();
generateTiles();
return Expanded(
child: ListView(
children: [
lessons[0],
const SizedBox(height: 18.0),
Text(
'dontfelt'.i18n,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22.0,
color: Colors.white,
),
),
const SizedBox(height: 18.0),
lessons[1],
const SizedBox(height: 18.0),
Text(
'youlate'.i18n,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22.0,
color: Colors.white,
),
),
const SizedBox(height: 18.0),
lessons[2],
],
),
);
}
Widget buildFaceWidget() {
int index = Random(DateTime.now().minute).nextInt(faces.length);
return Center(
child: Text.rich(
TextSpan(
text: faces[index],
style: const TextStyle(
fontSize: 32.0,
fontWeight: FontWeight.w500,
color: Colors.white,
),
children: [
TextSpan(
text: "\n${'no_lesson'.i18n}",
style: TextStyle(
fontSize: 18.0,
height: 2.0,
color: Colors.white.withOpacity(0.5)),
),
],
),
textAlign: TextAlign.center,
),
);
}
}

View File

@@ -0,0 +1,128 @@
import 'dart:io';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc_mobile_ui/common/personality_card/empty_card.dart';
import 'package:refilc_mobile_ui/common/personality_card/personality_card.dart';
import 'package:refilc_mobile_ui/screens/summary/summary_screen.i18n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
class PersonalityBody extends StatefulWidget {
const PersonalityBody({super.key});
@override
PersonalityBodyState createState() => PersonalityBodyState();
}
class PersonalityBodyState extends State<PersonalityBody> {
late UserProvider user;
bool isRevealed = false;
ScreenshotController screenshotController = ScreenshotController();
sharePersonality() async {
await screenshotController.capture().then((image) async {
if (image != null) {
final directory = await getApplicationDocumentsDirectory();
if (await File('${directory.path}/refilc_personality.png').exists()) {
await File('${directory.path}/refilc_personality.png').delete();
}
final imagePath =
await File('${directory.path}/refilc_personality.png').create();
await imagePath.writeAsBytes(image);
await Share.shareXFiles([XFile(imagePath.path)]);
}
}).catchError((err) {
throw err;
});
}
savePersonality() async {
await screenshotController.capture().then((image) async {
if (image != null) {
await ImageGallerySaver.saveImage(image, name: 'refilc_personality');
}
}).catchError((err) {
throw err;
});
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
return Expanded(
child: ListView(
children: [
const SizedBox(height: 30),
AnimatedCrossFade(
duration: const Duration(milliseconds: 1000),
sizeCurve: Curves.easeInToLinear,
firstChild: Screenshot(
controller: screenshotController,
child: PersonalityCard(user: user),
),
secondChild: GestureDetector(
onTap: () => setState(() {
isRevealed = true;
}),
child: EmptyCard(text: 'click_reveal'.i18n),
),
crossFadeState: isRevealed
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
),
const SizedBox(height: 30),
if (isRevealed)
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
onPressed: () async {
await sharePersonality();
},
icon: const Icon(
FeatherIcons.share,
color: Colors.white,
size: 30,
),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Colors.white.withOpacity(0.2)),
),
),
const SizedBox(
width: 10,
),
IconButton(
onPressed: () async {
await savePersonality();
},
icon: const Icon(
FeatherIcons.bookmark,
color: Colors.white,
size: 30,
),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Colors.white.withOpacity(0.2)),
),
),
],
),
),
const SizedBox(height: 60),
],
),
);
}
}

View File

@@ -0,0 +1,98 @@
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc_kreta_api/providers/grade_provider.dart';
import 'package:refilc_mobile_ui/screens/summary/summary_screen.dart';
import 'package:refilc_mobile_ui/screens/summary/summary_screen.i18n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'package:wtf_sliding_sheet/wtf_sliding_sheet.dart';
class StartBody extends StatefulWidget {
const StartBody({super.key});
@override
StartBodyState createState() => StartBodyState();
}
class StartBodyState extends State<StartBody> {
late UserProvider user;
late GradeProvider gradeProvider;
late SettingsProvider settings;
late String firstName;
@override
void initState() {
super.initState();
gradeProvider = Provider.of<GradeProvider>(context, listen: false);
settings = Provider.of<SettingsProvider>(context, listen: false);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(height: 40.0),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
showSlidingBottomSheet(
context,
useRootNavigator: true,
builder: (context) => SlidingSheetDialog(
color: Colors.black.withOpacity(0.99),
duration: const Duration(milliseconds: 400),
scrollSpec: const ScrollSpec.bouncingScroll(),
snapSpec: const SnapSpec(
snap: true,
snappings: [1.0],
initialSnap: 1.0,
positioning: SnapPositioning.relativeToAvailableSpace,
),
minHeight: MediaQuery.of(context).size.height,
cornerRadius: 16,
cornerRadiusOnFullscreen: 0,
builder: (context, state) => const Material(
color: Colors.black,
child: SummaryScreen(
currentPage: 'grades',
isBottomSheet: true,
),
),
),
);
},
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
FeatherIcons.arrowRight,
size: 145,
color: Colors.white,
grade: 0.001,
weight: 0.001,
),
Text(
'start'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16.0,
color: Colors.white.withOpacity(0.7),
),
),
],
),
),
),
const SizedBox(height: 169.69),
],
);
}
}

View File

@@ -0,0 +1,222 @@
import 'package:confetti/confetti.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/models/settings.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:wtf_sliding_sheet/wtf_sliding_sheet.dart';
import 'summary_screen.i18n.dart';
import 'pages/allsum_page.dart';
import 'pages/start_page.dart';
import 'pages/grades_page.dart';
import 'pages/lessons_page.dart';
import 'pages/personality_page.dart';
class SummaryScreen extends StatefulWidget {
final String currentPage;
final bool isBottomSheet;
const SummaryScreen({
super.key,
this.currentPage = 'personality',
this.isBottomSheet = false,
});
@override
SummaryScreenState createState() => SummaryScreenState();
static show(
{required BuildContext context,
String currentPage = 'personality'}) =>
Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute(
builder: (context) => SummaryScreen(currentPage: currentPage)));
}
class SummaryScreenState extends State<SummaryScreen>
with SingleTickerProviderStateMixin {
late UserProvider user;
late SettingsProvider settings;
ConfettiController? _confettiController;
late String firstName;
final LinearGradient _backgroundGradient = const LinearGradient(
colors: [
Color(0xff1d56ac),
Color(0xff170a3d),
],
begin: Alignment(-0.8, -1.0),
end: Alignment(0.8, 1.0),
stops: [-1.0, 1.0],
);
@override
void initState() {
super.initState();
}
@override
void dispose() {
_confettiController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
settings = Provider.of<SettingsProvider>(context);
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
if (!settings.presentationMode) {
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
} else {
firstName = "János";
}
return widget.isBottomSheet
? buildContainer()
: Scaffold(
body: buildContainer(),
);
}
Widget buildContainer() {
return Container(
decoration: BoxDecoration(gradient: _backgroundGradient),
child: Container(
decoration: BoxDecoration(gradient: _backgroundGradient),
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.only(
left: 24.0,
right: 24.0,
top: 15.0,
bottom: 40.0,
),
child: Column(
crossAxisAlignment: widget.currentPage == 'start'
? CrossAxisAlignment.center
: CrossAxisAlignment.start,
mainAxisAlignment: widget.currentPage == 'start'
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'greeting'.i18n.fill([firstName]),
textAlign: TextAlign.left,
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 26.0,
color: Colors.white,
),
),
Text(
widget.currentPage == 'start'
? 'title_start'.i18n
: widget.currentPage == 'grades'
? 'title_grades'.i18n
: widget.currentPage == 'lessons'
? 'title_lessons'.i18n
: widget.currentPage == 'personality'
? 'title_personality'.i18n
: '',
maxLines: 1,
overflow: TextOverflow.fade,
softWrap: false,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22.0,
color: Colors.white,
),
),
],
),
),
widget.currentPage != 'start'
? IconButton(
onPressed: () async {
Navigator.of(context).pop();
if (widget.currentPage == 'grades') {
openNewPage(page: 'lessons');
} else if (widget.currentPage == 'lessons') {
openNewPage(page: 'allsum');
} else if (widget.currentPage == 'allsum') {
openNewPage(page: 'personality');
} else {
Navigator.of(context).maybePop();
}
},
icon: Icon(
widget.currentPage == 'personality'
? FeatherIcons.x
: FeatherIcons.arrowRight,
color: Colors.white,
),
)
: Container()
],
),
const SizedBox(height: 12.0),
widget.currentPage == 'start'
? const StartBody()
: widget.currentPage == 'grades'
? const GradesBody()
: widget.currentPage == 'lessons'
? const LessonsBody()
: widget.currentPage == 'allsum'
? const AllSumBody()
: const PersonalityBody(),
],
),
),
),
),
);
}
void openNewPage({String page = 'personality'}) {
showSlidingBottomSheet(
context,
useRootNavigator: true,
builder: (context) => SlidingSheetDialog(
color: Colors.black.withOpacity(0.99),
duration: const Duration(milliseconds: 400),
scrollSpec: const ScrollSpec.bouncingScroll(),
snapSpec: const SnapSpec(
snap: true,
snappings: [1.0],
initialSnap: 1.0,
positioning: SnapPositioning.relativeToAvailableSpace,
),
minHeight: MediaQuery.of(context).size.height,
cornerRadius: 16,
cornerRadiusOnFullscreen: 0,
builder: (context, state) => Material(
color: Colors.black,
child: SummaryScreen(
currentPage: page,
isBottomSheet: true,
),
),
),
);
//SummaryScreen.show(context: context, currentPage: page);
}
}

View File

@@ -0,0 +1,117 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
// main thingies
"no_grades": "No grades found",
"no_lesson": "No lessons found",
"greeting": "You had a good year, %s!",
"title_start": "So let's summarize...",
"title_grades": "Let's look at your marks... 📖",
"title_lessons": "Your favorite lesson 💓",
"title_personality": "Your personality is...",
// start page
"start": "Start",
// grades page
"tryagain": "He puts the master to the test! 🔃",
"oops": "Ouch... 🥴",
"endyear_avg": "Year-end average",
// lessons page
"absence": "%s absence(s)",
"delay": "A total of %s minute(s) late",
"dontfelt": "You didn't like it...",
"youlate": "You're late!",
// allsum page
"test": "test(s)",
"closingtest": "module test(s)",
"grade": "grades",
"hw": "homework",
"subject": "subjects",
"lesson": "lessons",
"absence_sum": "absence(s)",
"excused": "excused",
"unexcused": "unexcused",
"delay_sum": "delay(s)",
"min": "minute(s)",
// personality page
"click_reveal": "Click to reveal...",
},
"hu_hu": {
// main thingies
"no_grades": "Nincsenek jegyek",
"no_lesson": "Nincsenek tanórák",
"greeting": "Jó éved volt, %s!",
"title_start": "Összegezzünk hát...",
"title_grades": "Nézzük a jegyeidet... 📖",
"title_lessons": "A kedvenc órád 💓",
"title_personality": "A te személyiséged...",
// start page
"start": "Kezdés",
// grades page
"tryagain": "Próba teszi a mestert! 🔃",
"oops": "Ajjaj... 🥴",
"endyear_avg": "Év végi átlagod",
// lessons page
"absence": "%s hiányzás",
"delay": "Összesen %s perc késés",
"dontfelt": "Nem volt kedved hozzá...",
"youlate": "Késtél!",
// allsum page
"test": "dolgozat",
"closingtest": "témazáró",
"grade": "jegy",
"hw": "házi",
"subject": "tantárgy",
"lesson": "óra",
"absence_sum": "hiányzás",
"excused": "igazolt",
"unexcused": "igazolatlan",
"delay_sum": "késés",
"min": "perc",
// personality page
"click_reveal": "Kattints a felfedéshez...",
},
"de_de": {
// main thingies
"no_grades": "Keine Grade gefunden",
"no_lesson": "Keine Lektionen gefunden",
"greeting": "Du hattest ein gutes Jahr, %s!",
"title_start": "Fassen wir also zusammen...",
"title_grades": "Schauen wir uns eure Tickets an... 📖",
"title_lessons": "Deine Lieblingsuhr 💓",
"title_personality": "Deine Persönlichkeit...",
// start page
"start": "Anfang",
// grades page
"tryagain": "Er stellt den Meister auf die Probe! 🔃",
"oops": "Autsch... 🥴",
"endyear_avg": "Ihr Jahresenddurchschnitt",
// lessons page
"absence": "%s Abwesenheit(en)",
"delay": "Insgesamt %s Minute(n) zu spät",
"dontfelt": "Es hat dir nicht gefallen...",
"youlate": "Du bist spät!",
// allsum page
"test": "These(n)",
"closingtest": "Modultest",
"grade": "Grad",
"hw": "Hausaufgaben",
"subject": "Themen",
"lesson": "Lektionen",
"absence_sum": "Abwesenheit(en)",
"excused": "bescheinigte",
"unexcused": "unentschuldigte",
"delay_sum": "Verzögerung(en)",
"min": "Minute(n)",
// personality page
"click_reveal": "Klicken Sie hier, um es anzuzeigen...",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}