changed everything from filcnaplo to refilc finally
This commit is contained in:
29
refilc_mobile_ui/LICENSE
Normal file
29
refilc_mobile_ui/LICENSE
Normal file
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2023, reFilc
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
0
refilc_mobile_ui/README.md
Normal file
0
refilc_mobile_ui/README.md
Normal file
28
refilc_mobile_ui/analysis_options.yaml
Normal file
28
refilc_mobile_ui/analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
36
refilc_mobile_ui/lib/common/action_button.dart
Normal file
36
refilc_mobile_ui/lib/common/action_button.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ActionButton extends StatelessWidget {
|
||||
const ActionButton({super.key, required this.label, this.activeColor, this.onTap});
|
||||
|
||||
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))),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
refilc_mobile_ui/lib/common/average_display.dart
Normal file
44
refilc_mobile_ui/lib/common/average_display.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
|
||||
class AverageDisplay extends StatelessWidget {
|
||||
const AverageDisplay({super.key, this.average = 0.0, this.border = false});
|
||||
|
||||
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: 6.0 - (border ? 2 : 0), vertical: 5.0 - (border ? 2 : 0)),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(45.0),
|
||||
border: border
|
||||
? Border.fromBorderSide(
|
||||
BorderSide(color: color.withOpacity(.5), width: 1.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, fontSize: 15.0),
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
41
refilc_mobile_ui/lib/common/beta_chip.dart
Normal file
41
refilc_mobile_ui/lib/common/beta_chip.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BetaChip extends StatelessWidget {
|
||||
const BetaChip({super.key, this.disabled = false});
|
||||
|
||||
final bool disabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 25,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
color: !disabled
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: AppColors.of(context).text.withOpacity(.25),
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"BETA",
|
||||
softWrap: true,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: disabled
|
||||
? AppColors.of(context).text.withOpacity(.5)
|
||||
: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
51
refilc_mobile_ui/lib/common/bottom_card.dart
Normal file
51
refilc_mobile_ui/lib/common/bottom_card.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomCard extends StatelessWidget {
|
||||
const BottomCard({super.key, this.child});
|
||||
|
||||
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));
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomSheetMenu extends StatelessWidget {
|
||||
const BottomSheetMenu({super.key, this.items = const []});
|
||||
|
||||
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));
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:refilc_mobile_ui/common/panel/panel_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomSheetMenuItem extends StatelessWidget {
|
||||
const BottomSheetMenuItem(
|
||||
{super.key, required this.onPressed, required this.title, this.icon});
|
||||
|
||||
final void Function()? onPressed;
|
||||
final Widget? title;
|
||||
final Widget? icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PanelButton(
|
||||
onPressed: onPressed,
|
||||
leading: icon,
|
||||
title: title,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RoundedBottomSheet extends StatelessWidget {
|
||||
const RoundedBottomSheet(
|
||||
{super.key,
|
||||
this.child,
|
||||
this.borderRadius = 12.0,
|
||||
this.shrink = true,
|
||||
this.showHandle = true});
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
43
refilc_mobile_ui/lib/common/custom_snack_bar.dart
Normal file
43
refilc_mobile_ui/lib/common/custom_snack_bar.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:refilc/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
|
||||
// ignore: no_leading_underscores_for_local_identifiers
|
||||
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),
|
||||
);
|
||||
}
|
||||
38
refilc_mobile_ui/lib/common/detail.dart
Normal file
38
refilc_mobile_ui/lib/common/detail.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Detail extends StatelessWidget {
|
||||
const Detail(
|
||||
{super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.maxLines = 3});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final int? maxLines;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 18.0),
|
||||
child: SelectableText.rich(
|
||||
TextSpan(
|
||||
text: "$title: ",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600, color: AppColors.of(context).text),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: description,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.of(context).text.withOpacity(0.85)),
|
||||
),
|
||||
],
|
||||
),
|
||||
minLines: 1,
|
||||
maxLines: maxLines,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
refilc_mobile_ui/lib/common/dialog_button.dart
Normal file
23
refilc_mobile_ui/lib/common/dialog_button.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DialogButton extends StatelessWidget {
|
||||
const DialogButton({super.key, required this.label, this.onTap});
|
||||
|
||||
final String label;
|
||||
final Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawMaterialButton(
|
||||
onPressed: onTap,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||
child: Text(
|
||||
label.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
20
refilc_mobile_ui/lib/common/dot.dart
Normal file
20
refilc_mobile_ui/lib/common/dot.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Dot extends StatelessWidget {
|
||||
final Color color;
|
||||
final double size;
|
||||
|
||||
const Dot({super.key, this.color = Colors.grey, this.size = 16.0});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
54
refilc_mobile_ui/lib/common/empty.dart
Normal file
54
refilc_mobile_ui/lib/common/empty.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
List<String> faces = [
|
||||
"(·.·)",
|
||||
"(≥o≤)",
|
||||
"(·_·)",
|
||||
"(˚Δ˚)b",
|
||||
"(^-^*)",
|
||||
"(='X'=)",
|
||||
"(>_<)",
|
||||
"(;-;)",
|
||||
"\\(^Д^)/",
|
||||
"\\(o_o)/",
|
||||
];
|
||||
|
||||
class Empty extends StatelessWidget {
|
||||
const Empty({super.key, this.subtitle});
|
||||
|
||||
final String? subtitle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
130
refilc_mobile_ui/lib/common/filter_bar.dart
Normal file
130
refilc_mobile_ui/lib/common/filter_bar.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FilterBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
const FilterBar({
|
||||
super.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,
|
||||
this.tabAlignment = TabAlignment.start,
|
||||
}) : assert(items.length == controller.length);
|
||||
|
||||
final List<Widget> items;
|
||||
final TabController controller;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final Function(int)? onTap;
|
||||
final bool disableFading;
|
||||
final bool scrollable;
|
||||
final bool censored;
|
||||
final TabAlignment tabAlignment;
|
||||
|
||||
@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) => Tab(
|
||||
child: 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,
|
||||
tabAlignment: widget.tabAlignment,
|
||||
);
|
||||
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 48.0,
|
||||
padding: widget.padding,
|
||||
child: widget.disableFading
|
||||
? tabbar
|
||||
: AnimatedBuilder(
|
||||
animation: widget.controller.animation!,
|
||||
builder: (ctx, child) {
|
||||
// avoid fading over selected tab
|
||||
return ShaderMask(
|
||||
shaderCallback: (Rect bounds) {
|
||||
final Color bg =
|
||||
Theme.of(context).scaffoldBackgroundColor;
|
||||
final double index = widget.controller.animation!.value;
|
||||
return LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
index < 0.2 ? Colors.transparent : bg,
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
index > widget.controller.length - 1.2
|
||||
? Colors.transparent
|
||||
: bg
|
||||
],
|
||||
stops: const [
|
||||
0,
|
||||
0.1,
|
||||
0.9,
|
||||
1
|
||||
]).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.dstOut,
|
||||
child: child);
|
||||
},
|
||||
child: tabbar,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
35
refilc_mobile_ui/lib/common/hero_dialog_route.dart
Normal file
35
refilc_mobile_ui/lib/common/hero_dialog_route.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HeroDialogRoute<T> extends PageRoute<T> {
|
||||
HeroDialogRoute({required this.builder}) : super();
|
||||
|
||||
final WidgetBuilder builder;
|
||||
|
||||
@override
|
||||
bool get opaque => false;
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => true;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => "livecard";
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 250);
|
||||
|
||||
@override
|
||||
bool get maintainState => true;
|
||||
|
||||
@override
|
||||
Color get barrierColor => Colors.black38;
|
||||
|
||||
@override
|
||||
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||
return FadeTransition(opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||
return builder(context);
|
||||
}
|
||||
}
|
||||
136
refilc_mobile_ui/lib/common/hero_scrollview.dart
Normal file
136
refilc_mobile_ui/lib/common/hero_scrollview.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
|
||||
class HeroScrollView extends StatefulWidget {
|
||||
const HeroScrollView(
|
||||
{super.key,
|
||||
required this.child,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
this.italic = false,
|
||||
this.navBarItems = const [],
|
||||
this.onClose,
|
||||
this.iconSize = 64.0,
|
||||
this.scrollController});
|
||||
|
||||
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,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
leading: BackButton(
|
||||
color: AppColors.of(context).text,
|
||||
onPressed: () {
|
||||
if (widget.onClose != null) {
|
||||
widget.onClose!();
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}),
|
||||
actions: widget.navBarItems,
|
||||
expandedHeight: 145.69,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
37
refilc_mobile_ui/lib/common/material_action_button.dart
Normal file
37
refilc_mobile_ui/lib/common/material_action_button.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc/utils/color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MaterialActionButton extends StatelessWidget {
|
||||
const MaterialActionButton({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
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(),
|
||||
fillColor: backgroundColor ?? AppColors.of(context).text.withOpacity(.15),
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
onPressed: onPressed,
|
||||
child: DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: backgroundColor != null
|
||||
? ColorUtils.foregroundColor(backgroundColor!)
|
||||
: null,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
36
refilc_mobile_ui/lib/common/new_content_indicator.dart
Normal file
36
refilc_mobile_ui/lib/common/new_content_indicator.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NewContentIndicator extends StatelessWidget {
|
||||
const NewContentIndicator({super.key, this.size = 64.0});
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
160
refilc_mobile_ui/lib/common/panel/panel.dart
Normal file
160
refilc_mobile_ui/lib/common/panel/panel.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class Panel extends StatelessWidget {
|
||||
const Panel({
|
||||
super.key,
|
||||
this.child,
|
||||
this.title,
|
||||
this.padding,
|
||||
this.hasShadow = true,
|
||||
this.isTransparent = false,
|
||||
});
|
||||
|
||||
final Widget? child;
|
||||
final Widget? title;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final bool hasShadow;
|
||||
final bool isTransparent;
|
||||
|
||||
@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: isTransparent
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.background,
|
||||
boxShadow: [
|
||||
if ((hasShadow && !isTransparent) &&
|
||||
Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
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({super.key, required this.title});
|
||||
|
||||
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({super.key, required this.padding});
|
||||
|
||||
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: [
|
||||
if (Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PanelBody extends StatelessWidget {
|
||||
const PanelBody({super.key, this.child, this.padding});
|
||||
|
||||
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: [
|
||||
if (Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
padding: padding,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PanelFooter extends StatelessWidget {
|
||||
const PanelFooter({super.key, required this.padding});
|
||||
|
||||
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: [
|
||||
if (Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
refilc_mobile_ui/lib/common/panel/panel_action_button.dart
Normal file
44
refilc_mobile_ui/lib/common/panel/panel_action_button.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PanelActionButton extends StatelessWidget {
|
||||
const PanelActionButton({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 14.0),
|
||||
this.leading,
|
||||
this.title,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
90
refilc_mobile_ui/lib/common/panel/panel_button.dart
Normal file
90
refilc_mobile_ui/lib/common/panel/panel_button.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PanelButton extends StatelessWidget {
|
||||
const PanelButton({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 14.0),
|
||||
this.leading,
|
||||
this.title,
|
||||
this.trailing,
|
||||
this.background = false,
|
||||
this.trailingDivider = false,
|
||||
this.borderRadius,
|
||||
this.longPressInstead = false,
|
||||
});
|
||||
|
||||
final void Function()? onPressed;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final Widget? leading;
|
||||
final Widget? title;
|
||||
final Widget? trailing;
|
||||
final bool background;
|
||||
final bool trailingDivider;
|
||||
final BorderRadius? borderRadius;
|
||||
final bool longPressInstead;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final button = RawMaterialButton(
|
||||
onPressed: !longPressInstead ? onPressed : null,
|
||||
onLongPress: longPressInstead ? onPressed : null,
|
||||
padding: padding,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: 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);
|
||||
}
|
||||
}
|
||||
72
refilc_mobile_ui/lib/common/personality_card/empty_card.dart
Normal file
72
refilc_mobile_ui/lib/common/personality_card/empty_card.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EmptyCard extends StatefulWidget {
|
||||
const EmptyCard({
|
||||
super.key,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
State<EmptyCard> createState() => _EmptyCardState();
|
||||
}
|
||||
|
||||
class _EmptyCardState extends State<EmptyCard> {
|
||||
bool hold = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onLongPressDown: (_) => setState(() => hold = true),
|
||||
onLongPressEnd: (_) => setState(() => hold = false),
|
||||
onLongPressCancel: () => setState(() => hold = false),
|
||||
child: AnimatedScale(
|
||||
scale: hold ? 1.018 : 1.0,
|
||||
curve: Curves.easeInOutBack,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: 444,
|
||||
padding:
|
||||
const EdgeInsets.only(top: 12, bottom: 12, left: 12, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x280008FF),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
boxShadow: [
|
||||
if (Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
offset: const Offset(0, 5),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: DottedBorder(
|
||||
color: Colors.black.withOpacity(0.9),
|
||||
dashPattern: const [12, 12],
|
||||
padding:
|
||||
const EdgeInsets.only(top: 20, bottom: 20, left: 20, right: 20),
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.text,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/helpers/average_helper.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/models/personality.dart';
|
||||
import 'package:refilc_kreta_api/models/absence.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_kreta_api/models/subject.dart';
|
||||
import 'package:refilc_kreta_api/models/week.dart';
|
||||
import 'package:refilc_kreta_api/providers/absence_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'personality_card.i18n.dart';
|
||||
|
||||
class PersonalityCard extends StatefulWidget {
|
||||
const PersonalityCard({
|
||||
super.key,
|
||||
required this.user,
|
||||
});
|
||||
|
||||
final UserProvider user;
|
||||
|
||||
@override
|
||||
State<PersonalityCard> createState() => _PersonalityCardState();
|
||||
}
|
||||
|
||||
class _PersonalityCardState extends State<PersonalityCard> {
|
||||
late GradeProvider gradeProvider;
|
||||
late AbsenceProvider absenceProvider;
|
||||
late TimetableProvider timetableProvider;
|
||||
late SettingsProvider settings;
|
||||
|
||||
late List<int> subjectAvgsList = [];
|
||||
late Map<GradeSubject, double> subjectAvgs = {};
|
||||
late double subjectAvg;
|
||||
late List<Grade> classWorkGrades;
|
||||
late Map<int, int> mostCommonGrade;
|
||||
late List<Absence> absences = [];
|
||||
final Map<GradeSubject, Lesson> _lessonCount = {};
|
||||
late int totalDelays;
|
||||
late int unexcusedAbsences;
|
||||
|
||||
late PersonalityType finalPersonality;
|
||||
|
||||
bool hold = false;
|
||||
|
||||
List<Grade> getSubjectGrades(GradeSubject subject, {int days = 0}) =>
|
||||
gradeProvider.grades
|
||||
.where((e) =>
|
||||
e.subject == subject &&
|
||||
e.type == GradeType.midYear &&
|
||||
(days == 0 ||
|
||||
e.date
|
||||
.isBefore(DateTime.now().subtract(Duration(days: days)))))
|
||||
.toList();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
gradeProvider = Provider.of<GradeProvider>(context, listen: false);
|
||||
absenceProvider = Provider.of<AbsenceProvider>(context, listen: false);
|
||||
timetableProvider = Provider.of<TimetableProvider>(context, listen: false);
|
||||
settings = Provider.of<SettingsProvider>(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 getGrades() {
|
||||
List<GradeSubject> subjects = gradeProvider.grades
|
||||
.map((e) => e.subject)
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort((a, b) => a.name.compareTo(b.name));
|
||||
|
||||
for (GradeSubject subject in subjects) {
|
||||
List<Grade> subjectGrades = getSubjectGrades(subject);
|
||||
|
||||
double avg = AverageHelper.averageEvals(subjectGrades);
|
||||
if (avg != 0) subjectAvgs[subject] = avg;
|
||||
|
||||
subjectAvgsList.add(avg.round());
|
||||
}
|
||||
|
||||
subjectAvg = subjectAvgs.isNotEmpty
|
||||
? subjectAvgs.values.fold(0.0, (double a, double b) => a + b) /
|
||||
subjectAvgs.length
|
||||
: 0.0;
|
||||
|
||||
classWorkGrades =
|
||||
gradeProvider.grades.where((a) => a.value.weight <= 75).toList();
|
||||
}
|
||||
|
||||
void getMostCommonGrade() {
|
||||
Map<int, int> counts = {};
|
||||
|
||||
subjectAvgsList.map((e) {
|
||||
if (counts.containsKey(e)) {
|
||||
counts.update(e, (value) => value++);
|
||||
} else {
|
||||
counts[e] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
var maxValue = 0;
|
||||
var maxKey = 0;
|
||||
|
||||
counts.forEach((k, v) {
|
||||
if (v > maxValue) {
|
||||
maxValue = v;
|
||||
maxKey = k;
|
||||
}
|
||||
});
|
||||
|
||||
mostCommonGrade = {maxKey: maxValue};
|
||||
}
|
||||
|
||||
void getAbsences() {
|
||||
absences = absenceProvider.absences.where((a) => a.delay == 0).toList();
|
||||
|
||||
unexcusedAbsences = absences
|
||||
.where((a) => a.state == Justification.unexcused && a.delay == 0)
|
||||
.length;
|
||||
}
|
||||
|
||||
void getAndSortDelays() {
|
||||
Iterable<int> unexcusedDelays = absences
|
||||
.where((a) => a.state == Justification.unexcused && a.delay > 0)
|
||||
.map((e) => e.delay);
|
||||
totalDelays = unexcusedDelays.isNotEmpty
|
||||
? unexcusedDelays.reduce((a, b) => a + b)
|
||||
: 0;
|
||||
}
|
||||
|
||||
void doEverything() {
|
||||
getGrades();
|
||||
getMostCommonGrade();
|
||||
getAbsences();
|
||||
getAndSortDelays();
|
||||
}
|
||||
|
||||
void getPersonality() {
|
||||
if (settings.goodStudent) {
|
||||
finalPersonality = PersonalityType.cheater;
|
||||
} else if (subjectAvg > 4.7) {
|
||||
finalPersonality = PersonalityType.geek;
|
||||
} else if (mostCommonGrade.keys.toList()[0] == 1 &&
|
||||
mostCommonGrade.values.toList()[0] > 1) {
|
||||
finalPersonality = PersonalityType.fallible;
|
||||
} else if (absences.length <= 12) {
|
||||
finalPersonality = PersonalityType.healthy;
|
||||
} else if (unexcusedAbsences >= 8) {
|
||||
finalPersonality = PersonalityType.quitter;
|
||||
} else if (totalDelays > 50) {
|
||||
finalPersonality = PersonalityType.late;
|
||||
} else if (absences.length >= 120) {
|
||||
finalPersonality = PersonalityType.sick;
|
||||
} else if (mostCommonGrade.keys.toList()[0] == 2) {
|
||||
finalPersonality = PersonalityType.acceptable;
|
||||
} else if (mostCommonGrade.keys.toList()[0] == 3) {
|
||||
finalPersonality = PersonalityType.average;
|
||||
} else if (classWorkGrades.length >= 5) {
|
||||
finalPersonality = PersonalityType.diligent;
|
||||
} else {
|
||||
finalPersonality = PersonalityType.npc;
|
||||
}
|
||||
}
|
||||
|
||||
Widget cardInnerBuilder() {
|
||||
Map<PersonalityType, Map<String, String>> personality = {
|
||||
PersonalityType.geek: {
|
||||
'emoji': '🤓',
|
||||
'title': 't_geek',
|
||||
'description': 'd_geek',
|
||||
'subtitle': 's_geek',
|
||||
'subvalue': subjectAvg.toStringAsFixed(2),
|
||||
},
|
||||
PersonalityType.sick: {
|
||||
'emoji': '🤒',
|
||||
'title': 't_sick',
|
||||
'description': 'd_sick',
|
||||
'subtitle': 's_sick',
|
||||
'subvalue': absences.length.toString(),
|
||||
},
|
||||
PersonalityType.late: {
|
||||
'emoji': '⌛',
|
||||
'title': 't_late',
|
||||
'description': 'd_late',
|
||||
'subtitle': 's_late',
|
||||
'subvalue': totalDelays.toString(),
|
||||
},
|
||||
PersonalityType.quitter: {
|
||||
'emoji': '❓',
|
||||
'title': 't_quitter',
|
||||
'description': 'd_quitter',
|
||||
'subtitle': 's_quitter',
|
||||
'subvalue': unexcusedAbsences.toString(),
|
||||
},
|
||||
PersonalityType.healthy: {
|
||||
'emoji': '😷',
|
||||
'title': 't_healthy',
|
||||
'description': 'd_healthy',
|
||||
'subtitle': 's_healthy',
|
||||
'subvalue': absences.length.toString(),
|
||||
},
|
||||
PersonalityType.acceptable: {
|
||||
'emoji': '🤏',
|
||||
'title': 't_acceptable',
|
||||
'description': 'd_acceptable',
|
||||
'subtitle': 's_acceptable',
|
||||
'subvalue': mostCommonGrade.values.toList()[0].toString(),
|
||||
},
|
||||
PersonalityType.fallible: {
|
||||
'emoji': '📉',
|
||||
'title': 't_fallible',
|
||||
'description': 'd_fallible',
|
||||
'subtitle': 's_fallible',
|
||||
'subvalue': mostCommonGrade.values.toList()[0].toString(),
|
||||
},
|
||||
PersonalityType.average: {
|
||||
'emoji': '👌',
|
||||
'title': 't_average',
|
||||
'description': 'd_average',
|
||||
'subtitle': 's_average',
|
||||
'subvalue': mostCommonGrade.values.toList()[0].toString(),
|
||||
},
|
||||
PersonalityType.diligent: {
|
||||
'emoji': '💫',
|
||||
'title': 't_diligent',
|
||||
'description': 'd_diligent',
|
||||
'subtitle': 's_diligent',
|
||||
'subvalue': classWorkGrades.length.toString(),
|
||||
},
|
||||
PersonalityType.cheater: {
|
||||
'emoji': '🧑💻',
|
||||
'title': 't_cheater',
|
||||
'description': 'd_cheater',
|
||||
'subtitle': 's_cheater',
|
||||
'subvalue': '0',
|
||||
},
|
||||
PersonalityType.npc: {
|
||||
'emoji': '⛰️',
|
||||
'title': 't_npc',
|
||||
'description': 'd_npc',
|
||||
'subtitle': 's_npc',
|
||||
'subvalue': '69420',
|
||||
}
|
||||
};
|
||||
|
||||
Map<PersonalityType, Widget> personalityWidgets = {};
|
||||
|
||||
for (var i in personality.keys) {
|
||||
Widget w = Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
personality[i]?['emoji'] ?? '❓',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 128.0,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(personality[i]?['title'] ?? 'unknown').i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 38.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
(personality[i]?['description'] ?? 'unknown_personality').i18n,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1.2,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
Text(
|
||||
(personality[i]?['subtitle'] ?? 'unknown').i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
personality[i]?['subvalue'] ?? '0',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 69.0,
|
||||
height: 1.15,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
personalityWidgets.addAll({i: w});
|
||||
}
|
||||
|
||||
return personalityWidgets[finalPersonality] ?? Container();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
doEverything();
|
||||
getPersonality();
|
||||
|
||||
return GestureDetector(
|
||||
onLongPressDown: (_) => setState(() => hold = true),
|
||||
onLongPressEnd: (_) => setState(() => hold = false),
|
||||
onLongPressCancel: () => setState(() => hold = false),
|
||||
child: AnimatedScale(
|
||||
scale: hold ? 1.018 : 1.0,
|
||||
curve: Curves.easeInOutBack,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 12, bottom: 12, left: 12, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x280008FF),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
offset: const Offset(0, 5),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: DottedBorder(
|
||||
color: Colors.black.withOpacity(0.9),
|
||||
dashPattern: const [12, 12],
|
||||
padding:
|
||||
const EdgeInsets.only(top: 20, bottom: 20, left: 20, right: 20),
|
||||
child: cardInnerBuilder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
// main
|
||||
"unknown": "???",
|
||||
"unknown_personality": "Unknown personality...",
|
||||
// personalities
|
||||
"t_geek": "Know-It-All",
|
||||
"d_geek":
|
||||
"You learn a lot, but don't worry - Being a know-it-all is a blessing in disguise. You'll be successful in life.",
|
||||
"s_geek": "Year-end average",
|
||||
"t_sick": "Sick",
|
||||
"d_sick":
|
||||
"Get well soon, bro. Even if you lied about being sick to skip school.",
|
||||
"s_sick": "Absences",
|
||||
"t_late": "Late",
|
||||
"d_late":
|
||||
"The tram's wheel got punctured. The airplane was derailed. Your dog ate your shoe. We believe you.",
|
||||
"s_late": "Delays (minutes)",
|
||||
"t_quitter": "Skipper",
|
||||
"d_quitter": "Supplementary exam incoming.",
|
||||
"s_quitter": "Igazolatlan hiányzások",
|
||||
"t_healthy": "Healthy",
|
||||
"d_healthy":
|
||||
"As cool as a cucumber! You almost never missed a class.",
|
||||
"s_healthy": "Absences",
|
||||
"t_acceptable": "Acceptable",
|
||||
"d_acceptable":
|
||||
"Final exams are D. But who cares? It's still a grade. Not a good one, but it's definitely a grade.",
|
||||
"s_acceptable": "D's",
|
||||
"t_fallible": "Failed",
|
||||
"d_fallible": "Good luck next year.",
|
||||
"s_fallible": "F's",
|
||||
"t_average": "It's okay",
|
||||
"d_average": "Not good, not bad. The golden mean, if you will...",
|
||||
"s_average": "C's",
|
||||
"t_diligent": "Hard-worker",
|
||||
"d_diligent":
|
||||
"You noted everything, you made that presentation, and you lead the group project.",
|
||||
"s_diligent": "Class work A's",
|
||||
"t_cheater": "Cheater",
|
||||
"d_cheater":
|
||||
"You enabled the \"Good Student\" mode. Wow. You may have outsmarted me, but I have outsmarted your outsmarting.",
|
||||
"s_cheater": "Bitches",
|
||||
"t_npc": "NPC",
|
||||
"d_npc":
|
||||
"You're such a non-player character, we couldn't give you a personality.",
|
||||
"s_npc": "In-game playtime (hours)",
|
||||
},
|
||||
"hu_hu": {
|
||||
// main
|
||||
"unknown": "???",
|
||||
"unknown_personality": "Ismeretlen személyiség...",
|
||||
// personalities
|
||||
"t_geek": "Stréber",
|
||||
"d_geek":
|
||||
"Sokat tanulsz, de ezzel semmi baj! Ez egyben áldás és átok, de legalább az életben sikeres leszel.",
|
||||
"s_geek": "Év végi átlagod",
|
||||
"t_sick": "Beteges",
|
||||
"d_sick":
|
||||
"Jobbulást, tesó. Még akkor is, ha hazudtál arról, hogy beteg vagy, hogy ne kelljen suliba menned.",
|
||||
"s_sick": "Hiányzásaid",
|
||||
"t_late": "Késős",
|
||||
"d_late":
|
||||
"Kilyukadt a villamos kereke. Kisiklott a repülő. A kutyád megette a cipőd. Elhisszük.",
|
||||
"s_late": "Késések (perc)",
|
||||
"t_quitter": "Lógós",
|
||||
"d_quitter": "Osztályzóvizsga incoming.",
|
||||
"s_quitter": "Igazolatlan hiányzások",
|
||||
"t_healthy": "Makk",
|
||||
"d_healthy":
|
||||
"...egészséges vagy! Egész évben alig hiányoztál az iskolából.",
|
||||
"s_healthy": "Hiányzásaid",
|
||||
"t_acceptable": "Elmegy",
|
||||
"d_acceptable":
|
||||
"A kettes érettségi is érettségi. Nem egy jó érettségi, de biztos, hogy egy érettségi.",
|
||||
"s_acceptable": "Kettesek",
|
||||
"t_fallible": "Bukós",
|
||||
"d_fallible": "Jövőre több sikerrel jársz.",
|
||||
"s_fallible": "Karók",
|
||||
"t_average": "Közepes",
|
||||
"d_average": "Se jó, se rossz. Az arany középút, ha akarsz...",
|
||||
"s_average": "Hármasok",
|
||||
"t_diligent": "Szorgalmas",
|
||||
"d_diligent":
|
||||
"Leírtad a jegyzetet, megcsináltad a prezentációt, és te vezetted a projektmunkát.",
|
||||
"s_diligent": "Órai munka ötösök",
|
||||
"t_cheater": "Csaló",
|
||||
"d_cheater":
|
||||
"Bekapcsoltad a “Jó Tanuló” módot. Wow. Azt hitted, túl járhatsz az eszemen, de kijátszottam a kijátszásod.",
|
||||
"s_cheater": "Bitches",
|
||||
"t_npc": "NPC",
|
||||
"d_npc":
|
||||
"Egy akkora nagy non-player character vagy, hogy neked semmilyen személyiség nem jutott ezen kívül.",
|
||||
"s_npc": "In-game playtime (óra)",
|
||||
},
|
||||
"de_de": {
|
||||
// main
|
||||
"unknown": "???",
|
||||
"unknown_personality": "Unbekannte Persönlichkeit...",
|
||||
// personalities
|
||||
"t_geek": "Besserwisser",
|
||||
"d_geek":
|
||||
"Du lernst eine Menge, aber sorge dich nicht - ein Besserwisser zu sein wird sich letzten Endes doch als Segen erweisen. Du wirst erfolgreich sein im Leben.",
|
||||
"s_geek": "Durchschnittsschüler",
|
||||
"t_sick": "Krank",
|
||||
"d_sick":
|
||||
"Werd schnell wieder gesund, Brudi. Selbst wenn du gelogen hast, nur um Schule zu schwänzen zu können.",
|
||||
"s_sick": "Abwesenheiten",
|
||||
"t_late": "Verspätet",
|
||||
"d_late":
|
||||
"Die Straßenbahn hat eine Reifenpanne. Das Flugzeug ist entgleist. Dein Hund hat deinen Schuh gefressen. Klar, wir glauben dir.",
|
||||
"s_late": "Verspätung (Minuten)",
|
||||
"t_quitter": "Schulschwänzer",
|
||||
"d_quitter": "Ein zusätzlicher Test wird anstehen.",
|
||||
"s_quitter": "Unentschuldigte Abwesenheiten",
|
||||
"t_healthy": "Gesund",
|
||||
"d_healthy":
|
||||
"Du bist die Ruhe selbst! Du hast fast nie eine Unterrichtsstunde verpasst.",
|
||||
"s_healthy": "Abwesenheiten",
|
||||
"t_acceptable": "Akzeptabel",
|
||||
"d_acceptable":
|
||||
"Die Abschlussprüfungen waren gerade einmal eine 4. Aber wen juckt's? Es ist immer noch positiv. Nicht allzu gut, aber definitiv positiv.",
|
||||
"s_acceptable": "4er",
|
||||
"t_fallible": "Durchgefallen",
|
||||
"d_fallible": "Viel Glück im nächsten Jahr.",
|
||||
"s_fallible": "5er",
|
||||
"t_average": "Es ist in Ordnung",
|
||||
"d_average":
|
||||
"Nicht gut, nicht schlecht. Der goldene Durchschnitt, wenn du so willst...",
|
||||
"s_average": "3er",
|
||||
"t_diligent": "Ein Fleißiger",
|
||||
"d_diligent":
|
||||
"Du hast bei allem mitgeschrieben, du hast den Vortrag gehalten, und du hast die Gruppenarbeit geleitet.",
|
||||
"s_diligent": "1er Schüler",
|
||||
"t_cheater": "Geschummelt",
|
||||
"d_cheater":
|
||||
"Du hast den „Guter Schüler“ Modus aktiviert. Wow. Du magst mich zwar vielleicht überlistet haben, aber ich habe deine Überlistung überlistet.",
|
||||
"s_cheater": "Bitches",
|
||||
"t_npc": "COM",
|
||||
"d_npc":
|
||||
"Du bist einfach so sehr wie ein Computer, dass wir dir nicht einmal eine Persönlichkeit geben konnten.",
|
||||
"s_npc": "Spielzeit (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);
|
||||
}
|
||||
100
refilc_mobile_ui/lib/common/profile_image/profile_button.dart
Normal file
100
refilc_mobile_ui/lib/common/profile_image/profile_button.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/models/user.dart';
|
||||
import 'package:refilc_kreta_api/client/client.dart';
|
||||
import 'package:refilc_kreta_api/providers/absence_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/event_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/exam_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/homework_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/message_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/note_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:refilc_mobile_ui/common/profile_image/profile_image.dart';
|
||||
import 'package:refilc_mobile_ui/screens/settings/settings_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wtf_sliding_sheet/wtf_sliding_sheet.dart';
|
||||
|
||||
class ProfileButton extends StatelessWidget {
|
||||
const ProfileButton({super.key, required this.child});
|
||||
|
||||
final ProfileImage child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool pMode =
|
||||
Provider.of<SettingsProvider>(context, listen: false).presentationMode;
|
||||
|
||||
late UserProvider user;
|
||||
late User? account;
|
||||
|
||||
Future<void> restore() => Future.wait([
|
||||
Provider.of<GradeProvider>(context, listen: false).restore(),
|
||||
Provider.of<TimetableProvider>(context, listen: false).restoreUser(),
|
||||
Provider.of<ExamProvider>(context, listen: false).restore(),
|
||||
Provider.of<HomeworkProvider>(context, listen: false).restore(),
|
||||
Provider.of<MessageProvider>(context, listen: false).restore(),
|
||||
Provider.of<MessageProvider>(context, listen: false)
|
||||
.restoreRecipients(),
|
||||
Provider.of<NoteProvider>(context, listen: false).restore(),
|
||||
Provider.of<EventProvider>(context, listen: false).restore(),
|
||||
Provider.of<AbsenceProvider>(context, listen: false).restore(),
|
||||
Provider.of<KretaClient>(context, listen: false).refreshLogin(),
|
||||
]);
|
||||
|
||||
user = Provider.of<UserProvider>(context);
|
||||
try {
|
||||
user.getUsers().forEach((acc) {
|
||||
if (user.name!.toLowerCase().replaceAll(' ', '') !=
|
||||
acc.name.toLowerCase().replaceAll(' ', '')) {
|
||||
account = acc;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
account = null;
|
||||
}
|
||||
|
||||
return ProfileImage(
|
||||
backgroundColor: !pMode
|
||||
? child.backgroundColor
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
heroTag: child.heroTag,
|
||||
key: child.key,
|
||||
name: !pMode ? child.name : "János",
|
||||
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],
|
||||
initialSnap: 1.0,
|
||||
positioning: SnapPositioning.relativeToSheetHeight,
|
||||
),
|
||||
cornerRadius: 16,
|
||||
cornerRadiusOnFullscreen: 0,
|
||||
builder: (context, state) => Material(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: const SettingsScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
if (account != null) {
|
||||
user.setUser(account!.id);
|
||||
restore().then((_) => user.setUser(account!.id));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
259
refilc_mobile_ui/lib/common/profile_image/profile_image.dart
Normal file
259
refilc_mobile_ui/lib/common/profile_image/profile_image.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:refilc/models/user.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_mobile_ui/common/new_content_indicator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/utils/color.dart';
|
||||
|
||||
class ProfileImage extends StatefulWidget {
|
||||
const ProfileImage({
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.onDoubleTap,
|
||||
this.onLongPress,
|
||||
this.name,
|
||||
this.backgroundColor,
|
||||
this.radius = 20.0,
|
||||
this.heroTag,
|
||||
this.badge = false,
|
||||
this.role = Role.student,
|
||||
this.censored = false,
|
||||
this.profilePictureString = "",
|
||||
this.isNotePfp = false,
|
||||
});
|
||||
|
||||
final void Function()? onTap;
|
||||
final void Function()? onDoubleTap;
|
||||
final void Function()? onLongPress;
|
||||
final String? name;
|
||||
final Color? backgroundColor;
|
||||
final double radius;
|
||||
final String? heroTag;
|
||||
final bool badge;
|
||||
final Role? role;
|
||||
final bool censored;
|
||||
final String profilePictureString;
|
||||
final bool isNotePfp;
|
||||
|
||||
@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.name != null && widget.name! == 'Rendszerüzenet'
|
||||
? widget.backgroundColor?.withOpacity(0.5) ??
|
||||
AppColors.of(context).text.withOpacity(0.5)
|
||||
: widget.backgroundColor ??
|
||||
AppColors.of(context).text.withOpacity(.15),
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
onDoubleTap: widget.onDoubleTap,
|
||||
onLongPress: widget.onLongPress,
|
||||
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: (widget.isNotePfp ? 20 : 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,
|
||||
type: MaterialType.transparency,
|
||||
child: profilePicture ?? child,
|
||||
),
|
||||
),
|
||||
|
||||
// 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,
|
||||
onDoubleTap: widget.onDoubleTap,
|
||||
onLongPress: widget.onLongPress,
|
||||
child: SizedBox(
|
||||
height: widget.radius * 2,
|
||||
width: widget.radius * 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
79
refilc_mobile_ui/lib/common/progress_bar.dart
Normal file
79
refilc_mobile_ui/lib/common/progress_bar.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProgressBar extends StatelessWidget {
|
||||
const ProgressBar(
|
||||
{super.key, required this.value, this.backgroundColor, this.height = 8.0});
|
||||
|
||||
final double value;
|
||||
final Color? backgroundColor;
|
||||
final double height;
|
||||
|
||||
@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: height,
|
||||
),
|
||||
|
||||
// Slider
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
width: double.infinity,
|
||||
child: CustomPaint(
|
||||
painter: ProgressPainter(
|
||||
backgroundColor:
|
||||
backgroundColor ?? Theme.of(context).colorScheme.secondary,
|
||||
height: height,
|
||||
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;
|
||||
}
|
||||
}
|
||||
31
refilc_mobile_ui/lib/common/round_border_icon.dart
Normal file
31
refilc_mobile_ui/lib/common/round_border_icon.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RoundBorderIcon extends StatelessWidget {
|
||||
final Color? color;
|
||||
final double width;
|
||||
final double padding;
|
||||
final Widget icon;
|
||||
|
||||
const RoundBorderIcon(
|
||||
{super.key,
|
||||
this.color,
|
||||
this.width = 1.5,
|
||||
this.padding = 5.0,
|
||||
required this.icon});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: color ?? AppColors.of(context).text, width: width),
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(padding),
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
refilc_mobile_ui/lib/common/screens.i18n.dart
Normal file
33
refilc_mobile_ui/lib/common/screens.i18n.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension ScreensLocalization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"home": "Home",
|
||||
"grades": "Grades",
|
||||
"timetable": "Timetable",
|
||||
"messages": "Messages",
|
||||
"absences": "Absences",
|
||||
},
|
||||
"hu_hu": {
|
||||
"home": "Kezdőlap",
|
||||
"grades": "Jegyek",
|
||||
"timetable": "Órarend",
|
||||
"messages": "Üzenetek",
|
||||
"absences": "Hiányok",
|
||||
},
|
||||
"de_de": {
|
||||
"home": "Zuhause",
|
||||
"grades": "Noten",
|
||||
"timetable": "Zeitplan",
|
||||
"messages": "Mitteilungen",
|
||||
"absences": "Fehlen",
|
||||
},
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
46
refilc_mobile_ui/lib/common/sliding_bottom_sheet.dart
Normal file
46
refilc_mobile_ui/lib/common/sliding_bottom_sheet.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wtf_sliding_sheet/wtf_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),
|
||||
);
|
||||
},
|
||||
));
|
||||
22
refilc_mobile_ui/lib/common/soon_alert/soon_alert.dart
Normal file
22
refilc_mobile_ui/lib/common/soon_alert/soon_alert.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:refilc_mobile_ui/common/action_button.dart';
|
||||
import 'package:refilc_mobile_ui/common/soon_alert/soon_alert.i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SoonAlert extends StatelessWidget {
|
||||
const SoonAlert({super.key});
|
||||
|
||||
static show({required BuildContext context}) =>
|
||||
showDialog(context: context, builder: (context) => const SoonAlert());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
|
||||
title: Text('soon'.i18n),
|
||||
content: Text('soon_body'.i18n),
|
||||
actions: [
|
||||
ActionButton(label: "Ok", onTap: () => Navigator.of(context).pop())
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
24
refilc_mobile_ui/lib/common/soon_alert/soon_alert.i18n.dart
Normal file
24
refilc_mobile_ui/lib/common/soon_alert/soon_alert.i18n.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"soon": "Soon...",
|
||||
"soon_body": "This feature is currently not available yet!",
|
||||
},
|
||||
"hu_hu": {
|
||||
"soon": "Hamarosan...",
|
||||
"soon_body": "Ez a funkció jelenleg még nem elérhető!",
|
||||
},
|
||||
"de_de": {
|
||||
"soon": "Bald...",
|
||||
"soon_body": "Diese Funktion ist derzeit noch nicht 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);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SplittedMenuOption extends StatelessWidget {
|
||||
const SplittedMenuOption({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.padding,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(2.0)),
|
||||
child: InkWell(
|
||||
splashColor: Colors.grey,
|
||||
onLongPress: () {
|
||||
if (kDebugMode) {
|
||||
print('object');
|
||||
}
|
||||
},
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (leading != null) leading!,
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
Text(text),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
127
refilc_mobile_ui/lib/common/splitted_panel/splitted_panel.dart
Normal file
127
refilc_mobile_ui/lib/common/splitted_panel/splitted_panel.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SplittedPanel extends StatelessWidget {
|
||||
const SplittedPanel({
|
||||
super.key,
|
||||
this.children,
|
||||
this.title,
|
||||
this.padding,
|
||||
this.cardPadding,
|
||||
this.hasShadow = true,
|
||||
this.isSeparated = false,
|
||||
this.spacing = 6.0,
|
||||
this.isTransparent = false,
|
||||
this.hasBorder = false,
|
||||
});
|
||||
|
||||
final List<Widget>? children;
|
||||
final Widget? title;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? cardPadding;
|
||||
final bool hasShadow;
|
||||
final bool isSeparated;
|
||||
final double spacing;
|
||||
final bool isTransparent;
|
||||
final bool hasBorder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double sp = spacing;
|
||||
|
||||
if (isSeparated && spacing == 6.0) {
|
||||
sp = 9.0;
|
||||
}
|
||||
|
||||
List<Widget> childrenInMyBasement = [];
|
||||
|
||||
if (children != null) {
|
||||
var i = 0;
|
||||
|
||||
for (var widget in children!) {
|
||||
var w = Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: isTransparent
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(i == 0 ? 16.0 : 8.0),
|
||||
bottom: Radius.circular(children!.length == i + 1 ? 16.0 : 8.0),
|
||||
),
|
||||
border: hasBorder
|
||||
? Border.all(
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary.withOpacity(.25),
|
||||
width: 1.0)
|
||||
: null,
|
||||
),
|
||||
margin: EdgeInsets.only(top: i == 0 ? 0.0 : sp),
|
||||
padding: cardPadding ?? EdgeInsets.zero,
|
||||
child: widget,
|
||||
);
|
||||
|
||||
childrenInMyBasement.add(w);
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// title
|
||||
if (title != null)
|
||||
SplittedPanelTitle(
|
||||
title: title!,
|
||||
leftPadding: (padding?.horizontal ?? 48.0) / 2,
|
||||
),
|
||||
|
||||
// body
|
||||
if (children != null)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
boxShadow: [
|
||||
if (hasShadow &&
|
||||
Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
padding: padding ??
|
||||
const EdgeInsets.only(bottom: 20.0, left: 24.0, right: 24.0),
|
||||
child: Column(children: childrenInMyBasement),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SplittedPanelTitle extends StatelessWidget {
|
||||
const SplittedPanelTitle(
|
||||
{super.key, required this.title, this.leftPadding = 24.0});
|
||||
|
||||
final Widget title;
|
||||
final double leftPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 14.0 + leftPadding, 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
15
refilc_mobile_ui/lib/common/system_chrome.dart
Normal file
15
refilc_mobile_ui/lib/common/system_chrome.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
void setSystemChrome(BuildContext context) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom]);
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Theme.of(context).brightness == Brightness.light ? Brightness.dark : Brightness.light,
|
||||
systemNavigationBarColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
|
||||
systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.light ? Brightness.dark : Brightness.light,
|
||||
statusBarBrightness: Platform.isIOS ? Theme.of(context).brightness : null,
|
||||
));
|
||||
}
|
||||
59
refilc_mobile_ui/lib/common/trend_display.dart
Normal file
59
refilc_mobile_ui/lib/common/trend_display.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
|
||||
class TrendDisplay<T extends num> extends StatelessWidget {
|
||||
const TrendDisplay({super.key, required this.current, required this.previous, this.padding});
|
||||
|
||||
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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
973
refilc_mobile_ui/lib/common/viewable.dart
Normal file
973
refilc_mobile_ui/lib/common/viewable.dart
Normal file
@@ -0,0 +1,973 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart' show kMinFlingVelocity;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
double valueFromPercentageInRange({required final double min, max, percentage}) {
|
||||
return percentage * (max - min) + min;
|
||||
}
|
||||
|
||||
double percentageFromValueInRange({required final double min, max, value}) {
|
||||
return (value - min) / (max - min);
|
||||
}
|
||||
|
||||
const double _kOpenScale = 1.025;
|
||||
|
||||
const Color _borderColor = CupertinoDynamicColor.withBrightness(
|
||||
color: Color(0xFFA9A9AF),
|
||||
darkColor: Color(0xFF57585A),
|
||||
);
|
||||
|
||||
typedef _DismissCallback = void Function(
|
||||
BuildContext context,
|
||||
double scale,
|
||||
double opacity,
|
||||
);
|
||||
|
||||
typedef ViewablePreviewBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Widget child,
|
||||
);
|
||||
|
||||
typedef _ViewablePreviewBuilderChildless = Widget Function(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
);
|
||||
|
||||
Rect _getRect(GlobalKey globalKey) {
|
||||
assert(globalKey.currentContext != null);
|
||||
final RenderBox renderBoxContainer = globalKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
final Offset containerOffset = renderBoxContainer.localToGlobal(
|
||||
renderBoxContainer.paintBounds.topLeft,
|
||||
);
|
||||
return containerOffset & renderBoxContainer.paintBounds.size;
|
||||
}
|
||||
|
||||
enum _ViewableLocation {
|
||||
center,
|
||||
left,
|
||||
right,
|
||||
}
|
||||
|
||||
class Viewable extends StatefulWidget {
|
||||
const Viewable({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.tile,
|
||||
this.actions = const [],
|
||||
this.previewBuilder,
|
||||
});
|
||||
|
||||
final Widget tile;
|
||||
final Widget view;
|
||||
|
||||
final List<Widget> actions;
|
||||
|
||||
final ViewablePreviewBuilder? previewBuilder;
|
||||
|
||||
@override
|
||||
State<Viewable> createState() => _ViewableState();
|
||||
}
|
||||
|
||||
class _ViewableState extends State<Viewable> with TickerProviderStateMixin {
|
||||
final GlobalKey _childGlobalKey = GlobalKey();
|
||||
bool _childHidden = false;
|
||||
|
||||
late AnimationController _openController;
|
||||
Rect? _decoyChildEndRect;
|
||||
OverlayEntry? _lastOverlayEntry;
|
||||
_ViewableRoute<void>? _route;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_openController = AnimationController(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
vsync: this,
|
||||
);
|
||||
_openController.addStatusListener(_onDecoyAnimationStatusChange);
|
||||
}
|
||||
|
||||
_ViewableLocation get _contextMenuLocation {
|
||||
final Rect childRect = _getRect(_childGlobalKey);
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final double center = screenWidth / 2;
|
||||
final bool centerDividesChild = childRect.left < center && childRect.right > center;
|
||||
final double distanceFromCenter = (center - childRect.center.dx).abs();
|
||||
if (centerDividesChild && distanceFromCenter <= childRect.width / 4) {
|
||||
return _ViewableLocation.center;
|
||||
}
|
||||
|
||||
if (childRect.center.dx > center) {
|
||||
return _ViewableLocation.right;
|
||||
}
|
||||
|
||||
return _ViewableLocation.left;
|
||||
}
|
||||
|
||||
void _openContextMenu() {
|
||||
setState(() {
|
||||
_childHidden = true;
|
||||
});
|
||||
|
||||
_route = _ViewableRoute<void>(
|
||||
actions: widget.actions,
|
||||
barrierLabel: 'Dismiss',
|
||||
filter: ui.ImageFilter.blur(
|
||||
sigmaX: 5.0,
|
||||
sigmaY: 5.0,
|
||||
),
|
||||
contextMenuLocation: _contextMenuLocation,
|
||||
previousChildRect: _decoyChildEndRect!,
|
||||
builder: (BuildContext context, Animation<double> animation) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: animation.status == AnimationStatus.forward
|
||||
? Curves.easeOutCirc.transform(animation.value)
|
||||
: Curves.easeInCirc.transform(animation.value),
|
||||
child: widget.view,
|
||||
),
|
||||
Opacity(
|
||||
opacity: 1 -
|
||||
(animation.status == AnimationStatus.forward
|
||||
? Curves.easeOutCirc.transform(animation.value)
|
||||
: Curves.easeInCirc.transform(animation.value)),
|
||||
child: widget.tile,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
Navigator.of(context, rootNavigator: true).push<void>(_route!);
|
||||
_route!.animation!.addStatusListener(_routeAnimationStatusListener);
|
||||
}
|
||||
|
||||
void _onDecoyAnimationStatusChange(AnimationStatus animationStatus) {
|
||||
switch (animationStatus) {
|
||||
case AnimationStatus.dismissed:
|
||||
if (_route == null) {
|
||||
setState(() {
|
||||
_childHidden = false;
|
||||
});
|
||||
}
|
||||
_lastOverlayEntry?.remove();
|
||||
_lastOverlayEntry = null;
|
||||
break;
|
||||
|
||||
case AnimationStatus.completed:
|
||||
setState(() {
|
||||
_childHidden = true;
|
||||
});
|
||||
_openContextMenu();
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
|
||||
_lastOverlayEntry?.remove();
|
||||
_lastOverlayEntry = null;
|
||||
_openController.reset();
|
||||
});
|
||||
break;
|
||||
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.reverse:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void _routeAnimationStatusListener(AnimationStatus status) {
|
||||
if (status != AnimationStatus.dismissed) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_childHidden = false;
|
||||
});
|
||||
_route!.animation!.removeStatusListener(_routeAnimationStatusListener);
|
||||
_route = null;
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
_onTapDown(TapDownDetails(), anim: false);
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details, {anim = true}) {
|
||||
setState(() {
|
||||
_childHidden = true;
|
||||
});
|
||||
|
||||
final Rect childRect = _getRect(_childGlobalKey);
|
||||
_decoyChildEndRect = Rect.fromCenter(
|
||||
center: childRect.center,
|
||||
width: childRect.width * _kOpenScale,
|
||||
height: childRect.height * _kOpenScale,
|
||||
);
|
||||
|
||||
_lastOverlayEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return _DecoyChild(
|
||||
beginRect: childRect,
|
||||
controller: _openController,
|
||||
endRect: _decoyChildEndRect,
|
||||
child: widget.tile,
|
||||
);
|
||||
},
|
||||
);
|
||||
Overlay.of(context, rootOverlay: true).insert(_lastOverlayEntry!);
|
||||
_openController.forward(from: anim ? 0.0 : 1.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _onTap,
|
||||
child: TickerMode(
|
||||
enabled: !_childHidden,
|
||||
child: Opacity(
|
||||
key: _childGlobalKey,
|
||||
opacity: _childHidden ? 0.0 : 1.0,
|
||||
child: widget.tile,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_openController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _DecoyChild extends StatefulWidget {
|
||||
const _DecoyChild({
|
||||
this.beginRect,
|
||||
required this.controller,
|
||||
this.endRect,
|
||||
this.child,
|
||||
});
|
||||
|
||||
final Rect? beginRect;
|
||||
final AnimationController controller;
|
||||
final Rect? endRect;
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
_DecoyChildState createState() => _DecoyChildState();
|
||||
}
|
||||
|
||||
class _DecoyChildState extends State<_DecoyChild> with TickerProviderStateMixin {
|
||||
static const Color _lightModeMaskColor = Color(0xFF888888);
|
||||
static const Color _masklessColor = Color(0xFFFFFFFF);
|
||||
|
||||
final GlobalKey _childGlobalKey = GlobalKey();
|
||||
late Animation<Color> _mask;
|
||||
late Animation<Rect?> _rect;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_mask = _OnOffAnimation<Color>(
|
||||
controller: widget.controller,
|
||||
onValue: _lightModeMaskColor,
|
||||
offValue: _masklessColor,
|
||||
intervalOn: 0.0,
|
||||
intervalOff: 0.5,
|
||||
);
|
||||
|
||||
final Rect midRect = widget.beginRect!.deflate(
|
||||
widget.beginRect!.width * (_kOpenScale - 1.0) / 2,
|
||||
);
|
||||
_rect = TweenSequence<Rect?>(<TweenSequenceItem<Rect?>>[
|
||||
TweenSequenceItem<Rect?>(
|
||||
tween: RectTween(
|
||||
begin: widget.beginRect,
|
||||
end: midRect,
|
||||
).chain(CurveTween(curve: Curves.easeInOutCubic)),
|
||||
weight: 1.0,
|
||||
),
|
||||
TweenSequenceItem<Rect?>(
|
||||
tween: RectTween(
|
||||
begin: midRect,
|
||||
end: widget.endRect,
|
||||
).chain(CurveTween(curve: Curves.easeOutCubic)),
|
||||
weight: 1.0,
|
||||
),
|
||||
]).animate(widget.controller);
|
||||
_rect.addListener(_rectListener);
|
||||
}
|
||||
|
||||
void _rectListener() {
|
||||
if (widget.controller.value < 0.5) {
|
||||
return;
|
||||
}
|
||||
HapticFeedback.selectionClick();
|
||||
_rect.removeListener(_rectListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_rect.removeListener(_rectListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildAnimation(BuildContext context, Widget? child) {
|
||||
final Color color = widget.controller.status == AnimationStatus.reverse ? _masklessColor : _mask.value;
|
||||
return Positioned.fromRect(
|
||||
rect: _rect.value!,
|
||||
child: ShaderMask(
|
||||
key: _childGlobalKey,
|
||||
shaderCallback: (Rect bounds) {
|
||||
return LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[color, color],
|
||||
).createShader(bounds);
|
||||
},
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
AnimatedBuilder(
|
||||
builder: _buildAnimation,
|
||||
animation: widget.controller,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ViewableRoute<T> extends PopupRoute<T> {
|
||||
_ViewableRoute({
|
||||
required List<Widget> actions,
|
||||
required _ViewableLocation contextMenuLocation,
|
||||
this.barrierLabel,
|
||||
_ViewablePreviewBuilderChildless? builder,
|
||||
super.filter,
|
||||
required Rect previousChildRect,
|
||||
super.settings,
|
||||
}) : _actions = actions,
|
||||
_builder = builder,
|
||||
_contextMenuLocation = contextMenuLocation,
|
||||
_previousChildRect = previousChildRect;
|
||||
|
||||
static const Color _kModalBarrierColor = Color(0x6604040F);
|
||||
|
||||
static const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335);
|
||||
|
||||
final List<Widget> _actions;
|
||||
final _ViewablePreviewBuilderChildless? _builder;
|
||||
final GlobalKey _childGlobalKey = GlobalKey();
|
||||
final _ViewableLocation _contextMenuLocation;
|
||||
bool _externalOffstage = false;
|
||||
bool _internalOffstage = false;
|
||||
Orientation? _lastOrientation;
|
||||
|
||||
final Rect _previousChildRect;
|
||||
double? _scale = 1.0;
|
||||
final GlobalKey _sheetGlobalKey = GlobalKey();
|
||||
|
||||
static final CurveTween _curve = CurveTween(
|
||||
curve: Curves.easeOutBack,
|
||||
);
|
||||
static final CurveTween _curveReverse = CurveTween(
|
||||
curve: Curves.easeInBack,
|
||||
);
|
||||
static final RectTween _rectTween = RectTween();
|
||||
static final Animatable<Rect?> _rectAnimatable = _rectTween.chain(_curve);
|
||||
static final RectTween _rectTweenReverse = RectTween();
|
||||
static final Animatable<Rect?> _rectAnimatableReverse = _rectTweenReverse.chain(
|
||||
_curveReverse,
|
||||
);
|
||||
static final RectTween _sheetRectTween = RectTween();
|
||||
final Animatable<Rect?> _sheetRectAnimatable = _sheetRectTween.chain(
|
||||
_curve,
|
||||
);
|
||||
final Animatable<Rect?> _sheetRectAnimatableReverse = _sheetRectTween.chain(
|
||||
_curveReverse,
|
||||
);
|
||||
static final Tween<double> _sheetScaleTween = Tween<double>();
|
||||
static final Animatable<double> _sheetScaleAnimatable = _sheetScaleTween.chain(
|
||||
_curve,
|
||||
);
|
||||
static final Animatable<double> _sheetScaleAnimatableReverse = _sheetScaleTween.chain(
|
||||
_curveReverse,
|
||||
);
|
||||
final Tween<double> _opacityTween = Tween<double>(begin: 0.0, end: 1.0);
|
||||
late Animation<double> _sheetOpacity;
|
||||
|
||||
@override
|
||||
final String? barrierLabel;
|
||||
|
||||
@override
|
||||
Color get barrierColor => _kModalBarrierColor;
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => true;
|
||||
|
||||
@override
|
||||
bool get semanticsDismissible => false;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => _kModalPopupTransitionDuration;
|
||||
|
||||
static Rect _getScaledRect(GlobalKey globalKey, double scale) {
|
||||
final Rect childRect = _getRect(globalKey);
|
||||
final Size sizeScaled = childRect.size * scale;
|
||||
final Offset offsetScaled = Offset(
|
||||
childRect.left + (childRect.size.width - sizeScaled.width) / 2,
|
||||
childRect.top + (childRect.size.height - sizeScaled.height) / 2,
|
||||
);
|
||||
return offsetScaled & sizeScaled;
|
||||
}
|
||||
|
||||
static AlignmentDirectional getSheetAlignment(_ViewableLocation contextMenuLocation) {
|
||||
switch (contextMenuLocation) {
|
||||
case _ViewableLocation.center:
|
||||
return AlignmentDirectional.topCenter;
|
||||
case _ViewableLocation.right:
|
||||
return AlignmentDirectional.topEnd;
|
||||
case _ViewableLocation.left:
|
||||
return AlignmentDirectional.topStart;
|
||||
}
|
||||
}
|
||||
|
||||
static Rect _getSheetRectBegin(Orientation? orientation, _ViewableLocation contextMenuLocation, Rect childRect, Rect sheetRect) {
|
||||
switch (contextMenuLocation) {
|
||||
case _ViewableLocation.center:
|
||||
final Offset target = orientation == Orientation.portrait ? childRect.bottomCenter : childRect.topCenter;
|
||||
final Offset centered = target - Offset(sheetRect.width / 2, 0.0);
|
||||
return centered & sheetRect.size;
|
||||
case _ViewableLocation.right:
|
||||
final Offset target = orientation == Orientation.portrait ? childRect.bottomRight : childRect.topRight;
|
||||
return (target - Offset(sheetRect.width, 0.0)) & sheetRect.size;
|
||||
case _ViewableLocation.left:
|
||||
final Offset target = orientation == Orientation.portrait ? childRect.bottomLeft : childRect.topLeft;
|
||||
return target & sheetRect.size;
|
||||
}
|
||||
}
|
||||
|
||||
void _onDismiss(BuildContext context, double scale, double opacity) {
|
||||
_scale = scale;
|
||||
_opacityTween.end = opacity;
|
||||
_sheetOpacity = _opacityTween.animate(CurvedAnimation(
|
||||
parent: animation!,
|
||||
curve: const Interval(0.9, 1.0),
|
||||
));
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _updateTweenRects() {
|
||||
final Rect childRect = _scale == null ? _getRect(_childGlobalKey) : _getScaledRect(_childGlobalKey, _scale!);
|
||||
_rectTween.begin = _previousChildRect;
|
||||
_rectTween.end = childRect;
|
||||
|
||||
final Rect childRectOriginal = Rect.fromCenter(
|
||||
center: _previousChildRect.center,
|
||||
width: _previousChildRect.width / _kOpenScale,
|
||||
height: _previousChildRect.height / _kOpenScale,
|
||||
);
|
||||
|
||||
final Rect sheetRect = _getRect(_sheetGlobalKey);
|
||||
final Rect sheetRectBegin = _getSheetRectBegin(
|
||||
_lastOrientation,
|
||||
_contextMenuLocation,
|
||||
childRectOriginal,
|
||||
sheetRect,
|
||||
);
|
||||
_sheetRectTween.begin = sheetRectBegin;
|
||||
_sheetRectTween.end = sheetRect;
|
||||
_sheetScaleTween.begin = 0.0;
|
||||
_sheetScaleTween.end = _scale;
|
||||
|
||||
_rectTweenReverse.begin = childRectOriginal;
|
||||
_rectTweenReverse.end = childRect;
|
||||
}
|
||||
|
||||
void _setOffstageInternally() {
|
||||
super.offstage = _externalOffstage || _internalOffstage;
|
||||
|
||||
changedInternalState();
|
||||
}
|
||||
|
||||
@override
|
||||
bool didPop(T? result) {
|
||||
_updateTweenRects();
|
||||
return super.didPop(result);
|
||||
}
|
||||
|
||||
@override
|
||||
set offstage(bool value) {
|
||||
_externalOffstage = value;
|
||||
_setOffstageInternally();
|
||||
}
|
||||
|
||||
@override
|
||||
TickerFuture didPush() {
|
||||
_internalOffstage = true;
|
||||
_setOffstageInternally();
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
|
||||
_updateTweenRects();
|
||||
_internalOffstage = false;
|
||||
_setOffstageInternally();
|
||||
});
|
||||
return super.didPush();
|
||||
}
|
||||
|
||||
@override
|
||||
Animation<double> createAnimation() {
|
||||
final Animation<double> animation = super.createAnimation();
|
||||
_sheetOpacity = _opacityTween.animate(CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.linear,
|
||||
));
|
||||
return animation;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||
return OrientationBuilder(
|
||||
builder: (BuildContext context, Orientation orientation) {
|
||||
_lastOrientation = orientation;
|
||||
|
||||
if (!animation.isCompleted) {
|
||||
final bool reverse = animation.status == AnimationStatus.reverse;
|
||||
final Rect rect = reverse ? _rectAnimatableReverse.evaluate(animation)! : _rectAnimatable.evaluate(animation)!;
|
||||
final Rect sheetRect = reverse ? _sheetRectAnimatableReverse.evaluate(animation)! : _sheetRectAnimatable.evaluate(animation)!;
|
||||
final double sheetScale = reverse ? _sheetScaleAnimatableReverse.evaluate(animation) : _sheetScaleAnimatable.evaluate(animation);
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fromRect(
|
||||
rect: sheetRect,
|
||||
child: FadeTransition(
|
||||
opacity: _sheetOpacity,
|
||||
child: Transform.scale(
|
||||
alignment: getSheetAlignment(_contextMenuLocation),
|
||||
scale: sheetScale,
|
||||
child: _ViewableSheet(
|
||||
key: _sheetGlobalKey,
|
||||
actions: _actions,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fromRect(
|
||||
key: _childGlobalKey,
|
||||
rect: rect,
|
||||
child: _builder!(context, animation),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return _ContextMenuRouteStatic(
|
||||
actions: _actions,
|
||||
childGlobalKey: _childGlobalKey,
|
||||
contextMenuLocation: _contextMenuLocation,
|
||||
onDismiss: _onDismiss,
|
||||
orientation: orientation,
|
||||
sheetGlobalKey: _sheetGlobalKey,
|
||||
child: _builder!(context, animation),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContextMenuRouteStatic extends StatefulWidget {
|
||||
const _ContextMenuRouteStatic({
|
||||
this.actions,
|
||||
required this.child,
|
||||
this.childGlobalKey,
|
||||
required this.contextMenuLocation,
|
||||
this.onDismiss,
|
||||
required this.orientation,
|
||||
this.sheetGlobalKey,
|
||||
});
|
||||
|
||||
final List<Widget>? actions;
|
||||
final Widget child;
|
||||
final GlobalKey? childGlobalKey;
|
||||
final _ViewableLocation contextMenuLocation;
|
||||
final _DismissCallback? onDismiss;
|
||||
final Orientation orientation;
|
||||
final GlobalKey? sheetGlobalKey;
|
||||
|
||||
@override
|
||||
_ContextMenuRouteStaticState createState() => _ContextMenuRouteStaticState();
|
||||
}
|
||||
|
||||
class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with TickerProviderStateMixin {
|
||||
static const double _kMinScale = 0.8;
|
||||
|
||||
static const double _kSheetScaleThreshold = 0.9;
|
||||
static const double _kPadding = 20.0;
|
||||
static const double _kDamping = 400.0;
|
||||
static const Duration _kMoveControllerDuration = Duration(milliseconds: 600);
|
||||
|
||||
late Offset _dragOffset;
|
||||
double _lastScale = 1.0;
|
||||
late AnimationController _moveController;
|
||||
late AnimationController _sheetController;
|
||||
late Animation<Offset> _moveAnimation;
|
||||
late Animation<double> _sheetScaleAnimation;
|
||||
late Animation<double> _sheetOpacityAnimation;
|
||||
|
||||
static double _getScale(Orientation orientation, double maxDragDistance, double dy) {
|
||||
final double dyDirectional = dy <= 0.0 ? dy : -dy;
|
||||
return math.max(
|
||||
_kMinScale,
|
||||
(maxDragDistance + dyDirectional) / maxDragDistance,
|
||||
);
|
||||
}
|
||||
|
||||
void _onPanStart(DragStartDetails details) {
|
||||
_moveController.value = 1.0;
|
||||
_setDragOffset(Offset.zero);
|
||||
}
|
||||
|
||||
void _onPanUpdate(DragUpdateDetails details) {
|
||||
_setDragOffset(_dragOffset + details.delta);
|
||||
}
|
||||
|
||||
void _onPanEnd(DragEndDetails details) {
|
||||
if (details.velocity.pixelsPerSecond.dy.abs() >= kMinFlingVelocity) {
|
||||
final bool flingIsAway = details.velocity.pixelsPerSecond.dy > 0;
|
||||
final double finalPosition = flingIsAway ? _moveAnimation.value.dy + 100.0 : 0.0;
|
||||
|
||||
if (flingIsAway && _sheetController.status != AnimationStatus.forward) {
|
||||
_sheetController.forward();
|
||||
} else if (!flingIsAway && _sheetController.status != AnimationStatus.reverse) {
|
||||
_sheetController.reverse();
|
||||
}
|
||||
|
||||
_moveAnimation = Tween<Offset>(
|
||||
begin: Offset(0.0, _moveAnimation.value.dy),
|
||||
end: Offset(0.0, finalPosition),
|
||||
).animate(_moveController);
|
||||
_moveController.reset();
|
||||
_moveController.duration = const Duration(
|
||||
milliseconds: 64,
|
||||
);
|
||||
_moveController.forward();
|
||||
_moveController.addStatusListener(_flingStatusListener);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lastScale == _kMinScale) {
|
||||
widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value);
|
||||
return;
|
||||
}
|
||||
|
||||
_moveController.addListener(_moveListener);
|
||||
_moveController.reverse();
|
||||
}
|
||||
|
||||
void _moveListener() {
|
||||
if (_lastScale > _kSheetScaleThreshold) {
|
||||
_moveController.removeListener(_moveListener);
|
||||
if (_sheetController.status != AnimationStatus.dismissed) {
|
||||
_sheetController.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _flingStatusListener(AnimationStatus status) {
|
||||
if (status != AnimationStatus.completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
_moveController.duration = _kMoveControllerDuration;
|
||||
|
||||
_moveController.removeStatusListener(_flingStatusListener);
|
||||
|
||||
if (_moveAnimation.value.dy == 0.0) {
|
||||
return;
|
||||
}
|
||||
widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value);
|
||||
}
|
||||
|
||||
Alignment _getChildAlignment(Orientation orientation, _ViewableLocation contextMenuLocation) {
|
||||
switch (contextMenuLocation) {
|
||||
case _ViewableLocation.center:
|
||||
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topRight;
|
||||
case _ViewableLocation.right:
|
||||
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topLeft;
|
||||
case _ViewableLocation.left:
|
||||
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topRight;
|
||||
}
|
||||
}
|
||||
|
||||
void _setDragOffset(Offset dragOffset) {
|
||||
final double endX = _kPadding * dragOffset.dx / _kDamping;
|
||||
final double endY = dragOffset.dy >= 0.0 ? dragOffset.dy : _kPadding * dragOffset.dy / _kDamping;
|
||||
setState(() {
|
||||
_dragOffset = dragOffset;
|
||||
_moveAnimation = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: Offset(
|
||||
endX.clamp(-_kPadding, _kPadding),
|
||||
endY,
|
||||
),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _moveController,
|
||||
curve: Curves.elasticIn,
|
||||
),
|
||||
);
|
||||
|
||||
if (_lastScale <= _kSheetScaleThreshold && _sheetController.status != AnimationStatus.forward && _sheetScaleAnimation.value != 0.0) {
|
||||
_sheetController.forward();
|
||||
} else if (_lastScale > _kSheetScaleThreshold && _sheetController.status != AnimationStatus.reverse && _sheetScaleAnimation.value != 1.0) {
|
||||
_sheetController.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> _getChildren(Orientation orientation, _ViewableLocation contextMenuLocation) {
|
||||
final Expanded child = Expanded(
|
||||
child: Align(
|
||||
alignment: _getChildAlignment(
|
||||
widget.orientation,
|
||||
widget.contextMenuLocation,
|
||||
),
|
||||
child: AnimatedBuilder(
|
||||
animation: _moveController,
|
||||
builder: _buildChildAnimation,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
const SizedBox spacer = SizedBox(
|
||||
width: _kPadding,
|
||||
height: _kPadding,
|
||||
);
|
||||
final sheet = AnimatedBuilder(
|
||||
animation: _sheetController,
|
||||
builder: _buildSheetAnimation,
|
||||
child: _ViewableSheet(
|
||||
key: widget.sheetGlobalKey,
|
||||
actions: widget.actions!,
|
||||
),
|
||||
);
|
||||
|
||||
switch (contextMenuLocation) {
|
||||
case _ViewableLocation.center:
|
||||
return <Widget>[child, spacer, sheet];
|
||||
case _ViewableLocation.right:
|
||||
return orientation == Orientation.portrait ? <Widget>[child, spacer, sheet] : <Widget>[sheet, spacer, child];
|
||||
case _ViewableLocation.left:
|
||||
return <Widget>[child, spacer, sheet];
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSheetAnimation(BuildContext context, Widget? child) {
|
||||
return Transform.scale(
|
||||
alignment: _ViewableRoute.getSheetAlignment(widget.contextMenuLocation),
|
||||
scale: _sheetScaleAnimation.value,
|
||||
child: FadeTransition(
|
||||
opacity: _sheetOpacityAnimation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChildAnimation(BuildContext context, Widget? child) {
|
||||
_lastScale = _getScale(
|
||||
widget.orientation,
|
||||
MediaQuery.of(context).size.height,
|
||||
_moveAnimation.value.dy,
|
||||
);
|
||||
return Transform.scale(
|
||||
key: widget.childGlobalKey,
|
||||
scale: _lastScale,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimation(BuildContext context, Widget? child) {
|
||||
return Transform.translate(
|
||||
offset: _moveAnimation.value,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_moveController = AnimationController(
|
||||
duration: _kMoveControllerDuration,
|
||||
value: 1.0,
|
||||
vsync: this,
|
||||
);
|
||||
_sheetController = AnimationController(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
reverseDuration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
_sheetScaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _sheetController,
|
||||
curve: Curves.linear,
|
||||
reverseCurve: Curves.easeInBack,
|
||||
),
|
||||
);
|
||||
_sheetOpacityAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
).animate(_sheetController);
|
||||
_setDragOffset(Offset.zero);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_moveController.dispose();
|
||||
_sheetController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> children = _getChildren(
|
||||
widget.orientation,
|
||||
widget.contextMenuLocation,
|
||||
);
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(_kPadding),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: GestureDetector(
|
||||
onPanEnd: _onPanEnd,
|
||||
onPanStart: _onPanStart,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
child: AnimatedBuilder(
|
||||
animation: _moveController,
|
||||
builder: _buildAnimation,
|
||||
child: widget.orientation == Orientation.portrait
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
)
|
||||
: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ViewableSheet extends StatelessWidget {
|
||||
const _ViewableSheet({
|
||||
super.key,
|
||||
required this.actions,
|
||||
});
|
||||
|
||||
final List<Widget> actions;
|
||||
|
||||
List<Widget> getChildren(BuildContext context) {
|
||||
if (actions.isEmpty) return [];
|
||||
|
||||
final Widget menu = Expanded(
|
||||
child: IntrinsicHeight(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(13.0)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
actions.first,
|
||||
for (Widget action in actions.skip(1))
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: CupertinoDynamicColor.resolve(_borderColor, context),
|
||||
width: 0.5,
|
||||
)),
|
||||
),
|
||||
position: DecorationPosition.foreground,
|
||||
child: action,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return [menu];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: getChildren(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OnOffAnimation<T> extends CompoundAnimation<T> {
|
||||
_OnOffAnimation({
|
||||
required AnimationController controller,
|
||||
required T onValue,
|
||||
required T offValue,
|
||||
required double intervalOn,
|
||||
required double intervalOff,
|
||||
}) : _offValue = offValue,
|
||||
assert(intervalOn >= 0.0 && intervalOn <= 1.0),
|
||||
assert(intervalOff >= 0.0 && intervalOff <= 1.0),
|
||||
assert(intervalOn <= intervalOff),
|
||||
super(
|
||||
first: Tween<T>(begin: offValue, end: onValue).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(intervalOn, intervalOn),
|
||||
),
|
||||
),
|
||||
next: Tween<T>(begin: onValue, end: offValue).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(intervalOff, intervalOff),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final T _offValue;
|
||||
|
||||
@override
|
||||
T get value => next.value == _offValue ? next.value : first.value;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:refilc/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, {super.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)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_kreta_api/models/subject.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence/absence_display.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AbsenceSubjectTile extends StatelessWidget {
|
||||
const AbsenceSubjectTile(this.subject,
|
||||
{super.key,
|
||||
this.percentage = 0.0,
|
||||
this.excused = 0,
|
||||
this.unexcused = 0,
|
||||
this.pending = 0,
|
||||
this.onTap});
|
||||
|
||||
final GradeSubject subject;
|
||||
final void Function()? onTap;
|
||||
final double percentage;
|
||||
|
||||
final int excused;
|
||||
final int unexcused;
|
||||
final int pending;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(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 && settingsProvider.renamedSubjectsItalics
|
||||
? 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(
|
||||
opacity: 0,
|
||||
child: Text("100%",
|
||||
style: TextStyle(fontFamily: "monospace"))),
|
||||
Text(
|
||||
"${percentage.round()}%",
|
||||
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);
|
||||
}
|
||||
151
refilc_mobile_ui/lib/common/widgets/absence/absence_tile.dart
Normal file
151
refilc_mobile_ui/lib/common/widgets/absence/absence_tile.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_kreta_api/models/absence.dart';
|
||||
import 'package:refilc_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 'package:provider/provider.dart';
|
||||
import 'absence_tile.i18n.dart';
|
||||
|
||||
class AbsenceTile extends StatelessWidget {
|
||||
const AbsenceTile(this.absence,
|
||||
{super.key, this.onTap, this.elevation = 0.0, this.padding});
|
||||
|
||||
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;
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
|
||||
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: 0.0,
|
||||
// )),
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
dense: group,
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 15.5, right: 12.0, top: 2.0, bottom: 2.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(!group ? 14.0 : 12.0)),
|
||||
leading: Container(
|
||||
width: 39,
|
||||
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 &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? 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 &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"excused": "excused %s",
|
||||
"pending": "%s to be excused",
|
||||
"unexcused": "unexcused %s",
|
||||
"absence": "absence",
|
||||
"delay": "delay",
|
||||
"minute": " minutes of ".one(" minute of "),
|
||||
},
|
||||
"hu_hu": {
|
||||
"excused": "igazolt %s",
|
||||
"pending": "igazolandó %s",
|
||||
"unexcused": "igazolatlan %s",
|
||||
"absence": "hiányzás",
|
||||
"delay": "késés",
|
||||
"minute": " perc ",
|
||||
},
|
||||
"de_de": {
|
||||
"excused": "anerkannt %s",
|
||||
"pending": "%s zu anerkennen",
|
||||
"unexcused": "unanerkannt %s",
|
||||
"absence": "Abwesenheit",
|
||||
"delay": "Verspätung",
|
||||
"minute": " Minuten ".one(" Minute "),
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
155
refilc_mobile_ui/lib/common/widgets/absence/absence_view.dart
Normal file
155
refilc_mobile_ui/lib/common/widgets/absence/absence_view.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
// ignore_for_file: empty_catches
|
||||
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/absence.dart';
|
||||
import 'package:refilc_mobile_ui/common/bottom_card.dart';
|
||||
import 'package:refilc_mobile_ui/common/custom_snack_bar.dart';
|
||||
import 'package:refilc_mobile_ui/common/detail.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel_action_button.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence/absence_tile.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_mobile_ui/pages/timetable/timetable_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:refilc/utils/reverse_search.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'absence_view.i18n.dart';
|
||||
|
||||
class AbsenceView extends StatelessWidget {
|
||||
const AbsenceView(this.absence,
|
||||
{super.key, this.outsideContext, this.viewable = false});
|
||||
|
||||
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);
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(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 &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? FontStyle.italic
|
||||
: null),
|
||||
),
|
||||
subtitle: Text(
|
||||
(absence.teacher.isRenamed
|
||||
? absence.teacher.renamedTo
|
||||
: absence.teacher.name) ??
|
||||
'',
|
||||
// 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} ${"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: () {
|
||||
// https://discord.com/channels/1111649116020285532/1149964760130002945
|
||||
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,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"Lesson": "Lesson",
|
||||
"Excuse": "Excuse",
|
||||
"Mode": "Mode",
|
||||
"Submit date": "Submit Date",
|
||||
"show in timetable": "Show in timetable",
|
||||
"minutes": "minutes".one("minute"),
|
||||
"delay": "Delay",
|
||||
},
|
||||
"hu_hu": {
|
||||
"Lesson": "Óra",
|
||||
"Excuse": "Igazolás",
|
||||
"Mode": "Típus",
|
||||
"Submit date": "Rögzítés dátuma",
|
||||
"show in timetable": "Megtekintés az órarendben",
|
||||
"minutes": "perc",
|
||||
"delay": "Késés",
|
||||
},
|
||||
"de_de": {
|
||||
"Lesson": "Stunde",
|
||||
"Excuse": "Anerkannt",
|
||||
"Mode": "Typ",
|
||||
"Submit date": "Datum einreichen",
|
||||
"show in timetable": "im Stundenplan anzeigen",
|
||||
"minutes": "Minuten".one("Minute"),
|
||||
"delay": "Verspätung",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/absence.dart';
|
||||
import 'package:refilc_mobile_ui/common/custom_snack_bar.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel_button.dart';
|
||||
import 'package:refilc_mobile_ui/common/viewable.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence/absence_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence/absence_view.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence_group/absence_group_container.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/card_handle.dart';
|
||||
import 'package:refilc_mobile_ui/pages/absences/absence_subject_view_container.dart';
|
||||
import 'package:refilc_mobile_ui/pages/timetable/timetable_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/utils/reverse_search.dart';
|
||||
|
||||
import 'absence_view.i18n.dart';
|
||||
|
||||
class AbsenceViewable extends StatelessWidget {
|
||||
const AbsenceViewable(this.absence, {super.key, this.padding});
|
||||
|
||||
final Absence absence;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subject = AbsenceSubjectViewContainer.of(context) != null;
|
||||
final group = AbsenceGroupContainer.of(context) != null;
|
||||
final tile = AbsenceTile(absence, padding: padding);
|
||||
|
||||
return Viewable(
|
||||
tile: group ? AbsenceGroupContainer(child: tile) : tile,
|
||||
view: CardHandle(child: AbsenceView(absence, viewable: true)),
|
||||
actions: [
|
||||
PanelButton(
|
||||
background: true,
|
||||
title: Text(
|
||||
"show in timetable".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
|
||||
if (subject) {
|
||||
Future.delayed(const Duration(milliseconds: 250)).then((_) {
|
||||
Navigator.of(context, rootNavigator: true).pop(absence);
|
||||
});
|
||||
} else {
|
||||
Future.delayed(const Duration(milliseconds: 250)).then((_) {
|
||||
ReverseSearch.getLessonByAbsence(absence, context)
|
||||
.then((lesson) {
|
||||
if (lesson != null) {
|
||||
TimetablePage.jump(context, lesson: lesson);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
|
||||
content: Text("Cannot find lesson".i18n,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
backgroundColor: AppColors.of(context).red,
|
||||
context: context,
|
||||
));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AbsenceGroupContainer extends InheritedWidget {
|
||||
const AbsenceGroupContainer({super.key, required super.child});
|
||||
|
||||
static AbsenceGroupContainer? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<AbsenceGroupContainer>();
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(AbsenceGroupContainer oldWidget) => false;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/absence.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence/absence_viewable.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence_group/absence_group_container.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence/absence_tile.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:rounded_expansion_tile/rounded_expansion_tile.dart';
|
||||
import 'absence_group_tile.i18n.dart';
|
||||
|
||||
class AbsenceGroupTile extends StatelessWidget {
|
||||
const AbsenceGroupTile(this.absences,
|
||||
{super.key, this.showDate = false, this.padding});
|
||||
|
||||
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 ?? EdgeInsets.zero,
|
||||
child: AbsenceGroupContainer(
|
||||
child: RoundedExpansionTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 0),
|
||||
tileColor: Colors.transparent,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
trailingDuration: 0.5,
|
||||
trailing: const Icon(FeatherIcons.chevronDown),
|
||||
leading: Container(
|
||||
width: 39.0,
|
||||
height: 39.0,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color.withOpacity(.25),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(AbsenceTile.justificationIcon(state),
|
||||
color: color)),
|
||||
),
|
||||
title: Text.rich(TextSpan(
|
||||
text:
|
||||
"${absences.where((a) => a.absence.state == state).length} ",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.of(context).text),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: AbsenceTile.justificationName(state)
|
||||
.fill(["absence".i18n]),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.of(context).text),
|
||||
),
|
||||
],
|
||||
)),
|
||||
subtitle: showDate
|
||||
? Text(
|
||||
absences.first.absence.date
|
||||
.format(context, weekday: true),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.of(context).text.withOpacity(0.8)),
|
||||
)
|
||||
: null,
|
||||
children: absences,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Justification getState(List<Justification> states) {
|
||||
Justification state;
|
||||
|
||||
if (states.any((element) => element == Justification.unexcused)) {
|
||||
state = Justification.unexcused;
|
||||
} else if (states.any((element) => element == Justification.pending)) {
|
||||
state = Justification.pending;
|
||||
} else {
|
||||
state = Justification.excused;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"absence": "absences",
|
||||
},
|
||||
"hu_hu": {
|
||||
"absence": "hiányzás",
|
||||
},
|
||||
"de_de": {
|
||||
"absence": "Fehlen",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
53
refilc_mobile_ui/lib/common/widgets/ad/ad_tile.dart
Normal file
53
refilc_mobile_ui/lib/common/widgets/ad/ad_tile.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:refilc/models/ad.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel_button.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
|
||||
class AdTile extends StatelessWidget {
|
||||
const AdTile(this.ad, {super.key, this.onTap, this.padding});
|
||||
|
||||
final Ad ad;
|
||||
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.only(left: 8.0, right: 16.0),
|
||||
onPressed: onTap,
|
||||
title: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
ad.title,
|
||||
),
|
||||
Text(
|
||||
ad.description,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.of(context).text.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: ad.logoUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
child: Image.network(
|
||||
ad.logoUrl.toString(),
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
ad.logoUrl = null;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
)
|
||||
: null,
|
||||
trailing: const Icon(FeatherIcons.externalLink),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
22
refilc_mobile_ui/lib/common/widgets/ad/ad_viewable.dart
Normal file
22
refilc_mobile_ui/lib/common/widgets/ad/ad_viewable.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:refilc/models/ad.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'ad_tile.dart';
|
||||
|
||||
class AdViewable extends StatelessWidget {
|
||||
const AdViewable(this.ad, {super.key});
|
||||
|
||||
final Ad ad;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AdTile(
|
||||
ad,
|
||||
onTap: () => launchUrl(
|
||||
ad.launchUrl,
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
27
refilc_mobile_ui/lib/common/widgets/card_handle.dart
Normal file
27
refilc_mobile_ui/lib/common/widgets/card_handle.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CardHandle extends StatelessWidget {
|
||||
const CardHandle({super.key, this.child});
|
||||
|
||||
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!,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:refilc/helpers/average_helper.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/cretification/certification_view.dart';
|
||||
import 'package:refilc/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,
|
||||
{super.key, required this.gradeType, this.padding});
|
||||
|
||||
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: 6.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
gradient: LinearGradient(
|
||||
colors: [color, color.withOpacity(.75)],
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0)),
|
||||
leading: Text(
|
||||
averageText,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24.0,
|
||||
),
|
||||
),
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
text: title,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: " • ${grades.length}",
|
||||
style: TextStyle(
|
||||
color: textColor.withOpacity(.75),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
trailing: Icon(FeatherIcons.arrowRight, color: textColor),
|
||||
onTap: () => CertificationView.show(grades,
|
||||
context: context, gradeType: gradeType),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String getGradeTypeTitle(GradeType gradeType) {
|
||||
String title;
|
||||
|
||||
switch (gradeType) {
|
||||
case GradeType.halfYear:
|
||||
title = "mid".i18n;
|
||||
break;
|
||||
case GradeType.firstQ:
|
||||
title = "1q".i18n;
|
||||
break;
|
||||
case GradeType.secondQ:
|
||||
title = "2q".i18n;
|
||||
break;
|
||||
case GradeType.thirdQ:
|
||||
title = "3q".i18n;
|
||||
break;
|
||||
case GradeType.fourthQ:
|
||||
title = "4q".i18n;
|
||||
break;
|
||||
default:
|
||||
title = "final".i18n;
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"final": "Final grades",
|
||||
"mid": "Midterm grades",
|
||||
"1q": "1. Quarter grades",
|
||||
"2q": "2. Quarter grades",
|
||||
"3q": "3. Quarter grades",
|
||||
"4q": "4. Quarter grades",
|
||||
},
|
||||
"hu_hu": {
|
||||
"final": "Év végi jegyek",
|
||||
"mid": "Félévi jegyek",
|
||||
"1q": "1. Negyedéves jegyek",
|
||||
"2q": "2. Negyedéves jegyek",
|
||||
"3q": "3. Negyedéves jegyek",
|
||||
"4q": "4. Negyedéves jegyek",
|
||||
},
|
||||
"de_de": {
|
||||
"final": "Zeugnis Noten",
|
||||
"mid": "Halbjährlich Noten",
|
||||
"1q": "1. Quartal Noten",
|
||||
"2q": "2. Quartal Noten",
|
||||
"3q": "3. Quartal Noten",
|
||||
"4q": "4. Quartal Noten",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/subject_grades_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'certification_tile.i18n.dart';
|
||||
|
||||
class CertificationTile extends StatelessWidget {
|
||||
const CertificationTile(this.grade, {super.key, this.onTap, this.padding});
|
||||
|
||||
final Function()? onTap;
|
||||
final Grade grade;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isSubjectView = SubjectGradesContainer.of(context) != null;
|
||||
String certificationName;
|
||||
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
|
||||
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 &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? FontStyle.italic
|
||||
: null)),
|
||||
subtitle: Text(grade.value.valueName,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"final": "Final",
|
||||
"mid": "Mid year",
|
||||
"1q": "1. Quarter",
|
||||
"2q": "2. Quarter",
|
||||
"3q": "3. Quarter",
|
||||
"4q": "4. Quarter",
|
||||
"equivalency": "Equivalency test",
|
||||
"unknown": "Unknown",
|
||||
"classavg": "Class Average",
|
||||
},
|
||||
"hu_hu": {
|
||||
"final": "Év vége",
|
||||
"mid": "Félév",
|
||||
"1q": "1. Negyedév",
|
||||
"2q": "2. Negyedév",
|
||||
"3q": "3. Negyedév",
|
||||
"4q": "4. Negyedév",
|
||||
"equivalency": "Osztályozó",
|
||||
"unknown": "Ismeretlen",
|
||||
"classavg": "Osztályátlag",
|
||||
},
|
||||
"de_de": {
|
||||
"final": "Zeugnis",
|
||||
"mid": "Halbjährlich",
|
||||
"1q": "1. Quartal",
|
||||
"2q": "2. Quartal",
|
||||
"3q": "3. Quartal",
|
||||
"4q": "4. Quartal",
|
||||
"equivalency": "Zulassungsprüfung",
|
||||
"unknown": "Unbekannt",
|
||||
"classavg": "Klassendurchschnitt",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/cretification/certification_card.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/cretification/certification_tile.dart';
|
||||
import 'package:refilc_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, {super.key, required this.gradeType});
|
||||
|
||||
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(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
SafeArea(
|
||||
child: Panel(
|
||||
child: Column(
|
||||
children: tiles,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
60
refilc_mobile_ui/lib/common/widgets/custom_switch.dart
Normal file
60
refilc_mobile_ui/lib/common/widgets/custom_switch.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomSwitch extends StatelessWidget {
|
||||
final ValueChanged<bool> onChanged;
|
||||
final bool value;
|
||||
|
||||
const CustomSwitch({
|
||||
super.key,
|
||||
required this.onChanged,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
53
refilc_mobile_ui/lib/common/widgets/event/event_tile.dart
Normal file
53
refilc_mobile_ui/lib/common/widgets/event/event_tile.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:refilc_kreta_api/models/event.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_mobile_ui/common/profile_image/profile_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EventTile extends StatelessWidget {
|
||||
const EventTile(this.event, {super.key, this.onTap, this.padding});
|
||||
|
||||
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: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
),
|
||||
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: 19.2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
58
refilc_mobile_ui/lib/common/widgets/event/event_view.dart
Normal file
58
refilc_mobile_ui/lib/common/widgets/event/event_view.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:refilc_kreta_api/models/event.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_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, {super.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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:refilc_kreta_api/models/event.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/event/event_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/event/event_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EventViewable extends StatelessWidget {
|
||||
const EventViewable(this.event, {super.key});
|
||||
|
||||
final Event event;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EventTile(
|
||||
event,
|
||||
onTap: () => EventView.show(event, context: context),
|
||||
);
|
||||
}
|
||||
}
|
||||
70
refilc_mobile_ui/lib/common/widgets/exam/exam_tile.dart
Normal file
70
refilc_mobile_ui/lib/common/widgets/exam/exam_tile.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/exam.dart';
|
||||
import 'package:refilc_mobile_ui/common/round_border_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
|
||||
class ExamTile extends StatelessWidget {
|
||||
const ExamTile(this.exam,
|
||||
{super.key, this.onTap, this.padding, this.showSubject = true});
|
||||
|
||||
final Exam exam;
|
||||
final void Function()? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final bool showSubject;
|
||||
|
||||
@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: 10.0),
|
||||
onTap: onTap,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
|
||||
leading: RoundBorderIcon(
|
||||
icon: Icon(
|
||||
Icons.edit_document,
|
||||
size: showSubject ? 24.0 : 22.0,
|
||||
weight: 2.5,
|
||||
),
|
||||
padding: showSubject ? 6.0 : 5.0,
|
||||
width: 1.0,
|
||||
),
|
||||
title: Text(
|
||||
showSubject
|
||||
? exam.mode?.description ?? 'Számonkérés'
|
||||
: (exam.description != ""
|
||||
? exam.description
|
||||
: (exam.mode?.description ?? "Számonkérés")),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(
|
||||
showSubject
|
||||
? (exam.subject.isRenamed
|
||||
? exam.subject.renamedTo!
|
||||
: exam.subject.name.capital())
|
||||
: exam.mode?.description ?? 'Számonkérés',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14.0),
|
||||
),
|
||||
trailing: showSubject
|
||||
? Icon(
|
||||
SubjectIcon.resolveVariant(
|
||||
context: context, subject: exam.subject),
|
||||
color: AppColors.of(context).text.withOpacity(.5),
|
||||
)
|
||||
: null,
|
||||
minLeadingWidth: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
76
refilc_mobile_ui/lib/common/widgets/exam/exam_view.dart
Normal file
76
refilc_mobile_ui/lib/common/widgets/exam/exam_view.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/exam.dart';
|
||||
import 'package:refilc_mobile_ui/common/bottom_card.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_mobile_ui/common/detail.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'exam_view.i18n.dart';
|
||||
|
||||
class ExamView extends StatelessWidget {
|
||||
const ExamView(this.exam, {super.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(
|
||||
subject: exam.subject, context: context),
|
||||
size: 36.0,
|
||||
color: AppColors.of(context).text.withOpacity(.75),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
exam.subject.isRenamed
|
||||
? exam.subject.renamedTo!
|
||||
: exam.subject.name.capital(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(
|
||||
(exam.teacher.isRenamed
|
||||
? exam.teacher.renamedTo
|
||||
: exam.teacher.name) ??
|
||||
'',
|
||||
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,
|
||||
maxLines: 5),
|
||||
if (exam.mode != null)
|
||||
Detail(title: "mode".i18n, description: exam.mode!.description),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
27
refilc_mobile_ui/lib/common/widgets/exam/exam_view.i18n.dart
Normal file
27
refilc_mobile_ui/lib/common/widgets/exam/exam_view.i18n.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"date": "Date",
|
||||
"description": "Description",
|
||||
"mode": "Type",
|
||||
},
|
||||
"hu_hu": {
|
||||
"date": "Írás ideje",
|
||||
"description": "Leírás",
|
||||
"mode": "Típus",
|
||||
},
|
||||
"de_de": {
|
||||
"date": "Prüfungszeit",
|
||||
"description": "Bezeichnung",
|
||||
"mode": "Typ",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
27
refilc_mobile_ui/lib/common/widgets/exam/exam_viewable.dart
Normal file
27
refilc_mobile_ui/lib/common/widgets/exam/exam_viewable.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:refilc_kreta_api/models/exam.dart';
|
||||
import 'package:refilc_mobile_ui/common/viewable.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/card_handle.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/exam/exam_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/exam/exam_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ExamViewable extends StatelessWidget {
|
||||
const ExamViewable(this.exam,
|
||||
{super.key, this.showSubject = true, this.tilePadding});
|
||||
|
||||
final Exam exam;
|
||||
final bool showSubject;
|
||||
final EdgeInsetsGeometry? tilePadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Viewable(
|
||||
tile: ExamTile(
|
||||
exam,
|
||||
showSubject: showSubject,
|
||||
padding: tilePadding,
|
||||
),
|
||||
view: CardHandle(child: ExamView(exam)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_kreta_api/models/subject.dart';
|
||||
import 'package:refilc_mobile_ui/common/average_display.dart';
|
||||
import 'package:refilc_mobile_ui/common/round_border_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class GradeSubjectTile extends StatelessWidget {
|
||||
const GradeSubjectTile(this.subject,
|
||||
{super.key,
|
||||
this.average = 0.0,
|
||||
this.groupAverage = 0.0,
|
||||
this.onTap,
|
||||
this.averageBefore = 0.0});
|
||||
|
||||
final GradeSubject 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;
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
|
||||
// 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: RoundBorderIcon(
|
||||
icon: Icon(
|
||||
SubjectIcon.resolveVariant(
|
||||
context: context,
|
||||
subject: subject,
|
||||
),
|
||||
size: 22.0,
|
||||
weight: 2.5,
|
||||
),
|
||||
padding: 5.0,
|
||||
width: 1.0,
|
||||
),
|
||||
title: Text(
|
||||
subject.renamedTo ?? subject.name.capital(),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16.0,
|
||||
color: textColor,
|
||||
fontStyle:
|
||||
settingsProvider.renamedSubjectsItalics && 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)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
80
refilc_mobile_ui/lib/common/widgets/grade/grade_view.dart
Normal file
80
refilc_mobile_ui/lib/common/widgets/grade/grade_view.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_mobile_ui/common/bottom_card.dart';
|
||||
import 'package:refilc_mobile_ui/common/detail.dart';
|
||||
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
|
||||
import 'package:refilc/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, {super.key});
|
||||
|
||||
static show(Grade grade, {required BuildContext context}) =>
|
||||
showBottomCard(context: context, child: GradeView(grade));
|
||||
|
||||
final Grade grade;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(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 &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? FontStyle.italic
|
||||
: null),
|
||||
),
|
||||
subtitle: Text(
|
||||
!Provider.of<SettingsProvider>(context, listen: false)
|
||||
.presentationMode
|
||||
? (grade.teacher.isRenamed
|
||||
? grade.teacher.renamedTo
|
||||
: grade.teacher.name) ??
|
||||
''
|
||||
: "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}%"
|
||||
: "";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"value": "Value",
|
||||
"date": "Date",
|
||||
"description": "Description",
|
||||
"mode": "Type",
|
||||
},
|
||||
"hu_hu": {
|
||||
"value": "Érték",
|
||||
"date": "Írás ideje",
|
||||
"description": "Leírás",
|
||||
"mode": "Típus",
|
||||
},
|
||||
"de_de": {
|
||||
"value": "Notenwert",
|
||||
"date": "Prüfungszeit",
|
||||
"description": "Bezeichnung",
|
||||
"mode": "Typ",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/card_handle.dart';
|
||||
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/grade/grade_view.dart';
|
||||
import 'package:refilc_mobile_ui/common/viewable.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/subject_grades_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GradeViewable extends StatelessWidget {
|
||||
const GradeViewable(this.grade, {super.key, this.padding});
|
||||
|
||||
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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
164
refilc_mobile_ui/lib/common/widgets/grade/new_grades.dart
Normal file
164
refilc_mobile_ui/lib/common/widgets/grade/new_grades.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc_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, {super.key, this.censored = false});
|
||||
|
||||
final List<Grade> grades;
|
||||
final bool censored;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0, right: 4.0, top: 1.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(
|
||||
grades.length == 1 ? "new_grade".i18n : "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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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_grade": "New grade",
|
||||
"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_grade": "Új jegy",
|
||||
"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_grade": "Neue Note",
|
||||
"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);
|
||||
}
|
||||
509
refilc_mobile_ui/lib/common/widgets/grade/surprise_grade.dart
Normal file
509
refilc_mobile_ui/lib/common/widgets/grade/surprise_grade.dart
Normal file
@@ -0,0 +1,509 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:animated_background/animated_background.dart' as bg;
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_mobile_ui/pages/home/particle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:rive/rive.dart' as rive;
|
||||
|
||||
import 'new_grades.i18n.dart';
|
||||
|
||||
class SurpriseGrade extends StatefulWidget {
|
||||
const SurpriseGrade(this.grade, {super.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;
|
||||
|
||||
late SettingsProvider settingsProvider;
|
||||
|
||||
List<String> defaultRarities = [
|
||||
"common",
|
||||
"uncommon",
|
||||
"rare",
|
||||
"epic",
|
||||
"legendary",
|
||||
];
|
||||
Map<String, String> rarities = {};
|
||||
|
||||
@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);
|
||||
});
|
||||
});
|
||||
|
||||
_fetchRarities();
|
||||
});
|
||||
|
||||
seed = Random().nextInt(100000000);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_revealAnimFade.dispose();
|
||||
_revealAnimScale.dispose();
|
||||
_revealAnimGrade.dispose();
|
||||
_revealAnimParticle.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_fetchRarities() async {
|
||||
rarities = await Provider.of<DatabaseProvider>(context, listen: false)
|
||||
.userQuery
|
||||
.getGradeRarities(
|
||||
userId: Provider.of<UserProvider>(context, listen: false).id!);
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
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) {
|
||||
settingsProvider = Provider.of<SettingsProvider>(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 &&
|
||||
settingsProvider
|
||||
.renamedSubjectsItalics
|
||||
? 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;
|
||||
}
|
||||
|
||||
String? rr =
|
||||
rarities[widget.grade.value.value.toString()];
|
||||
rr ??= '';
|
||||
|
||||
if (rr.replaceAll(' ', '') == '') {
|
||||
rr = defaultRarities[widget.grade.value.value - 1].i18n;
|
||||
}
|
||||
|
||||
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(
|
||||
rr,
|
||||
style: TextStyle(
|
||||
fontSize: 46.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: gradeColor(
|
||||
context: context,
|
||||
value: widget.grade.value.value),
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: gradeColor(
|
||||
context: context,
|
||||
value:
|
||||
widget.grade.value.value)
|
||||
.withOpacity(.5),
|
||||
blurRadius: 24.0,
|
||||
),
|
||||
Shadow(
|
||||
color: gradeColor(
|
||||
context: context,
|
||||
value:
|
||||
widget.grade.value.value)
|
||||
.withOpacity(.3),
|
||||
offset: const Offset(-3, -3),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32.0),
|
||||
ScaleTransition(
|
||||
scale: CurvedAnimation(
|
||||
curve: Curves.easeInOutBack,
|
||||
parent: _revealAnimParticle
|
||||
.drive(Tween(begin: 0.6, end: 1.0))),
|
||||
child: CustomPaint(
|
||||
painter: PimpPainter(
|
||||
particle: Sprinkles(),
|
||||
controller: _revealAnimParticle,
|
||||
seed: seed + 1,
|
||||
shouldPaint: shouldPaint,
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: PimpPainter(
|
||||
particle: Sprinkles(),
|
||||
controller: _revealAnimParticle,
|
||||
seed: seed,
|
||||
shouldPaint: shouldPaint,
|
||||
),
|
||||
child: RotationTransition(
|
||||
turns: CurvedAnimation(
|
||||
curve: Curves.easeInBack,
|
||||
parent: _revealAnimGrade)
|
||||
.drive(
|
||||
Tween(begin: 0.95, end: 1.0)),
|
||||
child: GradeValueWidget(
|
||||
widget.grade.value,
|
||||
fill: true,
|
||||
contrast: true,
|
||||
shadow: true,
|
||||
outline: true,
|
||||
size: 100.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PimpPainter extends CustomPainter {
|
||||
PimpPainter(
|
||||
{required this.particle,
|
||||
required this.seed,
|
||||
required this.controller,
|
||||
required this.shouldPaint})
|
||||
: super(repaint: controller);
|
||||
|
||||
final Particle particle;
|
||||
final int seed;
|
||||
final AnimationController controller;
|
||||
final bool shouldPaint;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (shouldPaint) {
|
||||
canvas.translate(size.width / 2, size.height / 2);
|
||||
particle.paint(canvas, size, controller.value, seed);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(PimpPainter oldDelegate) => shouldPaint;
|
||||
}
|
||||
|
||||
Color randomColor(int c) {
|
||||
c = c % 5;
|
||||
if (c == 0) return Colors.red.shade300;
|
||||
if (c == 1) return Colors.green.shade300;
|
||||
if (c == 2) return Colors.orange.shade300;
|
||||
if (c == 3) return Colors.blue.shade300;
|
||||
if (c == 4) return Colors.pink.shade300;
|
||||
if (c == 5) return Colors.brown.shade300;
|
||||
return Colors.black;
|
||||
}
|
||||
|
||||
class Sprinkles extends Particle {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, progress, seed) {
|
||||
Random random = Random(seed);
|
||||
int randomMirrorOffset = random.nextInt(8) + 1;
|
||||
CompositeParticle(children: [
|
||||
Firework(),
|
||||
RectangleMirror.builder(
|
||||
numberOfParticles: 6,
|
||||
particleBuilder: (n) {
|
||||
return AnimatedPositionedParticle(
|
||||
begin: const Offset(0.0, -10.0),
|
||||
end: const Offset(0.0, -60.0),
|
||||
child:
|
||||
FadingRect(width: 5.0, height: 15.0, color: randomColor(n)),
|
||||
);
|
||||
},
|
||||
initialDistance: -pi / randomMirrorOffset),
|
||||
]).paint(canvas, size, progress, seed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'dart:io';
|
||||
import 'package:refilc/helpers/attachment_helper.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_mobile_ui/common/custom_snack_bar.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/message/image_view.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
|
||||
import 'package:refilc_kreta_api/models/homework.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'homework_attachment_tile.i18n.dart';
|
||||
|
||||
class HomeworkAttachmentTile extends StatelessWidget {
|
||||
const HomeworkAttachmentTile(this.attachment, {super.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!),
|
||||
));
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Ink.image(
|
||||
image: FileImage(File(snapshot.data ?? "")),
|
||||
height: 200.0,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.isImage) return buildImage(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
onTap: () {
|
||||
attachment.open(context).then((value) {
|
||||
if (!value) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
|
||||
context: context,
|
||||
content: Text("Failed to open attachment".i18n),
|
||||
backgroundColor: AppColors.of(context).red,
|
||||
duration: const Duration(seconds: 1),
|
||||
));
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(children: [
|
||||
const Icon(FeatherIcons.paperclip),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text(attachment.name,
|
||||
maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"Failed to open attachment": "Failed to open attachment",
|
||||
},
|
||||
"hu_hu": {
|
||||
"Failed to open attachment": "Nem sikerült megnyitni a mellékletet",
|
||||
},
|
||||
"de_de": {
|
||||
"Failed to open attachment": "Anhang konnte nicht geöffnet werden",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
115
refilc_mobile_ui/lib/common/widgets/homework/homework_tile.dart
Normal file
115
refilc_mobile_ui/lib/common/widgets/homework/homework_tile.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/homework.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class HomeworkTile extends StatelessWidget {
|
||||
const HomeworkTile(
|
||||
this.homework, {
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.padding,
|
||||
this.censored = false,
|
||||
});
|
||||
|
||||
final Homework homework;
|
||||
final void Function()? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final bool censored;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
|
||||
return 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(
|
||||
subject: homework.subject, 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.subject.renamedTo ?? homework.subject.name.capital(),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontStyle: homework.subject.isRenamed &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? FontStyle.italic
|
||||
: null),
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
104
refilc_mobile_ui/lib/common/widgets/homework/homework_view.dart
Normal file
104
refilc_mobile_ui/lib/common/widgets/homework/homework_view.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc_kreta_api/models/homework.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_mobile_ui/common/detail.dart';
|
||||
import 'package:refilc_mobile_ui/common/sliding_bottom_sheet.dart';
|
||||
import 'package:refilc_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 'package:provider/provider.dart';
|
||||
import 'homework_view.i18n.dart';
|
||||
|
||||
class HomeworkView extends StatelessWidget {
|
||||
const HomeworkView(this.homework, {super.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 = [];
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
|
||||
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(
|
||||
subject: homework.subject, context: context),
|
||||
size: 36.0,
|
||||
),
|
||||
title: Text(
|
||||
homework.subject.renamedTo ?? homework.subject.name.capital(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontStyle: homework.subject.isRenamed &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? FontStyle.italic
|
||||
: null),
|
||||
),
|
||||
subtitle: Text(
|
||||
(homework.teacher.isRenamed
|
||||
? homework.teacher.renamedTo
|
||||
: homework.teacher.name) ??
|
||||
'',
|
||||
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,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"deadline": "Deadline",
|
||||
},
|
||||
"hu_hu": {
|
||||
"deadline": "Határidő",
|
||||
},
|
||||
"de_de": {
|
||||
"deadline": "Termin",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:refilc_kreta_api/models/homework.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/homework/homework_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/homework/homework_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HomeworkViewable extends StatelessWidget {
|
||||
const HomeworkViewable(this.homework, {super.key});
|
||||
|
||||
final Homework homework;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HomeworkTile(
|
||||
homework,
|
||||
onTap: () => HomeworkView.show(homework, context: context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'changed_lesson_tile.i18n.dart';
|
||||
|
||||
class ChangedLessonTile extends StatelessWidget {
|
||||
const ChangedLessonTile(this.lesson, {super.key, this.onTap, this.padding});
|
||||
|
||||
final Lesson lesson;
|
||||
final void Function()? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(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?.name != '') {
|
||||
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.status?.name == "Elmaradt" &&
|
||||
lesson.substituteTeacher?.name != ""
|
||||
? "cancelled".i18n
|
||||
: "substituted".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 &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? FontStyle.italic
|
||||
: null),
|
||||
),
|
||||
trailing: const Icon(FeatherIcons.arrowRight),
|
||||
minLeadingWidth: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"cancelled": "Cancelled lesson",
|
||||
"substituted": "Substituted lesson",
|
||||
},
|
||||
"hu_hu": {
|
||||
"cancelled": "Elmaradó óra",
|
||||
"substituted": "Helyettesített óra",
|
||||
},
|
||||
"de_de": {
|
||||
"cancelled": "Abgesagte Stunde",
|
||||
"substituted": "Vertretene Stunden",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/lesson/changed_lesson_tile.dart';
|
||||
import 'package:refilc_mobile_ui/pages/timetable/timetable_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChangedLessonViewable extends StatelessWidget {
|
||||
const ChangedLessonViewable(this.lesson, {super.key});
|
||||
|
||||
final Lesson lesson;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangedLessonTile(
|
||||
lesson,
|
||||
onTap: () => TimetablePage.jump(context, lesson: lesson),
|
||||
);
|
||||
}
|
||||
}
|
||||
120
refilc_mobile_ui/lib/common/widgets/lesson/lesson_view.dart
Normal file
120
refilc_mobile_ui/lib/common/widgets/lesson/lesson_view.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_mobile_ui/common/bottom_card.dart';
|
||||
import 'package:refilc_mobile_ui/common/detail.dart';
|
||||
import 'package:refilc_mobile_ui/common/round_border_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'lesson_view.i18n.dart';
|
||||
|
||||
class LessonView extends StatelessWidget {
|
||||
const LessonView(this.lesson, {super.key});
|
||||
|
||||
final Lesson lesson;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color accent = AppColors.of(context).text;
|
||||
String lessonIndexTrailing = "";
|
||||
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
|
||||
if (RegExp(r'\d').hasMatch(lesson.lessonIndex)) lessonIndexTrailing = ".";
|
||||
|
||||
if (lesson.substituteTeacher != null &&
|
||||
lesson.substituteTeacher?.name != "") {
|
||||
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: RoundBorderIcon(
|
||||
color: accent,
|
||||
width: 1.0,
|
||||
icon: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 3.0),
|
||||
child: Text(
|
||||
lesson.lessonIndex + lessonIndexTrailing,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 17.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
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 &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? FontStyle.italic
|
||||
: null),
|
||||
),
|
||||
subtitle: Text(
|
||||
((lesson.substituteTeacher == null ||
|
||||
lesson.substituteTeacher!.name == "")
|
||||
? (lesson.teacher.isRenamed
|
||||
? lesson.teacher.renamedTo
|
||||
: lesson.teacher.name)
|
||||
: (lesson.substituteTeacher!.isRenamed
|
||||
? lesson.substituteTeacher!.renamedTo
|
||||
: lesson.substituteTeacher!.name)) ??
|
||||
'',
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"Room": "Room",
|
||||
"Description": "Description",
|
||||
"Lesson Number": "Lesson Number",
|
||||
"Group": "Group",
|
||||
},
|
||||
"hu_hu": {
|
||||
"Room": "Terem",
|
||||
"Description": "Leírás",
|
||||
"Lesson Number": "Éves óraszám",
|
||||
"Group": "Csoport",
|
||||
},
|
||||
"de_de": {
|
||||
"Room": "Raum",
|
||||
"Description": "Bezeichnung",
|
||||
"Lesson Number": "Ordinalzahl",
|
||||
"Group": "Gruppe",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_mobile_ui/common/viewable.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/card_handle.dart';
|
||||
import 'package:refilc/ui/widgets/lesson/lesson_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/lesson/lesson_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LessonViewable extends StatelessWidget {
|
||||
const LessonViewable(this.lesson, {super.key, this.swapDesc = false});
|
||||
|
||||
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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc_kreta_api/models/attachment.dart';
|
||||
import 'package:refilc/helpers/attachment_helper.dart';
|
||||
import 'package:refilc_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, {super.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!);
|
||||
},
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Ink.image(
|
||||
image: FileImage(File(snapshot.data ?? "")),
|
||||
height: 200.0,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} 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),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
47
refilc_mobile_ui/lib/common/widgets/message/image_view.dart
Normal file
47
refilc_mobile_ui/lib/common/widgets/message/image_view.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc/helpers/share_helper.dart';
|
||||
import 'package:refilc/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, {super.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/message.dart';
|
||||
import 'package:refilc_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, {super.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]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/message.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel.dart';
|
||||
import 'package:refilc_mobile_ui/common/profile_image/profile_image.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/message/attachment_tile.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
// import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'message_view_tile.i18n.dart';
|
||||
|
||||
class MessageViewTile extends StatelessWidget {
|
||||
const MessageViewTile(this.message, {super.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
|
||||
Center(
|
||||
child: Text(
|
||||
message.subject,
|
||||
softWrap: true,
|
||||
maxLines: 10,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 24.0,
|
||||
height: 1.2,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
|
||||
// date
|
||||
Center(
|
||||
child: Text(
|
||||
DateFormat("yyyy. MM. dd.", I18n.locale.languageCode)
|
||||
.format(message.date),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28.0),
|
||||
|
||||
// author
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(.25),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: ProfileImage(
|
||||
isNotePfp: true,
|
||||
name: message.author,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
radius: 19.0,
|
||||
),
|
||||
title: Text(
|
||||
message.author,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
subtitle: Text(
|
||||
"${"to".i18n} $recipientLabel",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.2,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.color
|
||||
?.withOpacity(0.6),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: Icon(FeatherIcons.cornerUpLeft,
|
||||
// color: AppColors.of(context).text),
|
||||
// splashRadius: 24.0,
|
||||
// highlightColor: Colors.transparent,
|
||||
// padding: EdgeInsets.zero,
|
||||
// visualDensity: VisualDensity.compact,
|
||||
// ),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Share.share(
|
||||
message.content.escapeHtml(),
|
||||
subject: 'reFilc',
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
FeatherIcons.share2,
|
||||
color: AppColors.of(context).text,
|
||||
size: 20,
|
||||
),
|
||||
splashRadius: 20.0,
|
||||
highlightColor: Colors.transparent,
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
|
||||
// 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,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"me": "me",
|
||||
"to": "to",
|
||||
},
|
||||
"hu_hu": {
|
||||
"me": "én",
|
||||
"to": "Címzett:",
|
||||
},
|
||||
"de_de": {
|
||||
"me": "mich",
|
||||
"to": "zu",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:refilc_kreta_api/models/message.dart';
|
||||
import 'package:refilc/ui/widgets/message/message_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/message/message_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageViewable extends StatelessWidget {
|
||||
const MessageViewable(this.message, {super.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,
|
||||
);
|
||||
}
|
||||
}
|
||||
52
refilc_mobile_ui/lib/common/widgets/miss_tile.dart
Normal file
52
refilc_mobile_ui/lib/common/widgets/miss_tile.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:refilc_kreta_api/models/note.dart';
|
||||
import 'package:refilc/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, {super.key});
|
||||
|
||||
final Note note;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(_missIcon(),
|
||||
color: Theme.of(context).colorScheme.secondary, size: 36.0),
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: Text(
|
||||
_missName(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
note.content.split("órán nem volt")[0].capital(),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _missIcon() {
|
||||
if (note.type?.name == "HaziFeladatHiany") {
|
||||
return FeatherIcons.home;
|
||||
} else if (note.type?.name == "Felszereleshiany") {
|
||||
return FeatherIcons.book;
|
||||
}
|
||||
return FeatherIcons.slash;
|
||||
}
|
||||
|
||||
String _missName() {
|
||||
if (note.type?.name == "HaziFeladatHiany") {
|
||||
return "Missing homework".i18n;
|
||||
} else if (note.type?.name == "Felszereleshiany") {
|
||||
return "Missing equipment".i18n;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
24
refilc_mobile_ui/lib/common/widgets/miss_tile.i18n.dart
Normal file
24
refilc_mobile_ui/lib/common/widgets/miss_tile.i18n.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"Missing homework": "Missing homework",
|
||||
"Missing equipment": "Missing equipment",
|
||||
},
|
||||
"hu_hu": {
|
||||
"Missing homework": "Házi feladat hiány",
|
||||
"Missing equipment": "Felszerelés Hiány",
|
||||
},
|
||||
"de_de": {
|
||||
"Missing homework": "Fehlende Hausaufgaben",
|
||||
"Missing equipment": "Fehlende Ausrüstung",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_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, {super.key, this.onTap, this.padding});
|
||||
|
||||
final List<Lesson> missedExams;
|
||||
final Function()? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: PanelButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6),
|
||||
leading: SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: Icon(
|
||||
FeatherIcons.slash,
|
||||
color: AppColors.of(context).red.withOpacity(.75),
|
||||
size: 28.0,
|
||||
)),
|
||||
title: Text("missed_exams"
|
||||
.plural(missedExams.length)
|
||||
.fill([missedExams.length])),
|
||||
trailing: const Icon(FeatherIcons.arrowRight),
|
||||
onPressed: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"goodmorning": "Good morning, %s!",
|
||||
"goodafternoon": "Good afternoon, %s!",
|
||||
"goodevening": "Good evening, %s!",
|
||||
"goodrest": "⛱️ Have a nice holiday, %s!",
|
||||
"happybirthday": "🎂 Happy birthday, %s!",
|
||||
"merryxmas": "🎄 Merry Christmas, %s!",
|
||||
"happynewyear": "🎉 Happy New Year, %s!",
|
||||
"empty": "Nothing to see here.",
|
||||
"All": "All",
|
||||
"Grades": "Grades",
|
||||
"Messages": "Messages",
|
||||
"Absences": "Absences",
|
||||
"update_available": "Update Available",
|
||||
"missed_exams": "You missed %s exams this week.".one("You missed an exam this week."),
|
||||
"missed_exam_contact": "Contact %s, to resolve it!",
|
||||
},
|
||||
"hu_hu": {
|
||||
"goodmorning": "Jó reggelt, %s!",
|
||||
"goodafternoon": "Szép napot, %s!",
|
||||
"goodevening": "Szép estét, %s!",
|
||||
"goodrest": "⛱️ Jó szünetet, %s!",
|
||||
"happybirthday": "🎂 Boldog születésnapot, %s!",
|
||||
"merryxmas": "🎄 Boldog Karácsonyt, %s!",
|
||||
"happynewyear": "🎉 Boldog új évet, %s!",
|
||||
"empty": "Nincs itt semmi látnivaló.",
|
||||
"All": "Összes",
|
||||
"Grades": "Jegyek",
|
||||
"Messages": "Üzenetek",
|
||||
"Absences": "Hiányok",
|
||||
"update_available": "Frissítés elérhető",
|
||||
"missed_exams": "Ezen a héten hiányoztál %s dolgozatról.".one("Ezen a héten hiányoztál egy dolgozatról."),
|
||||
"missed_exam_contact": "Keresd %s-t, ha pótolni szeretnéd!",
|
||||
},
|
||||
"de_de": {
|
||||
"goodmorning": "Guten morgen, %s!",
|
||||
"goodafternoon": "Guten Tag, %s!",
|
||||
"goodevening": "Guten Abend, %s!",
|
||||
"goodrest": "⛱️ Schöne Ferien, %s!",
|
||||
"happybirthday": "🎂 Alles Gute zum Geburtstag, %s!",
|
||||
"merryxmas": "🎄 Frohe Weihnachten, %s!",
|
||||
"happynewyear": "🎉 Frohes neues Jahr, %s!",
|
||||
"empty": "Hier gibt es nichts zu sehen.",
|
||||
"All": "Alles",
|
||||
"Grades": "Noten",
|
||||
"Messages": "Nachrichten",
|
||||
"Absences": "Fehlen",
|
||||
"update_available": "Update verfügbar",
|
||||
"missed_exams": "Diese Woche haben Sie %s Prüfungen verpasst.".one("Diese Woche haben Sie eine Prüfung verpasst."),
|
||||
"missed_exam_contact": "Wenden Sie sich an %s, um sie zu erneuern!",
|
||||
},
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart';
|
||||
import 'package:refilc_mobile_ui/pages/timetable/timetable_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'missed_exam_tile.i18n.dart';
|
||||
|
||||
class MissedExamView extends StatelessWidget {
|
||||
const MissedExamView(this.missedExams, {super.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, {super.key, this.padding});
|
||||
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final Lesson lesson;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
|
||||
String? teacherName = lesson.teacher.isRenamed
|
||||
? lesson.teacher.renamedTo
|
||||
: lesson.teacher.name;
|
||||
|
||||
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 &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? FontStyle.italic
|
||||
: null),
|
||||
),
|
||||
subtitle: Text(
|
||||
"missed_exam_contact".i18n.fill([teacherName ?? '']),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
trailing: const Icon(FeatherIcons.arrowRight),
|
||||
onTap: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
TimetablePage.jump(context, lesson: lesson);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/missed_exam/missed_exam_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/missed_exam/missed_exam_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MissedExamViewable extends StatelessWidget {
|
||||
const MissedExamViewable(this.missedExams, {super.key});
|
||||
|
||||
final List<Lesson> missedExams;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MissedExamTile(
|
||||
missedExams,
|
||||
onTap: () => MissedExamView.show(missedExams, context: context),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
refilc_mobile_ui/lib/common/widgets/note/note_tile.dart
Normal file
56
refilc_mobile_ui/lib/common/widgets/note/note_tile.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:refilc_kreta_api/models/note.dart';
|
||||
import 'package:refilc_mobile_ui/common/profile_image/profile_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NoteTile extends StatelessWidget {
|
||||
const NoteTile(this.note, {super.key, this.onTap, this.padding});
|
||||
|
||||
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: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
),
|
||||
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(
|
||||
isNotePfp: true,
|
||||
name: (note.teacher.isRenamed
|
||||
? note.teacher.renamedTo
|
||||
: note.teacher.name) ??
|
||||
'',
|
||||
radius: 19.2,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
84
refilc_mobile_ui/lib/common/widgets/note/note_view.dart
Normal file
84
refilc_mobile_ui/lib/common/widgets/note/note_view.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:refilc/utils/color.dart';
|
||||
import 'package:refilc_kreta_api/models/note.dart';
|
||||
import 'package:refilc_mobile_ui/common/profile_image/profile_image.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_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, {super.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.isRenamed
|
||||
? note.teacher.renamedTo
|
||||
: note.teacher.name) ??
|
||||
'',
|
||||
radius: 22.0,
|
||||
backgroundColor: ColorUtils.stringToColor(
|
||||
(note.teacher.isRenamed
|
||||
? note.teacher.renamedTo
|
||||
: note.teacher.name) ??
|
||||
'',
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
note.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(
|
||||
(note.teacher.isRenamed
|
||||
? note.teacher.renamedTo
|
||||
: note.teacher.name) ??
|
||||
'',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
trailing: Text(
|
||||
note.date.format(context),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
|
||||
// Details
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: SelectableLinkify(
|
||||
text: note.content.escapeHtml(),
|
||||
options: const LinkifyOptions(looseUrl: true, removeWww: true),
|
||||
onOpen: (link) {
|
||||
launch(link.url,
|
||||
customTabsOption: CustomTabsOption(
|
||||
toolbarColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
showPageTitle: true,
|
||||
));
|
||||
},
|
||||
style: const TextStyle(fontWeight: FontWeight.w400),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
refilc_mobile_ui/lib/common/widgets/note/note_viewable.dart
Normal file
18
refilc_mobile_ui/lib/common/widgets/note/note_viewable.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:refilc_kreta_api/models/note.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/note/note_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/note/note_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NoteViewable extends StatelessWidget {
|
||||
const NoteViewable(this.note, {super.key});
|
||||
|
||||
final Note note;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NoteTile(
|
||||
note,
|
||||
onTap: () => NoteView.show(note, context: context),
|
||||
);
|
||||
}
|
||||
}
|
||||
119
refilc_mobile_ui/lib/common/widgets/statistics_tile.dart
Normal file
119
refilc_mobile_ui/lib/common/widgets/statistics_tile.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class StatisticsTile extends StatelessWidget {
|
||||
const StatisticsTile({
|
||||
super.key,
|
||||
required this.value,
|
||||
this.title,
|
||||
this.decimal = true,
|
||||
this.color,
|
||||
this.valueSuffix = '',
|
||||
this.fill = false,
|
||||
this.outline = false,
|
||||
});
|
||||
|
||||
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: [
|
||||
if (Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
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 ? 5.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: 28.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user