This commit is contained in:
Márton Kiss
2023-05-26 21:25:00 +02:00
parent 9e3b805fdd
commit 1558794e93
528 changed files with 38239 additions and 37732 deletions

View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pub" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

47
filcnaplo_mobile_ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,47 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
pubspec.lock

View File

@@ -1,29 +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.
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,50 +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(),
),
),
);
},
);
}
}
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sliding_sheet/sliding_sheet.dart';
class ProfileButton extends StatelessWidget {
const ProfileButton({Key? key, required this.child}) : super(key: key);
final ProfileImage child;
@override
Widget build(BuildContext context) {
final bool pMode = Provider.of<SettingsProvider>(context, listen: false).presentationMode;
return ProfileImage(
backgroundColor: !pMode ? child.backgroundColor : Theme.of(context).colorScheme.secondary,
heroTag: child.heroTag,
key: child.key,
name: !pMode ? child.name : "Béla",
radius: child.radius,
badge: child.badge,
role: child.role,
profilePictureString: child.profilePictureString,
onTap: () {
showSlidingBottomSheet(
context,
useRootNavigator: true,
builder: (context) => SlidingSheetDialog(
color: Theme.of(context).scaffoldBackgroundColor,
duration: const Duration(milliseconds: 400),
scrollSpec: const ScrollSpec.bouncingScroll(),
snapSpec: const SnapSpec(
snap: true,
snappings: [1.0],
positioning: SnapPositioning.relativeToSheetHeight,
),
cornerRadius: 16,
cornerRadiusOnFullscreen: 0,
builder: (context, state) => Material(
color: Theme.of(context).scaffoldBackgroundColor,
child: const SettingsScreen(),
),
),
);
},
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,59 +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)),
],
),
);
}
}
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)),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,167 +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();
}
}
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();
}
}

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