remelem mukszik
This commit is contained in:
200
filcnaplo_mobile_ui/lib/screens/error_report_screen.dart
Executable file
200
filcnaplo_mobile_ui/lib/screens/error_report_screen.dart
Executable 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)))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
45
filcnaplo_mobile_ui/lib/screens/error_report_screen.i18n.dart
Executable file
45
filcnaplo_mobile_ui/lib/screens/error_report_screen.i18n.dart
Executable 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);
|
||||
}
|
||||
64
filcnaplo_mobile_ui/lib/screens/error_screen.dart
Executable file
64
filcnaplo_mobile_ui/lib/screens/error_screen.dart
Executable 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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
29
filcnaplo_mobile_ui/lib/screens/login/login_button.dart
Executable file
29
filcnaplo_mobile_ui/lib/screens/login/login_button.dart
Executable 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
97
filcnaplo_mobile_ui/lib/screens/login/login_input.dart
Executable file
97
filcnaplo_mobile_ui/lib/screens/login/login_input.dart
Executable 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
21
filcnaplo_mobile_ui/lib/screens/login/login_route.dart
Executable file
21
filcnaplo_mobile_ui/lib/screens/login/login_route.dart
Executable 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
303
filcnaplo_mobile_ui/lib/screens/login/login_screen.dart
Executable file
303
filcnaplo_mobile_ui/lib/screens/login/login_screen.dart
Executable 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));
|
||||
}
|
||||
}
|
||||
51
filcnaplo_mobile_ui/lib/screens/login/login_screen.i18n.dart
Executable file
51
filcnaplo_mobile_ui/lib/screens/login/login_screen.i18n.dart
Executable 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);
|
||||
}
|
||||
117
filcnaplo_mobile_ui/lib/screens/login/school_input/school_input.dart
Executable file
117
filcnaplo_mobile_ui/lib/screens/login/school_input/school_input.dart
Executable 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;
|
||||
}
|
||||
72
filcnaplo_mobile_ui/lib/screens/login/school_input/school_input_overlay.dart
Executable file
72
filcnaplo_mobile_ui/lib/screens/login/school_input/school_input_overlay.dart
Executable 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
64
filcnaplo_mobile_ui/lib/screens/login/school_input/school_input_tile.dart
Executable file
64
filcnaplo_mobile_ui/lib/screens/login/school_input/school_input_tile.dart
Executable 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
25
filcnaplo_mobile_ui/lib/screens/login/school_input/school_search.dart
Executable file
25
filcnaplo_mobile_ui/lib/screens/login/school_input/school_search.dart
Executable 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;
|
||||
}
|
||||
27
filcnaplo_mobile_ui/lib/screens/navigation/nabar.dart
Executable file
27
filcnaplo_mobile_ui/lib/screens/navigation/nabar.dart
Executable 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
59
filcnaplo_mobile_ui/lib/screens/navigation/navbar_item.dart
Executable file
59
filcnaplo_mobile_ui/lib/screens/navigation/navbar_item.dart
Executable 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
25
filcnaplo_mobile_ui/lib/screens/navigation/navigation_route.dart
Executable file
25
filcnaplo_mobile_ui/lib/screens/navigation/navigation_route.dart
Executable 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);
|
||||
}
|
||||
}
|
||||
38
filcnaplo_mobile_ui/lib/screens/navigation/navigation_route_handler.dart
Executable file
38
filcnaplo_mobile_ui/lib/screens/navigation/navigation_route_handler.dart
Executable 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
302
filcnaplo_mobile_ui/lib/screens/navigation/navigation_screen.dart
Executable file
302
filcnaplo_mobile_ui/lib/screens/navigation/navigation_screen.dart
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
filcnaplo_mobile_ui/lib/screens/navigation/status_bar.dart
Executable file
110
filcnaplo_mobile_ui/lib/screens/navigation/status_bar.dart
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
filcnaplo_mobile_ui/lib/screens/navigation/status_bar.i18n.dart
Executable file
27
filcnaplo_mobile_ui/lib/screens/navigation/status_bar.i18n.dart
Executable 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);
|
||||
}
|
||||
61
filcnaplo_mobile_ui/lib/screens/news/news_screen.dart
Executable file
61
filcnaplo_mobile_ui/lib/screens/news/news_screen.dart
Executable 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"),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
30
filcnaplo_mobile_ui/lib/screens/news/news_tile.dart
Executable file
30
filcnaplo_mobile_ui/lib/screens/news/news_tile.dart
Executable 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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
116
filcnaplo_mobile_ui/lib/screens/news/news_view.dart
Executable file
116
filcnaplo_mobile_ui/lib/screens/news/news_view.dart
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
filcnaplo_mobile_ui/lib/screens/settings/accounts/account_tile.dart
Executable file
40
filcnaplo_mobile_ui/lib/screens/settings/accounts/account_tile.dart
Executable 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
54
filcnaplo_mobile_ui/lib/screens/settings/accounts/account_view.dart
Executable file
54
filcnaplo_mobile_ui/lib/screens/settings/accounts/account_view.dart
Executable 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(", ")),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
filcnaplo_mobile_ui/lib/screens/settings/accounts/account_view.i18n.dart
Executable file
33
filcnaplo_mobile_ui/lib/screens/settings/accounts/account_view.i18n.dart
Executable 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);
|
||||
}
|
||||
81
filcnaplo_mobile_ui/lib/screens/settings/debug/subject_icon_gallery.dart
Executable file
81
filcnaplo_mobile_ui/lib/screens/settings/debug/subject_icon_gallery.dart
Executable 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
61
filcnaplo_mobile_ui/lib/screens/settings/privacy_view.dart
Executable file
61
filcnaplo_mobile_ui/lib/screens/settings/privacy_view.dart
Executable 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,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
548
filcnaplo_mobile_ui/lib/screens/settings/settings_helper.dart
Executable file
548
filcnaplo_mobile_ui/lib/screens/settings/settings_helper.dart
Executable 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
21
filcnaplo_mobile_ui/lib/screens/settings/settings_route.dart
Executable file
21
filcnaplo_mobile_ui/lib/screens/settings/settings_route.dart
Executable 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
816
filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart
Executable file
816
filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart
Executable 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);
|
||||
}
|
||||
194
filcnaplo_mobile_ui/lib/screens/settings/settings_screen.i18n.dart
Executable file
194
filcnaplo_mobile_ui/lib/screens/settings/settings_screen.i18n.dart
Executable 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);
|
||||
}
|
||||
Reference in New Issue
Block a user