remelem mukszik
This commit is contained in:
parent
baec76c29f
commit
0ece9382af
29
filcnaplo_mobile_ui/LICENSE
Executable file
29
filcnaplo_mobile_ui/LICENSE
Executable 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
0
filcnaplo_mobile_ui/README.md
Executable file
28
filcnaplo_mobile_ui/analysis_options.yaml
Executable file
28
filcnaplo_mobile_ui/analysis_options.yaml
Executable 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
|
36
filcnaplo_mobile_ui/lib/common/action_button.dart
Executable file
36
filcnaplo_mobile_ui/lib/common/action_button.dart
Executable 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))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
filcnaplo_mobile_ui/lib/common/average_display.dart
Executable file
35
filcnaplo_mobile_ui/lib/common/average_display.dart
Executable 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
51
filcnaplo_mobile_ui/lib/common/bottom_card.dart
Executable file
51
filcnaplo_mobile_ui/lib/common/bottom_card.dart
Executable 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));
|
22
filcnaplo_mobile_ui/lib/common/bottom_sheet_menu/bottom_sheet_menu.dart
Executable file
22
filcnaplo_mobile_ui/lib/common/bottom_sheet_menu/bottom_sheet_menu.dart
Executable 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));
|
19
filcnaplo_mobile_ui/lib/common/bottom_sheet_menu/bottom_sheet_menu_item.dart
Executable file
19
filcnaplo_mobile_ui/lib/common/bottom_sheet_menu/bottom_sheet_menu_item.dart
Executable 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
70
filcnaplo_mobile_ui/lib/common/bottom_sheet_menu/rounded_bottom_sheet.dart
Executable file
70
filcnaplo_mobile_ui/lib/common/bottom_sheet_menu/rounded_bottom_sheet.dart
Executable 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),
|
||||||
|
);
|
||||||
|
}
|
34
filcnaplo_mobile_ui/lib/common/custom_snack_bar.dart
Executable file
34
filcnaplo_mobile_ui/lib/common/custom_snack_bar.dart
Executable 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),
|
||||||
|
);
|
||||||
|
}
|
31
filcnaplo_mobile_ui/lib/common/detail.dart
Executable file
31
filcnaplo_mobile_ui/lib/common/detail.dart
Executable 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
23
filcnaplo_mobile_ui/lib/common/dialog_button.dart
Executable file
23
filcnaplo_mobile_ui/lib/common/dialog_button.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
20
filcnaplo_mobile_ui/lib/common/dot.dart
Executable file
20
filcnaplo_mobile_ui/lib/common/dot.dart
Executable 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
45
filcnaplo_mobile_ui/lib/common/empty.dart
Executable file
45
filcnaplo_mobile_ui/lib/common/empty.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
117
filcnaplo_mobile_ui/lib/common/filter_bar.dart
Executable file
117
filcnaplo_mobile_ui/lib/common/filter_bar.dart
Executable 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
filcnaplo_mobile_ui/lib/common/hero_dialog_route.dart
Executable file
35
filcnaplo_mobile_ui/lib/common/hero_dialog_route.dart
Executable 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);
|
||||||
|
}
|
||||||
|
}
|
133
filcnaplo_mobile_ui/lib/common/hero_scrollview.dart
Executable file
133
filcnaplo_mobile_ui/lib/common/hero_scrollview.dart
Executable 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
filcnaplo_mobile_ui/lib/common/material_action_button.dart
Executable file
35
filcnaplo_mobile_ui/lib/common/material_action_button.dart
Executable 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
34
filcnaplo_mobile_ui/lib/common/new_content_indicator.dart
Executable file
34
filcnaplo_mobile_ui/lib/common/new_content_indicator.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
135
filcnaplo_mobile_ui/lib/common/panel/panel.dart
Executable file
135
filcnaplo_mobile_ui/lib/common/panel/panel.dart
Executable 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,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
44
filcnaplo_mobile_ui/lib/common/panel/panel_action_button.dart
Executable file
44
filcnaplo_mobile_ui/lib/common/panel/panel_action_button.dart
Executable 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
74
filcnaplo_mobile_ui/lib/common/panel/panel_button.dart
Executable file
74
filcnaplo_mobile_ui/lib/common/panel/panel_button.dart
Executable 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);
|
||||||
|
}
|
||||||
|
}
|
50
filcnaplo_mobile_ui/lib/common/profile_image/profile_button.dart
Executable file
50
filcnaplo_mobile_ui/lib/common/profile_image/profile_button.dart
Executable 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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
229
filcnaplo_mobile_ui/lib/common/profile_image/profile_image.dart
Executable file
229
filcnaplo_mobile_ui/lib/common/profile_image/profile_image.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
69
filcnaplo_mobile_ui/lib/common/progress_bar.dart
Executable file
69
filcnaplo_mobile_ui/lib/common/progress_bar.dart
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
33
filcnaplo_mobile_ui/lib/common/screens.i18n.dart
Executable file
33
filcnaplo_mobile_ui/lib/common/screens.i18n.dart
Executable 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);
|
||||||
|
}
|
42
filcnaplo_mobile_ui/lib/common/sliding_bottom_sheet.dart
Executable file
42
filcnaplo_mobile_ui/lib/common/sliding_bottom_sheet.dart
Executable 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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
15
filcnaplo_mobile_ui/lib/common/system_chrome.dart
Executable file
15
filcnaplo_mobile_ui/lib/common/system_chrome.dart
Executable 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,
|
||||||
|
));
|
||||||
|
}
|
59
filcnaplo_mobile_ui/lib/common/trend_display.dart
Executable file
59
filcnaplo_mobile_ui/lib/common/trend_display.dart
Executable 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
979
filcnaplo_mobile_ui/lib/common/viewable.dart
Executable file
979
filcnaplo_mobile_ui/lib/common/viewable.dart
Executable 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;
|
||||||
|
}
|
50
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_display.dart
Executable file
50
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_display.dart
Executable 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)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
80
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_subject_tile.dart
Executable file
80
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_subject_tile.dart
Executable 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);
|
||||||
|
}
|
118
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_tile.dart
Executable file
118
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_tile.dart
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_tile.i18n.dart
Executable file
36
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_tile.i18n.dart
Executable 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);
|
||||||
|
}
|
128
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_view.dart
Executable file
128
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_view.dart
Executable 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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
39
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_view.i18n.dart
Executable file
39
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_view.i18n.dart
Executable 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);
|
||||||
|
}
|
68
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_viewable.dart
Executable file
68
filcnaplo_mobile_ui/lib/common/widgets/absence/absence_viewable.dart
Executable 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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
80
filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart
Executable file
80
filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
27
filcnaplo_mobile_ui/lib/common/widgets/card_handle.dart
Executable file
27
filcnaplo_mobile_ui/lib/common/widgets/card_handle.dart
Executable 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!,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
108
filcnaplo_mobile_ui/lib/common/widgets/cretification/certification_card.dart
Executable file
108
filcnaplo_mobile_ui/lib/common/widgets/cretification/certification_card.dart
Executable 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;
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
87
filcnaplo_mobile_ui/lib/common/widgets/cretification/certification_tile.dart
Executable file
87
filcnaplo_mobile_ui/lib/common/widgets/cretification/certification_tile.dart
Executable 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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
43
filcnaplo_mobile_ui/lib/common/widgets/cretification/certification_view.dart
Executable file
43
filcnaplo_mobile_ui/lib/common/widgets/cretification/certification_view.dart
Executable 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(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
60
filcnaplo_mobile_ui/lib/common/widgets/custom_switch.dart
Executable file
60
filcnaplo_mobile_ui/lib/common/widgets/custom_switch.dart
Executable 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,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
filcnaplo_mobile_ui/lib/common/widgets/event/event_tile.dart
Executable file
46
filcnaplo_mobile_ui/lib/common/widgets/event/event_tile.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
57
filcnaplo_mobile_ui/lib/common/widgets/event/event_view.dart
Executable file
57
filcnaplo_mobile_ui/lib/common/widgets/event/event_view.dart
Executable 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
filcnaplo_mobile_ui/lib/common/widgets/event/event_viewable.dart
Executable file
18
filcnaplo_mobile_ui/lib/common/widgets/event/event_viewable.dart
Executable 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
58
filcnaplo_mobile_ui/lib/common/widgets/exam/exam_tile.dart
Executable file
58
filcnaplo_mobile_ui/lib/common/widgets/exam/exam_tile.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
61
filcnaplo_mobile_ui/lib/common/widgets/exam/exam_view.dart
Executable file
61
filcnaplo_mobile_ui/lib/common/widgets/exam/exam_view.dart
Executable 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
27
filcnaplo_mobile_ui/lib/common/widgets/exam/exam_view.i18n.dart
Executable file
27
filcnaplo_mobile_ui/lib/common/widgets/exam/exam_view.i18n.dart
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:i18n_extension/i18n_extension.dart';
|
||||||
|
|
||||||
|
extension Localization on String {
|
||||||
|
static final _t = Translations.byLocale("hu_hu") +
|
||||||
|
{
|
||||||
|
"en_en": {
|
||||||
|
"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);
|
||||||
|
}
|
20
filcnaplo_mobile_ui/lib/common/widgets/exam/exam_viewable.dart
Executable file
20
filcnaplo_mobile_ui/lib/common/widgets/exam/exam_viewable.dart
Executable 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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
70
filcnaplo_mobile_ui/lib/common/widgets/grade/grade_subject_tile.dart
Executable file
70
filcnaplo_mobile_ui/lib/common/widgets/grade/grade_subject_tile.dart
Executable 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)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
60
filcnaplo_mobile_ui/lib/common/widgets/grade/grade_view.dart
Executable file
60
filcnaplo_mobile_ui/lib/common/widgets/grade/grade_view.dart
Executable 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}%" : "";
|
||||||
|
}
|
30
filcnaplo_mobile_ui/lib/common/widgets/grade/grade_view.i18n.dart
Executable file
30
filcnaplo_mobile_ui/lib/common/widgets/grade/grade_view.i18n.dart
Executable 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);
|
||||||
|
}
|
25
filcnaplo_mobile_ui/lib/common/widgets/grade/grade_viewable.dart
Executable file
25
filcnaplo_mobile_ui/lib/common/widgets/grade/grade_viewable.dart
Executable 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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
158
filcnaplo_mobile_ui/lib/common/widgets/grade/new_grades.dart
Executable file
158
filcnaplo_mobile_ui/lib/common/widgets/grade/new_grades.dart
Executable 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
42
filcnaplo_mobile_ui/lib/common/widgets/grade/new_grades.i18n.dart
Executable file
42
filcnaplo_mobile_ui/lib/common/widgets/grade/new_grades.i18n.dart
Executable 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);
|
||||||
|
}
|
389
filcnaplo_mobile_ui/lib/common/widgets/grade/surprise_grade.dart
Executable file
389
filcnaplo_mobile_ui/lib/common/widgets/grade/surprise_grade.dart
Executable 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
103
filcnaplo_mobile_ui/lib/common/widgets/homework/homework_tile.dart
Executable file
103
filcnaplo_mobile_ui/lib/common/widgets/homework/homework_tile.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
88
filcnaplo_mobile_ui/lib/common/widgets/homework/homework_view.dart
Executable file
88
filcnaplo_mobile_ui/lib/common/widgets/homework/homework_view.dart
Executable 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,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
21
filcnaplo_mobile_ui/lib/common/widgets/homework/homework_view.i18n.dart
Executable file
21
filcnaplo_mobile_ui/lib/common/widgets/homework/homework_view.i18n.dart
Executable 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);
|
||||||
|
}
|
18
filcnaplo_mobile_ui/lib/common/widgets/homework/homework_viewable.dart
Executable file
18
filcnaplo_mobile_ui/lib/common/widgets/homework/homework_viewable.dart
Executable 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
75
filcnaplo_mobile_ui/lib/common/widgets/lesson/changed_lesson_tile.dart
Executable file
75
filcnaplo_mobile_ui/lib/common/widgets/lesson/changed_lesson_tile.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
24
filcnaplo_mobile_ui/lib/common/widgets/lesson/changed_lesson_tile.i18n.dart
Executable file
24
filcnaplo_mobile_ui/lib/common/widgets/lesson/changed_lesson_tile.i18n.dart
Executable 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);
|
||||||
|
}
|
18
filcnaplo_mobile_ui/lib/common/widgets/lesson/changed_lesson_viewable.dart
Executable file
18
filcnaplo_mobile_ui/lib/common/widgets/lesson/changed_lesson_viewable.dart
Executable 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
80
filcnaplo_mobile_ui/lib/common/widgets/lesson/lesson_view.dart
Executable file
80
filcnaplo_mobile_ui/lib/common/widgets/lesson/lesson_view.dart
Executable 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));
|
||||||
|
}
|
||||||
|
}
|
30
filcnaplo_mobile_ui/lib/common/widgets/lesson/lesson_view.i18n.dart
Executable file
30
filcnaplo_mobile_ui/lib/common/widgets/lesson/lesson_view.i18n.dart
Executable 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);
|
||||||
|
}
|
25
filcnaplo_mobile_ui/lib/common/widgets/lesson/lesson_viewable.dart
Executable file
25
filcnaplo_mobile_ui/lib/common/widgets/lesson/lesson_viewable.dart
Executable 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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
83
filcnaplo_mobile_ui/lib/common/widgets/message/attachment_tile.dart
Executable file
83
filcnaplo_mobile_ui/lib/common/widgets/message/attachment_tile.dart
Executable 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
filcnaplo_mobile_ui/lib/common/widgets/message/image_view.dart
Executable file
46
filcnaplo_mobile_ui/lib/common/widgets/message/image_view.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
53
filcnaplo_mobile_ui/lib/common/widgets/message/message_view.dart
Executable file
53
filcnaplo_mobile_ui/lib/common/widgets/message/message_view.dart
Executable 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]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
122
filcnaplo_mobile_ui/lib/common/widgets/message/message_view_tile.dart
Executable file
122
filcnaplo_mobile_ui/lib/common/widgets/message/message_view_tile.dart
Executable 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,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
24
filcnaplo_mobile_ui/lib/common/widgets/message/message_view_tile.i18n.dart
Executable file
24
filcnaplo_mobile_ui/lib/common/widgets/message/message_view_tile.i18n.dart
Executable 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);
|
||||||
|
}
|
32
filcnaplo_mobile_ui/lib/common/widgets/message/message_viewable.dart
Executable file
32
filcnaplo_mobile_ui/lib/common/widgets/message/message_viewable.dart
Executable 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
51
filcnaplo_mobile_ui/lib/common/widgets/miss_tile.dart
Executable file
51
filcnaplo_mobile_ui/lib/common/widgets/miss_tile.dart
Executable 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 "?";
|
||||||
|
}
|
||||||
|
}
|
24
filcnaplo_mobile_ui/lib/common/widgets/miss_tile.i18n.dart
Executable file
24
filcnaplo_mobile_ui/lib/common/widgets/miss_tile.i18n.dart
Executable 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);
|
||||||
|
}
|
35
filcnaplo_mobile_ui/lib/common/widgets/missed_exam/missed_exam_tile.dart
Executable file
35
filcnaplo_mobile_ui/lib/common/widgets/missed_exam/missed_exam_tile.dart
Executable 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
61
filcnaplo_mobile_ui/lib/common/widgets/missed_exam/missed_exam_view.dart
Executable file
61
filcnaplo_mobile_ui/lib/common/widgets/missed_exam/missed_exam_view.dart
Executable 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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
filcnaplo_mobile_ui/lib/common/widgets/missed_exam/missed_exam_viewable.dart
Executable file
18
filcnaplo_mobile_ui/lib/common/widgets/missed_exam/missed_exam_viewable.dart
Executable 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
filcnaplo_mobile_ui/lib/common/widgets/note/note_tile.dart
Executable file
46
filcnaplo_mobile_ui/lib/common/widgets/note/note_tile.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
72
filcnaplo_mobile_ui/lib/common/widgets/note/note_view.dart
Executable file
72
filcnaplo_mobile_ui/lib/common/widgets/note/note_view.dart
Executable 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
filcnaplo_mobile_ui/lib/common/widgets/note/note_viewable.dart
Executable file
18
filcnaplo_mobile_ui/lib/common/widgets/note/note_viewable.dart
Executable 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
107
filcnaplo_mobile_ui/lib/common/widgets/statistics_tile.dart
Executable file
107
filcnaplo_mobile_ui/lib/common/widgets/statistics_tile.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
32
filcnaplo_mobile_ui/lib/common/widgets/update/update_tile.dart
Executable file
32
filcnaplo_mobile_ui/lib/common/widgets/update/update_tile.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
21
filcnaplo_mobile_ui/lib/common/widgets/update/update_tile.i18n.dart
Executable file
21
filcnaplo_mobile_ui/lib/common/widgets/update/update_tile.i18n.dart
Executable 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);
|
||||||
|
}
|
18
filcnaplo_mobile_ui/lib/common/widgets/update/update_viewable.dart
Executable file
18
filcnaplo_mobile_ui/lib/common/widgets/update/update_viewable.dart
Executable 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
170
filcnaplo_mobile_ui/lib/common/widgets/update/updates_view.dart
Executable file
170
filcnaplo_mobile_ui/lib/common/widgets/update/updates_view.dart
Executable 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
46
filcnaplo_mobile_ui/lib/common/widgets/update/updates_view.i18n.dart
Executable file
46
filcnaplo_mobile_ui/lib/common/widgets/update/updates_view.i18n.dart
Executable 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);
|
||||||
|
}
|
79
filcnaplo_mobile_ui/lib/pages/absences/absence_subject_view.dart
Executable file
79
filcnaplo_mobile_ui/lib/pages/absences/absence_subject_view.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
10
filcnaplo_mobile_ui/lib/pages/absences/absence_subject_view_container.dart
Executable file
10
filcnaplo_mobile_ui/lib/pages/absences/absence_subject_view_container.dart
Executable 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;
|
||||||
|
}
|
382
filcnaplo_mobile_ui/lib/pages/absences/absences_page.dart
Executable file
382
filcnaplo_mobile_ui/lib/pages/absences/absences_page.dart
Executable 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
57
filcnaplo_mobile_ui/lib/pages/absences/absences_page.i18n.dart
Executable file
57
filcnaplo_mobile_ui/lib/pages/absences/absences_page.i18n.dart
Executable 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);
|
||||||
|
}
|
167
filcnaplo_mobile_ui/lib/pages/grades/calculator/grade_calculator.dart
Executable file
167
filcnaplo_mobile_ui/lib/pages/grades/calculator/grade_calculator.dart
Executable 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();
|
||||||
|
}
|
||||||
|
}
|
33
filcnaplo_mobile_ui/lib/pages/grades/calculator/grade_calculator.i18n.dart
Executable file
33
filcnaplo_mobile_ui/lib/pages/grades/calculator/grade_calculator.i18n.dart
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
import 'package:i18n_extension/i18n_extension.dart';
|
||||||
|
|
||||||
|
extension Localization on String {
|
||||||
|
static final _t = Translations.byLocale("hu_hu") +
|
||||||
|
{
|
||||||
|
"en_en": {
|
||||||
|
"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
Loading…
x
Reference in New Issue
Block a user