remelem mukszik

This commit is contained in:
ReinerRego
2023-05-26 21:51:21 +02:00
parent baec76c29f
commit 0ece9382af
170 changed files with 15575 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
import 'dart:io';
import 'dart:math';
import 'package:filcnaplo/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, {Key? key}) : super(key: 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(
child: BackButton(),
alignment: Alignment.topLeft,
),
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, {Key? key}) : super(key: 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, {Key? key}) : super(key: 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(
child: Text(
content,
style: const TextStyle(fontFamily: 'SpaceMono', color: Colors.white),
),
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)))
],
),
);
}
}

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,64 @@
import 'package:filcnaplo/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, {Key? key}) : super(key: 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,29 @@
import 'package:flutter/material.dart';
class LoginButton extends StatelessWidget {
const LoginButton({Key? key, required this.onPressed, required this.child}) : super(key: key);
final void Function()? onPressed;
final Widget? child;
@override
Widget build(BuildContext context) {
return MaterialButton(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 15.0,
),
child: child,
),
elevation: 0,
focusElevation: 0,
hoverElevation: 0,
highlightElevation: 0,
minWidth: MediaQuery.of(context).size.width - 64.0,
onPressed: onPressed,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
color: Colors.white,
textColor: Colors.black,
);
}
}

View File

@@ -0,0 +1,97 @@
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({Key? key, required this.style, this.controller, this.focusNode, this.onClear}) : super(key: key);
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: const Color(0xff20AC9B),
textInputAction: TextInputAction.next,
autofillHints: [autofill],
obscureText: obscure,
scrollPhysics: const BouncingScrollPhysics(),
decoration: InputDecoration(
fillColor: Colors.black.withOpacity(0.15),
filled: true,
enabledBorder: UnderlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(width: 0, color: Colors.transparent),
),
focusedBorder: UnderlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(width: 0, color: Colors.transparent),
),
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: Icon(
widget.style == LoginInputStyle.password
? obscure
? FeatherIcons.eye
: FeatherIcons.eyeOff
: FeatherIcons.x,
color: Colors.white),
),
),
)
: null,
),
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: Colors.white,
),
);
}
}

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,303 @@
import 'dart:ui';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/api/login.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
import 'package:filcnaplo_mobile_ui/common/system_chrome.dart';
import 'package:filcnaplo_mobile_ui/screens/login/login_button.dart';
import 'package:filcnaplo_mobile_ui/screens/login/login_input.dart';
import 'package:filcnaplo_mobile_ui/screens/login/school_input/school_input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'login_screen.i18n.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key, this.back = false}) : super(key: key);
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(0xff20AC9B),
Color(0xff20AC9B),
Color(0xff123323),
],
begin: Alignment(-0.8, -1.0),
end: Alignment(0.8, 1.0),
stops: [-1.0, 0.0, 1.0],
);
@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(gradient: _backgroundGradient),
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
controller: _scrollController,
child: Container(
decoration: BoxDecoration(gradient: _backgroundGradient),
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (showBack)
Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.only(left: 16.0, top: 12.0),
child: const ClipOval(
child: Material(
type: MaterialType.transparency,
child: BackButton(color: Colors.white),
),
),
),
const Spacer(),
// App logo
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: ClipRect(
child: Container(
// Png shadow *hack*
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Opacity(child: Image.asset("assets/icons/ic_splash.png", color: Colors.black), opacity: 0.3),
),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6.0, sigmaY: 6.0),
child: Image.asset("assets/icons/ic_splash.png"),
)
],
),
width: MediaQuery.of(context).size.width / 4,
margin: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 12.0),
),
),
),
// Inputs
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.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: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 14.0,
),
),
),
Expanded(
child: Text(
"usernameHint".i18n,
maxLines: 1,
textAlign: TextAlign.right,
style: const TextStyle(
color: Colors.white54,
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: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 14.0,
),
),
),
Expanded(
child: Text(
"passwordHint".i18n,
maxLines: 1,
textAlign: TextAlign.right,
style: const TextStyle(
color: Colors.white54,
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: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 14.0,
),
),
),
SchoolInput(
scroll: _scrollController,
controller: schoolController,
),
],
),
),
),
// Log in button
Padding(
padding: const EdgeInsets.only(top: 42.0),
child: Visibility(
child: LoginButton(
child: Text("login".i18n,
maxLines: 1,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15.0,
)),
onPressed: () => _loginApi(context: context),
),
visible: _loginState != LoginState.inProgress,
replacement: const Padding(
padding: EdgeInsets.symmetric(vertical: 6.0),
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
),
),
if (_loginState == LoginState.missingFields || _loginState == LoginState.invalidGrant || _loginState == LoginState.failed)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
["missing_fields", "invalid_grant", "error"][_loginState.index].i18n,
style: const TextStyle(color: Colors.red, fontWeight: FontWeight.w500),
),
),
const Spacer()
],
),
),
),
),
),
);
}
void _loginApi({required BuildContext context}) {
String username = usernameController.text;
String password = passwordController.text;
if (username == "" || password == "" || schoolController.selectedSchool == null) {
return setState(() => _loginState = LoginState.missingFields);
}
setState(() => _loginState = LoginState.inProgress);
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(() => _loginState = res));
}
}

View File

@@ -0,0 +1,51 @@
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!",
"error": "Failed to log in.",
"schools_error": "Failed to get schools."
},
"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ó!",
"error": "Sikertelen bejelentkezés.",
"schools_error": "Nem sikerült lekérni az iskolákat."
},
"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."
},
};
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,117 @@
import 'package:filcnaplo_mobile_ui/screens/login/login_input.dart';
import 'package:filcnaplo_mobile_ui/screens/login/school_input/school_input_overlay.dart';
import 'package:filcnaplo_mobile_ui/screens/login/school_input/school_input_tile.dart';
import 'package:filcnaplo_mobile_ui/screens/login/school_input/school_search.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo_kreta_api/models/school.dart';
class SchoolInput extends StatefulWidget {
const SchoolInput({Key? key, required this.controller, required this.scroll}) : super(key: key);
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,72 @@
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(
children: children,
size: size,
layerLink: layerLink,
);
}
}
class SchoolInputOverlayWidget extends StatelessWidget {
const SchoolInputOverlayWidget({
Key? key,
required this.children,
required this.size,
required this.layerLink,
}) : super(key: key);
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,64 @@
import 'package:filcnaplo_kreta_api/models/school.dart';
import 'package:flutter/material.dart';
class SchoolInputTile extends StatelessWidget {
const SchoolInputTile({Key? key, required this.school, this.onTap}) : super(key: key);
final School school;
final Function()? onTap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: GestureDetector(
onPanDown: (e) {
onTap!();
},
child: InkWell(
onTapDown: (e) {},
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,25 @@
import 'package:filcnaplo_kreta_api/models/school.dart';
import 'package:filcnaplo/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);
}
results.sort((a, b) => a.name.compareTo(b.name));
return results;
}

View File

@@ -0,0 +1,27 @@
import 'package:filcnaplo_mobile_ui/screens/navigation/navbar_item.dart';
import 'package:flutter/material.dart';
class Navbar extends StatelessWidget {
const Navbar({Key? key, required this.selectedIndex, required this.onSelected, required this.items}) : super(key: key);
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,59 @@
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({
Key? key,
required this.item,
required this.active,
required this.onTap,
}) : super(key: key);
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: InkWell(
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:filcnaplo_mobile_ui/pages/absences/absences_page.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/grades_page.dart';
import 'package:filcnaplo_mobile_ui/pages/home/home_page.dart';
import 'package:filcnaplo_mobile_ui/pages/messages/messages_page.dart';
import 'package:filcnaplo_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,302 @@
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo/helpers/quick_actions.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_mobile_ui/common/system_chrome.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/nabar.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/navbar_item.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_route.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_route_handler.dart';
import 'package:filcnaplo/icons/filc_icons.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/status_bar.dart';
import 'package:filcnaplo_mobile_ui/screens/news/news_view.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.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:filcnaplo_mobile_ui/common/screens.i18n.dart';
import 'package:filcnaplo/api/providers/news_provider.dart';
import 'package:filcnaplo/api/providers/sync.dart';
import 'package:home_widget/home_widget.dart';
import 'package:sliding_sheet/sliding_sheet.dart';
import 'package:background_fetch/background_fetch.dart';
class NavigationScreen extends StatefulWidget {
const NavigationScreen({Key? key}) : super(key: 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 UpdateProvider updateProvider;
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],
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.
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)
print("[BackgroundFetch] TASK TIMEOUT taskId: $taskId");
BackgroundFetch.finish(taskId);
});
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();
initPlatformState();
HomeWidget.setAppGroupId('hu.filc.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());
// Get releases
updateProvider = Provider.of<UpdateProvider>(context, listen: false);
updateProvider.fetch();
// Initial sync
syncAll(context);
setupQuickActions();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangePlatformBrightness() {
if (settings.theme == ThemeMode.system) {
Brightness? brightness = 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);
// Show news
WidgetsBinding.instance.addPostFrameCallback((_) {
if (newsProvider.show) {
newsProvider.lock();
NewsView.show(newsProvider.news[newsProvider.state], context: context).then((value) => newsProvider.release());
}
});
handleQuickActions(context, (page) {
setPage(page);
_navigatorState.currentState?.pushReplacementNamed(page);
});
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,110 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'status_bar.i18n.dart';
class StatusBar extends StatefulWidget {
const StatusBar({Key? key}) : super(key: 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.network:
return "No connection".i18n;
default:
return "";
}
}
Color _statusColor(Status? status) {
switch (status) {
case Status.maintenance:
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,27 @@
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",
"No connection": "No connection",
},
"hu_hu": {
"Syncing data": "Adatok frissítése",
"KRETA Maintenance": "KRÉTA Karbantartás",
"No connection": "Nincs kapcsolat",
},
"de_de": {
"Syncing data": "Daten aktualisieren",
"KRETA Maintenance": "KRETA Wartung",
"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,61 @@
import 'dart:math';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_mobile_ui/common/empty.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/screens/news/news_tile.dart';
import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo_mobile_ui/screens/news/news_view.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/api/providers/news_provider.dart';
class NewsScreen extends StatelessWidget {
const NewsScreen({Key? key}) : super(key: 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", 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:filcnaplo/models/news.dart';
import 'package:filcnaplo/utils/format.dart';
class NewsTile extends StatelessWidget {
const NewsTile(this.news, {Key? key, this.onTap}) : super(key: key);
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,116 @@
import 'dart:io';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_mobile_ui/common/dialog_button.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo/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:filcnaplo_mobile_ui/screens/settings/settings_screen.i18n.dart';
class NewsView extends StatelessWidget {
const NewsView(this.news, {Key? key}) : super(key: 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 == '' || 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;
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,40 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
class AccountTile extends StatelessWidget {
const AccountTile({Key? key, this.onTap, this.onTapMenu, this.profileImage, this.name, this.username}) : super(key: key);
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,54 @@
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_card.dart';
import 'package:filcnaplo_mobile_ui/common/detail.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo_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, {Key? key}) : super(key: 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: ColorUtils.stringToColor(user.name),
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,81 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class SubjectIconGallery extends StatelessWidget {
const SubjectIconGallery({Key? key}) : super(key: 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"),
],
),
);
}
}
class SubjectIconItem extends StatelessWidget {
const SubjectIconItem(this.name, {Key? key}) : super(key: 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,61 @@
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({Key? key}) : super(key: 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 Filc Napló egy 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 Filc fejlesztői és üzemeltetői a tanulmányi adataidat semmilyen célból nem másolják, nem tárolják és harmadik félnek nem továbbítják. Ezeket így az e-Kréta Informatikai Zrt. kezeli, az ő 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. Ezt a beállításokban kikapcsolhatod.
Kérünk, hogy ha csak teheted, hagyd ezt a funkciót bekapcsolva.
Amikor az alkalmazás hibába ütközik, lehetőség van hibajelentés küldésére.
Ez személyes- vagy tanulmányi adatokat nem tartalmaz, viszont részletes információval szolgál a hibá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 Filc 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 belépéskor a GitHub API segítségével ellenőrzi, hogy elérhető-e új verzió, és kérésre innen is tölti le a telepítőt.
Ha az adataiddal kapcsolatban bármilyen kérdésed van (törlés, módosítás, adathordozás), keress minket a filcnaplo@filcnaplo.hu címen.
Az alkalmazás használatával jelzed, hogy ezt a tájékoztatót tudomásul vetted.
Utolsó módosítás: 2021. 09. 25.
""",
onOpen: (link) => launch(link.url,
customTabsOption: CustomTabsOption(
toolbarColor: Theme.of(context).scaffoldBackgroundColor,
showPageTitle: true,
)),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,548 @@
// ignore_for_file: prefer_function_declarations_over_variables
import 'dart:io';
import 'package:filcnaplo/helpers/quick_actions.dart';
import 'package:filcnaplo/icons/filc_icons.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart';
import 'package:filcnaplo_mobile_ui/common/filter_bar.dart';
import 'package:filcnaplo_mobile_ui/common/material_action_button.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.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:i18n_extension/i18n_widget.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo_mobile_ui/common/screens.i18n.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:filcnaplo/models/icon_pack.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/theme.dart';
class SettingsHelper {
static const Map<String, String> langMap = {"en": "🇬🇧 English", "hu": "🇭🇺 Magyar", "de": "🇩🇪 Deutsch"};
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 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(),
);
}
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 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(),
);
}
}
// Rounding modal
class RoundingSetting extends StatefulWidget {
const RoundingSetting({Key? key}) : super(key: key);
@override
_RoundingSettingState createState() => _RoundingSettingState();
}
class _RoundingSettingState extends State<RoundingSetting> {
late double rounding;
@override
void initState() {
super.initState();
rounding = Provider.of<SettingsProvider>(context, listen: false).rounding / 10;
}
@override
Widget build(BuildContext context) {
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: () {
Provider.of<SettingsProvider>(context, listen: false).update(rounding: (rounding * 10).toInt());
Navigator.of(context).maybePop();
},
),
),
]);
}
}
// Bell Delay Modal
class BellDelaySetting extends StatefulWidget {
const BellDelaySetting({Key? key}) : super(key: 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: false,
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: AppColors.of(context).filc,
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({Key? key}) : super(key: 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),
),
),
);
}),
),
),
]);
}
}

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

View File

@@ -0,0 +1,816 @@
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo/theme/colors/accent.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_mobile_ui/common/action_button.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo_mobile_ui/common/system_chrome.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/update/updates_view.dart';
import 'package:filcnaplo_mobile_ui/premium/components/active_sponsor_card.dart';
import 'package:filcnaplo_mobile_ui/premium/premium_button.dart';
import 'package:filcnaplo_mobile_ui/screens/news/news_screen.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/accounts/account_tile.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/accounts/account_view.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/debug/subject_icon_gallery.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/privacy_view.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_helper.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart' as tabs;
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:url_launcher/url_launcher.dart';
import 'settings_screen.i18n.dart';
import 'package:flutter/services.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/nickname.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/profile_pic.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/icon_pack.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/modify_subject_names.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({Key? key}) : super(key: key);
@override
_SettingsScreenState createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> with SingleTickerProviderStateMixin {
int devmodeCountdown = 3;
bool __ss = false; // secret settings
late UserProvider user;
late UpdateProvider updateProvider;
late SettingsProvider settings;
late KretaClient kretaClient;
late String firstName;
List<Widget> accountTiles = [];
late AnimationController _hideContainersController;
Future<void> restore() => Future.wait([
Provider.of<GradeProvider>(context, listen: false).restore(),
Provider.of<TimetableProvider>(context, listen: false).restoreUser(),
Provider.of<ExamProvider>(context, listen: false).restore(),
Provider.of<HomeworkProvider>(context, listen: false).restore(),
Provider.of<MessageProvider>(context, listen: false).restore(),
Provider.of<NoteProvider>(context, listen: false).restore(),
Provider.of<EventProvider>(context, listen: false).restore(),
Provider.of<AbsenceProvider>(context, listen: false).restore(),
Provider.of<KretaClient>(context, listen: false).refreshLogin(),
]);
void buildAccountTiles() {
accountTiles = [];
user.getUsers().forEach((account) {
if (account.id == user.id) return;
String _firstName;
List<String> _nameParts = user.displayName?.split(" ") ?? ["?"];
if (!settings.presentationMode) {
_firstName = _nameParts.length > 1 ? _nameParts[1] : _nameParts[0];
} else {
_firstName = "Béla";
}
accountTiles.add(AccountTile(
name: Text(!settings.presentationMode ? account.name : "Béla", style: const TextStyle(fontWeight: FontWeight.w500)),
username: Text(!settings.presentationMode ? account.username : "72469696969"),
profileImage: ProfileImage(
name: _firstName,
backgroundColor: !settings.presentationMode ? ColorUtils.stringToColor(account.name) : Theme.of(context).colorScheme.secondary,
role: account.role,
),
onTap: () {
user.setUser(account.id);
restore().then((_) => user.setUser(account.id));
Navigator.of(context).pop();
},
onTapMenu: () => _showBottomSheet(account),
));
});
}
void _showBottomSheet(User u) {
showBottomSheetMenu(context, items: [
BottomSheetMenuItem(
onPressed: () => AccountView.show(u, context: context),
icon: const Icon(FeatherIcons.user),
title: Text("personal_details".i18n),
),
BottomSheetMenuItem(
onPressed: () => _openDKT(u),
icon: Icon(FeatherIcons.grid, color: AppColors.of(context).teal),
title: Text("open_dkt".i18n),
),
const UserMenuNickname(),
const UserMenuProfilePic(),
// BottomSheetMenuItem(
// onPressed: () {},
// icon: Icon(FeatherIcons.camera),
// title: Text("edit_profile_picture".i18n),
// ),
// BottomSheetMenuItem(
// onPressed: () {},
// icon: Icon(FeatherIcons.trash2, color: AppColors.of(context).red),
// title: Text("remove_profile_picture".i18n),
// ),
]);
}
void _openDKT(User u) => tabs.launch("https://dkttanulo.e-kreta.hu/sso?id_token=${kretaClient.idToken}",
customTabsOption: tabs.CustomTabsOption(
toolbarColor: Theme.of(context).scaffoldBackgroundColor,
showPageTitle: true,
));
@override
void initState() {
super.initState();
_hideContainersController = AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
settings = Provider.of<SettingsProvider>(context);
updateProvider = Provider.of<UpdateProvider>(context);
kretaClient = Provider.of<KretaClient>(context);
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
if (!settings.presentationMode) {
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
} else {
firstName = "Béla";
}
String startPageTitle = SettingsHelper.localizedPageTitles()[settings.startPage] ?? "?";
String themeModeText = {ThemeMode.light: "light".i18n, ThemeMode.dark: "dark".i18n, ThemeMode.system: "system".i18n}[settings.theme] ?? "?";
String languageText = SettingsHelper.langMap[settings.language] ?? "?";
String vibrateTitle = {
VibrationStrength.off: "voff".i18n,
VibrationStrength.light: "vlight".i18n,
VibrationStrength.medium: "vmedium".i18n,
VibrationStrength.strong: "vstrong".i18n,
}[settings.vibrate] ??
"?";
buildAccountTiles();
if (settings.developerMode) devmodeCountdown = -1;
return AnimatedBuilder(
animation: _hideContainersController,
builder: (context, child) => Opacity(
opacity: 1 - _hideContainersController.value,
child: Column(
children: [
const SizedBox(height: 32.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
splashRadius: 32.0,
onPressed: () => _showBottomSheet(user.getUser(user.id ?? "")),
icon: Icon(FeatherIcons.moreVertical, color: AppColors.of(context).text.withOpacity(0.8)),
),
IconButton(
splashRadius: 26.0,
onPressed: () {
Navigator.of(context).pop();
},
icon: Icon(FeatherIcons.x, color: AppColors.of(context).text.withOpacity(0.8)),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ProfileImage(
heroTag: "profile",
radius: 36.0,
onTap: () => _showBottomSheet(user.getUser(user.id ?? "")),
name: firstName,
badge: updateProvider.available,
role: user.role,
profilePictureString: user.picture,
backgroundColor:
!settings.presentationMode ? ColorUtils.stringToColor(user.displayName ?? "?") : Theme.of(context).colorScheme.secondary,
),
),
Padding(
padding: const EdgeInsets.only(top: 4.0, bottom: 12.0),
child: GestureDetector(
onTap: () => _showBottomSheet(user.getUser(user.id ?? "")),
onDoubleTap: () => setState(() => __ss = true),
child: Text(
!settings.presentationMode ? (user.displayName ?? "?") : "Béla",
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w600, color: AppColors.of(context).text),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Panel(
child: Column(
children: [
// Account list
...accountTiles,
if (accountTiles.isNotEmpty)
Center(
child: Container(
margin: const EdgeInsets.only(top: 12.0, bottom: 4.0),
height: 3.0,
width: 75.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: AppColors.of(context).text.withOpacity(.25),
),
),
),
// Account settings
PanelButton(
onPressed: () {
Navigator.of(context).pushNamed("login_back").then((value) {
setSystemChrome(context);
});
},
title: Text("add_user".i18n),
leading: const Icon(FeatherIcons.userPlus),
),
PanelButton(
onPressed: () async {
String? userId = user.id;
if (userId == null) return;
// Delete User
user.removeUser(userId);
await Provider.of<DatabaseProvider>(context, listen: false).store.removeUser(userId);
// If no other Users left, go back to LoginScreen
if (user.getUsers().isNotEmpty) {
user.setUser(user.getUsers().first.id);
restore().then((_) => user.setUser(user.getUsers().first.id));
} else {
Navigator.of(context).pushNamedAndRemoveUntil("login", (_) => false);
}
},
title: Text("log_out".i18n),
leading: Icon(FeatherIcons.logOut, color: AppColors.of(context).red),
),
],
),
),
),
// Updates
if (updateProvider.available)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Panel(
child: PanelButton(
onPressed: () => _openUpdates(context),
title: Text("update_available".i18n),
leading: const Icon(FeatherIcons.download),
trailing: Text(
updateProvider.releases.first.tag,
style: TextStyle(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
),
),
),
// const Padding(
// padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
// child: PremiumBannerButton(),
// ),
if (!context.watch<PremiumProvider>().hasPremium)
const ClipRect(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 12.0),
child: PremiumButton(),
),
)
else
const Padding(
padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: ActiveSponsorCard(),
),
// General Settings
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Panel(
title: Text("general".i18n),
child: Column(
children: [
PanelButton(
onPressed: () {
SettingsHelper.language(context);
setState(() {});
},
title: Text("language".i18n),
leading: const Icon(FeatherIcons.globe),
trailing: Text(languageText),
),
PanelButton(
onPressed: () {
SettingsHelper.startPage(context);
setState(() {});
},
title: Text("startpage".i18n),
leading: const Icon(FeatherIcons.play),
trailing: Text(startPageTitle.capital()),
),
PanelButton(
onPressed: () {
SettingsHelper.rounding(context);
setState(() {});
},
title: Text("rounding".i18n),
leading: const Icon(FeatherIcons.gitCommit),
trailing: Text((settings.rounding / 10).toStringAsFixed(1)),
),
PanelButton(
onPressed: () {
SettingsHelper.vibrate(context);
setState(() {});
},
title: Text("vibrate".i18n),
leading: const Icon(FeatherIcons.radio),
trailing: Text(vibrateTitle),
),
PanelButton(
padding: const EdgeInsets.only(left: 14.0),
onPressed: () {
SettingsHelper.bellDelay(context);
setState(() {});
},
title: Text(
"bell_delay".i18n,
style: TextStyle(color: AppColors.of(context).text.withOpacity(settings.bellDelayEnabled ? 1.0 : .5)),
),
leading: settings.bellDelayEnabled
? const Icon(FeatherIcons.bell)
: Icon(FeatherIcons.bellOff, color: AppColors.of(context).text.withOpacity(.25)),
trailingDivider: true,
trailing: Switch(
onChanged: (v) => settings.update(bellDelayEnabled: v),
value: settings.bellDelayEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
),
],
),
),
),
if (kDebugMode)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Panel(
title: const Text("Debug"),
child: Column(
children: [
PanelButton(
title: const Text("Subject Icon Gallery"),
leading: const Icon(CupertinoIcons.rectangle_3_offgrid_fill),
trailing: const Icon(Icons.arrow_forward),
onPressed: () {
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(builder: (context) => const SubjectIconGallery()),
);
},
)
],
),
),
),
// Secret Settings
if (__ss)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Panel(
title: Text("secret".i18n),
child: Column(
children: [
// Good student mode
Material(
type: MaterialType.transparency,
child: SwitchListTile(
contentPadding: const EdgeInsets.only(left: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
title: Text("goodstudent".i18n, style: const TextStyle(fontWeight: FontWeight.w500)),
onChanged: (v) {
if (v) {
showDialog(
context: context,
builder: (context) => WillPopScope(
onWillPop: () async => false,
child: AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
title: Text("attention".i18n),
content: Text("goodstudent_disclaimer".i18n),
actions: [
ActionButton(
label: "understand".i18n,
onTap: () {
Navigator.of(context).pop();
settings.update(goodStudent: v);
Provider.of<GradeProvider>(context, listen: false).convertBySettings();
})
],
),
),
);
} else {
settings.update(goodStudent: v);
Provider.of<GradeProvider>(context, listen: false).convertBySettings();
}
},
value: settings.goodStudent,
activeColor: Theme.of(context).colorScheme.secondary,
),
),
// Presentation mode
Material(
type: MaterialType.transparency,
child: SwitchListTile(
contentPadding: const EdgeInsets.only(left: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
title: const Text("Presentation Mode", style: TextStyle(fontWeight: FontWeight.w500)),
onChanged: (v) => settings.update(presentationMode: v),
value: settings.presentationMode,
activeColor: Theme.of(context).colorScheme.secondary,
),
),
],
),
),
),
// Theme Settings
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Panel(
title: Text("appearance".i18n),
child: Column(
children: [
PanelButton(
onPressed: () {
SettingsHelper.theme(context);
setState(() {});
},
title: Text("theme".i18n),
leading: const Icon(FeatherIcons.sun),
trailing: Text(themeModeText),
),
PanelButton(
onPressed: () async {
await _hideContainersController.forward();
SettingsHelper.accentColor(context);
setState(() {});
_hideContainersController.reset();
},
title: Text("color".i18n),
leading: const Icon(FeatherIcons.droplet),
trailing: Container(
width: 12.0,
height: 12.0,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
shape: BoxShape.circle,
),
),
),
PanelButton(
onPressed: () {
SettingsHelper.gradeColors(context);
setState(() {});
},
title: Text("grade_colors".i18n),
leading: const Icon(FeatherIcons.star),
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: settings.gradeColors[i],
),
),
),
),
),
Material(
type: MaterialType.transparency,
child: SwitchListTile(
contentPadding: const EdgeInsets.only(left: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
title: Row(
children: [
Icon(
FeatherIcons.barChart,
color: settings.graphClassAvg ? Theme.of(context).colorScheme.secondary : AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 24.0),
Expanded(
child: Text(
"graph_class_avg".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(settings.graphClassAvg ? 1.0 : .5),
),
),
),
],
),
onChanged: (v) => settings.update(graphClassAvg: v),
value: settings.graphClassAvg,
activeColor: Theme.of(context).colorScheme.secondary,
),
),
const PremiumIconPackSelector(),
],
),
),
),
// Notifications
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Panel(
title: Text("notifications".i18n),
child: Material(
type: MaterialType.transparency,
child: SwitchListTile(
contentPadding: const EdgeInsets.only(left: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
title: Row(
children: [
Icon(
Icons.newspaper_outlined,
color: settings.newsEnabled ? Theme.of(context).colorScheme.secondary : AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 24.0),
Expanded(
child: Text(
"news".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(settings.newsEnabled ? 1.0 : .5),
),
),
),
],
),
onChanged: (v) => settings.update(newsEnabled: v),
value: settings.newsEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
),
),
),
// Extras
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Panel(
title: Text("extras".i18n),
child: Column(
children: [
Material(
type: MaterialType.transparency,
child: SwitchListTile(
contentPadding: const EdgeInsets.only(left: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
title: Row(
children: [
Icon(
FeatherIcons.gift,
color: settings.gradeOpeningFun ? Theme.of(context).colorScheme.secondary : AppColors.of(context).text.withOpacity(.25),
),
const SizedBox(width: 24.0),
Expanded(
child: Text(
"surprise_grades".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(settings.gradeOpeningFun ? 1.0 : .5),
),
),
),
],
),
onChanged: (v) => settings.update(gradeOpeningFun: v),
value: settings.gradeOpeningFun,
activeColor: Theme.of(context).colorScheme.secondary,
),
),
MenuRenamedSubjects(
settings: settings,
),
],
),
),
),
// About
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Panel(
title: Text("about".i18n),
child: Column(children: [
PanelButton(
leading: const Icon(FeatherIcons.atSign),
title: const Text("Discord"),
onPressed: () => launchUrl(Uri.parse("https://filcnaplo.hu/discord"), mode: LaunchMode.externalApplication),
),
PanelButton(
leading: const Icon(FeatherIcons.globe),
title: const Text("www.filcnaplo.hu"),
onPressed: () => launchUrl(Uri.parse("https://filcnaplo.hu"), mode: LaunchMode.externalApplication),
),
PanelButton(
leading: const Icon(FeatherIcons.github),
title: const Text("Github"),
onPressed: () => launchUrl(Uri.parse("https://github.com/filc"), mode: LaunchMode.externalApplication),
),
PanelButton(
leading: const Icon(FeatherIcons.mail),
title: Text("news".i18n),
onPressed: () => _openNews(context),
),
PanelButton(
leading: const Icon(FeatherIcons.lock),
title: Text("privacy".i18n),
onPressed: () => _openPrivacy(context),
),
PanelButton(
leading: const Icon(FeatherIcons.award),
title: Text("licenses".i18n),
onPressed: () => showLicensePage(context: context),
),
Tooltip(
message: "data_collected".i18n,
padding: const EdgeInsets.all(4.0),
textStyle: TextStyle(fontWeight: FontWeight.w500, color: AppColors.of(context).text),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.background),
child: Material(
type: MaterialType.transparency,
child: SwitchListTile(
contentPadding: const EdgeInsets.only(left: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
secondary: Icon(
FeatherIcons.barChart2,
color: settings.xFilcId != "none" ? Theme.of(context).colorScheme.secondary : AppColors.of(context).text.withOpacity(.25),
),
title: Text(
"Analytics".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(settings.xFilcId != "none" ? 1.0 : .5),
),
),
subtitle: Text(
"Anonymous Usage Analytics".i18n,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(settings.xFilcId != "none" ? .5 : .2),
),
),
onChanged: (v) {
String newId;
if (v == false) {
newId = "none";
} else if (settings.xFilcId == "none") {
newId = SettingsProvider.defaultSettings().xFilcId;
} else {
newId = settings.xFilcId;
}
settings.update(xFilcId: newId);
},
value: settings.xFilcId != "none",
activeColor: Theme.of(context).colorScheme.secondary,
),
),
),
]),
),
),
if (settings.developerMode)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Panel(
title: const Text("Developer Settings"),
child: Column(
children: [
Material(
type: MaterialType.transparency,
child: SwitchListTile(
contentPadding: const EdgeInsets.only(left: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
title: const Text("Developer Mode", style: TextStyle(fontWeight: FontWeight.w500)),
onChanged: (v) => settings.update(developerMode: false),
value: settings.developerMode,
activeColor: Theme.of(context).colorScheme.secondary,
),
),
PanelButton(
leading: const Icon(FeatherIcons.copy),
title: const Text("Copy JWT"),
onPressed: () => Clipboard.setData(ClipboardData(text: Provider.of<KretaClient>(context, listen: false).accessToken)),
),
if (Provider.of<PremiumProvider>(context, listen: false).hasPremium)
PanelButton(
leading: const Icon(FeatherIcons.key),
title: const Text("Remove Premium"),
onPressed: () {
Provider.of<PremiumProvider>(context, listen: false).activate(removePremium: true);
settings.update(accentColor: AccentColor.filc, store: true);
Provider.of<ThemeModeObserver>(context, listen: false).changeTheme(settings.theme);
},
),
],
),
),
),
SafeArea(
top: false,
child: Center(
child: GestureDetector(
child: const Panel(title: Text("v" + String.fromEnvironment("APPVER", defaultValue: "?"))),
onTap: () {
if (devmodeCountdown > 0) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
duration: const Duration(milliseconds: 200),
content: Text("You are $devmodeCountdown taps away from Developer Mode."),
));
setState(() => devmodeCountdown--);
} else if (devmodeCountdown == 0) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("Developer Mode successfully activated."),
));
settings.update(developerMode: true);
setState(() => devmodeCountdown--);
}
},
),
),
),
],
),
),
);
}
void _openNews(BuildContext context) =>
Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute(builder: (context) => const NewsScreen()));
void _openUpdates(BuildContext context) => UpdateView.show(updateProvider.releases.first, context: context);
void _openPrivacy(BuildContext context) => PrivacyView.show(context);
}

View File

@@ -0,0 +1,194 @@
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 DKT",
"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",
"notifications": "Notifications",
"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. Android), App version (eg. 3.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":
"Filc Napló® Informatikai Zrt. 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",
},
"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",
"notifications": "Értesítések",
"news": "Hírek",
"extras": "Extrák",
"about": "Névjegy",
"supporters": "Támogatók",
"privacy": "Adatvédelmi irányelvek",
"licenses": "Licenszek",
"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. Android), App verzió (pl. 3.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 Filc Napló® Informatikai Zrt. 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 magadadat 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",
},
"de_de": {
"personal_details": "Persönliche Angaben",
"open_dkt": "Öffnen DKT",
"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",
"notifications": "Benachrichtigungen",
"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. Android), App version (z.B. 3.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": "Same in English.",
"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",
},
};
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);
}