remelem mukszik

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

29
filcnaplo_mobile_ui/LICENSE Executable file
View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, Filc
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

0
filcnaplo_mobile_ui/README.md Executable file
View File

View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class ActionButton extends StatelessWidget {
const ActionButton({Key? key, required this.label, this.activeColor, this.onTap}) : super(key: key);
final Color? activeColor;
final void Function()? onTap;
final String label;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 6.0, bottom: 6.0, right: 3.0),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(6.0),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
height: 32.0,
decoration: BoxDecoration(
color: (activeColor ?? Theme.of(context).colorScheme.secondary).withOpacity(0.25),
borderRadius: BorderRadius.circular(6.0),
),
padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0),
child: Center(
child: Text(label,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.w600, color: activeColor ?? Theme.of(context).colorScheme.secondary))),
),
),
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:flutter/material.dart';
import 'package:i18n_extension/i18n_widget.dart';
class AverageDisplay extends StatelessWidget {
const AverageDisplay({Key? key, this.average = 0.0, this.border = false}) : super(key: key);
final double average;
final bool border;
@override
Widget build(BuildContext context) {
Color color = average == 0.0 ? AppColors.of(context).text.withOpacity(.8) : gradeColor(context: context, value: average);
String averageText = average.toStringAsFixed(2);
if (I18n.of(context).locale.languageCode != "en") averageText = averageText.replaceAll(".", ",");
return Container(
width: border ? 57.0 : 54.0,
padding: EdgeInsets.symmetric(horizontal: 8.0 - (border ? 2 : 0), vertical: 6.0 - (border ? 2 : 0)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(45.0),
border: border ? Border.fromBorderSide(BorderSide(color: color.withOpacity(.5), width: 3.0)) : null,
color: !border ? color.withOpacity(average == 0.0 ? .15 : .25) : null,
),
child: Text(
average == 0.0 ? "-" : averageText,
textAlign: TextAlign.center,
style: TextStyle(color: color, fontWeight: FontWeight.w600),
maxLines: 1,
),
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class BottomCard extends StatelessWidget {
const BottomCard({Key? key, this.child}) : super(key: key);
final Widget? child;
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14.0),
color: Theme.of(context).colorScheme.background,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 42.0,
height: 4.0,
margin: const EdgeInsets.only(top: 12.0, bottom: 4.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(45.0),
color: AppColors.of(context).text.withOpacity(0.10),
),
),
if (child != null) child!,
],
),
),
),
);
}
}
Future<void> showBottomCard({
required BuildContext context,
Widget? child,
bool rootNavigator = true,
}) async =>
await showModalBottomSheet(
backgroundColor: const Color(0x00000000),
useRootNavigator: rootNavigator,
elevation: 0,
isDismissible: true,
context: context,
builder: (context) => BottomCard(child: child));

View File

@ -0,0 +1,22 @@
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart';
import 'package:flutter/material.dart';
class BottomSheetMenu extends StatelessWidget {
const BottomSheetMenu({Key? key, this.items = const []}) : super(key: key);
final List<Widget> items;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: items,
),
);
}
}
void showBottomSheetMenu(BuildContext context, {List<Widget> items = const []}) =>
showRoundedModalBottomSheet(context, child: BottomSheetMenu(items: items));

View File

@ -0,0 +1,19 @@
import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
import 'package:flutter/material.dart';
class BottomSheetMenuItem extends StatelessWidget {
const BottomSheetMenuItem({Key? key, required this.onPressed, required this.title, this.icon}) : super(key: key);
final void Function()? onPressed;
final Widget? title;
final Widget? icon;
@override
Widget build(BuildContext context) {
return PanelButton(
onPressed: onPressed,
leading: icon,
title: title,
);
}
}

View File

@ -0,0 +1,70 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class RoundedBottomSheet extends StatelessWidget {
const RoundedBottomSheet({Key? key, this.child, this.borderRadius = 12.0, this.shrink = true, this.showHandle = true}) : super(key: key);
final Widget? child;
final double borderRadius;
final bool shrink;
final bool showHandle;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadius),
topRight: Radius.circular(borderRadius),
),
),
child: SafeArea(
child: Column(
mainAxisSize: shrink ? MainAxisSize.min : MainAxisSize.max,
children: [
if (showHandle)
Container(
width: 42.0,
height: 4.0,
margin: const EdgeInsets.only(top: 12.0, bottom: 4.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(45.0),
color: AppColors.of(context).text.withOpacity(0.10),
),
),
if (child != null) child!,
SizedBox(height: MediaQuery.of(context).padding.bottom),
],
),
),
);
}
}
Future<T?> showRoundedModalBottomSheet<T>(
BuildContext context, {
required Widget child,
bool rootNavigator = true,
}) async {
return await showModalBottomSheet<T>(
context: context,
backgroundColor: const Color(0x00000000),
elevation: 0,
isDismissible: true,
useRootNavigator: rootNavigator,
builder: (context) => RoundedBottomSheet(child: child));
}
PersistentBottomSheetController<T> showRoundedBottomSheet<T>(
BuildContext context, {
required Widget child,
}) {
return showBottomSheet<T>(
context: context,
backgroundColor: const Color(0x00000000),
elevation: 12.0,
builder: (context) => RoundedBottomSheet(child: child),
);
}

View File

@ -0,0 +1,34 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
// ignore: non_constant_identifier_names
SnackBar CustomSnackBar({
required Widget content,
required BuildContext context,
Brightness? brightness,
Color? backgroundColor,
Duration? duration,
}) {
// backgroundColor > Brightness > Theme Background
Color _backgroundColor = backgroundColor ?? (AppColors.fromBrightness(brightness ?? Theme.of(context).brightness).highlight);
Color textColor = AppColors.fromBrightness(brightness ?? Theme.of(context).brightness).text;
return SnackBar(
duration: duration ?? const Duration(seconds: 4),
content: Container(
decoration: BoxDecoration(
color: _backgroundColor,
borderRadius: BorderRadius.circular(6.0),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(.15), blurRadius: 4.0)],
),
padding: const EdgeInsets.all(12.0),
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor, fontWeight: FontWeight.w500),
child: content,
),
),
backgroundColor: const Color(0x00000000),
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
);
}

View File

@ -0,0 +1,31 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class Detail extends StatelessWidget {
const Detail({Key? key, required this.title, required this.description, this.maxLines = 3}) : super(key: key);
final String title;
final String description;
final int? maxLines;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 18.0),
child: SelectableText.rich(
TextSpan(
text: "$title: ",
style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.of(context).text),
children: [
TextSpan(
text: description,
style: TextStyle(fontWeight: FontWeight.w500, color: AppColors.of(context).text.withOpacity(0.85)),
),
],
),
minLines: 1,
maxLines: maxLines,
),
);
}
}

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class DialogButton extends StatelessWidget {
const DialogButton({Key? key, required this.label, this.onTap}) : super(key: key);
final String label;
final Function()? onTap;
@override
Widget build(BuildContext context) {
return RawMaterialButton(
onPressed: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
child: Text(
label.toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.secondary,
),
),
);
}
}

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class Dot extends StatelessWidget {
final Color color;
final double size;
const Dot({Key? key, this.color = Colors.grey, this.size = 16.0}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
width: size,
height: size,
);
}
}

View File

@ -0,0 +1,45 @@
import 'dart:math';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
List<String> faces = [
"(·.·)",
"(≥o≤)",
"(·_·)",
"(˚Δ˚)b",
"(^-^*)",
"(='X'=)",
"(>_<)",
"(;-;)",
"\\(^Д^)/",
"\\(o_o)/",
];
class Empty extends StatelessWidget {
const Empty({Key? key, this.subtitle}) : super(key: key);
final String? subtitle;
@override
Widget build(BuildContext context) {
// make the face randomness a bit more constant (to avoid strokes)
int index = Random(DateTime.now().minute).nextInt(faces.length);
return Center(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text.rich(
TextSpan(
text: faces[index],
style: TextStyle(fontSize: 32.0, fontWeight: FontWeight.w500, color: AppColors.of(context).text.withOpacity(.75)),
children: subtitle != null
? [TextSpan(text: "\n" + subtitle!, style: TextStyle(fontSize: 18.0, height: 2.0, color: AppColors.of(context).text.withOpacity(.5)))]
: [],
),
textAlign: TextAlign.center,
),
),
);
}
}

View File

@ -0,0 +1,117 @@
import 'dart:math';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class FilterBar extends StatefulWidget implements PreferredSizeWidget {
const FilterBar({
Key? key,
required this.items,
required this.controller,
this.onTap,
this.padding = const EdgeInsets.symmetric(horizontal: 24.0),
this.disableFading = false,
this.scrollable = true,
this.censored = false,
}) : assert(items.length == controller.length),
super(key: key);
final List<Widget> items;
final TabController controller;
final EdgeInsetsGeometry padding;
final Function(int)? onTap;
final bool disableFading;
final bool scrollable;
final bool censored;
@override
final Size preferredSize = const Size.fromHeight(42.0);
@override
State<FilterBar> createState() => _FilterBarState();
}
class _FilterBarState extends State<FilterBar> {
List<double> censoredItemsWidth = [];
@override
void initState() {
super.initState();
censoredItemsWidth = List.generate(widget.items.length, (index) => 25 + Random().nextDouble() * 50).toList();
}
@override
Widget build(BuildContext context) {
final tabbar = TabBar(
controller: widget.controller,
isScrollable: widget.scrollable,
physics: const BouncingScrollPhysics(),
// Label
labelStyle: Theme.of(context).textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w600,
fontSize: 15.0,
),
labelPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
labelColor: Theme.of(context).colorScheme.secondary,
unselectedLabelColor: AppColors.of(context).text.withOpacity(0.65),
// Indicator
indicatorSize: TabBarIndicatorSize.tab,
indicatorPadding: const EdgeInsets.symmetric(vertical: 8.0),
indicator: BoxDecoration(
color: Theme.of(context).colorScheme.secondary.withOpacity(0.25),
borderRadius: BorderRadius.circular(45.0),
),
overlayColor: MaterialStateProperty.all(const Color(0x00000000)),
// Tabs
padding: EdgeInsets.zero,
tabs: widget.censored
? censoredItemsWidth
.map(
(e) => Container(
width: e,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
),
)
.toList()
: widget.items,
onTap: widget.onTap,
);
return Container(
width: MediaQuery.of(context).size.width,
height: 48.0,
padding: widget.padding,
child: widget.disableFading
? tabbar
: AnimatedBuilder(
animation: widget.controller.animation!,
builder: (ctx, child) {
// avoid fading over selected tab
return ShaderMask(
shaderCallback: (Rect bounds) {
final Color bg = Theme.of(context).scaffoldBackgroundColor;
final double index = widget.controller.animation!.value;
return LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [
index < 0.2 ? Colors.transparent : bg,
Colors.transparent,
Colors.transparent,
index > widget.controller.length - 1.2 ? Colors.transparent : bg
], stops: const [
0,
0.1,
0.9,
1
]).createShader(bounds);
},
blendMode: BlendMode.dstOut,
child: child);
},
child: tabbar,
),
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class HeroDialogRoute<T> extends PageRoute<T> {
HeroDialogRoute({required this.builder}) : super();
final WidgetBuilder builder;
@override
bool get opaque => false;
@override
bool get barrierDismissible => true;
@override
String? get barrierLabel => "livecard";
@override
Duration get transitionDuration => const Duration(milliseconds: 250);
@override
bool get maintainState => true;
@override
Color get barrierColor => Colors.black38;
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child);
}
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return builder(context);
}
}

View File

@ -0,0 +1,133 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/utils/format.dart';
class HeroScrollView extends StatefulWidget {
const HeroScrollView(
{Key? key,
required this.child,
required this.title,
required this.icon,
this.italic = false,
this.navBarItems = const [],
this.onClose,
this.iconSize = 64.0,
this.scrollController})
: super(key: key);
final Widget child;
final String title;
final IconData? icon;
final List<Widget> navBarItems;
final VoidCallback? onClose;
final double iconSize;
final ScrollController? scrollController;
final bool italic;
@override
_HeroScrollViewState createState() => _HeroScrollViewState();
}
class _HeroScrollViewState extends State<HeroScrollView> {
late ScrollController _scrollController;
bool showBarTitle = false;
@override
void initState() {
super.initState();
_scrollController = widget.scrollController ?? ScrollController();
_scrollController.addListener(() {
if (_scrollController.offset > 42.0) {
if (showBarTitle == false) setState(() => showBarTitle = true);
} else {
if (showBarTitle == true) setState(() => showBarTitle = false);
}
});
}
@override
void dispose() {
super.dispose();
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return NestedScrollView(
controller: _scrollController,
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
headerSliverBuilder: (context, _) => [
SliverAppBar(
pinned: true,
floating: false,
snap: false,
centerTitle: false,
titleSpacing: 0,
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
title: AnimatedOpacity(
opacity: showBarTitle ? 1.0 : 0.0,
child: Row(
children: [
Icon(widget.icon, color: AppColors.of(context).text.withOpacity(.8)),
const SizedBox(width: 8.0),
Expanded(
child: Text(
widget.title.capital(),
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: TextStyle(
color: AppColors.of(context).text, fontWeight: FontWeight.w500, fontStyle: widget.italic ? FontStyle.italic : null),
),
),
],
),
duration: const Duration(milliseconds: 200)),
leading: BackButton(
color: AppColors.of(context).text,
onPressed: () {
if (widget.onClose != null) {
widget.onClose!();
} else {
Navigator.of(context).pop();
}
}),
actions: widget.navBarItems,
expandedHeight: 124.0,
stretch: true,
flexibleSpace: FlexibleSpaceBar(
background: Stack(
children: [
Center(
child: Icon(
widget.icon,
size: widget.iconSize,
color: AppColors.of(context).text.withOpacity(.15),
),
),
Container(
alignment: Alignment.center,
margin: const EdgeInsets.only(top: 82),
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(
widget.title.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 36.0,
color: AppColors.of(context).text.withOpacity(.9),
fontStyle: widget.italic ? FontStyle.italic : null,
fontWeight: FontWeight.bold),
),
),
],
),
),
),
],
body: widget.child,
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:flutter/material.dart';
class MaterialActionButton extends StatelessWidget {
const MaterialActionButton({
Key? key,
required this.child,
this.onPressed,
this.backgroundColor,
}) : super(key: key);
final Widget child;
final Function()? onPressed;
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
return RawMaterialButton(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
shape: const StadiumBorder(),
child: DefaultTextStyle(
child: child,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w600,
color: backgroundColor != null ? ColorUtils.foregroundColor(backgroundColor!) : null,
),
),
fillColor: backgroundColor ?? AppColors.of(context).text.withOpacity(.15),
elevation: 0,
highlightElevation: 0,
onPressed: onPressed,
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class NewContentIndicator extends StatelessWidget {
const NewContentIndicator({Key? key, this.size = 64.0}) : super(key: key);
final double size;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
alignment: Alignment.topRight,
width: size,
height: size,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: size / 3.0,
width: size / 3.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Theme.of(context).scaffoldBackgroundColor, width: size / 20.0),
),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: AppColors.of(context).red,
shape: BoxShape.circle,
),
),
),
);
}
}

View File

@ -0,0 +1,135 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class Panel extends StatelessWidget {
const Panel({Key? key, this.child, this.title, this.padding, this.hasShadow = true}) : super(key: key);
final Widget? child;
final Widget? title;
final EdgeInsetsGeometry? padding;
final bool hasShadow;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Panel Title
if (title != null) PanelTitle(title: title!),
// Panel Body
if (child != null)
Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
color: Theme.of(context).colorScheme.background,
boxShadow: [
if (hasShadow)
BoxShadow(
offset: const Offset(0, 21),
blurRadius: 23.0,
color: Theme.of(context).shadowColor,
)
],
),
padding: padding ?? const EdgeInsets.all(8.0),
child: child,
),
],
);
}
}
class PanelTitle extends StatelessWidget {
const PanelTitle({Key? key, required this.title}) : super(key: key);
final Widget title;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 14.0, bottom: 8.0),
child: DefaultTextStyle(
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600, color: AppColors.of(context).text.withOpacity(0.65)),
child: title,
),
);
}
}
class PanelHeader extends StatelessWidget {
const PanelHeader({Key? key, required this.padding}) : super(key: key);
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: padding,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0)),
color: Theme.of(context).colorScheme.background,
boxShadow: [
BoxShadow(
offset: const Offset(0, 21),
blurRadius: 23.0,
color: Theme.of(context).shadowColor,
)
],
),
);
}
}
class PanelBody extends StatelessWidget {
const PanelBody({Key? key, this.child, this.padding}) : super(key: key);
final Widget? child;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
boxShadow: [
BoxShadow(
offset: const Offset(0, 21),
blurRadius: 23.0,
color: Theme.of(context).shadowColor,
)
],
),
padding: padding,
child: child,
);
}
}
class PanelFooter extends StatelessWidget {
const PanelFooter({Key? key, required this.padding}) : super(key: key);
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: padding,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16.0), bottomRight: Radius.circular(16.0)),
color: Theme.of(context).colorScheme.background,
boxShadow: [
BoxShadow(
offset: const Offset(0, 21),
blurRadius: 23.0,
color: Theme.of(context).shadowColor,
)
],
),
);
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
class PanelActionButton extends StatelessWidget {
const PanelActionButton({
Key? key,
this.onPressed,
this.padding = const EdgeInsets.symmetric(horizontal: 14.0),
this.leading,
this.title,
this.trailing,
}) : super(key: key);
final void Function()? onPressed;
final EdgeInsetsGeometry padding;
final Widget? leading;
final Widget? title;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return RawMaterialButton(
onPressed: onPressed,
padding: padding,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
side: BorderSide(color: Theme.of(context).colorScheme.secondary.withOpacity(.6), width: 2),
),
child: ListTile(
leading: leading != null
? Theme(
data: Theme.of(context).copyWith(iconTheme: IconThemeData(color: Theme.of(context).colorScheme.secondary)),
child: leading!,
)
: null,
trailing: trailing,
title: title != null
? DefaultTextStyle(style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w500, fontSize: 15.0), child: title!)
: null,
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
);
}
}

View File

@ -0,0 +1,74 @@
import 'dart:ui';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class PanelButton extends StatelessWidget {
const PanelButton({
Key? key,
this.onPressed,
this.padding = const EdgeInsets.symmetric(horizontal: 14.0),
this.leading,
this.title,
this.trailing,
this.background = false,
this.trailingDivider = false,
}) : super(key: key);
final void Function()? onPressed;
final EdgeInsetsGeometry padding;
final Widget? leading;
final Widget? title;
final Widget? trailing;
final bool background;
final bool trailingDivider;
@override
Widget build(BuildContext context) {
final button = RawMaterialButton(
onPressed: onPressed,
padding: padding,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
fillColor: background ? Colors.white.withOpacity(Theme.of(context).brightness == Brightness.light ? .35 : .2) : null,
child: ListTile(
leading: leading != null
? Theme(
data: Theme.of(context).copyWith(iconTheme: IconThemeData(color: Theme.of(context).colorScheme.secondary)),
child: leading!,
)
: null,
trailing: trailingDivider
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
margin: const EdgeInsets.only(right: 6.0),
width: 2.0,
height: 32.0,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.15),
borderRadius: BorderRadius.circular(45.0),
),
),
if (trailing != null) trailing!,
],
)
: trailing,
title: title != null
? DefaultTextStyle(style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600, fontSize: 16.0), child: title!)
: null,
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
);
if (!background) return button;
return BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 12.0,
sigmaY: 12.0,
),
child: button);
}
}

View File

@ -0,0 +1,50 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sliding_sheet/sliding_sheet.dart';
class ProfileButton extends StatelessWidget {
const ProfileButton({Key? key, required this.child}) : super(key: key);
final ProfileImage child;
@override
Widget build(BuildContext context) {
final bool pMode = Provider.of<SettingsProvider>(context, listen: false).presentationMode;
return ProfileImage(
backgroundColor: !pMode ? child.backgroundColor : Theme.of(context).colorScheme.secondary,
heroTag: child.heroTag,
key: child.key,
name: !pMode ? child.name : "Béla",
radius: child.radius,
badge: child.badge,
role: child.role,
profilePictureString: child.profilePictureString,
onTap: () {
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(),
),
),
);
},
);
}
}

View File

@ -0,0 +1,229 @@
import 'dart:convert';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_mobile_ui/common/new_content_indicator.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/utils/color.dart';
class ProfileImage extends StatefulWidget {
const ProfileImage({
Key? key,
this.onTap,
this.name,
this.backgroundColor,
this.radius = 20.0,
this.heroTag,
this.badge = false,
this.role = Role.student,
this.censored = false,
this.profilePictureString = "",
}) : super(key: key);
final void Function()? onTap;
final String? name;
final Color? backgroundColor;
final double radius;
final String? heroTag;
final bool badge;
final Role? role;
final bool censored;
final String profilePictureString;
@override
State<ProfileImage> createState() => _ProfileImageState();
}
class _ProfileImageState extends State<ProfileImage> {
Image? profilePicture;
String? profPicSaved;
@override
void initState() {
super.initState();
updatePic();
}
void updatePic() {
profilePicture = widget.profilePictureString != ""
? Image.memory(const Base64Decoder().convert(widget.profilePictureString), fit: BoxFit.scaleDown, gaplessPlayback: true)
: null;
profPicSaved = widget.profilePictureString;
}
@override
Widget build(BuildContext context) {
if (profPicSaved != widget.profilePictureString) updatePic();
if (widget.heroTag == null) {
return buildWithoutHero(context);
} else {
return buildWithHero(context);
}
}
Widget buildWithoutHero(BuildContext context) {
Color color = ColorUtils.foregroundColor(widget.backgroundColor ?? Theme.of(context).scaffoldBackgroundColor);
Color roleColor;
if (Theme.of(context).brightness == Brightness.light) {
roleColor = const Color(0xFF444444);
} else {
roleColor = const Color(0xFF555555);
}
return Stack(
alignment: Alignment.center,
children: [
Material(
clipBehavior: Clip.hardEdge,
shape: const CircleBorder(),
color: widget.backgroundColor ?? AppColors.of(context).text.withOpacity(.15),
child: InkWell(
onTap: widget.onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: widget.radius * 2,
width: widget.radius * 2,
decoration: const BoxDecoration(
shape: BoxShape.circle,
),
child: widget.name != null && (widget.name?.trim().length ?? 0) > 0
? Center(
child: widget.censored
? Container(
width: 15,
height: 15,
decoration: BoxDecoration(
color: color.withOpacity(.5),
borderRadius: BorderRadius.circular(8.0),
),
)
: profilePicture ??
Text(
(widget.name?.trim().length ?? 0) > 0 ? (widget.name ?? "?").trim()[0] : "?",
style: TextStyle(
color: color,
fontWeight: FontWeight.w600,
fontSize: 18.0 * (widget.radius / 20.0),
),
),
)
: Container(),
),
),
),
// Role indicator
if (widget.role == Role.parent)
SizedBox(
height: widget.radius * 2,
width: widget.radius * 2,
child: Container(
alignment: Alignment.bottomRight,
child: Icon(Icons.shield, color: roleColor, size: widget.radius / 1.3),
),
),
],
);
}
Widget buildWithHero(BuildContext context) {
Color color = ColorUtils.foregroundColor(widget.backgroundColor ?? Theme.of(context).scaffoldBackgroundColor);
Color roleColor;
if (Theme.of(context).brightness == Brightness.light) {
roleColor = const Color(0xFF444444);
} else {
roleColor = const Color(0xFF555555);
}
Widget child = FittedBox(
fit: BoxFit.fitHeight,
child: Text(
(widget.name?.trim().length ?? 0) > 0 ? (widget.name ?? "?").trim()[0] : "?",
style: TextStyle(
color: color,
fontWeight: FontWeight.w600,
fontSize: 18.0 * (widget.radius / 20.0),
),
),
);
return SizedBox(
height: widget.radius * 2,
width: widget.radius * 2,
child: Stack(
alignment: Alignment.center,
children: [
if (widget.name != null && (widget.name?.trim().length ?? 0) > 0)
Hero(
tag: widget.heroTag! + "background",
transitionOnUserGestures: true,
child: Material(
clipBehavior: Clip.hardEdge,
shape: const CircleBorder(),
color: profilePicture != null ? Colors.transparent : widget.backgroundColor ?? AppColors.of(context).text.withOpacity(.15),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: widget.radius * 2,
width: widget.radius * 2,
decoration: const BoxDecoration(
shape: BoxShape.circle,
),
child: profilePicture,
),
),
),
Hero(
tag: widget.heroTag! + "child",
transitionOnUserGestures: true,
child: Material(
clipBehavior: Clip.hardEdge,
shape: profilePicture != null ? const CircleBorder() : null,
child: profilePicture ?? child,
type: MaterialType.transparency,
),
),
// Badge
if (widget.badge)
Hero(
tag: widget.heroTag! + "new_content_indicator",
child: NewContentIndicator(size: widget.radius * 2),
),
// Role indicator
if (widget.role == Role.parent)
Hero(
tag: widget.heroTag! + "role_indicator",
child: FittedBox(
fit: BoxFit.fitHeight,
child: SizedBox(
height: widget.radius * 2,
width: widget.radius * 2,
child: Container(
alignment: Alignment.bottomRight,
child: Icon(Icons.shield, color: roleColor, size: widget.radius / 1.3),
),
),
),
),
Material(
color: Colors.transparent,
clipBehavior: Clip.hardEdge,
shape: const CircleBorder(),
child: InkWell(
onTap: widget.onTap,
child: SizedBox(
height: widget.radius * 2,
width: widget.radius * 2,
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
class ProgressBar extends StatelessWidget {
const ProgressBar({Key? key, required this.value, this.backgroundColor}) : super(key: key);
final double value;
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Background
Container(
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.light ? Colors.black.withOpacity(0.1) : Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(45.0),
),
width: double.infinity,
height: 8.0,
),
// Slider
AnimatedContainer(
duration: const Duration(milliseconds: 500),
width: double.infinity,
child: CustomPaint(
painter: ProgressPainter(
backgroundColor: backgroundColor ?? Theme.of(context).colorScheme.secondary,
height: 8.0,
value: value.clamp(0, 1),
),
),
)
],
);
}
}
class ProgressPainter extends CustomPainter {
ProgressPainter({required this.height, required this.value, required this.backgroundColor});
final double height;
final double value;
final Color backgroundColor;
@override
void paint(Canvas canvas, Size size) {
double width = size.width * value;
if (width <= 0) return;
// Slider
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, width, height),
const Radius.circular(45.0),
),
Paint()
..color = backgroundColor
..style = PaintingStyle.fill,
);
}
@override
bool shouldRepaint(ProgressPainter oldDelegate) {
return value != oldDelegate.value || height != oldDelegate.height || backgroundColor != oldDelegate.backgroundColor;
}
}

View File

@ -0,0 +1,33 @@
import 'package:i18n_extension/i18n_extension.dart';
extension ScreensLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"home": "Home",
"grades": "Grades",
"timetable": "Timetable",
"messages": "Messages",
"absences": "Absences",
},
"hu_hu": {
"home": "Kezdőlap",
"grades": "Jegyek",
"timetable": "Órarend",
"messages": "Üzenetek",
"absences": "Hiányok",
},
"de_de": {
"home": "Zuhause",
"grades": "Noten",
"timetable": "Zeitplan",
"messages": "Mitteilungen",
"absences": "Fehlen",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:sliding_sheet/sliding_sheet.dart' as ss;
void showSlidingBottomSheet({required Widget child, required BuildContext context}) => ss.showSlidingBottomSheet(context,
useRootNavigator: true,
builder: (context) => ss.SlidingSheetDialog(
cornerRadius: 16,
cornerRadiusOnFullscreen: 0,
avoidStatusBar: true,
color: Theme.of(context).colorScheme.background,
duration: const Duration(milliseconds: 400),
snapSpec: const ss.SnapSpec(
snap: true,
snappings: [0.5, 1.0],
positioning: ss.SnapPositioning.relativeToAvailableSpace,
),
headerBuilder: (context, state) {
return Material(
color: Theme.of(context).colorScheme.background,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(12.0),
),
height: 4.0,
width: 60.0,
margin: const EdgeInsets.all(12.0),
),
],
),
);
},
builder: (context, state) {
return Material(
color: Theme.of(context).colorScheme.background,
child: Padding(padding: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 8.0), child: child),
);
},
));

View File

@ -0,0 +1,15 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void setSystemChrome(BuildContext context) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom]);
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Theme.of(context).brightness == Brightness.light ? Brightness.dark : Brightness.light,
systemNavigationBarColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.light ? Brightness.dark : Brightness.light,
statusBarBrightness: Platform.isIOS ? Theme.of(context).brightness : null,
));
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:i18n_extension/i18n_widget.dart';
class TrendDisplay<T extends num> extends StatelessWidget {
const TrendDisplay({Key? key, required this.current, required this.previous, this.padding}) : super(key: key);
final T current;
final T previous;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
const upIcon = "";
const downIcon = "";
final upColor = Colors.lightGreenAccent.shade700;
const downColor = Colors.redAccent;
Color color;
String icon;
double percentage;
if (previous > 0) {
percentage = (current - previous) * 100.0;
} else {
percentage = 0.0;
}
final String percentageText = percentage.abs().toStringAsFixed(1).replaceAll('.', I18n.of(context).locale.languageCode != 'en' ? ',' : '.');
if (!percentage.isNegative) {
color = upColor;
icon = upIcon;
} else {
color = downColor;
icon = downIcon;
}
if (percentage == 0) {
return const SizedBox();
}
return Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 2.0),
child: Text(
icon,
style: TextStyle(fontSize: 18.0, color: color),
),
),
Text("$percentageText%", style: TextStyle(color: color)),
],
),
);
}
}

View File

@ -0,0 +1,979 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart' show kMinFlingVelocity;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
double valueFromPercentageInRange({required final double min, max, percentage}) {
return percentage * (max - min) + min;
}
double percentageFromValueInRange({required final double min, max, value}) {
return (value - min) / (max - min);
}
const double _kOpenScale = 1.025;
const Color _borderColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFFA9A9AF),
darkColor: Color(0xFF57585A),
);
typedef _DismissCallback = void Function(
BuildContext context,
double scale,
double opacity,
);
typedef ViewablePreviewBuilder = Widget Function(
BuildContext context,
Animation<double> animation,
Widget child,
);
typedef _ViewablePreviewBuilderChildless = Widget Function(
BuildContext context,
Animation<double> animation,
);
Rect _getRect(GlobalKey globalKey) {
assert(globalKey.currentContext != null);
final RenderBox renderBoxContainer = globalKey.currentContext!.findRenderObject()! as RenderBox;
final Offset containerOffset = renderBoxContainer.localToGlobal(
renderBoxContainer.paintBounds.topLeft,
);
return containerOffset & renderBoxContainer.paintBounds.size;
}
enum _ViewableLocation {
center,
left,
right,
}
class Viewable extends StatefulWidget {
const Viewable({
Key? key,
required this.view,
required this.tile,
this.actions = const [],
this.previewBuilder,
}) : super(key: key);
final Widget tile;
final Widget view;
final List<Widget> actions;
final ViewablePreviewBuilder? previewBuilder;
@override
State<Viewable> createState() => _ViewableState();
}
class _ViewableState extends State<Viewable> with TickerProviderStateMixin {
final GlobalKey _childGlobalKey = GlobalKey();
bool _childHidden = false;
late AnimationController _openController;
Rect? _decoyChildEndRect;
OverlayEntry? _lastOverlayEntry;
_ViewableRoute<void>? _route;
@override
void initState() {
super.initState();
_openController = AnimationController(
duration: const Duration(milliseconds: 100),
vsync: this,
);
_openController.addStatusListener(_onDecoyAnimationStatusChange);
}
_ViewableLocation get _contextMenuLocation {
final Rect childRect = _getRect(_childGlobalKey);
final double screenWidth = MediaQuery.of(context).size.width;
final double center = screenWidth / 2;
final bool centerDividesChild = childRect.left < center && childRect.right > center;
final double distanceFromCenter = (center - childRect.center.dx).abs();
if (centerDividesChild && distanceFromCenter <= childRect.width / 4) {
return _ViewableLocation.center;
}
if (childRect.center.dx > center) {
return _ViewableLocation.right;
}
return _ViewableLocation.left;
}
void _openContextMenu() {
setState(() {
_childHidden = true;
});
_route = _ViewableRoute<void>(
actions: widget.actions,
barrierLabel: 'Dismiss',
filter: ui.ImageFilter.blur(
sigmaX: 5.0,
sigmaY: 5.0,
),
contextMenuLocation: _contextMenuLocation,
previousChildRect: _decoyChildEndRect!,
builder: (BuildContext context, Animation<double> animation) {
return ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: Material(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(16.0),
child: Stack(
children: [
Opacity(
opacity: animation.status == AnimationStatus.forward
? Curves.easeOutCirc.transform(animation.value)
: Curves.easeInCirc.transform(animation.value),
child: widget.view,
),
Opacity(
opacity: 1 -
(animation.status == AnimationStatus.forward
? Curves.easeOutCirc.transform(animation.value)
: Curves.easeInCirc.transform(animation.value)),
child: widget.tile,
),
],
),
),
);
},
);
Navigator.of(context, rootNavigator: true).push<void>(_route!);
_route!.animation!.addStatusListener(_routeAnimationStatusListener);
}
void _onDecoyAnimationStatusChange(AnimationStatus animationStatus) {
switch (animationStatus) {
case AnimationStatus.dismissed:
if (_route == null) {
setState(() {
_childHidden = false;
});
}
_lastOverlayEntry?.remove();
_lastOverlayEntry = null;
break;
case AnimationStatus.completed:
setState(() {
_childHidden = true;
});
_openContextMenu();
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_lastOverlayEntry?.remove();
_lastOverlayEntry = null;
_openController.reset();
});
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
return;
}
}
void _routeAnimationStatusListener(AnimationStatus status) {
if (status != AnimationStatus.dismissed) {
return;
}
setState(() {
_childHidden = false;
});
_route!.animation!.removeStatusListener(_routeAnimationStatusListener);
_route = null;
}
void _onTap() {
_onTapDown(TapDownDetails(), anim: false);
}
void _onTapDown(TapDownDetails details, {anim = true}) {
setState(() {
_childHidden = true;
});
final Rect childRect = _getRect(_childGlobalKey);
_decoyChildEndRect = Rect.fromCenter(
center: childRect.center,
width: childRect.width * _kOpenScale,
height: childRect.height * _kOpenScale,
);
_lastOverlayEntry = OverlayEntry(
builder: (BuildContext context) {
return _DecoyChild(
beginRect: childRect,
controller: _openController,
endRect: _decoyChildEndRect,
child: widget.tile,
);
},
);
Overlay.of(context, rootOverlay: true).insert(_lastOverlayEntry!);
_openController.forward(from: anim ? 0.0 : 1.0);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onTap,
child: TickerMode(
enabled: !_childHidden,
child: Opacity(
key: _childGlobalKey,
opacity: _childHidden ? 0.0 : 1.0,
child: widget.tile,
),
),
);
}
@override
void dispose() {
_openController.dispose();
super.dispose();
}
}
class _DecoyChild extends StatefulWidget {
const _DecoyChild({
Key? key,
this.beginRect,
required this.controller,
this.endRect,
this.child,
}) : super(key: key);
final Rect? beginRect;
final AnimationController controller;
final Rect? endRect;
final Widget? child;
@override
_DecoyChildState createState() => _DecoyChildState();
}
class _DecoyChildState extends State<_DecoyChild> with TickerProviderStateMixin {
static const Color _lightModeMaskColor = Color(0xFF888888);
static const Color _masklessColor = Color(0xFFFFFFFF);
final GlobalKey _childGlobalKey = GlobalKey();
late Animation<Color> _mask;
late Animation<Rect?> _rect;
@override
void initState() {
super.initState();
_mask = _OnOffAnimation<Color>(
controller: widget.controller,
onValue: _lightModeMaskColor,
offValue: _masklessColor,
intervalOn: 0.0,
intervalOff: 0.5,
);
final Rect midRect = widget.beginRect!.deflate(
widget.beginRect!.width * (_kOpenScale - 1.0) / 2,
);
_rect = TweenSequence<Rect?>(<TweenSequenceItem<Rect?>>[
TweenSequenceItem<Rect?>(
tween: RectTween(
begin: widget.beginRect,
end: midRect,
).chain(CurveTween(curve: Curves.easeInOutCubic)),
weight: 1.0,
),
TweenSequenceItem<Rect?>(
tween: RectTween(
begin: midRect,
end: widget.endRect,
).chain(CurveTween(curve: Curves.easeOutCubic)),
weight: 1.0,
),
]).animate(widget.controller);
_rect.addListener(_rectListener);
}
void _rectListener() {
if (widget.controller.value < 0.5) {
return;
}
HapticFeedback.selectionClick();
_rect.removeListener(_rectListener);
}
@override
void dispose() {
_rect.removeListener(_rectListener);
super.dispose();
}
Widget _buildAnimation(BuildContext context, Widget? child) {
final Color color = widget.controller.status == AnimationStatus.reverse ? _masklessColor : _mask.value;
return Positioned.fromRect(
rect: _rect.value!,
child: ShaderMask(
key: _childGlobalKey,
shaderCallback: (Rect bounds) {
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: <Color>[color, color],
).createShader(bounds);
},
child: widget.child,
),
);
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
AnimatedBuilder(
builder: _buildAnimation,
animation: widget.controller,
),
],
);
}
}
class _ViewableRoute<T> extends PopupRoute<T> {
_ViewableRoute({
required List<Widget> actions,
required _ViewableLocation contextMenuLocation,
this.barrierLabel,
_ViewablePreviewBuilderChildless? builder,
ui.ImageFilter? filter,
required Rect previousChildRect,
RouteSettings? settings,
}) : _actions = actions,
_builder = builder,
_contextMenuLocation = contextMenuLocation,
_previousChildRect = previousChildRect,
super(
filter: filter,
settings: settings,
);
static const Color _kModalBarrierColor = Color(0x6604040F);
static const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335);
final List<Widget> _actions;
final _ViewablePreviewBuilderChildless? _builder;
final GlobalKey _childGlobalKey = GlobalKey();
final _ViewableLocation _contextMenuLocation;
bool _externalOffstage = false;
bool _internalOffstage = false;
Orientation? _lastOrientation;
final Rect _previousChildRect;
double? _scale = 1.0;
final GlobalKey _sheetGlobalKey = GlobalKey();
static final CurveTween _curve = CurveTween(
curve: Curves.easeOutBack,
);
static final CurveTween _curveReverse = CurveTween(
curve: Curves.easeInBack,
);
static final RectTween _rectTween = RectTween();
static final Animatable<Rect?> _rectAnimatable = _rectTween.chain(_curve);
static final RectTween _rectTweenReverse = RectTween();
static final Animatable<Rect?> _rectAnimatableReverse = _rectTweenReverse.chain(
_curveReverse,
);
static final RectTween _sheetRectTween = RectTween();
final Animatable<Rect?> _sheetRectAnimatable = _sheetRectTween.chain(
_curve,
);
final Animatable<Rect?> _sheetRectAnimatableReverse = _sheetRectTween.chain(
_curveReverse,
);
static final Tween<double> _sheetScaleTween = Tween<double>();
static final Animatable<double> _sheetScaleAnimatable = _sheetScaleTween.chain(
_curve,
);
static final Animatable<double> _sheetScaleAnimatableReverse = _sheetScaleTween.chain(
_curveReverse,
);
final Tween<double> _opacityTween = Tween<double>(begin: 0.0, end: 1.0);
late Animation<double> _sheetOpacity;
@override
final String? barrierLabel;
@override
Color get barrierColor => _kModalBarrierColor;
@override
bool get barrierDismissible => true;
@override
bool get semanticsDismissible => false;
@override
Duration get transitionDuration => _kModalPopupTransitionDuration;
static Rect _getScaledRect(GlobalKey globalKey, double scale) {
final Rect childRect = _getRect(globalKey);
final Size sizeScaled = childRect.size * scale;
final Offset offsetScaled = Offset(
childRect.left + (childRect.size.width - sizeScaled.width) / 2,
childRect.top + (childRect.size.height - sizeScaled.height) / 2,
);
return offsetScaled & sizeScaled;
}
static AlignmentDirectional getSheetAlignment(_ViewableLocation contextMenuLocation) {
switch (contextMenuLocation) {
case _ViewableLocation.center:
return AlignmentDirectional.topCenter;
case _ViewableLocation.right:
return AlignmentDirectional.topEnd;
case _ViewableLocation.left:
return AlignmentDirectional.topStart;
}
}
static Rect _getSheetRectBegin(Orientation? orientation, _ViewableLocation contextMenuLocation, Rect childRect, Rect sheetRect) {
switch (contextMenuLocation) {
case _ViewableLocation.center:
final Offset target = orientation == Orientation.portrait ? childRect.bottomCenter : childRect.topCenter;
final Offset centered = target - Offset(sheetRect.width / 2, 0.0);
return centered & sheetRect.size;
case _ViewableLocation.right:
final Offset target = orientation == Orientation.portrait ? childRect.bottomRight : childRect.topRight;
return (target - Offset(sheetRect.width, 0.0)) & sheetRect.size;
case _ViewableLocation.left:
final Offset target = orientation == Orientation.portrait ? childRect.bottomLeft : childRect.topLeft;
return target & sheetRect.size;
}
}
void _onDismiss(BuildContext context, double scale, double opacity) {
_scale = scale;
_opacityTween.end = opacity;
_sheetOpacity = _opacityTween.animate(CurvedAnimation(
parent: animation!,
curve: const Interval(0.9, 1.0),
));
Navigator.of(context).pop();
}
void _updateTweenRects() {
final Rect childRect = _scale == null ? _getRect(_childGlobalKey) : _getScaledRect(_childGlobalKey, _scale!);
_rectTween.begin = _previousChildRect;
_rectTween.end = childRect;
final Rect childRectOriginal = Rect.fromCenter(
center: _previousChildRect.center,
width: _previousChildRect.width / _kOpenScale,
height: _previousChildRect.height / _kOpenScale,
);
final Rect sheetRect = _getRect(_sheetGlobalKey);
final Rect sheetRectBegin = _getSheetRectBegin(
_lastOrientation,
_contextMenuLocation,
childRectOriginal,
sheetRect,
);
_sheetRectTween.begin = sheetRectBegin;
_sheetRectTween.end = sheetRect;
_sheetScaleTween.begin = 0.0;
_sheetScaleTween.end = _scale;
_rectTweenReverse.begin = childRectOriginal;
_rectTweenReverse.end = childRect;
}
void _setOffstageInternally() {
super.offstage = _externalOffstage || _internalOffstage;
changedInternalState();
}
@override
bool didPop(T? result) {
_updateTweenRects();
return super.didPop(result);
}
@override
set offstage(bool value) {
_externalOffstage = value;
_setOffstageInternally();
}
@override
TickerFuture didPush() {
_internalOffstage = true;
_setOffstageInternally();
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_updateTweenRects();
_internalOffstage = false;
_setOffstageInternally();
});
return super.didPush();
}
@override
Animation<double> createAnimation() {
final Animation<double> animation = super.createAnimation();
_sheetOpacity = _opacityTween.animate(CurvedAnimation(
parent: animation,
curve: Curves.linear,
));
return animation;
}
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return Container();
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return OrientationBuilder(
builder: (BuildContext context, Orientation orientation) {
_lastOrientation = orientation;
if (!animation.isCompleted) {
final bool reverse = animation.status == AnimationStatus.reverse;
final Rect rect = reverse ? _rectAnimatableReverse.evaluate(animation)! : _rectAnimatable.evaluate(animation)!;
final Rect sheetRect = reverse ? _sheetRectAnimatableReverse.evaluate(animation)! : _sheetRectAnimatable.evaluate(animation)!;
final double sheetScale = reverse ? _sheetScaleAnimatableReverse.evaluate(animation) : _sheetScaleAnimatable.evaluate(animation);
return Stack(
children: <Widget>[
Positioned.fromRect(
rect: sheetRect,
child: FadeTransition(
opacity: _sheetOpacity,
child: Transform.scale(
alignment: getSheetAlignment(_contextMenuLocation),
scale: sheetScale,
child: _ViewableSheet(
key: _sheetGlobalKey,
actions: _actions,
),
),
),
),
Positioned.fromRect(
key: _childGlobalKey,
rect: rect,
child: _builder!(context, animation),
),
],
);
}
return _ContextMenuRouteStatic(
actions: _actions,
childGlobalKey: _childGlobalKey,
contextMenuLocation: _contextMenuLocation,
onDismiss: _onDismiss,
orientation: orientation,
sheetGlobalKey: _sheetGlobalKey,
child: _builder!(context, animation),
);
},
);
}
}
class _ContextMenuRouteStatic extends StatefulWidget {
const _ContextMenuRouteStatic({
Key? key,
this.actions,
required this.child,
this.childGlobalKey,
required this.contextMenuLocation,
this.onDismiss,
required this.orientation,
this.sheetGlobalKey,
}) : super(key: key);
final List<Widget>? actions;
final Widget child;
final GlobalKey? childGlobalKey;
final _ViewableLocation contextMenuLocation;
final _DismissCallback? onDismiss;
final Orientation orientation;
final GlobalKey? sheetGlobalKey;
@override
_ContextMenuRouteStaticState createState() => _ContextMenuRouteStaticState();
}
class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with TickerProviderStateMixin {
static const double _kMinScale = 0.8;
static const double _kSheetScaleThreshold = 0.9;
static const double _kPadding = 20.0;
static const double _kDamping = 400.0;
static const Duration _kMoveControllerDuration = Duration(milliseconds: 600);
late Offset _dragOffset;
double _lastScale = 1.0;
late AnimationController _moveController;
late AnimationController _sheetController;
late Animation<Offset> _moveAnimation;
late Animation<double> _sheetScaleAnimation;
late Animation<double> _sheetOpacityAnimation;
static double _getScale(Orientation orientation, double maxDragDistance, double dy) {
final double dyDirectional = dy <= 0.0 ? dy : -dy;
return math.max(
_kMinScale,
(maxDragDistance + dyDirectional) / maxDragDistance,
);
}
void _onPanStart(DragStartDetails details) {
_moveController.value = 1.0;
_setDragOffset(Offset.zero);
}
void _onPanUpdate(DragUpdateDetails details) {
_setDragOffset(_dragOffset + details.delta);
}
void _onPanEnd(DragEndDetails details) {
if (details.velocity.pixelsPerSecond.dy.abs() >= kMinFlingVelocity) {
final bool flingIsAway = details.velocity.pixelsPerSecond.dy > 0;
final double finalPosition = flingIsAway ? _moveAnimation.value.dy + 100.0 : 0.0;
if (flingIsAway && _sheetController.status != AnimationStatus.forward) {
_sheetController.forward();
} else if (!flingIsAway && _sheetController.status != AnimationStatus.reverse) {
_sheetController.reverse();
}
_moveAnimation = Tween<Offset>(
begin: Offset(0.0, _moveAnimation.value.dy),
end: Offset(0.0, finalPosition),
).animate(_moveController);
_moveController.reset();
_moveController.duration = const Duration(
milliseconds: 64,
);
_moveController.forward();
_moveController.addStatusListener(_flingStatusListener);
return;
}
if (_lastScale == _kMinScale) {
widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value);
return;
}
_moveController.addListener(_moveListener);
_moveController.reverse();
}
void _moveListener() {
if (_lastScale > _kSheetScaleThreshold) {
_moveController.removeListener(_moveListener);
if (_sheetController.status != AnimationStatus.dismissed) {
_sheetController.reverse();
}
}
}
void _flingStatusListener(AnimationStatus status) {
if (status != AnimationStatus.completed) {
return;
}
_moveController.duration = _kMoveControllerDuration;
_moveController.removeStatusListener(_flingStatusListener);
if (_moveAnimation.value.dy == 0.0) {
return;
}
widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value);
}
Alignment _getChildAlignment(Orientation orientation, _ViewableLocation contextMenuLocation) {
switch (contextMenuLocation) {
case _ViewableLocation.center:
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topRight;
case _ViewableLocation.right:
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topLeft;
case _ViewableLocation.left:
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topRight;
}
}
void _setDragOffset(Offset dragOffset) {
final double endX = _kPadding * dragOffset.dx / _kDamping;
final double endY = dragOffset.dy >= 0.0 ? dragOffset.dy : _kPadding * dragOffset.dy / _kDamping;
setState(() {
_dragOffset = dragOffset;
_moveAnimation = Tween<Offset>(
begin: Offset.zero,
end: Offset(
endX.clamp(-_kPadding, _kPadding),
endY,
),
).animate(
CurvedAnimation(
parent: _moveController,
curve: Curves.elasticIn,
),
);
if (_lastScale <= _kSheetScaleThreshold && _sheetController.status != AnimationStatus.forward && _sheetScaleAnimation.value != 0.0) {
_sheetController.forward();
} else if (_lastScale > _kSheetScaleThreshold && _sheetController.status != AnimationStatus.reverse && _sheetScaleAnimation.value != 1.0) {
_sheetController.reverse();
}
});
}
List<Widget> _getChildren(Orientation orientation, _ViewableLocation contextMenuLocation) {
final Expanded child = Expanded(
child: Align(
alignment: _getChildAlignment(
widget.orientation,
widget.contextMenuLocation,
),
child: AnimatedBuilder(
animation: _moveController,
builder: _buildChildAnimation,
child: widget.child,
),
),
);
const SizedBox spacer = SizedBox(
width: _kPadding,
height: _kPadding,
);
final sheet = AnimatedBuilder(
animation: _sheetController,
builder: _buildSheetAnimation,
child: _ViewableSheet(
key: widget.sheetGlobalKey,
actions: widget.actions!,
),
);
switch (contextMenuLocation) {
case _ViewableLocation.center:
return <Widget>[child, spacer, sheet];
case _ViewableLocation.right:
return orientation == Orientation.portrait ? <Widget>[child, spacer, sheet] : <Widget>[sheet, spacer, child];
case _ViewableLocation.left:
return <Widget>[child, spacer, sheet];
}
}
Widget _buildSheetAnimation(BuildContext context, Widget? child) {
return Transform.scale(
alignment: _ViewableRoute.getSheetAlignment(widget.contextMenuLocation),
scale: _sheetScaleAnimation.value,
child: FadeTransition(
opacity: _sheetOpacityAnimation,
child: child,
),
);
}
Widget _buildChildAnimation(BuildContext context, Widget? child) {
_lastScale = _getScale(
widget.orientation,
MediaQuery.of(context).size.height,
_moveAnimation.value.dy,
);
return Transform.scale(
key: widget.childGlobalKey,
scale: _lastScale,
child: child,
);
}
Widget _buildAnimation(BuildContext context, Widget? child) {
return Transform.translate(
offset: _moveAnimation.value,
child: child,
);
}
@override
void initState() {
super.initState();
_moveController = AnimationController(
duration: _kMoveControllerDuration,
value: 1.0,
vsync: this,
);
_sheetController = AnimationController(
duration: const Duration(milliseconds: 100),
reverseDuration: const Duration(milliseconds: 200),
vsync: this,
);
_sheetScaleAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _sheetController,
curve: Curves.linear,
reverseCurve: Curves.easeInBack,
),
);
_sheetOpacityAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(_sheetController);
_setDragOffset(Offset.zero);
}
@override
void dispose() {
_moveController.dispose();
_sheetController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final List<Widget> children = _getChildren(
widget.orientation,
widget.contextMenuLocation,
);
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(_kPadding),
child: Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onPanEnd: _onPanEnd,
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
child: AnimatedBuilder(
animation: _moveController,
builder: _buildAnimation,
child: widget.orientation == Orientation.portrait
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
)
: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
),
),
),
);
}
}
class _ViewableSheet extends StatelessWidget {
const _ViewableSheet({
Key? key,
required this.actions,
}) : super(key: key);
final List<Widget> actions;
List<Widget> getChildren(BuildContext context) {
if (actions.isEmpty) return [];
final Widget menu = Expanded(
child: IntrinsicHeight(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(13.0)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
actions.first,
for (Widget action in actions.skip(1))
DecoratedBox(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: CupertinoDynamicColor.resolve(_borderColor, context),
width: 0.5,
)),
),
position: DecorationPosition.foreground,
child: action,
),
],
),
),
),
);
return [menu];
}
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: getChildren(context),
);
}
}
class _OnOffAnimation<T> extends CompoundAnimation<T> {
_OnOffAnimation({
required AnimationController controller,
required T onValue,
required T offValue,
required double intervalOn,
required double intervalOff,
}) : _offValue = offValue,
assert(intervalOn >= 0.0 && intervalOn <= 1.0),
assert(intervalOff >= 0.0 && intervalOff <= 1.0),
assert(intervalOn <= intervalOff),
super(
first: Tween<T>(begin: offValue, end: onValue).animate(
CurvedAnimation(
parent: controller,
curve: Interval(intervalOn, intervalOn),
),
),
next: Tween<T>(begin: onValue, end: offValue).animate(
CurvedAnimation(
parent: controller,
curve: Interval(intervalOff, intervalOff),
),
),
);
final T _offValue;
@override
T get value => next.value == _offValue ? next.value : first.value;
}

View File

@ -0,0 +1,50 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
class AbsenceDisplay extends StatelessWidget {
const AbsenceDisplay(this.excused, this.unexcused, this.pending, {Key? key}) : super(key: key);
final int excused;
final int unexcused;
final int pending;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(top: 5.0),
// padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0),
// decoration: BoxDecoration(
// color: Theme.of(context).scaffoldBackgroundColor.withOpacity(.2),
// borderRadius: BorderRadius.circular(12.0),
// ),
child: Row(children: [
if (excused > 0)
Icon(
FeatherIcons.check,
size: 16.0,
color: AppColors.of(context).green,
),
if (excused > 0) const SizedBox(width: 2.0),
if (excused > 0) Text(excused.toString(), style: const TextStyle(fontFamily: "monospace", fontSize: 14.0)),
if (excused > 0 && pending > 0) const SizedBox(width: 6.0),
if (pending > 0)
Icon(
FeatherIcons.slash,
size: 14.0,
color: AppColors.of(context).orange,
),
if (pending > 0) const SizedBox(width: 3.0),
if (pending > 0) Text(pending.toString(), style: const TextStyle(fontFamily: "monospace", fontSize: 14.0)),
if (unexcused > 0 && pending > 0) const SizedBox(width: 3.0),
if (unexcused > 0)
Icon(
FeatherIcons.x,
size: 18.0,
color: AppColors.of(context).red,
),
if (unexcused > 0) Text(unexcused.toString(), style: const TextStyle(fontFamily: "monospace", fontSize: 14.0)),
]),
);
}
}

View File

@ -0,0 +1,80 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_display.dart';
import 'package:flutter/material.dart';
class AbsenceSubjectTile extends StatelessWidget {
const AbsenceSubjectTile(this.subject, {Key? key, this.percentage = 0.0, this.excused = 0, this.unexcused = 0, this.pending = 0, this.onTap})
: super(key: key);
final Subject subject;
final void Function()? onTap;
final double percentage;
final int excused;
final int unexcused;
final int pending;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: ListTile(
// minLeadingWidth: 32.0,
dense: true,
contentPadding: const EdgeInsets.only(left: 8.0, right: 6.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
visualDensity: VisualDensity.compact,
onTap: onTap,
leading: Icon(SubjectIcon.resolveVariant(subject: subject, context: context), size: 32.0),
title: Text(
subject.renamedTo ?? subject.name.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15.0, fontStyle: subject.isRenamed ? FontStyle.italic : null),
),
subtitle: AbsenceDisplay(excused, unexcused, pending),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 8.0),
if (percentage >= 0)
Stack(
alignment: Alignment.centerRight,
children: [
const Opacity(child: Text("100%", style: TextStyle(fontFamily: "monospace")), opacity: 0),
Text(
percentage.round().toString() + "%",
style: TextStyle(
// fontFamily: "monospace",
color: getColorByPercentage(percentage, context: context),
fontWeight: FontWeight.w700,
fontSize: 24.0,
),
),
],
),
],
),
),
);
}
}
Color getColorByPercentage(double percentage, {required BuildContext context}) {
Color color = AppColors.of(context).text;
percentage = percentage.round().toDouble();
if (percentage > 35) {
color = AppColors.of(context).red;
} else if (percentage > 25) {
color = AppColors.of(context).orange;
} else if (percentage > 15) {
color = AppColors.of(context).yellow;
}
return color.withOpacity(.8);
}

View File

@ -0,0 +1,118 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence_group/absence_group_container.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'absence_tile.i18n.dart';
class AbsenceTile extends StatelessWidget {
const AbsenceTile(this.absence, {Key? key, this.onTap, this.elevation = 0.0, this.padding}) : super(key: key);
final Absence absence;
final void Function()? onTap;
final double elevation;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
Color color = justificationColor(absence.state, context: context);
bool group = AbsenceGroupContainer.of(context) != null;
return Container(
decoration: BoxDecoration(
boxShadow: [
if (elevation > 0)
BoxShadow(
offset: Offset(0, 21 * elevation),
blurRadius: 23.0 * elevation,
color: Theme.of(context).shadowColor,
)
],
borderRadius: BorderRadius.circular(14.0),
),
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? (group ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 8.0)),
child: ListTile(
onTap: onTap,
visualDensity: VisualDensity.compact,
dense: group,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(!group ? 14.0 : 12.0)),
leading: Container(
width: 44.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: !group ? color.withOpacity(.25) : null,
),
child: Center(child: Icon(justificationIcon(absence.state), color: color)),
),
title: !group
? Text.rich(TextSpan(
text: "${absence.delay == 0 ? "" : absence.delay}",
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15.5),
children: [
TextSpan(
text: absence.delay == 0
? justificationName(absence.state).fill(["absence".i18n]).capital()
: 'minute'.plural(absence.delay) + justificationName(absence.state).fill(["delay".i18n]),
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
))
: Text(
(absence.lessonIndex != null ? "${absence.lessonIndex}. " : "") + (absence.subject.renamedTo ?? absence.subject.name.capital()),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14.0, fontStyle: absence.subject.isRenamed ? FontStyle.italic : null),
),
subtitle: !group
? Text(
absence.subject.renamedTo ?? absence.subject.name.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
// DateFormat("MM. dd. (EEEEE)", I18n.of(context).locale.toString()).format(absence.date),
style: TextStyle(fontWeight: FontWeight.w500, fontStyle: absence.subject.isRenamed ? FontStyle.italic : null),
)
: null,
),
),
),
);
}
static String justificationName(Justification state) {
switch (state) {
case Justification.excused:
return "excused".i18n;
case Justification.pending:
return "pending".i18n;
case Justification.unexcused:
return "unexcused".i18n;
}
}
static Color justificationColor(Justification state, {required BuildContext context}) {
switch (state) {
case Justification.excused:
return AppColors.of(context).green;
case Justification.pending:
return AppColors.of(context).orange;
case Justification.unexcused:
return AppColors.of(context).red;
}
}
static IconData justificationIcon(Justification state) {
switch (state) {
case Justification.excused:
return FeatherIcons.check;
case Justification.pending:
return FeatherIcons.slash;
case Justification.unexcused:
return FeatherIcons.x;
}
}
}

View File

@ -0,0 +1,36 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"excused": "excused %s",
"pending": "%s to be excused",
"unexcused": "unexcused %s",
"absence": "absence",
"delay": "delay",
"minute": " minutes of ".one(" minute of "),
},
"hu_hu": {
"excused": "igazolt %s",
"pending": "igazolandó %s",
"unexcused": "igazolatlan %s",
"absence": "hiányzás",
"delay": "késés",
"minute": " perc ",
},
"de_de": {
"excused": "anerkannt %s",
"pending": "%s zu anerkennen",
"unexcused": "unanerkannt %s",
"absence": "Abwesenheit",
"delay": "Verspätung",
"minute": " Minuten ".one(" Minute "),
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,128 @@
// ignore_for_file: empty_catches
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_card.dart';
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
import 'package:filcnaplo_mobile_ui/common/detail.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel_action_button.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_tile.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/pages/timetable/timetable_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:filcnaplo/utils/reverse_search.dart';
import 'absence_view.i18n.dart';
class AbsenceView extends StatelessWidget {
const AbsenceView(this.absence, {Key? key, this.outsideContext, this.viewable = false}) : super(key: key);
final Absence absence;
final BuildContext? outsideContext;
final bool viewable;
static show(Absence absence, {required BuildContext context}) {
showBottomCard(context: context, child: AbsenceView(absence, outsideContext: context));
}
@override
Widget build(BuildContext context) {
Color color = AbsenceTile.justificationColor(absence.state, context: context);
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 16.0, right: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
leading: Container(
width: 44.0,
height: 44.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color.withOpacity(.25),
),
child: Center(
child: Icon(
AbsenceTile.justificationIcon(absence.state),
color: color,
),
),
),
title: Text(
absence.subject.renamedTo ?? absence.subject.name.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w700, fontStyle: absence.subject.isRenamed ? FontStyle.italic : null),
),
subtitle: Text(
absence.teacher,
// DateFormat("MM. dd. (EEEEE)", I18n.of(context).locale.toString()).format(absence.date),
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: Text(
absence.date.format(context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
// Absence Details
if (absence.delay > 0)
Detail(
title: "delay".i18n,
description: absence.delay.toString() + " " + "minutes".i18n.plural(absence.delay),
),
if (absence.lessonIndex != null)
Detail(
title: "Lesson".i18n,
description: "${absence.lessonIndex}. (${absence.lessonStart.format(context, timeOnly: true)}"
" - "
"${absence.lessonEnd.format(context, timeOnly: true)})",
),
if (absence.justification != null)
Detail(
title: "Excuse".i18n,
description: absence.justification?.description ?? "",
),
if (absence.mode != null) Detail(title: "Mode".i18n, description: absence.mode?.description ?? ""),
Detail(title: "Submit date".i18n, description: absence.submitDate.format(context)),
// Show in timetable
if (!viewable)
Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 6.0, top: 12.0),
child: PanelActionButton(
leading: const Icon(FeatherIcons.calendar),
title: Text(
"show in timetable".i18n,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onPressed: () {
Navigator.of(context).pop();
if (outsideContext != null) {
ReverseSearch.getLessonByAbsence(absence, context).then((lesson) {
if (lesson != null) {
TimetablePage.jump(outsideContext!, lesson: lesson);
} else {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
content: Text("Cannot find lesson".i18n, style: const TextStyle(color: Colors.white)),
backgroundColor: AppColors.of(context).red,
context: context,
));
}
});
}
},
),
),
],
),
);
}
}

View File

@ -0,0 +1,39 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Lesson": "Lesson",
"Excuse": "Excuse",
"Mode": "Mode",
"Submit date": "Submit Date",
"show in timetable": "Show in timetable",
"minutes": "minutes".one("minute"),
"delay": "Delay",
},
"hu_hu": {
"Lesson": "Óra",
"Excuse": "Igazolás",
"Mode": "Típus",
"Submit date": "Rögzítés dátuma",
"show in timetable": "Megtekintés az órarendben",
"minutes": "perc",
"delay": "Késés",
},
"de_de": {
"Lesson": "Stunde",
"Excuse": "Anerkannt",
"Mode": "Typ",
"Submit date": "Datum einreichen",
"show in timetable": "im Stundenplan anzeigen",
"minutes": "Minuten".one("Minute"),
"delay": "Verspätung",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,68 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
import 'package:filcnaplo_mobile_ui/common/viewable.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_view.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence_group/absence_group_container.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/card_handle.dart';
import 'package:filcnaplo_mobile_ui/pages/absences/absence_subject_view_container.dart';
import 'package:filcnaplo_mobile_ui/pages/timetable/timetable_page.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/utils/reverse_search.dart';
import 'absence_view.i18n.dart';
class AbsenceViewable extends StatelessWidget {
const AbsenceViewable(this.absence, {Key? key, this.padding}) : super(key: key);
final Absence absence;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
final subject = AbsenceSubjectViewContainer.of(context) != null;
final group = AbsenceGroupContainer.of(context) != null;
final tile = AbsenceTile(absence, padding: padding);
return Viewable(
tile: group ? AbsenceGroupContainer(child: tile) : tile,
view: CardHandle(child: AbsenceView(absence, viewable: true)),
actions: [
PanelButton(
background: true,
title: Text(
"show in timetable".i18n,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
if (subject) {
Future.delayed(const Duration(milliseconds: 250)).then((_) {
Navigator.of(context, rootNavigator: true).pop(absence);
});
} else {
Future.delayed(const Duration(milliseconds: 250)).then((_) {
ReverseSearch.getLessonByAbsence(absence, context).then((lesson) {
if (lesson != null) {
TimetablePage.jump(context, lesson: lesson);
} else {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
content: Text("Cannot find lesson".i18n, style: const TextStyle(color: Colors.white)),
backgroundColor: AppColors.of(context).red,
context: context,
));
}
});
});
}
},
),
],
);
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class AbsenceGroupContainer extends InheritedWidget {
const AbsenceGroupContainer({Key? key, required Widget child}) : super(key: key, child: child);
static AbsenceGroupContainer? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<AbsenceGroupContainer>();
@override
bool updateShouldNotify(AbsenceGroupContainer oldWidget) => false;
}

View File

@ -0,0 +1,80 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence_group/absence_group_container.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_tile.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:flutter/material.dart';
import 'absence_group_tile.i18n.dart';
class AbsenceGroupTile extends StatelessWidget {
const AbsenceGroupTile(this.absences, {Key? key, this.showDate = false, this.padding}) : super(key: key);
final List<AbsenceViewable> absences;
final bool showDate;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
Justification state = getState(absences.map((e) => e.absence.state).toList());
Color color = AbsenceTile.justificationColor(state, context: context);
absences.sort((a, b) => a.absence.lessonIndex?.compareTo(b.absence.lessonIndex ?? 0) ?? -1);
return ClipRRect(
borderRadius: BorderRadius.circular(14.0),
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: AbsenceGroupContainer(
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 8.0),
backgroundColor: Colors.transparent,
leading: Container(
width: 44.0,
height: 44.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color.withOpacity(.25),
),
child: Center(child: Icon(AbsenceTile.justificationIcon(state), color: color)),
),
title: Text.rich(TextSpan(
text: "${absences.where((a) => a.absence.state == state).length} ",
style: TextStyle(fontWeight: FontWeight.w700, color: AppColors.of(context).text),
children: [
TextSpan(
text: AbsenceTile.justificationName(state).fill(["absence".i18n]),
style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.of(context).text),
),
],
)),
subtitle: showDate
? Text(
absences.first.absence.date.format(context, weekday: true),
style: TextStyle(fontWeight: FontWeight.w500, color: AppColors.of(context).text.withOpacity(0.8)),
)
: null,
children: absences,
),
),
),
),
);
}
static Justification getState(List<Justification> states) {
Justification state;
if (states.any((element) => element == Justification.unexcused)) {
state = Justification.unexcused;
} else if (states.any((element) => element == Justification.pending)) {
state = Justification.pending;
} else {
state = Justification.excused;
}
return state;
}
}

View File

@ -0,0 +1,21 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"absence": "absences",
},
"hu_hu": {
"absence": "hiányzás",
},
"de_de": {
"absence": "Fehlen",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,27 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class CardHandle extends StatelessWidget {
const CardHandle({Key? key, this.child}) : super(key: key);
final Widget? child;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 42.0,
height: 4.0,
margin: const EdgeInsets.only(top: 12.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(45.0),
color: AppColors.of(context).text.withOpacity(0.10),
),
),
if (child != null) child!,
],
);
}
}

View File

@ -0,0 +1,108 @@
import 'package:filcnaplo/helpers/average_helper.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/cretification/certification_view.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'certification_card.i18n.dart';
class CertificationCard extends StatelessWidget {
const CertificationCard(this.grades, {Key? key, required this.gradeType, this.padding}) : super(key: key);
final List<Grade> grades;
final GradeType gradeType;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
String title = getGradeTypeTitle(gradeType);
double average = AverageHelper.averageEvals(grades, finalAvg: true);
String averageText = average.toStringAsFixed(1);
if (I18n.of(context).locale.languageCode != "en") averageText = averageText.replaceAll(".", ",");
Color color = gradeColor(context: context, value: average);
Color textColor;
if (color.computeLuminance() >= .5) {
textColor = Colors.black;
} else {
textColor = Colors.white;
}
return Padding(
padding: padding ?? const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
gradient: LinearGradient(
colors: [color, color.withOpacity(.75)],
),
),
child: Material(
type: MaterialType.transparency,
borderRadius: BorderRadius.circular(12.0),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
leading: Text(
averageText,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
fontSize: 24.0,
),
),
title: Text.rich(
TextSpan(
text: title,
children: [
TextSpan(
text: "${grades.length}",
style: TextStyle(
color: textColor.withOpacity(.75),
fontWeight: FontWeight.w600,
fontSize: 16.0,
),
),
],
),
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w700,
fontSize: 18.0,
),
),
trailing: Icon(FeatherIcons.arrowRight, color: textColor),
onTap: () => CertificationView.show(grades, context: context, gradeType: gradeType),
),
),
),
);
}
}
String getGradeTypeTitle(GradeType gradeType) {
String title;
switch (gradeType) {
case GradeType.halfYear:
title = "mid".i18n;
break;
case GradeType.firstQ:
title = "1q".i18n;
break;
case GradeType.secondQ:
title = "2q".i18n;
break;
case GradeType.thirdQ:
title = "3q".i18n;
break;
case GradeType.fourthQ:
title = "4q".i18n;
break;
default:
title = "final".i18n;
}
return title;
}

View File

@ -0,0 +1,36 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"final": "Final grades",
"mid": "Midterm grades",
"1q": "1. Quarter grades",
"2q": "2. Quarter grades",
"3q": "3. Quarter grades",
"4q": "4. Quarter grades",
},
"hu_hu": {
"final": "Év végi jegyek",
"mid": "Félévi jegyek",
"1q": "1. Negyedéves jegyek",
"2q": "2. Negyedéves jegyek",
"3q": "3. Negyedéves jegyek",
"4q": "4. Negyedéves jegyek",
},
"de_de": {
"final": "Zeugnis Noten",
"mid": "Halbjährlich Noten",
"1q": "1. Quartal Noten",
"2q": "2. Quartal Noten",
"3q": "3. Quartal Noten",
"4q": "4. Quartal Noten",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,87 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:filcnaplo/utils/format.dart';
import 'certification_tile.i18n.dart';
class CertificationTile extends StatelessWidget {
const CertificationTile(this.grade, {Key? key, this.onTap, this.padding}) : super(key: key);
final Function()? onTap;
final Grade grade;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
bool isSubjectView = SubjectGradesContainer.of(context) != null;
String certificationName;
switch (grade.type) {
case GradeType.endYear:
certificationName = "final".i18n;
break;
case GradeType.halfYear:
certificationName = "mid".i18n;
break;
case GradeType.firstQ:
certificationName = "1q".i18n;
break;
case GradeType.secondQ:
certificationName = "2q".i18n;
break;
case GradeType.thirdQ:
certificationName = "3q".i18n;
break;
case GradeType.fourthQ:
certificationName = "4q".i18n;
break;
case GradeType.levelExam:
certificationName = "equivalency".i18n;
break;
case GradeType.unknown:
default:
certificationName = "unknown".i18n;
}
return Material(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(8.0),
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding:
isSubjectView ? const EdgeInsets.only(left: 12.0, right: 12.0, top: 2.0, bottom: 8.0) : const EdgeInsets.only(left: 8.0, right: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
onTap: onTap,
leading: isSubjectView
? GradeValueWidget(
grade.value,
complemented: grade.description == 'Dicséret',
)
: Padding(
padding: const EdgeInsets.only(left: 2.0),
child: Icon(SubjectIcon.resolveVariant(subject: grade.subject, context: context),
size: 28.0, color: AppColors.of(context).text.withOpacity(.75)),
),
minLeadingWidth: isSubjectView ? 32.0 : 42.0,
trailing: isSubjectView
? const Icon(FeatherIcons.award)
: GradeValueWidget(
grade.value,
complemented: grade.description == 'Dicséret',
),
title: Text(isSubjectView ? certificationName : grade.subject.renamedTo ?? grade.subject.name.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 18.0, fontStyle: grade.subject.isRenamed ? FontStyle.italic : null)),
subtitle: Text(grade.value.valueName, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0)),
),
),
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"final": "Final",
"mid": "Mid year",
"1q": "1. Quarter",
"2q": "2. Quarter",
"3q": "3. Quarter",
"4q": "4. Quarter",
"equivalency": "Equivalency test",
"unknown": "Unknown",
"classavg": "Class Average",
},
"hu_hu": {
"final": "Év vége",
"mid": "Félév",
"1q": "1. Negyedév",
"2q": "2. Negyedév",
"3q": "3. Negyedév",
"4q": "4. Negyedév",
"equivalency": "Osztályozó",
"unknown": "Ismeretlen",
"classavg": "Osztályátlag",
},
"de_de": {
"final": "Zeugnis",
"mid": "Halbjährlich",
"1q": "1. Quartal",
"2q": "2. Quartal",
"3q": "3. Quartal",
"4q": "4. Quartal",
"equivalency": "Zulassungsprüfung",
"unknown": "Unbekannt",
"classavg": "Klassendurchschnitt",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,43 @@
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/cretification/certification_card.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/cretification/certification_tile.dart';
import 'package:filcnaplo_mobile_ui/common/hero_scrollview.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
class CertificationView extends StatelessWidget {
const CertificationView(this.grades, {Key? key, required this.gradeType}) : super(key: key);
final List<Grade> grades;
final GradeType gradeType;
static show(List<Grade> grades, {required BuildContext context, required GradeType gradeType}) =>
Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute(builder: (context) => CertificationView(grades, gradeType: gradeType)));
@override
Widget build(BuildContext context) {
grades.sort((a, b) => a.subject.name.compareTo(b.subject.name));
List<Widget> tiles = grades.map((e) => CertificationTile(e)).toList();
return Scaffold(
body: HeroScrollView(
title: getGradeTypeTitle(gradeType),
icon: FeatherIcons.award,
iconSize: 50,
child: ListView(
children: [
SafeArea(
child: Panel(
child: Column(
children: tiles,
),
),
)
],
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
physics: const BouncingScrollPhysics(),
)));
}
}

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
class CustomSwitch extends StatelessWidget {
final ValueChanged<bool> onChanged;
final bool value;
const CustomSwitch({
Key? key,
required this.onChanged,
required this.value,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onChanged(!value),
child: SizedBox(
height: 25,
width: 50,
child: Stack(
children: <Widget>[
AnimatedContainer(
height: 25,
width: 50,
curve: Curves.ease,
duration: const Duration(milliseconds: 400),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(25.0),
),
color: value ? Theme.of(context).colorScheme.secondary : Theme.of(context).highlightColor,
),
),
AnimatedAlign(
curve: Curves.ease,
duration: const Duration(milliseconds: 400),
alignment: !value ? Alignment.centerLeft : Alignment.centerRight,
child: Container(
height: 20,
width: 20,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.1),
spreadRadius: 0.5,
blurRadius: 1,
)
],
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:flutter/material.dart';
class EventTile extends StatelessWidget {
const EventTile(this.event, {Key? key, this.onTap, this.padding}) : super(key: key);
final Event event;
final void Function()? onTap;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(14.0),
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: const ProfileImage(
name: "!",
radius: 22.0,
),
title: Text(
event.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
event.content.escapeHtml().replaceAll('\n', ' '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
minLeadingWidth: 0,
),
),
);
}
}

View File

@ -0,0 +1,57 @@
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/common/sliding_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
class EventView extends StatelessWidget {
const EventView(this.event, {Key? key}) : super(key: key);
final Event event;
static void show(Event event, {required BuildContext context}) => showSlidingBottomSheet(context: context, child: EventView(event));
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header
ListTile(
title: Text(
event.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
trailing: Text(
event.start.format(context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
// Details
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: SelectableLinkify(
text: event.content.escapeHtml(),
options: const LinkifyOptions(looseUrl: true, removeWww: true),
onOpen: (link) {
launch(link.url,
customTabsOption: CustomTabsOption(
toolbarColor: Theme.of(context).scaffoldBackgroundColor,
showPageTitle: true,
));
},
style: const TextStyle(fontWeight: FontWeight.w400),
),
),
],
),
);
}
}

View File

@ -0,0 +1,18 @@
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/event/event_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/event/event_view.dart';
import 'package:flutter/material.dart';
class EventViewable extends StatelessWidget {
const EventViewable(this.event, {Key? key}) : super(key: key);
final Event event;
@override
Widget build(BuildContext context) {
return EventTile(
event,
onTap: () => EventView.show(event, context: context),
);
}
}

View File

@ -0,0 +1,58 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
class ExamTile extends StatelessWidget {
const ExamTile(this.exam, {Key? key, this.onTap, this.padding}) : super(key: key);
final Exam exam;
final void Function()? onTap;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: SizedBox(
width: 44,
height: 44,
child: Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Icon(
SubjectIcon.resolveVariant(subjectName: exam.subjectName, context: context),
size: 28.0,
color: AppColors.of(context).text.withOpacity(.75),
),
)),
title: Text(
exam.description != "" ? exam.description : (exam.mode?.description ?? "Számonkérés"),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
exam.subjectName.capital(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: Icon(
FeatherIcons.edit,
color: AppColors.of(context).text.withOpacity(.75),
),
minLeadingWidth: 0,
),
),
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_card.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/common/detail.dart';
import 'package:flutter/material.dart';
import 'exam_view.i18n.dart';
class ExamView extends StatelessWidget {
const ExamView(this.exam, {Key? key}) : super(key: key);
final Exam exam;
static show(Exam exam, {required BuildContext context}) => showBottomCard(context: context, child: ExamView(exam));
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header
ListTile(
leading: Padding(
padding: const EdgeInsets.only(left: 6.0),
child: Icon(
SubjectIcon.resolveVariant(subjectName: exam.subjectName, context: context),
size: 36.0,
color: AppColors.of(context).text.withOpacity(.75),
),
),
title: Text(
exam.subjectName.capital(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
exam.teacher,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: Text(
exam.date.format(context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
// Details
if (exam.writeDate.year != 0) Detail(title: "date".i18n, description: exam.writeDate.format(context)),
if (exam.description != "") Detail(title: "description".i18n, description: exam.description),
if (exam.mode != null) Detail(title: "mode".i18n, description: exam.mode!.description),
],
),
);
}
}

View File

@ -0,0 +1,27 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"date": "Date",
"description": "Description",
"mode": "Type",
},
"hu_hu": {
"date": "Írás ideje",
"description": "Leírás",
"mode": "Típus",
},
"de_de": {
"date": "Prüfungszeit",
"description": "Bezeichnung",
"mode": "Typ",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,20 @@
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_mobile_ui/common/viewable.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/card_handle.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/exam/exam_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/exam/exam_view.dart';
import 'package:flutter/material.dart';
class ExamViewable extends StatelessWidget {
const ExamViewable(this.exam, {Key? key}) : super(key: key);
final Exam exam;
@override
Widget build(BuildContext context) {
return Viewable(
tile: ExamTile(exam),
view: CardHandle(child: ExamView(exam)),
);
}
}

View File

@ -0,0 +1,70 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_mobile_ui/common/average_display.dart';
import 'package:flutter/material.dart';
class GradeSubjectTile extends StatelessWidget {
const GradeSubjectTile(this.subject, {Key? key, this.average = 0.0, this.groupAverage = 0.0, this.onTap, this.averageBefore = 0.0})
: super(key: key);
final Subject subject;
final void Function()? onTap;
final double average;
final double groupAverage;
final double averageBefore;
@override
Widget build(BuildContext context) {
Color textColor = AppColors.of(context).text;
// Failing indicator
if (average < 2.0 && average >= 1.0) {
textColor = AppColors.of(context).red;
}
final String changeIcon = average < averageBefore ? "" : "";
final Color changeColor = average < averageBefore ? Colors.redAccent : Colors.lightGreenAccent.shade700;
return Material(
type: MaterialType.transparency,
child: ListTile(
minLeadingWidth: 32.0,
dense: true,
contentPadding: const EdgeInsets.only(left: 8.0, right: 6.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
visualDensity: VisualDensity.compact,
onTap: onTap,
leading: Icon(SubjectIcon.resolveVariant(subject: subject, context: context), color: textColor.withOpacity(.75)),
title: Text(
subject.renamedTo ?? subject.name.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14.0, color: textColor, fontStyle: subject.isRenamed ? FontStyle.italic : null),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (groupAverage != 0 && averageBefore == 0.0) AverageDisplay(average: groupAverage, border: true),
const SizedBox(width: 6.0),
if (averageBefore != 0.0 && averageBefore != average) ...[
AverageDisplay(average: averageBefore),
Padding(
padding: const EdgeInsets.only(left: 6.0, right: 6.0, bottom: 3.5),
child: Text(
changeIcon,
style: TextStyle(
color: changeColor,
fontSize: 20.0,
),
),
)
],
AverageDisplay(average: average)
],
),
),
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_card.dart';
import 'package:filcnaplo_mobile_ui/common/detail.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'grade_view.i18n.dart';
class GradeView extends StatelessWidget {
const GradeView(this.grade, {Key? key}) : super(key: key);
static show(Grade grade, {required BuildContext context}) => showBottomCard(context: context, child: GradeView(grade));
final Grade grade;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: GradeValueWidget(grade.value, fill: true),
title: Text(
grade.subject.renamedTo ?? grade.subject.name.capital(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: grade.subject.isRenamed ? FontStyle.italic : null),
),
subtitle: Text(
!Provider.of<SettingsProvider>(context, listen: false).presentationMode ? grade.teacher : "Tanár",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: Text(
grade.date.format(context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
// Grade Details
Detail(
title: "value".i18n,
description: "${grade.value.valueName} " + percentText(),
),
if (grade.description != "") Detail(title: "description".i18n, description: grade.description),
if (grade.mode.description != "") Detail(title: "mode".i18n, description: grade.mode.description),
if (grade.writeDate.year != 0) Detail(title: "date".i18n, description: grade.writeDate.format(context)),
],
),
);
}
String percentText() => grade.value.weight != 100 && grade.value.weight > 0 ? "${grade.value.weight}%" : "";
}

View File

@ -0,0 +1,30 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"value": "Value",
"date": "Date",
"description": "Description",
"mode": "Type",
},
"hu_hu": {
"value": "Érték",
"date": "Írás ideje",
"description": "Leírás",
"mode": "Típus",
},
"de_de": {
"value": "Notenwert",
"date": "Prüfungszeit",
"description": "Bezeichnung",
"mode": "Typ",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,25 @@
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/card_handle.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/grade/grade_view.dart';
import 'package:filcnaplo_mobile_ui/common/viewable.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart';
import 'package:flutter/material.dart';
class GradeViewable extends StatelessWidget {
const GradeViewable(this.grade, {Key? key, this.padding}) : super(key: key);
final Grade grade;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
final subject = SubjectGradesContainer.of(context) != null;
final tile = GradeTile(grade, padding: subject ? EdgeInsets.zero : padding);
return Viewable(
tile: subject ? SubjectGradesContainer(child: tile) : tile,
view: CardHandle(child: GradeView(grade)),
);
}
}

View File

@ -0,0 +1,158 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/grade/surprise_grade.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:rive/rive.dart';
import 'new_grades.i18n.dart';
class NewGradesSurprise extends StatelessWidget {
const NewGradesSurprise(this.grades, {Key? key, this.censored = false}) : super(key: key);
final List<Grade> grades;
final bool censored;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).colorScheme.secondary,
width: 3.0,
),
borderRadius: BorderRadius.circular(14.0),
),
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: () => openingFun(context),
minLeadingWidth: 54,
leading: SizedBox(
width: 44,
height: 44,
child: Center(
child: Container(
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.secondary.withOpacity(.5),
blurRadius: 18.0,
)
]),
child: const RiveAnimation.asset("assets/animations/backpack-2.riv"),
),
),
),
title: censored
? Wrap(
children: [
Container(
width: 85,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.85),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
"new_grades".i18n,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: censored
? Wrap(
children: [
Container(
width: 125,
height: 10,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
"tap_to_open".i18n,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: censored
? Wrap(
children: [
Container(
width: 25,
height: 25,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(25.0),
),
),
],
)
: Text.rich(
TextSpan(children: [
TextSpan(
text: "${grades.length}",
style: TextStyle(
shadows: [
Shadow(
color: AppColors.of(context).text.withOpacity(.2),
offset: const Offset(2, 2),
)
],
)),
TextSpan(
text: "x",
style: TextStyle(
fontSize: 20.0,
color: AppColors.of(context).text.withOpacity(.5),
fontWeight: FontWeight.w800,
),
)
]),
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 28.0,
color: AppColors.of(context).text.withOpacity(.75),
),
),
),
),
);
}
void openingFun(BuildContext context) {
final settings = Provider.of<SettingsProvider>(context, listen: false);
if (!settings.gradeOpeningFun) return;
final gradeProvider = Provider.of<GradeProvider>(context, listen: false);
final newGrades = gradeProvider.grades.where((element) => element.date.isAfter(gradeProvider.lastSeenDate)).toList();
newGrades.sort((a, b) => a.date.compareTo(b.date));
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 100));
for (final grade in newGrades) {
await showDialog(
context: context,
builder: (context) => SurpriseGrade(grade),
useRootNavigator: true,
barrierDismissible: false,
barrierColor: Colors.transparent,
useSafeArea: false,
);
await Future.delayed(const Duration(milliseconds: 300));
}
await gradeProvider.seenAll();
});
}
}

View File

@ -0,0 +1,42 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"common": "Common",
"uncommon": "Uncommon",
"rare": "Rare",
"epic": "Epic",
"legendary": "Legendary",
"new_grades": "New grades",
"tap_to_open": "Tap to open now!",
"open_subtitle": "Tap to open...",
},
"hu_hu": {
"common": "Gyakori",
"uncommon": "Nem gyakori",
"rare": "Ritka",
"epic": "Epikus",
"legendary": "Legendás",
"new_grades": "Új jegyek",
"tap_to_open": "Nyisd ki őket!",
"open_subtitle": "Nyomd meg a kinyitáshoz...",
},
"de_de": {
"common": "Gemeinsam",
"uncommon": "Gelegentlich",
"rare": "Selten",
"epic": "Episch",
"legendary": "Legendär",
"new_grades": "Neue Noten",
"tap_to_open": "Tippen, um jetzt zu öffnen!",
"open_subtitle": "Antippen zum Öffnen...",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,389 @@
import 'dart:math';
import 'dart:ui';
import 'package:animated_background/animated_background.dart' as bg;
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/pages/home/particle.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:rive/rive.dart' as rive;
import 'new_grades.i18n.dart';
class SurpriseGrade extends StatefulWidget {
const SurpriseGrade(this.grade, {Key? key}) : super(key: key);
final Grade grade;
@override
State<SurpriseGrade> createState() => _SurpriseGradeState();
}
class _SurpriseGradeState extends State<SurpriseGrade> with TickerProviderStateMixin {
late AnimationController _revealAnimFade;
late AnimationController _revealAnimScale;
late AnimationController _revealAnimGrade;
late AnimationController _revealAnimParticle;
late rive.RiveAnimationController _controller;
@override
void initState() {
super.initState();
_revealAnimFade = AnimationController(vsync: this, duration: const Duration(milliseconds: 500));
_revealAnimScale = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
_revealAnimGrade = AnimationController(vsync: this, duration: const Duration(seconds: 1));
_revealAnimParticle = AnimationController(vsync: this, duration: const Duration(seconds: 2));
_revealAnimScale.animateTo(0.7, duration: Duration.zero);
_controller = rive.SimpleAnimation('Timeline 1', autoplay: false);
WidgetsBinding.instance.addPostFrameCallback((_) {
_revealAnimFade.animateTo(1.0, curve: Curves.easeInOut);
Future.delayed(const Duration(milliseconds: 200), () {
_revealAnimScale.animateTo(1.0, curve: Curves.easeInOut).then((_) {
setState(() => subtitle = true);
});
});
});
seed = Random().nextInt(100000000);
}
@override
void dispose() {
_revealAnimFade.dispose();
_revealAnimScale.dispose();
_revealAnimGrade.dispose();
_revealAnimParticle.dispose();
_controller.dispose();
super.dispose();
}
bool hold = false;
bool subtitle = false;
late int seed;
void reveal() async {
if (!subtitle) {
_revealAnimParticle.animateBack(0.0, curve: Curves.fastLinearToSlowEaseIn, duration: const Duration(milliseconds: 300));
await Future.delayed(const Duration(milliseconds: 50));
_revealAnimGrade.animateBack(0.0, curve: Curves.fastLinearToSlowEaseIn);
await Future.delayed(const Duration(milliseconds: 50));
_revealAnimFade.animateBack(0.0, curve: Curves.easeInOut);
_revealAnimScale.animateBack(0.0, curve: Curves.easeInOut);
if (mounted) Navigator.of(context).pop();
return;
}
subtitle = false;
setState(() => hold = false);
_controller.isActive = true;
await Future.delayed(const Duration(seconds: 2));
if (mounted) _revealAnimGrade.animateTo(1.0, curve: Curves.fastLinearToSlowEaseIn);
await Future.delayed(const Duration(milliseconds: 700));
if (mounted) await _revealAnimParticle.animateTo(1.0, curve: Curves.fastLinearToSlowEaseIn);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _revealAnimFade,
builder: (context, child) {
return FadeTransition(
opacity: _revealAnimFade,
child: Material(
color: Colors.black.withOpacity(.75),
child: Container(
color: Theme.of(context).colorScheme.secondary.withOpacity(.05),
child: Container(
decoration: const BoxDecoration(
gradient: RadialGradient(
colors: [Colors.transparent, Colors.black],
radius: 1.5,
stops: [0.2, 1.0],
),
),
child: bg.AnimatedBackground(
vsync: this,
behaviour: bg.RandomParticleBehaviour(
options: bg.ParticleOptions(
baseColor: Theme.of(context).colorScheme.secondary,
spawnMinSpeed: 5.0,
spawnMaxSpeed: 10.0,
minOpacity: .05,
maxOpacity: .08,
spawnMinRadius: 30.0,
spawnMaxRadius: 50.0,
particleCount: 20,
),
),
child: ScaleTransition(
scale: _revealAnimScale,
child: child,
),
),
),
),
),
);
},
child: AnimatedBuilder(
animation: _revealAnimGrade,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SlideTransition(
position: _revealAnimGrade.drive(Tween(begin: Offset.zero, end: const Offset(0, 0.7))),
child: AnimatedScale(
scale: hold ? 1.1 : 1.0,
curve: Curves.easeOutBack,
duration: const Duration(milliseconds: 200),
child: GestureDetector(
onLongPressDown: (_) => setState(() => hold = true),
onLongPressEnd: (_) => reveal(),
onLongPressCancel: reveal,
child: ScaleTransition(
scale: CurvedAnimation(curve: Curves.easeInOut, parent: _revealAnimGrade.drive(Tween(begin: 1.0, end: 0.8))),
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 300,
height: 300,
child: rive.RiveAnimation.asset(
"assets/animations/backpack-2.riv",
fit: BoxFit.contain,
controllers: [_controller],
antialiasing: false,
),
),
SlideTransition(
position: _revealAnimParticle.drive(Tween(begin: const Offset(0, 0.3), end: const Offset(0, 0.8))),
child: FadeTransition(
opacity: _revealAnimParticle,
child: ClipRRect(
borderRadius: BorderRadius.circular(24.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 32.0, sigmaY: 32.0),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 20.0),
decoration: BoxDecoration(
color: Colors.white.withOpacity(.3),
borderRadius: BorderRadius.circular(24.0),
border: Border.all(color: Colors.black.withOpacity(.3), width: 1.0),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (widget.grade.description != "")
Text(
widget.grade.description,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 26.0,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Text(
widget.grade.subject.renamedTo ?? widget.grade.subject.name.capital(),
style: TextStyle(
color: Colors.white.withOpacity(.8),
fontWeight: FontWeight.bold,
fontSize: 24.0,
fontStyle: widget.grade.subject.isRenamed ? FontStyle.italic : null),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
"${widget.grade.value.weight}%",
style: TextStyle(
color: Colors.white.withOpacity(.7),
fontWeight: FontWeight.w600,
fontSize: 20.0,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 20.0),
Icon(
SubjectIcon.resolveVariant(subject: widget.grade.subject, context: context),
color: Colors.white,
size: 82.0,
),
],
),
),
),
),
),
),
],
),
),
),
),
),
const SizedBox(height: 42.0),
AnimatedOpacity(
opacity: subtitle ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: Text(
"open_subtitle".i18n,
style: TextStyle(
color: Colors.white.withOpacity(.8),
fontWeight: FontWeight.w600,
fontSize: 24.0,
),
),
),
],
),
if (_revealAnimGrade.value > 0)
AnimatedBuilder(
animation: _revealAnimParticle,
builder: (context, child) {
bool shouldPaint = false;
if (_revealAnimParticle.status == AnimationStatus.forward || _revealAnimParticle.status == AnimationStatus.reverse) {
shouldPaint = true;
}
return ScaleTransition(
scale: _revealAnimGrade,
child: FadeTransition(
opacity: _revealAnimGrade,
child: SlideTransition(
position: _revealAnimGrade.drive(Tween(begin: Offset.zero, end: const Offset(0, -0.6))),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SlideTransition(
position: _revealAnimGrade.drive(Tween(begin: Offset.zero, end: const Offset(0, -0.9))),
child: Text(
["legendary", "epic", "rare", "uncommon", "common"][5 - widget.grade.value.value].i18n,
style: TextStyle(
fontSize: 46.0,
fontWeight: FontWeight.bold,
color: gradeColor(context: context, value: widget.grade.value.value),
shadows: [
Shadow(
color: gradeColor(context: context, value: widget.grade.value.value).withOpacity(.5),
blurRadius: 24.0,
),
Shadow(
color: gradeColor(context: context, value: widget.grade.value.value).withOpacity(.3),
offset: const Offset(-3, -3),
),
],
),
),
),
const SizedBox(height: 32.0),
ScaleTransition(
scale: CurvedAnimation(curve: Curves.easeInOutBack, parent: _revealAnimParticle.drive(Tween(begin: 0.6, end: 1.0))),
child: CustomPaint(
painter: PimpPainter(
particle: Sprinkles(),
controller: _revealAnimParticle,
seed: seed + 1,
shouldPaint: shouldPaint,
),
child: CustomPaint(
painter: PimpPainter(
particle: Sprinkles(),
controller: _revealAnimParticle,
seed: seed,
shouldPaint: shouldPaint,
),
child: RotationTransition(
turns:
CurvedAnimation(curve: Curves.easeInBack, parent: _revealAnimGrade).drive(Tween(begin: 0.95, end: 1.0)),
child: GradeValueWidget(
widget.grade.value,
fill: true,
contrast: true,
shadow: true,
outline: true,
size: 100.0,
),
),
),
),
),
],
),
),
),
);
},
),
],
);
}),
);
}
}
class PimpPainter extends CustomPainter {
PimpPainter({required this.particle, required this.seed, required this.controller, required this.shouldPaint}) : super(repaint: controller);
final Particle particle;
final int seed;
final AnimationController controller;
final bool shouldPaint;
@override
void paint(Canvas canvas, Size size) {
if (shouldPaint) {
canvas.translate(size.width / 2, size.height / 2);
particle.paint(canvas, size, controller.value, seed);
}
}
@override
bool shouldRepaint(PimpPainter oldDelegate) => shouldPaint;
}
Color randomColor(int c) {
c = c % 5;
if (c == 0) return Colors.red.shade300;
if (c == 1) return Colors.green.shade300;
if (c == 2) return Colors.orange.shade300;
if (c == 3) return Colors.blue.shade300;
if (c == 4) return Colors.pink.shade300;
if (c == 5) return Colors.brown.shade300;
return Colors.black;
}
class Sprinkles extends Particle {
@override
void paint(Canvas canvas, Size size, progress, seed) {
Random random = Random(seed);
int randomMirrorOffset = random.nextInt(8) + 1;
CompositeParticle(children: [
Firework(),
RectangleMirror.builder(
numberOfParticles: 6,
particleBuilder: (n) {
return AnimatedPositionedParticle(
begin: const Offset(0.0, -10.0),
end: const Offset(0.0, -60.0),
child: FadingRect(width: 5.0, height: 15.0, color: randomColor(n)),
);
},
initialDistance: -pi / randomMirrorOffset),
]).paint(canvas, size, progress, seed);
}
}

View File

@ -0,0 +1,89 @@
import 'dart:io';
import 'package:filcnaplo/helpers/attachment_helper.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/message/image_view.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:flutter/material.dart';
import 'homework_attachment_tile.i18n.dart';
class HomeworkAttachmentTile extends StatelessWidget {
const HomeworkAttachmentTile(this.attachment, {Key? key}) : super(key: key);
final HomeworkAttachment attachment;
Widget buildImage(BuildContext context) {
return FutureBuilder<String>(
future: attachment.download(context),
builder: (context, snapshot) {
return snapshot.hasData
? Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.0),
child: Material(
child: InkWell(
onTap: () {
Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(
builder: (context) => ImageView(snapshot.data!),
));
},
child: Ink.image(
image: FileImage(File(snapshot.data ?? "")),
height: 200.0,
width: double.infinity,
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(12.0),
),
),
),
)
: Center(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: CircularProgressIndicator(color: Theme.of(context).colorScheme.secondary),
));
},
);
}
@override
Widget build(BuildContext context) {
if (attachment.isImage) return buildImage(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: InkWell(
borderRadius: BorderRadius.circular(12.0),
onTap: () {
attachment.open(context).then((value) {
if (!value) {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
context: context,
content: Text("Failed to open attachment".i18n),
backgroundColor: AppColors.of(context).red,
duration: const Duration(seconds: 1),
));
}
});
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(children: [
const Icon(FeatherIcons.paperclip),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(attachment.name, maxLines: 2, overflow: TextOverflow.ellipsis),
),
),
]),
),
),
);
}
}

View File

@ -0,0 +1,21 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Failed to open attachment": "Failed to open attachment",
},
"hu_hu": {
"Failed to open attachment": "Nem sikerült megnyitni a mellékletet",
},
"de_de": {
"Failed to open attachment": "Anhang konnte nicht geöffnet werden",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,103 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
class HomeworkTile extends StatelessWidget {
const HomeworkTile(this.homework, {Key? key, this.onTap, this.padding, this.censored = false}) : super(key: key);
final Homework homework;
final void Function()? onTap;
final EdgeInsetsGeometry? padding;
final bool censored;
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(8.0),
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
leading: SizedBox(
width: 44,
height: 44,
child: censored
? Container(
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.55),
borderRadius: BorderRadius.circular(60.0),
),
)
: Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Icon(
SubjectIcon.resolveVariant(subjectName: homework.subjectName, context: context),
size: 28.0,
color: AppColors.of(context).text.withOpacity(.75),
),
),
),
title: censored
? Wrap(
children: [
Container(
width: 160,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.85),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
homework.subjectName.capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: censored
? Wrap(
children: [
Container(
width: 100,
height: 10,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
homework.content.escapeHtml().replaceAll('\n', ' '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: censored
? Container(
width: 15,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
)
: Icon(
FeatherIcons.home,
color: AppColors.of(context).text.withOpacity(.75),
),
minLeadingWidth: 0,
),
),
);
}
}

View File

@ -0,0 +1,88 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/common/detail.dart';
import 'package:filcnaplo_mobile_ui/common/sliding_bottom_sheet.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/homework/homework_attachment_tile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'homework_view.i18n.dart';
class HomeworkView extends StatelessWidget {
const HomeworkView(this.homework, {Key? key}) : super(key: key);
final Homework homework;
static show(Homework homework, {required BuildContext context}) {
showSlidingBottomSheet(context: context, child: HomeworkView(homework));
}
@override
Widget build(BuildContext context) {
List<Widget> attachmentTiles = [];
for (var attachment in homework.attachments) {
attachmentTiles.add(Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
child: HomeworkAttachmentTile(
attachment,
),
));
}
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header
ListTile(
leading: Icon(
SubjectIcon.resolveVariant(subjectName: homework.subjectName, context: context),
size: 36.0,
),
title: Text(
homework.subjectName.capital(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
homework.teacher,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: Text(
homework.date.format(context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
// Details
if (homework.deadline.year != 0) Detail(title: "deadline".i18n, description: homework.deadline.format(context)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 6.0),
child: SelectableLinkify(
text: homework.content.escapeHtml(),
options: const LinkifyOptions(looseUrl: true, removeWww: true),
onOpen: (link) {
launch(link.url,
customTabsOption: CustomTabsOption(
toolbarColor: Theme.of(context).scaffoldBackgroundColor,
showPageTitle: true,
));
},
style: const TextStyle(fontWeight: FontWeight.w400),
),
),
// Attachments
...attachmentTiles,
],
),
);
}
}

View File

@ -0,0 +1,21 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"deadline": "Deadline",
},
"hu_hu": {
"deadline": "Határidő",
},
"de_de": {
"deadline": "Termin",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,18 @@
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/homework/homework_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/homework/homework_view.dart';
import 'package:flutter/material.dart';
class HomeworkViewable extends StatelessWidget {
const HomeworkViewable(this.homework, {Key? key}) : super(key: key);
final Homework homework;
@override
Widget build(BuildContext context) {
return HomeworkTile(
homework,
onTap: () => HomeworkView.show(homework, context: context),
);
}
}

View File

@ -0,0 +1,75 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'changed_lesson_tile.i18n.dart';
class ChangedLessonTile extends StatelessWidget {
const ChangedLessonTile(this.lesson, {Key? key, this.onTap, this.padding}) : super(key: key);
final Lesson lesson;
final void Function()? onTap;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
String lessonIndexTrailing = "";
// Only put a trailing . if its a digit
if (RegExp(r'\d').hasMatch(lesson.lessonIndex)) lessonIndexTrailing = ".";
Color accent = Theme.of(context).colorScheme.secondary;
if (lesson.substituteTeacher != "") {
accent = AppColors.of(context).yellow;
}
if (lesson.status?.name == "Elmaradt") {
accent = AppColors.of(context).red;
}
return Material(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(14.0),
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: SizedBox(
width: 44.0,
height: 44.0,
child: Center(
child: Text(
lesson.lessonIndex + lessonIndexTrailing,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 30.0,
fontWeight: FontWeight.w600,
color: accent,
),
),
),
),
title: Text(
lesson.substituteTeacher != "" ? "substituted".i18n : "cancelled".i18n,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
lesson.subject.renamedTo ?? lesson.subject.name.capital(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w500, fontStyle: lesson.subject.isRenamed ? FontStyle.italic : null),
),
trailing: const Icon(FeatherIcons.arrowRight),
minLeadingWidth: 0,
),
),
);
}
}

View File

@ -0,0 +1,24 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"cancelled": "Cancelled lesson",
"substituted": "Substituted lesson",
},
"hu_hu": {
"cancelled": "Elmaradó óra",
"substituted": "Helyettesített óra",
},
"de_de": {
"cancelled": "Abgesagte Stunde",
"substituted": "Vertretene Stunden",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,18 @@
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/lesson/changed_lesson_tile.dart';
import 'package:filcnaplo_mobile_ui/pages/timetable/timetable_page.dart';
import 'package:flutter/material.dart';
class ChangedLessonViewable extends StatelessWidget {
const ChangedLessonViewable(this.lesson, {Key? key}) : super(key: key);
final Lesson lesson;
@override
Widget build(BuildContext context) {
return ChangedLessonTile(
lesson,
onTap: () => TimetablePage.jump(context, lesson: lesson),
);
}
}

View File

@ -0,0 +1,80 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_card.dart';
import 'package:filcnaplo_mobile_ui/common/detail.dart';
import 'package:flutter/material.dart';
import 'lesson_view.i18n.dart';
class LessonView extends StatelessWidget {
const LessonView(this.lesson, {Key? key}) : super(key: key);
final Lesson lesson;
@override
Widget build(BuildContext context) {
Color accent = Theme.of(context).colorScheme.secondary;
String lessonIndexTrailing = "";
if (RegExp(r'\d').hasMatch(lesson.lessonIndex)) lessonIndexTrailing = ".";
if (lesson.substituteTeacher != "") {
accent = AppColors.of(context).yellow;
}
if (lesson.status?.name == "Elmaradt") {
accent = AppColors.of(context).red;
}
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header
ListTile(
leading: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
lesson.lessonIndex + lessonIndexTrailing,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 38.0,
fontWeight: FontWeight.w600,
color: accent,
),
),
),
title: Text(
lesson.subject.renamedTo ?? lesson.subject.name.capital(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: lesson.subject.isRenamed ? FontStyle.italic : null),
),
subtitle: Text(
lesson.substituteTeacher == "" ? lesson.teacher : lesson.substituteTeacher,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: Text(
lesson.date.format(context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
// Details
if (lesson.room != "") Detail(title: "Room".i18n, description: lesson.room.replaceAll("_", " ")),
if (lesson.description != "") Detail(title: "Description".i18n, description: lesson.description),
if (lesson.lessonYearIndex != null) Detail(title: "Lesson Number".i18n, description: "${lesson.lessonYearIndex}."),
if (lesson.groupName != "") Detail(title: "Group".i18n, description: lesson.groupName),
],
),
);
}
static show(Lesson lesson, {required BuildContext context}) {
showBottomCard(context: context, child: LessonView(lesson));
}
}

View File

@ -0,0 +1,30 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Room": "Room",
"Description": "Description",
"Lesson Number": "Lesson Number",
"Group": "Group",
},
"hu_hu": {
"Room": "Terem",
"Description": "Leírás",
"Lesson Number": "Éves óraszám",
"Group": "Csoport",
},
"de_de": {
"Room": "Raum",
"Description": "Bezeichnung",
"Lesson Number": "Ordinalzahl",
"Group": "Gruppe",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,25 @@
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_mobile_ui/common/viewable.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/card_handle.dart';
import 'package:filcnaplo/ui/widgets/lesson/lesson_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/lesson/lesson_view.dart';
import 'package:flutter/material.dart';
class LessonViewable extends StatelessWidget {
const LessonViewable(this.lesson, {Key? key, this.swapDesc = false}) : super(key: key);
final Lesson lesson;
final bool swapDesc;
@override
Widget build(BuildContext context) {
final tile = LessonTile(lesson, swapDesc: swapDesc);
if (lesson.subject.id == '' || tile.lesson.isEmpty) return tile;
return Viewable(
tile: tile,
view: CardHandle(child: LessonView(lesson)),
);
}
}

View File

@ -0,0 +1,83 @@
import 'dart:io';
import 'package:filcnaplo_kreta_api/models/attachment.dart';
import 'package:filcnaplo/helpers/attachment_helper.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/message/image_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
class AttachmentTile extends StatelessWidget {
const AttachmentTile(this.attachment, {Key? key}) : super(key: key);
final Attachment attachment;
Widget buildImage(BuildContext context) {
return FutureBuilder<String>(
future: attachment.download(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.0),
child: Material(
child: InkWell(
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) {
return ImageView(snapshot.data!);
},
);
},
child: Ink.image(
image: FileImage(File(snapshot.data ?? "")),
height: 200.0,
width: double.infinity,
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(12.0),
),
),
),
);
} else {
return Center(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: CircularProgressIndicator(color: Theme.of(context).colorScheme.secondary),
));
}
},
);
}
@override
Widget build(BuildContext context) {
if (attachment.isImage) return buildImage(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: InkWell(
borderRadius: BorderRadius.circular(12.0),
onTap: () {
attachment.open(context);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(children: [
const Icon(FeatherIcons.paperclip),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(attachment.name, maxLines: 2, overflow: TextOverflow.ellipsis),
),
),
]),
),
),
);
}
}

View File

@ -0,0 +1,46 @@
import 'dart:io';
import 'package:filcnaplo/helpers/share_helper.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:photo_view/photo_view.dart';
class ImageView extends StatelessWidget {
const ImageView(this.path, {Key? key}) : super(key: key);
final String path;
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).scaffoldBackgroundColor,
child: SafeArea(
minimum: const EdgeInsets.only(top: 24.0),
child: Scaffold(
appBar: AppBar(
leading: BackButton(color: AppColors.of(context).text),
actions: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
onPressed: () => ShareHelper.shareFile(path),
icon: Icon(FeatherIcons.share2, color: AppColors.of(context).text),
splashRadius: 24.0,
),
),
],
),
body: PhotoView(
imageProvider: FileImage(File(path)),
maxScale: 4.0,
minScale: PhotoViewComputedScale.contained,
backgroundDecoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
),
),
),
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/message/message_view_tile.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class MessageView extends StatefulWidget {
const MessageView(this.messages, {Key? key}) : super(key: key);
final List<Message> messages;
static show(List<Message> messages, {required BuildContext context}) =>
Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute(builder: (context) => MessageView(messages)));
@override
_MessageViewState createState() => _MessageViewState();
}
class _MessageViewState extends State<MessageView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leadingWidth: 64.0,
leading: BackButton(color: AppColors.of(context).text),
elevation: 0,
actions: const [
// Padding(
// padding: EdgeInsets.only(right: 8.0),
// child: IconButton(
// onPressed: () {},
// icon: Icon(FeatherIcons.archive, color: AppColors.of(context).text),
// splashRadius: 32.0,
// ),
// ),
],
),
body: SafeArea(
child: ListView.builder(
padding: EdgeInsets.zero,
physics: const BouncingScrollPhysics(),
itemCount: widget.messages.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: MessageViewTile(widget.messages[index]),
);
},
),
),
);
}
}

View File

@ -0,0 +1,122 @@
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/message/attachment_tile.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
// import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:provider/provider.dart';
import 'message_view_tile.i18n.dart';
class MessageViewTile extends StatelessWidget {
const MessageViewTile(this.message, {Key? key}) : super(key: key);
final Message message;
@override
Widget build(BuildContext context) {
UserProvider user = Provider.of<UserProvider>(context, listen: false);
String recipientLabel = "";
if (message.recipients.any((r) => r.name == user.student?.name)) recipientLabel = "me".i18n;
if (recipientLabel != "" && message.recipients.length > 1) {
recipientLabel += " +";
recipientLabel += message.recipients.where((r) => r.name != user.student?.name).length.toString();
}
if (recipientLabel == "") {
// note: convertint to set to remove duplicates
recipientLabel += message.recipients.map((r) => r.name).toSet().join(", ");
}
List<Widget> attachments = [];
for (var a in message.attachments) {
attachments.add(AttachmentTile(a));
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Subject
Text(
message.subject,
softWrap: true,
maxLines: 10,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 24.0,
),
),
// Author
ListTile(
visualDensity: VisualDensity.compact,
contentPadding: EdgeInsets.zero,
leading: ProfileImage(
name: message.author,
backgroundColor: ColorUtils.stringToColor(message.author),
),
title: Text(
message.author,
style: const TextStyle(fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
subtitle: Text(
"to".i18n + " " + recipientLabel,
style: const TextStyle(fontWeight: FontWeight.w500),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: const [
// IconButton(
// onPressed: () {},
// icon: Icon(FeatherIcons.cornerUpLeft, color: AppColors.of(context).text),
// splashRadius: 24.0,
// padding: EdgeInsets.zero,
// visualDensity: VisualDensity.compact,
// ),
// IconButton(
// onPressed: () {},
// icon: Icon(FeatherIcons.share2, color: AppColors.of(context).text),
// splashRadius: 24.0,
// padding: EdgeInsets.zero,
// visualDensity: VisualDensity.compact,
// ),
],
),
),
// Content
Panel(
padding: const EdgeInsets.all(12.0),
child: SelectableLinkify(
text: message.content.escapeHtml(),
options: const LinkifyOptions(looseUrl: true, removeWww: true),
onOpen: (link) {
launch(link.url,
customTabsOption: CustomTabsOption(
toolbarColor: Theme.of(context).scaffoldBackgroundColor,
showPageTitle: true,
));
},
style: const TextStyle(fontWeight: FontWeight.w400),
),
),
// Attachments
...attachments,
],
),
);
}
}

View File

@ -0,0 +1,24 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"me": "me",
"to": "to",
},
"hu_hu": {
"me": "én",
"to": "Címzett:",
},
"de_de": {
"me": "mich",
"to": "zu",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,32 @@
import 'package:animations/animations.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo/ui/widgets/message/message_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/message/message_view.dart';
import 'package:flutter/material.dart';
class MessageViewable extends StatelessWidget {
const MessageViewable(this.message, {Key? key}) : super(key: key);
final Message message;
@override
Widget build(BuildContext context) {
return OpenContainer(
openBuilder: (context, _) {
return MessageView([message]);
},
closedBuilder: (context, VoidCallback openContainer) {
return MessageTile(message);
},
closedElevation: 0,
openShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
closedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
middleColor: Theme.of(context).colorScheme.background,
openColor: Theme.of(context).scaffoldBackgroundColor,
closedColor: Theme.of(context).colorScheme.background,
transitionType: ContainerTransitionType.fadeThrough,
transitionDuration: const Duration(milliseconds: 400),
useRootNavigator: true,
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'miss_tile.i18n.dart';
class MissTile extends StatelessWidget {
const MissTile(this.note, {Key? key}) : super(key: key);
final Note note;
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(_missIcon(), color: Theme.of(context).colorScheme.secondary, size: 36.0),
visualDensity: VisualDensity.compact,
title: Text(
_missName(),
style: const TextStyle(
fontWeight: FontWeight.w600,
),
),
subtitle: Text(
note.content.split("órán nem volt")[0].capital(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w500,
),
),
);
}
IconData _missIcon() {
if (note.type?.name == "HaziFeladatHiany") {
return FeatherIcons.home;
} else if (note.type?.name == "Felszereleshiany") {
return FeatherIcons.book;
}
return FeatherIcons.slash;
}
String _missName() {
if (note.type?.name == "HaziFeladatHiany") {
return "Missing homework".i18n;
} else if (note.type?.name == "Felszereleshiany") {
return "Missing equipment".i18n;
}
return "?";
}
}

View File

@ -0,0 +1,24 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Missing homework": "Missing homework",
"Missing equipment": "Missing equipment",
},
"hu_hu": {
"Missing homework": "Házi feladat hiány",
"Missing equipment": "Felszerelés Hiány",
},
"de_de": {
"Missing homework": "Fehlende Hausaufgaben",
"Missing equipment": "Fehlende Ausrüstung",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'missed_exam_tile.i18n.dart';
class MissedExamTile extends StatelessWidget {
const MissedExamTile(this.missedExams, {Key? key, this.onTap, this.padding}) : super(key: key);
final List<Lesson> missedExams;
final Function()? onTap;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: PanelButton(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6),
leading: SizedBox(
width: 36,
height: 36,
child: Icon(
FeatherIcons.slash,
color: AppColors.of(context).red.withOpacity(.75),
size: 28.0,
)),
title: Text("missed_exams".plural(missedExams.length).fill([missedExams.length])),
trailing: const Icon(FeatherIcons.arrowRight),
onPressed: onTap,
),
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"goodmorning": "Good morning, %s!",
"goodafternoon": "Good afternoon, %s!",
"goodevening": "Good evening, %s!",
"goodrest": "⛱️ Have a nice holiday, %s!",
"happybirthday": "🎂 Happy birthday, %s!",
"merryxmas": "🎄 Merry Christmas, %s!",
"happynewyear": "🎉 Happy New Year, %s!",
"empty": "Nothing to see here.",
"All": "All",
"Grades": "Grades",
"Messages": "Messages",
"Absences": "Absences",
"update_available": "Update Available",
"missed_exams": "You missed %s exams this week.".one("You missed an exam this week."),
"missed_exam_contact": "Contact %s, to resolve it!",
},
"hu_hu": {
"goodmorning": "Jó reggelt, %s!",
"goodafternoon": "Szép napot, %s!",
"goodevening": "Szép estét, %s!",
"goodrest": "⛱️ Jó szünetet, %s!",
"happybirthday": "🎂 Boldog születésnapot, %s!",
"merryxmas": "🎄 Boldog Karácsonyt, %s!",
"happynewyear": "🎉 Boldog új évet, %s!",
"empty": "Nincs itt semmi látnivaló.",
"All": "Összes",
"Grades": "Jegyek",
"Messages": "Üzenetek",
"Absences": "Hiányok",
"update_available": "Frissítés elérhető",
"missed_exams": "Ezen a héten hiányoztál %s dolgozatról.".one("Ezen a héten hiányoztál egy dolgozatról."),
"missed_exam_contact": "Keresd %s-t, ha pótolni szeretnéd!",
},
"de_de": {
"goodmorning": "Guten morgen, %s!",
"goodafternoon": "Guten Tag, %s!",
"goodevening": "Guten Abend, %s!",
"goodrest": "⛱️ Schöne Ferien, %s!",
"happybirthday": "🎂 Alles Gute zum Geburtstag, %s!",
"merryxmas": "🎄 Frohe Weihnachten, %s!",
"happynewyear": "🎉 Frohes neues Jahr, %s!",
"empty": "Hier gibt es nichts zu sehen.",
"All": "Alles",
"Grades": "Noten",
"Messages": "Nachrichten",
"Absences": "Fehlen",
"update_available": "Update verfügbar",
"missed_exams": "Diese Woche haben Sie %s Prüfungen verpasst.".one("Diese Woche haben Sie eine Prüfung verpasst."),
"missed_exam_contact": "Wenden Sie sich an %s, um sie zu erneuern!",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,61 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart';
import 'package:filcnaplo_mobile_ui/pages/timetable/timetable_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:filcnaplo/utils/format.dart';
import 'missed_exam_tile.i18n.dart';
class MissedExamView extends StatelessWidget {
const MissedExamView(this.missedExams, {Key? key}) : super(key: key);
final List<Lesson> missedExams;
static show(List<Lesson> missedExams, {required BuildContext context}) => showRoundedModalBottomSheet(context, child: MissedExamView(missedExams));
@override
Widget build(BuildContext context) {
List<Widget> tiles = missedExams.map((e) => MissedExamViewTile(e)).toList();
return Column(children: tiles);
}
}
class MissedExamViewTile extends StatelessWidget {
const MissedExamViewTile(this.lesson, {Key? key, this.padding}) : super(key: key);
final EdgeInsetsGeometry? padding;
final Lesson lesson;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
leading: Icon(
SubjectIcon.resolveVariant(subject: lesson.subject, context: context),
color: AppColors.of(context).text.withOpacity(.8),
size: 32.0,
),
title: Text(
"${lesson.subject.renamedTo ?? lesson.subject.name.capital()}${lesson.date.format(context)}",
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: lesson.subject.isRenamed ? FontStyle.italic : null),
),
subtitle: Text(
"missed_exam_contact".i18n.fill([lesson.teacher]),
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: const Icon(FeatherIcons.arrowRight),
onTap: () {
Navigator.of(context, rootNavigator: true).pop();
TimetablePage.jump(context, lesson: lesson);
},
),
),
);
}
}

View File

@ -0,0 +1,18 @@
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/missed_exam/missed_exam_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/missed_exam/missed_exam_view.dart';
import 'package:flutter/material.dart';
class MissedExamViewable extends StatelessWidget {
const MissedExamViewable(this.missedExams, {Key? key}) : super(key: key);
final List<Lesson> missedExams;
@override
Widget build(BuildContext context) {
return MissedExamTile(
missedExams,
onTap: () => MissedExamView.show(missedExams, context: context),
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:flutter/material.dart';
class NoteTile extends StatelessWidget {
const NoteTile(this.note, {Key? key, this.onTap, this.padding}) : super(key: key);
final Note note;
final void Function()? onTap;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: ProfileImage(
name: note.teacher,
radius: 22.0,
backgroundColor: ColorUtils.stringToColor(note.teacher),
),
title: Text(
note.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
note.content.replaceAll('\n', ' '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
minLeadingWidth: 0,
),
),
);
}
}

View File

@ -0,0 +1,72 @@
import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/common/sliding_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
class NoteView extends StatelessWidget {
const NoteView(this.note, {Key? key}) : super(key: key);
final Note note;
static void show(Note note, {required BuildContext context}) => showSlidingBottomSheet(context: context, child: NoteView(note));
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header
ListTile(
leading: ProfileImage(
name: note.teacher,
radius: 22.0,
backgroundColor: ColorUtils.stringToColor(note.teacher),
),
title: Text(
note.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
note.teacher,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: Text(
note.date.format(context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
// Details
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: SelectableLinkify(
text: note.content.escapeHtml(),
options: const LinkifyOptions(looseUrl: true, removeWww: true),
onOpen: (link) {
launch(link.url,
customTabsOption: CustomTabsOption(
toolbarColor: Theme.of(context).scaffoldBackgroundColor,
showPageTitle: true,
));
},
style: const TextStyle(fontWeight: FontWeight.w400),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,18 @@
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/note/note_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/note/note_view.dart';
import 'package:flutter/material.dart';
class NoteViewable extends StatelessWidget {
const NoteViewable(this.note, {Key? key}) : super(key: key);
final Note note;
@override
Widget build(BuildContext context) {
return NoteTile(
note,
onTap: () => NoteView.show(note, context: context),
);
}
}

View File

@ -0,0 +1,107 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:flutter/material.dart';
import 'package:i18n_extension/i18n_widget.dart';
class StatisticsTile extends StatelessWidget {
const StatisticsTile({
Key? key,
required this.value,
this.title,
this.decimal = true,
this.color,
this.valueSuffix = '',
this.fill = false,
this.outline = false,
}) : super(key: key);
final double value;
final Widget? title;
final bool decimal;
final Color? color;
final String valueSuffix;
final bool fill;
final bool outline;
@override
Widget build(BuildContext context) {
String valueText;
if (decimal) {
valueText = value.toStringAsFixed(2);
} else {
valueText = value.toStringAsFixed(0);
}
if (I18n.of(context).locale.languageCode != "en") valueText = valueText.replaceAll(".", ",");
if (value.isNaN) {
valueText = "?";
}
return Container(
width: double.infinity,
padding: const EdgeInsets.all(18.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: Theme.of(context).colorScheme.background,
boxShadow: [
BoxShadow(
offset: const Offset(0, 21),
blurRadius: 23.0,
color: Theme.of(context).shadowColor,
)
],
),
constraints: const BoxConstraints(
minHeight: 140.0,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (title != null)
DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18.0,
),
child: title!,
),
if (title != null) const SizedBox(height: 4.0),
Container(
margin: const EdgeInsets.only(top: 4.0),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
decoration: BoxDecoration(
color: fill ? (color ?? gradeColor(context: context, value: value)).withOpacity(.2) : null,
border: outline || fill
? Border.all(
color: (color ?? gradeColor(context: context, value: value)).withOpacity(outline ? 1.0 : 0.0),
width: fill ? 2.0 : 5.0,
)
: null,
borderRadius: BorderRadius.circular(45.0),
),
child: AutoSizeText.rich(
TextSpan(
text: valueText,
children: [
if (valueSuffix != "")
TextSpan(
text: valueSuffix,
style: const TextStyle(fontSize: 24.0),
),
],
),
maxLines: 1,
minFontSize: 5,
textAlign: TextAlign.center,
style: TextStyle(
color: color ?? gradeColor(context: context, value: value),
fontWeight: FontWeight.w800,
fontSize: 32.0,
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,32 @@
import 'package:filcnaplo/models/release.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'update_tile.i18n.dart';
class UpdateTile extends StatelessWidget {
const UpdateTile(this.release, {Key? key, this.onTap, this.padding}) : super(key: key);
final Release release;
final Function()? onTap;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: PanelButton(
onPressed: onTap,
title: Text("update_available".i18n),
leading: const Icon(FeatherIcons.download),
trailing: Text(
release.tag,
style: TextStyle(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
),
);
}
}

View File

@ -0,0 +1,21 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"update_available": "Update Available",
},
"hu_hu": {
"update_available": "Frissítés elérhető",
},
"de_de": {
"update_available": "Update verfügbar",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,18 @@
import 'package:filcnaplo/models/release.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/update/update_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/update/updates_view.dart';
import 'package:flutter/material.dart';
class UpdateViewable extends StatelessWidget {
const UpdateViewable(this.release, {Key? key}) : super(key: key);
final Release release;
@override
Widget build(BuildContext context) {
return UpdateTile(
release,
onTap: () => UpdateView.show(release, context: context),
);
}
}

View File

@ -0,0 +1,170 @@
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_card.dart';
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
import 'package:filcnaplo_mobile_ui/common/material_action_button.dart';
import 'package:filcnaplo/helpers/update_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:provider/provider.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'updates_view.i18n.dart';
class UpdateView extends StatefulWidget {
const UpdateView(this.release, {Key? key}) : super(key: key);
final Release release;
static void show(Release release, {required BuildContext context}) => showBottomCard(context: context, child: UpdateView(release));
@override
_UpdateViewState createState() => _UpdateViewState();
}
class _UpdateViewState extends State<UpdateView> {
double progress = 0.0;
UpdateState state = UpdateState.none;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"new_update".i18n,
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 18.0),
),
Text(
"${widget.release.version}",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(0.6),
),
),
],
),
ClipRRect(
borderRadius: BorderRadius.circular(18.0),
child: Image.asset(
"assets/icons/ic_launcher.png",
width: 64.0,
),
)
],
),
),
// Description
Container(
margin: const EdgeInsets.only(top: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
),
child: SizedBox(
height: 125.0,
child: Markdown(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
physics: const BouncingScrollPhysics(),
data: widget.release.body,
onTapLink: (text, href, title) => launch(href ?? ""),
),
),
),
// Download button
Center(
child: MaterialActionButton(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (state == UpdateState.downloading || state == UpdateState.preparing)
Container(
height: 18.0,
width: 18.0,
margin: const EdgeInsets.only(right: 8.0),
child: CircularProgressIndicator(
value: progress > 0.05 ? progress : null,
color: ColorUtils.foregroundColor(AppColors.of(context).filc),
),
),
Text(["download".i18n, "downloading".i18n, "downloading".i18n, "installing".i18n][state.index].toUpperCase()),
],
),
backgroundColor: AppColors.of(context).filc,
onPressed: state == UpdateState.none ? () => downloadPrecheck() : null,
),
),
],
),
);
}
String fmtSize() => "${(widget.release.downloads.first.size / 1024 / 1024).toStringAsFixed(1)} MB";
void downloadPrecheck() {
final status = Provider.of<StatusProvider>(context, listen: false);
if (status.networkType == ConnectivityResult.mobile) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("mobileAlertTitle".i18n),
content: Text("mobileAlertDesc".i18n.fill([fmtSize()])),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text("no".i18n),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text("yes".i18n),
),
],
),
).then((value) => value ? download() : null);
} else {
download();
}
}
void download() {
widget.release
.install(updateCallback: (p, s) {
if (mounted) {
setState(() {
progress = p;
state = s;
});
}
})
.then((_) => Navigator.of(context).maybePop())
.catchError((error, stackTrace) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
context: context,
content: Text("error".i18n),
backgroundColor: AppColors.of(context).red,
));
setState(() => state = UpdateState.none);
}
return true;
});
}
}

View File

@ -0,0 +1,46 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"new_update": "New Update",
"download": "download",
"downloading": "downloading",
"installing": "installing",
"error": "Failed to install update!",
"no": "No",
"yes": "Yes",
"mobileAlertTitle": "Hold up!",
"mobileAlertDesc": "You're on mobile network trying to download a %s update. Are you sure you want to continue?"
},
"hu_hu": {
"new_update": "Új frissítés",
"download": "Letöltés",
"downloading": "Letöltés",
"installing": "Telepítés",
"error": "Nem sikerült telepíteni a frissítést!",
"no": "Nem",
"yes": "Igen",
"mobileAlertTitle": "Figyelem!",
"mobileAlertDesc": "Jelenleg mobil interneten vagy, és egy %s méretű frissítést próbálsz letölteni. Biztosan folytatod?"
},
"de_de": {
"new_update": "Neues Update",
"download": "herunterladen",
"downloading": "Herunterladen",
"installing": "Installation",
"error": "Update konnte nicht installiert werden!",
"no": "Nein",
"yes": "Ja",
"mobileAlertTitle": "Achtung!",
"mobileAlertDesc":
"Sie befinden sich gerade im mobilen Internet und versuchen, ein %s Update herunterzuladen. Sind Sie sicher, dass Sie weitermachen wollen?"
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,79 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo/utils/reverse_search.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart';
import 'package:filcnaplo_mobile_ui/common/hero_scrollview.dart';
import 'package:filcnaplo_mobile_ui/pages/absences/absence_subject_view_container.dart';
import 'package:filcnaplo_mobile_ui/pages/timetable/timetable_page.dart';
import 'package:filcnaplo/ui/filter/sort.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_view.i18n.dart';
class AbsenceSubjectView extends StatelessWidget {
const AbsenceSubjectView(this.subject, {Key? key, this.absences = const []}) : super(key: key);
final Subject subject;
final List<Absence> absences;
static void show(Subject subject, List<Absence> absences, {required BuildContext context}) {
Navigator.of(context, rootNavigator: true)
.push<Absence>(CupertinoPageRoute(builder: (context) => AbsenceSubjectView(subject, absences: absences)))
.then((value) {
if (value == null) return;
Future.delayed(const Duration(milliseconds: 250)).then((_) {
ReverseSearch.getLessonByAbsence(value, context).then((lesson) {
if (lesson != null) {
TimetablePage.jump(context, lesson: lesson);
} else {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
content: Text("Cannot find lesson".i18n, style: const TextStyle(color: Colors.white)),
backgroundColor: AppColors.of(context).red,
context: context,
));
}
});
});
});
}
@override
Widget build(BuildContext context) {
final dateWidgets = absences
.map((a) => DateWidget(
widget: AbsenceViewable(a, padding: EdgeInsets.zero),
date: a.date,
))
.toList();
List<Widget> absenceTiles = sortDateWidgets(context, dateWidgets: dateWidgets, padding: EdgeInsets.zero, hasShadow: true);
return Scaffold(
body: HeroScrollView(
title: subject.renamedTo ?? subject.name.capital(),
italic: subject.isRenamed,
icon: SubjectIcon.resolveVariant(subject: subject, context: context),
child: AbsenceSubjectViewContainer(
child: CupertinoScrollbar(
child: ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(24.0),
shrinkWrap: true,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: absenceTiles[index],
),
itemCount: absenceTiles.length,
),
),
),
),
);
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class AbsenceSubjectViewContainer extends InheritedWidget {
const AbsenceSubjectViewContainer({Key? key, required Widget child}) : super(key: key, child: child);
static AbsenceSubjectViewContainer? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<AbsenceSubjectViewContainer>();
@override
bool updateShouldNotify(AbsenceSubjectViewContainer oldWidget) => false;
}

View File

@ -0,0 +1,382 @@
import 'dart:math';
import 'package:animations/animations.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo_mobile_ui/common/action_button.dart';
import 'package:filcnaplo_mobile_ui/common/empty.dart';
import 'package:filcnaplo_mobile_ui/common/filter_bar.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_button.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_subject_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/statistics_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/miss_tile.dart';
import 'package:filcnaplo_mobile_ui/pages/absences/absence_subject_view.dart';
import 'package:filcnaplo/ui/filter/sort.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/utils/color.dart';
import 'absences_page.i18n.dart';
enum AbsenceFilter { absences, delays, misses }
class SubjectAbsence {
Subject subject;
List<Absence> absences;
double percentage;
SubjectAbsence({required this.subject, this.absences = const [], this.percentage = 0.0});
}
class AbsencesPage extends StatefulWidget {
const AbsencesPage({Key? key}) : super(key: key);
@override
_AbsencesPageState createState() => _AbsencesPageState();
}
class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMixin {
late UserProvider user;
late AbsenceProvider absenceProvider;
late TimetableProvider timetableProvider;
late NoteProvider noteProvider;
late UpdateProvider updateProvider;
late String firstName;
late TabController _tabController;
late List<SubjectAbsence> absences = [];
final Map<Subject, Lesson> _lessonCount = {};
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
timetableProvider = Provider.of<TimetableProvider>(context, listen: false);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
for (final lesson in timetableProvider.getWeek(Week.current()) ?? []) {
if (!lesson.isEmpty && lesson.subject.id != '' && lesson.lessonYearIndex != null) {
_lessonCount.update(
lesson.subject,
(value) {
if (lesson.lessonYearIndex! > value.lessonYearIndex!) {
return lesson;
} else {
return value;
}
},
ifAbsent: () => lesson,
);
}
}
setState(() {});
});
}
void buildSubjectAbsences() {
Map<Subject, SubjectAbsence> _absences = {};
for (final absence in absenceProvider.absences) {
if (absence.delay != 0) continue;
if (!_absences.containsKey(absence.subject)) {
_absences[absence.subject] = SubjectAbsence(subject: absence.subject, absences: [absence]);
} else {
_absences[absence.subject]?.absences.add(absence);
}
}
_absences.forEach((subject, absence) {
final absentLessonsOfSubject = absenceProvider.absences.where((e) => e.subject == subject && e.delay == 0).length;
final totalLessonsOfSubject = _lessonCount[subject]?.lessonYearIndex ?? 0;
double absentLessonsOfSubjectPercentage;
if (absentLessonsOfSubject <= totalLessonsOfSubject) {
absentLessonsOfSubjectPercentage = absentLessonsOfSubject / totalLessonsOfSubject * 100;
} else {
absentLessonsOfSubjectPercentage = -1;
}
_absences[subject]?.percentage = absentLessonsOfSubjectPercentage.clamp(-1, 100.0);
});
absences = _absences.values.toList();
absences.sort((a, b) => -a.percentage.compareTo(b.percentage));
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
absenceProvider = Provider.of<AbsenceProvider>(context);
noteProvider = Provider.of<NoteProvider>(context);
updateProvider = Provider.of<UpdateProvider>(context);
timetableProvider = Provider.of<TimetableProvider>(context);
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
buildSubjectAbsences();
return Scaffold(
body: Padding(
padding: const EdgeInsets.only(top: 12.0),
child: NestedScrollView(
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
headerSliverBuilder: (context, _) => [
SliverAppBar(
pinned: true,
floating: false,
snap: false,
centerTitle: false,
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
actions: [
// Profile Icon
Padding(
padding: const EdgeInsets.only(right: 24.0),
child: ProfileButton(
child: ProfileImage(
heroTag: "profile",
name: firstName,
backgroundColor: ColorUtils.stringToColor(user.displayName ?? "?"),
badge: updateProvider.available,
role: user.role,
profilePictureString: user.picture,
),
),
),
],
automaticallyImplyLeading: false,
shadowColor: Theme.of(context).shadowColor,
title: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Absences".i18n,
style: TextStyle(color: AppColors.of(context).text, fontSize: 32.0, fontWeight: FontWeight.bold),
),
),
bottom: FilterBar(items: [
Tab(text: "Absences".i18n),
Tab(text: "Delays".i18n),
Tab(text: "Misses".i18n),
], controller: _tabController, disableFading: true),
),
],
body: TabBarView(
physics: const BouncingScrollPhysics(),
controller: _tabController,
children: List.generate(3, (index) => filterViewBuilder(context, index))),
),
),
);
}
List<DateWidget> getFilterWidgets(AbsenceFilter activeData) {
List<DateWidget> items = [];
switch (activeData) {
case AbsenceFilter.absences:
for (var a in absences) {
items.add(DateWidget(
date: DateTime.fromMillisecondsSinceEpoch(0),
widget: AbsenceSubjectTile(
a.subject,
percentage: a.percentage,
excused: a.absences.where((a) => a.state == Justification.excused).length,
unexcused: a.absences.where((a) => a.state == Justification.unexcused).length,
pending: a.absences.where((a) => a.state == Justification.pending).length,
onTap: () => AbsenceSubjectView.show(a.subject, a.absences, context: context),
),
));
}
break;
case AbsenceFilter.delays:
for (var absence in absenceProvider.absences) {
if (absence.delay != 0) {
items.add(DateWidget(
date: absence.date,
widget: AbsenceViewable(absence, padding: EdgeInsets.zero),
));
}
}
break;
case AbsenceFilter.misses:
for (var note in noteProvider.notes) {
if (note.type?.name == "HaziFeladatHiany" || note.type?.name == "Felszereleshiany") {
items.add(DateWidget(
date: note.date,
widget: MissTile(note),
));
}
}
break;
}
return items;
}
Widget filterViewBuilder(context, int activeData) {
List<Widget> filterWidgets = [];
if (activeData > 0) {
filterWidgets = sortDateWidgets(
context,
dateWidgets: getFilterWidgets(AbsenceFilter.values[activeData]),
padding: EdgeInsets.zero,
hasShadow: true,
);
} else {
filterWidgets = [
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: Panel(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Subjects".i18n),
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
title: Text("attention".i18n),
content: Text("attention_body".i18n),
actions: [ActionButton(label: "Ok", onTap: () => Navigator.of(context).pop())],
),
);
},
padding: EdgeInsets.zero,
splashRadius: 24.0,
visualDensity: VisualDensity.compact,
constraints: BoxConstraints.tight(const Size(42.0, 42.0)),
icon: const Icon(FeatherIcons.info),
),
),
],
),
child: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return FadeThroughTransition(
child: child,
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
fillColor: Theme.of(context).colorScheme.background,
);
},
child: Column(
children: getFilterWidgets(AbsenceFilter.values[activeData]).map((e) => e.widget).cast<Widget>().toList(),
),
),
),
)
];
}
return Padding(
padding: const EdgeInsets.only(top: 12.0),
child: RefreshIndicator(
color: Theme.of(context).colorScheme.secondary,
onRefresh: () async {
await absenceProvider.fetch();
await noteProvider.fetch();
},
child: ListView.builder(
padding: EdgeInsets.zero,
physics: const BouncingScrollPhysics(),
itemCount: max(filterWidgets.length + (activeData <= 1 ? 1 : 0), 1),
itemBuilder: (context, index) {
if (filterWidgets.isNotEmpty) {
if ((index == 0 && activeData == 1) || (index == 0 && activeData == 0)) {
int value1 = 0;
int value2 = 0;
String title1 = "";
String title2 = "";
String suffix = "";
if (activeData == AbsenceFilter.absences.index) {
value1 = absenceProvider.absences.where((e) => e.delay == 0 && e.state == Justification.excused).length;
value2 = absenceProvider.absences.where((e) => e.delay == 0 && e.state == Justification.unexcused).length;
title1 = "stat_1".i18n;
title2 = "stat_2".i18n;
suffix = " " + "hr".i18n;
} else if (activeData == AbsenceFilter.delays.index) {
value1 = absenceProvider.absences
.where((e) => e.delay != 0 && e.state == Justification.excused)
.map((e) => e.delay)
.fold(0, (a, b) => a + b);
value2 = absenceProvider.absences
.where((e) => e.delay != 0 && e.state == Justification.unexcused)
.map((e) => e.delay)
.fold(0, (a, b) => a + b);
title1 = "stat_3".i18n;
title2 = "stat_4".i18n;
suffix = " " + "min".i18n;
}
return Padding(
padding: const EdgeInsets.only(bottom: 24.0, left: 24.0, right: 24.0),
child: Row(children: [
Expanded(
child: StatisticsTile(
title: AutoSizeText(
title1,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
valueSuffix: suffix,
value: value1.toDouble(),
decimal: false,
color: AppColors.of(context).green,
),
),
const SizedBox(width: 24.0),
Expanded(
child: StatisticsTile(
title: AutoSizeText(
title2,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
valueSuffix: suffix,
value: value2.toDouble(),
decimal: false,
color: AppColors.of(context).red,
),
),
]),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
child: filterWidgets[index - (activeData <= 1 ? 1 : 0)],
);
} else {
return Empty(subtitle: "empty".i18n);
}
},
),
),
);
}
}

View File

@ -0,0 +1,57 @@
import 'package:i18n_extension/i18n_extension.dart';
extension ScreensLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Absences": "Absences",
"Delays": "Delays",
"Misses": "Misses",
"empty": "You have no absences.",
"stat_1": "Excused Absences",
"stat_2": "Unexcused Absences",
"stat_3": "Excused Delay",
"stat_4": "Unexcused Delay",
"min": "min",
"hr": "hrs",
"Subjects": "Subjects",
"attention": "Attention!",
"attention_body": "Percentage calculations are only an approximation so they may not be accurate.",
},
"hu_hu": {
"Absences": "Hiányzások",
"Delays": "Késések",
"Misses": "Hiányok",
"empty": "Nincsenek hiányaid.",
"stat_1": "Igazolt hiányzások",
"stat_2": "Igazolatlan hiányzások",
"stat_3": "Igazolt Késés",
"stat_4": "Igazolatlan Késés",
"min": "perc",
"hr": "óra",
"Subjects": "Tantárgyak",
"attention": "Figyelem!",
"attention_body": "A százalékos számítások csak közelítések, ezért előfordulhat, hogy nem pontosak.",
},
"de_de": {
"Absences": "Fehlen",
"Delays": "Verspätung",
"Misses": "Fehlt",
"empty": "Sie haben keine Fehlen.",
"stat_1": "Entschuldigte Fehlen",
"stat_2": "Unentschuldigte Fehlen",
"stat_3": "Entschuldigte Verspätung",
"stat_4": "Unentschuldigte Verspätung",
"min": "min",
"hr": "hrs",
"Subjects": "Fächer",
"attention": "Achtung!",
"attention_body": "Prozentberechnungen sind nur eine Annäherung und können daher ungenau sein.",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -0,0 +1,167 @@
import 'dart:math';
import 'package:filcnaplo_kreta_api/models/category.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
import 'package:filcnaplo_mobile_ui/common/material_action_button.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'grade_calculator.i18n.dart';
class GradeCalculator extends StatefulWidget {
const GradeCalculator(this.subject, {Key? key}) : super(key: key);
final Subject subject;
@override
_GradeCalculatorState createState() => _GradeCalculatorState();
}
class _GradeCalculatorState extends State<GradeCalculator> {
late GradeCalculatorProvider calculatorProvider;
final _weightController = TextEditingController(text: "100");
double newValue = 5.0;
double newWeight = 100.0;
@override
Widget build(BuildContext context) {
calculatorProvider = Provider.of<GradeCalculatorProvider>(context);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(6.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"Grade Calculator".i18n,
style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w600),
),
),
// Grade value
Row(children: [
Expanded(
child: Slider(
thumbColor: Theme.of(context).colorScheme.secondary,
activeColor: Theme.of(context).colorScheme.secondary,
value: newValue,
min: 1.0,
max: 5.0,
divisions: 4,
label: "${newValue.toInt()}",
onChanged: (value) => setState(() => newValue = value),
),
),
Container(
width: 80.0,
padding: const EdgeInsets.only(right: 12.0),
child: Center(child: GradeValueWidget(GradeValue(newValue.toInt(), "", "", 0))),
),
]),
// Grade weight
Row(children: [
Expanded(
child: Slider(
thumbColor: Theme.of(context).colorScheme.secondary,
activeColor: Theme.of(context).colorScheme.secondary,
value: newWeight.clamp(50, 400),
min: 50.0,
max: 400.0,
divisions: 7,
label: "${newWeight.toInt()}%",
onChanged: (value) => setState(() {
newWeight = value;
_weightController.text = newWeight.toInt().toString();
}),
),
),
Container(
width: 80.0,
padding: const EdgeInsets.only(right: 12.0),
child: Center(
child: TextField(
controller: _weightController,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 22.0),
autocorrect: false,
textAlign: TextAlign.right,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
LengthLimitingTextInputFormatter(3),
],
decoration: const InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: "100",
suffixText: "%",
suffixStyle: TextStyle(fontSize: 18.0),
),
onChanged: (value) {
setState(() {
newWeight = double.tryParse(value) ?? 100.0;
});
},
),
),
),
]),
Container(
width: 120.0,
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: MaterialActionButton(
child: Text("Add Grade".i18n),
onPressed: () {
if (calculatorProvider.ghosts.length >= 30) {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(content: Text("limit_reached".i18n), context: context));
return;
}
DateTime date;
if (calculatorProvider.ghosts.isNotEmpty) {
List<Grade> grades = calculatorProvider.ghosts;
grades.sort((a, b) => -a.writeDate.compareTo(b.writeDate));
date = grades.first.date.add(const Duration(days: 7));
} else {
List<Grade> grades = calculatorProvider.grades.where((e) => e.type == GradeType.midYear && e.subject == widget.subject).toList();
grades.sort((a, b) => -a.writeDate.compareTo(b.writeDate));
date = grades.first.date;
}
calculatorProvider.addGhost(Grade(
id: randomId(),
date: date,
writeDate: date,
description: "Ghost Grade".i18n,
value: GradeValue(newValue.toInt(), "", "", newWeight.toInt()),
teacher: "Ghost",
type: GradeType.ghost,
form: "",
subject: widget.subject,
mode: Category.fromJson({}),
seenDate: DateTime(0),
groupId: "",
));
},
),
),
],
),
);
}
String randomId() {
var rng = Random();
return rng.nextInt(1000000000).toString();
}
}

View File

@ -0,0 +1,33 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Grades": "Grades",
"Ghost Grade": "Ghost Grade",
"Grade Calculator": "Average calculator",
"Add Grade": "Add Grade",
"limit_reached": "You cannot add more Ghost Grades.",
},
"hu_hu": {
"Grades": "Jegyek",
"Ghost Grade": "Szellem jegy",
"Grade Calculator": "Átlag számoló",
"Add Grade": "Hozzáadás",
"limit_reached": "Nem adhatsz hozzá több jegyet.",
},
"de_de": {
"Grades": "Noten",
"Ghost Grade": "Geist Noten",
"Grade Calculator": "Mittelwert-Rechner",
"Add Grade": "Hinzufügen",
"limit_reached": "Sie können keine weiteren Noten hinzufügen.",
},
};
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);
}

Some files were not shown because too many files have changed in this diff Show More