changed everything from filcnaplo to refilc finally
This commit is contained in:
457
refilc_mobile_ui/lib/pages/home/home_page.dart
Normal file
457
refilc_mobile_ui/lib/pages/home/home_page.dart
Normal file
@@ -0,0 +1,457 @@
|
||||
// ignore_for_file: dead_code
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:refilc/api/providers/live_card_provider.dart';
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:refilc_plus/providers/premium_provider.dart';
|
||||
import 'package:animated_list_plus/animated_list_plus.dart';
|
||||
import 'package:refilc/api/providers/update_provider.dart';
|
||||
import 'package:refilc/api/providers/sync.dart';
|
||||
import 'package:confetti/confetti.dart';
|
||||
import 'package:refilc/models/settings.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/api/providers/user_provider.dart';
|
||||
import 'package:refilc/api/providers/status_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:refilc_mobile_ui/common/empty.dart';
|
||||
import 'package:refilc_mobile_ui/common/filter_bar.dart';
|
||||
import 'package:refilc_mobile_ui/common/profile_image/profile_button.dart';
|
||||
import 'package:refilc_mobile_ui/common/profile_image/profile_image.dart';
|
||||
import 'package:refilc_mobile_ui/pages/home/live_card/live_card.dart';
|
||||
import 'package:refilc_mobile_ui/screens/navigation/navigation_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'home_page.i18n.dart';
|
||||
import 'package:refilc/ui/filter/widgets.dart';
|
||||
import 'package:refilc/ui/filter/sort.dart';
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
// import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
HomePageState createState() => HomePageState();
|
||||
}
|
||||
|
||||
class HomePageState extends State<HomePage> with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
late UserProvider user;
|
||||
late SettingsProvider settings;
|
||||
late UpdateProvider updateProvider;
|
||||
late StatusProvider statusProvider;
|
||||
late GradeProvider gradeProvider;
|
||||
late TimetableProvider timetableProvider;
|
||||
late MessageProvider messageProvider;
|
||||
late AbsenceProvider absenceProvider;
|
||||
late HomeworkProvider homeworkProvider;
|
||||
late ExamProvider examProvider;
|
||||
late NoteProvider noteProvider;
|
||||
late EventProvider eventProvider;
|
||||
|
||||
late PageController _pageController;
|
||||
ConfettiController? _confettiController;
|
||||
late LiveCardProvider _liveCard;
|
||||
late AnimationController _liveCardAnimation;
|
||||
|
||||
late String greeting;
|
||||
late String firstName;
|
||||
|
||||
late List<String> listOrder;
|
||||
static const pageCount = 5;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_tabController = TabController(length: pageCount, vsync: this);
|
||||
_pageController = PageController();
|
||||
user = Provider.of<UserProvider>(context, listen: false);
|
||||
_liveCard = Provider.of<LiveCardProvider>(context, listen: false);
|
||||
_liveCardAnimation = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 500));
|
||||
|
||||
_liveCardAnimation.animateTo(_liveCard.show ? 1.0 : 0.0,
|
||||
duration: Duration.zero);
|
||||
|
||||
listOrder = List.generate(pageCount, (index) => "$index");
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// _filterController.dispose();
|
||||
_pageController.dispose();
|
||||
_tabController.dispose();
|
||||
_confettiController?.dispose();
|
||||
_liveCardAnimation.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void setGreeting() {
|
||||
DateTime now = DateTime.now();
|
||||
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
|
||||
if (!settings.presentationMode) {
|
||||
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
|
||||
} else {
|
||||
firstName = "János";
|
||||
}
|
||||
|
||||
bool customWelcome = false;
|
||||
|
||||
if (now.isBefore(DateTime(now.year, DateTime.august, 31)) &&
|
||||
now.isAfter(DateTime(now.year, DateTime.june, 14))) {
|
||||
greeting = "goodrest";
|
||||
|
||||
if (NavigationScreen.of(context)?.init("confetti") ?? false) {
|
||||
_confettiController =
|
||||
ConfettiController(duration: const Duration(seconds: 1));
|
||||
Future.delayed(const Duration(seconds: 1))
|
||||
.then((value) => mounted ? _confettiController?.play() : null);
|
||||
}
|
||||
} else if (now.month == user.student?.birth.month &&
|
||||
now.day == user.student?.birth.day) {
|
||||
greeting = "happybirthday";
|
||||
|
||||
if (NavigationScreen.of(context)?.init("confetti") ?? false) {
|
||||
_confettiController =
|
||||
ConfettiController(duration: const Duration(seconds: 3));
|
||||
Future.delayed(const Duration(seconds: 1))
|
||||
.then((value) => mounted ? _confettiController?.play() : null);
|
||||
}
|
||||
} else if (now.isAfter(DateTime(now.year, DateTime.may, 28)) &&
|
||||
now.isBefore(DateTime(now.year, DateTime.may, 30))) {
|
||||
greeting = "refilcopen";
|
||||
|
||||
if (NavigationScreen.of(context)?.init("confetti") ?? false) {
|
||||
_confettiController =
|
||||
ConfettiController(duration: const Duration(seconds: 3));
|
||||
Future.delayed(const Duration(seconds: 1))
|
||||
.then((value) => mounted ? _confettiController?.play() : null);
|
||||
}
|
||||
} else if (now.month == DateTime.december &&
|
||||
now.day >= 24 &&
|
||||
now.day <= 26) {
|
||||
greeting = "merryxmas";
|
||||
} else if (now.month == DateTime.january && now.day == 1) {
|
||||
greeting = "happynewyear";
|
||||
} else if (settings.welcomeMessage.replaceAll(' ', '') != '') {
|
||||
greeting = settings.welcomeMessage;
|
||||
greeting = localizeFill(
|
||||
settings.welcomeMessage,
|
||||
[firstName],
|
||||
);
|
||||
|
||||
customWelcome = true;
|
||||
} else if (now.hour >= 18) {
|
||||
greeting = "goodevening";
|
||||
} else if (now.hour >= 10) {
|
||||
greeting = "goodafternoon";
|
||||
} else if (now.hour >= 4) {
|
||||
greeting = "goodmorning";
|
||||
} else {
|
||||
greeting = "goodevening";
|
||||
}
|
||||
|
||||
greeting = customWelcome ? greeting : greeting.i18n.fill([firstName]);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
user = Provider.of<UserProvider>(context);
|
||||
settings = Provider.of<SettingsProvider>(context);
|
||||
statusProvider = Provider.of<StatusProvider>(context, listen: false);
|
||||
updateProvider = Provider.of<UpdateProvider>(context);
|
||||
_liveCard = Provider.of<LiveCardProvider>(context);
|
||||
gradeProvider = Provider.of<GradeProvider>(context);
|
||||
context.watch<PremiumProvider>();
|
||||
|
||||
_liveCardAnimation.animateTo(_liveCard.show ? 1.0 : 0.0);
|
||||
|
||||
setGreeting();
|
||||
//for extra filters
|
||||
|
||||
// final List<String> items = [
|
||||
// 'Item1',
|
||||
// 'Item2',
|
||||
// 'Item3',
|
||||
// 'Item4',
|
||||
// 'Item5',
|
||||
// 'Item6',
|
||||
// 'Item7',
|
||||
// 'Item8',
|
||||
// ];
|
||||
// String? selectedValue;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: NestedScrollView(
|
||||
physics: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics()),
|
||||
headerSliverBuilder: (context, _) => [
|
||||
AnimatedBuilder(
|
||||
animation: _liveCardAnimation,
|
||||
builder: (context, child) {
|
||||
return SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
surfaceTintColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
centerTitle: false,
|
||||
titleSpacing: 0.0,
|
||||
// Welcome text
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
greeting,
|
||||
overflow: TextOverflow.fade,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.color,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('EEEE, MMM d',
|
||||
I18n.locale.countryCode)
|
||||
.format(DateTime.now())
|
||||
.capital(),
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
// Profile Icon
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 24.0),
|
||||
child: ProfileButton(
|
||||
child: ProfileImage(
|
||||
heroTag: "profile",
|
||||
name: firstName,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary, //!settings.presentationMode
|
||||
//? ColorUtils.stringToColor(user.displayName ?? "?")
|
||||
//: Theme.of(context).colorScheme.secondary,
|
||||
badge: updateProvider.available,
|
||||
role: user.role,
|
||||
profilePictureString: user.picture,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
expandedHeight: _liveCardAnimation.value * 238.0,
|
||||
|
||||
// Live Card
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 24.0,
|
||||
right: 24.0,
|
||||
top:
|
||||
62.0 + MediaQuery.of(context).padding.top,
|
||||
bottom: 52.0,
|
||||
),
|
||||
child: Transform.scale(
|
||||
scale: _liveCardAnimation.value,
|
||||
child: Opacity(
|
||||
opacity: _liveCardAnimation.value,
|
||||
child: const LiveCard(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
shadowColor: Colors.black,
|
||||
// Filter Bar
|
||||
bottom: FilterBar(
|
||||
items: [
|
||||
Tab(text: "All".i18n),
|
||||
Tab(text: "Grades".i18n),
|
||||
Tab(text: "Exams".i18n),
|
||||
Tab(text: "Messages".i18n),
|
||||
Tab(text: "Absences".i18n),
|
||||
],
|
||||
controller: _tabController,
|
||||
disableFading: true,
|
||||
onTap: (i) async {
|
||||
int selectedPage =
|
||||
_pageController.page!.round();
|
||||
|
||||
if (i == selectedPage) return;
|
||||
if (_pageController.page?.roundToDouble() !=
|
||||
_pageController.page) {
|
||||
_pageController.animateToPage(i,
|
||||
curve: Curves.easeIn,
|
||||
duration: kTabScrollDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
// swap current page with target page
|
||||
setState(() {
|
||||
_pageController.jumpToPage(i);
|
||||
String currentList = listOrder[selectedPage];
|
||||
listOrder[selectedPage] = listOrder[i];
|
||||
listOrder[i] = currentList;
|
||||
});
|
||||
},
|
||||
),
|
||||
pinned: true,
|
||||
floating: false,
|
||||
snap: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
// from flutter source
|
||||
if (notification is ScrollUpdateNotification &&
|
||||
!_tabController.indexIsChanging) {
|
||||
if ((_pageController.page! - _tabController.index)
|
||||
.abs() >
|
||||
1.0) {
|
||||
_tabController.index = _pageController.page!.floor();
|
||||
}
|
||||
_tabController.offset =
|
||||
(_pageController.page! - _tabController.index)
|
||||
.clamp(-1.0, 1.0);
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
_tabController.index = _pageController.page!.round();
|
||||
if (!_tabController.indexIsChanging) {
|
||||
_tabController.offset =
|
||||
(_pageController.page! - _tabController.index)
|
||||
.clamp(-1.0, 1.0);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: PageView.custom(
|
||||
controller: _pageController,
|
||||
childrenDelegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return FutureBuilder<List<DateWidget>>(
|
||||
key: ValueKey<String>(listOrder[index]),
|
||||
future: getFilterWidgets(homeFilters[index],
|
||||
context: context),
|
||||
builder: (context, dateWidgets) => dateWidgets
|
||||
.data !=
|
||||
null
|
||||
? RefreshIndicator(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
onRefresh: () => syncAll(context),
|
||||
child: ImplicitlyAnimatedList<Widget>(
|
||||
items: [
|
||||
if (index == 0)
|
||||
const SizedBox(key: Key("\$premium")),
|
||||
...sortDateWidgets(context,
|
||||
dateWidgets: dateWidgets.data!,
|
||||
padding: EdgeInsets.zero),
|
||||
],
|
||||
itemBuilder: filterItemBuilder,
|
||||
spawnIsolate: false,
|
||||
areItemsTheSame: (a, b) => a.key == b.key,
|
||||
physics: const BouncingScrollPhysics(
|
||||
parent:
|
||||
AlwaysScrollableScrollPhysics()),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24.0),
|
||||
))
|
||||
: Container(),
|
||||
);
|
||||
},
|
||||
childCount: 5,
|
||||
findChildIndexCallback: (Key key) {
|
||||
final ValueKey<String> valueKey =
|
||||
key as ValueKey<String>;
|
||||
final String data = valueKey.value;
|
||||
return listOrder.indexOf(data);
|
||||
},
|
||||
),
|
||||
physics: const PageScrollPhysics()
|
||||
.applyTo(const BouncingScrollPhysics()),
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
|
||||
// confetti 🎊
|
||||
if (_confettiController != null)
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _confettiController!,
|
||||
blastDirectionality: BlastDirectionality.explosive,
|
||||
emissionFrequency: 0.02,
|
||||
numberOfParticles: 120,
|
||||
maxBlastForce: 20,
|
||||
minBlastForce: 10,
|
||||
gravity: 0.3,
|
||||
minimumSize: const Size(5, 5),
|
||||
maximumSize: const Size(20, 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Widget> filterViewBuilder(context, int activeData) async {
|
||||
final activeFilter = homeFilters[activeData];
|
||||
|
||||
List<Widget> filterWidgets = sortDateWidgets(
|
||||
context,
|
||||
dateWidgets: await getFilterWidgets(activeFilter, context: context),
|
||||
showDivider: true,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: RefreshIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
onRefresh: () => syncAll(context),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (filterWidgets.isNotEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: filterWidgets[index],
|
||||
);
|
||||
} else {
|
||||
return Empty(subtitle: "empty".i18n);
|
||||
}
|
||||
},
|
||||
itemCount: max(filterWidgets.length, 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
72
refilc_mobile_ui/lib/pages/home/home_page.i18n.dart
Normal file
72
refilc_mobile_ui/lib/pages/home/home_page.i18n.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
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!",
|
||||
"refilcopen": "🎈 reFilc is 1 year old, %s!",
|
||||
"empty": "Nothing to see here.",
|
||||
"All": "All",
|
||||
"Grades": "Grades",
|
||||
"Exams": "Exams",
|
||||
"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!",
|
||||
"refilcopen": "🎈 1 éves a reFilc, %s!",
|
||||
"empty": "Nincs itt semmi látnivaló.",
|
||||
"All": "Összes",
|
||||
"Grades": "Jegyek",
|
||||
"Exams": "Számonkérések",
|
||||
"Messages": "Üzenetek",
|
||||
"Absences": "Hiányzások",
|
||||
"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!",
|
||||
"refilcopen": "🎈 reFilc ist 1 Jahr alt, %s!",
|
||||
"empty": "Hier gibt es nichts zu sehen.",
|
||||
"All": "Alles",
|
||||
"Grades": "Noten",
|
||||
"Exams": "Aufsätze",
|
||||
"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,104 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:animated_flip_counter/animated_flip_counter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class HeadsUpCountdown extends StatefulWidget {
|
||||
const HeadsUpCountdown(
|
||||
{super.key, required this.maxTime, required this.elapsedTime});
|
||||
|
||||
final double maxTime;
|
||||
final double elapsedTime;
|
||||
|
||||
@override
|
||||
State<HeadsUpCountdown> createState() => _HeadsUpCountdownState();
|
||||
}
|
||||
|
||||
class _HeadsUpCountdownState extends State<HeadsUpCountdown> {
|
||||
static const _style = TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 70.0,
|
||||
letterSpacing: -.5,
|
||||
);
|
||||
|
||||
late final Timer _timer;
|
||||
late double elapsed;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
elapsed = widget.elapsedTime;
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (elapsed <= widget.maxTime) elapsed += 1;
|
||||
setState(() {});
|
||||
|
||||
if (elapsed >= widget.maxTime) {
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dur = Duration(seconds: (widget.maxTime - elapsed).round());
|
||||
return Center(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
opacity: dur.inSeconds > 0 ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if ((dur.inHours % 24) > 0) ...[
|
||||
AnimatedFlipCounter(
|
||||
value: dur.inHours % 24,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
textStyle: _style,
|
||||
),
|
||||
const Text(":", style: _style),
|
||||
],
|
||||
AnimatedFlipCounter(
|
||||
duration: const Duration(seconds: 2),
|
||||
value: dur.inMinutes % 60,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
wholeDigits: (dur.inHours % 24) > 0 ? 2 : 1,
|
||||
textStyle: _style,
|
||||
),
|
||||
const Text(":", style: _style),
|
||||
AnimatedFlipCounter(
|
||||
duration: const Duration(seconds: 1),
|
||||
value: dur.inSeconds % 60,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
wholeDigits: 2,
|
||||
textStyle: _style,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (dur.inSeconds < 0)
|
||||
AnimatedOpacity(
|
||||
opacity: dur.inSeconds > 0 ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: Lottie.asset("assets/animations/bell-alert.json",
|
||||
width: 400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
305
refilc_mobile_ui/lib/pages/home/live_card/live_card.dart
Normal file
305
refilc_mobile_ui/lib/pages/home/live_card/live_card.dart
Normal file
@@ -0,0 +1,305 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/icons/filc_icons.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc_mobile_ui/pages/home/live_card/heads_up_countdown.dart';
|
||||
import 'package:refilc_mobile_ui/screens/summary/summary_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc/api/providers/live_card_provider.dart';
|
||||
import 'package:refilc_mobile_ui/pages/home/live_card/live_card_widget.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:maps_launcher/maps_launcher.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'live_card.i18n.dart';
|
||||
|
||||
class LiveCard extends StatefulWidget {
|
||||
const LiveCard({super.key});
|
||||
|
||||
@override
|
||||
LiveCardStateA createState() => LiveCardStateA();
|
||||
}
|
||||
|
||||
class LiveCardStateA extends State<LiveCard> {
|
||||
late void Function() listener;
|
||||
late UserProvider _userProvider;
|
||||
late LiveCardProvider liveCard;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
listener = () => setState(() {});
|
||||
_userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
liveCard = Provider.of<LiveCardProvider>(context, listen: false);
|
||||
_userProvider.addListener(liveCard.update);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_userProvider.removeListener(liveCard.update);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
liveCard = Provider.of<LiveCardProvider>(context);
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
|
||||
if (!liveCard.show) return Container();
|
||||
|
||||
Widget child;
|
||||
Duration bellDelay = liveCard.delay;
|
||||
|
||||
// test
|
||||
// liveCard.currentState = LiveCardState.morning;
|
||||
|
||||
switch (liveCard.currentState) {
|
||||
case LiveCardState.summary:
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.summary'),
|
||||
title: 'Vége a tanévnek! 🥳',
|
||||
icon: FeatherIcons.arrowRight,
|
||||
description: Text(
|
||||
'Irány az összefoglaláshoz',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18.0,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
// showSlidingBottomSheet(
|
||||
// context,
|
||||
// useRootNavigator: true,
|
||||
// builder: (context) => SlidingSheetDialog(
|
||||
// color: Colors.black.withOpacity(0.99),
|
||||
// duration: const Duration(milliseconds: 400),
|
||||
// scrollSpec: const ScrollSpec.bouncingScroll(),
|
||||
// snapSpec: const SnapSpec(
|
||||
// snap: true,
|
||||
// snappings: [1.0],
|
||||
// initialSnap: 1.0,
|
||||
// positioning: SnapPositioning.relativeToAvailableSpace,
|
||||
// ),
|
||||
// minHeight: MediaQuery.of(context).size.height,
|
||||
// cornerRadius: 16,
|
||||
// cornerRadiusOnFullscreen: 0,
|
||||
// builder: (context, state) => const Material(
|
||||
// color: Colors.black,
|
||||
// child: SummaryScreen(
|
||||
// currentPage: 'start',
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
SummaryScreen.show(context: context, currentPage: 'start');
|
||||
},
|
||||
);
|
||||
break;
|
||||
case LiveCardState.morning:
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.morning'),
|
||||
title: DateFormat("EEEE", I18n.of(context).locale.toString())
|
||||
.format(DateTime.now())
|
||||
.capital(),
|
||||
icon: FeatherIcons.sun,
|
||||
onTap: () async {
|
||||
await MapsLauncher.launchQuery(
|
||||
'${_userProvider.student?.school.city ?? ''} ${_userProvider.student?.school.name ?? ''}');
|
||||
},
|
||||
description: liveCard.nextLesson != null
|
||||
? Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: "first_lesson_1".i18n),
|
||||
TextSpan(
|
||||
text: liveCard.nextLesson!.subject.renamedTo ??
|
||||
liveCard.nextLesson!.subject.name.capital(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(.85),
|
||||
fontStyle: liveCard.nextLesson!.subject.isRenamed &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? FontStyle.italic
|
||||
: null),
|
||||
),
|
||||
TextSpan(text: "first_lesson_2".i18n),
|
||||
TextSpan(
|
||||
text: liveCard.nextLesson!.room.capital(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(.85),
|
||||
),
|
||||
),
|
||||
TextSpan(text: "first_lesson_3".i18n),
|
||||
TextSpan(
|
||||
text: DateFormat('H:mm')
|
||||
.format(liveCard.nextLesson!.start),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(.85),
|
||||
),
|
||||
),
|
||||
TextSpan(text: "first_lesson_4".i18n),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
break;
|
||||
case LiveCardState.duringLesson:
|
||||
final elapsedTime = DateTime.now()
|
||||
.difference(liveCard.currentLesson!.start)
|
||||
.inSeconds
|
||||
.toDouble() +
|
||||
bellDelay.inSeconds;
|
||||
final maxTime = liveCard.currentLesson!.end
|
||||
.difference(liveCard.currentLesson!.start)
|
||||
.inSeconds
|
||||
.toDouble();
|
||||
|
||||
final showMinutes = maxTime - elapsedTime > 60;
|
||||
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.duringLesson'),
|
||||
leading: liveCard.currentLesson!.lessonIndex +
|
||||
(RegExp(r'\d').hasMatch(liveCard.currentLesson!.lessonIndex)
|
||||
? "."
|
||||
: ""),
|
||||
title: liveCard.currentLesson!.subject.renamedTo ??
|
||||
liveCard.currentLesson!.subject.name.capital(),
|
||||
titleItalic: liveCard.currentLesson!.subject.isRenamed &&
|
||||
settingsProvider.renamedSubjectsEnabled &&
|
||||
settingsProvider.renamedSubjectsItalics,
|
||||
subtitle: liveCard.currentLesson!.room,
|
||||
icon: SubjectIcon.resolveVariant(
|
||||
subject: liveCard.currentLesson!.subject, context: context),
|
||||
description: liveCard.currentLesson!.description != ""
|
||||
? Text(liveCard.currentLesson!.description)
|
||||
: null,
|
||||
nextSubject: liveCard.nextLesson?.subject.renamedTo ??
|
||||
liveCard.nextLesson?.subject.name.capital(),
|
||||
nextSubjectItalic: liveCard.nextLesson?.subject.isRenamed == true &&
|
||||
settingsProvider.renamedSubjectsEnabled &&
|
||||
settingsProvider.renamedSubjectsItalics,
|
||||
nextRoom: liveCard.nextLesson?.room,
|
||||
progressMax: showMinutes ? maxTime / 60 : maxTime,
|
||||
progressCurrent: showMinutes ? elapsedTime / 60 : elapsedTime,
|
||||
progressAccuracy:
|
||||
showMinutes ? ProgressAccuracy.minutes : ProgressAccuracy.seconds,
|
||||
onProgressTap: () {
|
||||
showDialog(
|
||||
barrierColor: Colors.black,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
HeadsUpCountdown(maxTime: maxTime, elapsedTime: elapsedTime),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case LiveCardState.duringBreak:
|
||||
final iconFloorMap = {
|
||||
"to room": FeatherIcons.chevronsRight,
|
||||
"up floor": FilcIcons.upstairs,
|
||||
"down floor": FilcIcons.downstairs,
|
||||
"ground floor": FilcIcons.downstairs,
|
||||
};
|
||||
|
||||
final diff = liveCard.getFloorDifference();
|
||||
|
||||
final maxTime = liveCard.nextLesson!.start
|
||||
.difference(liveCard.prevLesson!.end)
|
||||
.inSeconds
|
||||
.toDouble();
|
||||
final elapsedTime = DateTime.now()
|
||||
.difference(liveCard.prevLesson!.end)
|
||||
.inSeconds
|
||||
.toDouble() +
|
||||
bellDelay.inSeconds.toDouble();
|
||||
|
||||
final showMinutes = maxTime - elapsedTime > 60;
|
||||
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.duringBreak'),
|
||||
title: "break".i18n,
|
||||
icon: iconFloorMap[diff],
|
||||
description: liveCard.nextLesson!.room != liveCard.prevLesson!.room
|
||||
? Text("go $diff".i18n.fill([
|
||||
diff != "to room"
|
||||
? (liveCard.nextLesson!.getFloor() ?? 0)
|
||||
: liveCard.nextLesson!.room
|
||||
]))
|
||||
: Text("stay".i18n),
|
||||
nextSubject: liveCard.nextLesson?.subject.renamedTo ??
|
||||
liveCard.nextLesson?.subject.name.capital(),
|
||||
nextSubjectItalic: liveCard.nextLesson?.subject.isRenamed == true &&
|
||||
settingsProvider.renamedSubjectsItalics,
|
||||
nextRoom: diff != "to room" ? liveCard.nextLesson?.room : null,
|
||||
progressMax: showMinutes ? maxTime / 60 : maxTime,
|
||||
progressCurrent: showMinutes ? elapsedTime / 60 : elapsedTime,
|
||||
progressAccuracy:
|
||||
showMinutes ? ProgressAccuracy.minutes : ProgressAccuracy.seconds,
|
||||
onProgressTap: () {
|
||||
showDialog(
|
||||
barrierColor: Colors.black,
|
||||
context: context,
|
||||
builder: (context) => HeadsUpCountdown(
|
||||
maxTime: maxTime,
|
||||
elapsedTime: elapsedTime,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case LiveCardState.afternoon:
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.afternoon'),
|
||||
title: DateFormat("EEEE", I18n.of(context).locale.toString())
|
||||
.format(DateTime.now())
|
||||
.capital(),
|
||||
icon: FeatherIcons.coffee,
|
||||
);
|
||||
break;
|
||||
case LiveCardState.night:
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.night'),
|
||||
title: DateFormat("EEEE", I18n.of(context).locale.toString())
|
||||
.format(DateTime.now())
|
||||
.capital(),
|
||||
icon: FeatherIcons.moon,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
child = Container();
|
||||
}
|
||||
|
||||
return PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"next": "Next",
|
||||
"remaining min": "%d mins".one("%d min"),
|
||||
"remaining sec": "%d secs".one("%d sec"),
|
||||
"break": "Break",
|
||||
"go to room": "Go to room %s.",
|
||||
"go ground floor": "Go to the ground floor.",
|
||||
"go up floor": "Go upstairs, to floor %d.",
|
||||
"go down floor": "Go downstaris, to floor %d.",
|
||||
"stay": "Stay in this room.",
|
||||
"first_lesson_1": "Your first lesson will be ",
|
||||
"first_lesson_2": " in room ",
|
||||
"first_lesson_3": ", at ",
|
||||
"first_lesson_4": ".",
|
||||
},
|
||||
"hu_hu": {
|
||||
"next": "Következő",
|
||||
"remaining min": "%d perc".one("%d perc"),
|
||||
"remaining sec": "%d másodperc".one("%d másodperc"),
|
||||
"break": "Szünet",
|
||||
"go to room": "Menj a(z) %s terembe.",
|
||||
"go ground floor": "Menj a földszintre.",
|
||||
"go up floor": "Menj fel a(z) %d. emeletre.",
|
||||
"go down floor": "Menj le a(z) %d. emeletre.",
|
||||
"stay": "Maradj ebben a teremben.",
|
||||
"first_lesson_1": "Az első órád ",
|
||||
"first_lesson_2": " lesz, a ",
|
||||
"first_lesson_3": " teremben, ",
|
||||
"first_lesson_4": "-kor.",
|
||||
},
|
||||
"de_de": {
|
||||
"next": "Nächste",
|
||||
"remaining min": "%d Minuten".one("%d Minute"),
|
||||
"remaining sec": "%d Sekunden".one("%d Sekunden"),
|
||||
"break": "Pause",
|
||||
"go to room": "Geh in den Raum %s.",
|
||||
"go ground floor": "Geh dir Treppe hinunter.",
|
||||
"go up floor": "Geh in die %d. Stock hinauf.",
|
||||
"go down floor": "Geh runter in den %d. Stock.",
|
||||
"stay": "Im Zimmer bleiben.",
|
||||
"first_lesson_1": "Ihre erste Stunde ist ",
|
||||
"first_lesson_2": ", in Raum ",
|
||||
"first_lesson_3": ", um ",
|
||||
"first_lesson_4": " Uhr.",
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
390
refilc_mobile_ui/lib/pages/home/live_card/live_card_widget.dart
Normal file
390
refilc_mobile_ui/lib/pages/home/live_card/live_card_widget.dart
Normal file
@@ -0,0 +1,390 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_mobile_ui/common/progress_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'live_card.i18n.dart';
|
||||
|
||||
enum ProgressAccuracy { minutes, seconds }
|
||||
|
||||
class LiveCardWidget extends StatefulWidget {
|
||||
const LiveCardWidget({
|
||||
super.key,
|
||||
this.isEvent = false,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.titleItalic = false,
|
||||
this.subtitle,
|
||||
this.icon,
|
||||
this.description,
|
||||
this.nextRoom,
|
||||
this.nextSubject,
|
||||
this.nextSubjectItalic = false,
|
||||
this.progressCurrent,
|
||||
this.progressMax,
|
||||
this.progressAccuracy = ProgressAccuracy.minutes,
|
||||
this.onProgressTap,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final bool isEvent;
|
||||
final String? leading;
|
||||
final String? title;
|
||||
final bool titleItalic;
|
||||
final String? subtitle;
|
||||
final IconData? icon;
|
||||
final Widget? description;
|
||||
final String? nextSubject;
|
||||
final bool nextSubjectItalic;
|
||||
final String? nextRoom;
|
||||
final double? progressCurrent;
|
||||
final double? progressMax;
|
||||
final ProgressAccuracy? progressAccuracy;
|
||||
final Function()? onProgressTap;
|
||||
final Function()? onTap;
|
||||
|
||||
@override
|
||||
State<LiveCardWidget> createState() => _LiveCardWidgetState();
|
||||
}
|
||||
|
||||
class _LiveCardWidgetState extends State<LiveCardWidget> {
|
||||
bool hold = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onLongPressDown: (_) => setState(() => hold = true),
|
||||
onLongPressEnd: (_) => setState(() => hold = false),
|
||||
onLongPressCancel: () => setState(() => hold = false),
|
||||
onTap: widget.onTap,
|
||||
child: AnimatedScale(
|
||||
scale: hold ? 1.03 : 1.0,
|
||||
curve: Curves.easeInOutBack,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
boxShadow: [
|
||||
if (Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: OverflowBox(
|
||||
maxHeight: 96.0,
|
||||
child: widget.isEvent
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.title ?? 'Esemény',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24.0,
|
||||
color:
|
||||
Theme.of(context).textTheme.bodyMedium?.color,
|
||||
fontStyle:
|
||||
widget.titleItalic ? FontStyle.italic : null,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
widget.description ??
|
||||
Text(
|
||||
'Nincs leírás megadva.',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18.0,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.color,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(0.5),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(widget.icon),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.leading != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 12.0, top: 8.0),
|
||||
child: Text(
|
||||
widget.leading!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (widget.title != null)
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: widget.title!,
|
||||
style: TextStyle(
|
||||
fontStyle: widget
|
||||
.titleItalic
|
||||
? FontStyle.italic
|
||||
: null)),
|
||||
if (widget.subtitle != null)
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
margin: const EdgeInsets
|
||||
.only(
|
||||
left: 6.0,
|
||||
bottom: 3.0),
|
||||
padding:
|
||||
const EdgeInsets
|
||||
.symmetric(
|
||||
horizontal: 4.0,
|
||||
vertical: 2.0),
|
||||
decoration:
|
||||
BoxDecoration(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(.3),
|
||||
borderRadius:
|
||||
BorderRadius
|
||||
.circular(
|
||||
4.0),
|
||||
),
|
||||
child: Text(
|
||||
widget.subtitle!,
|
||||
style: TextStyle(
|
||||
height: 1.2,
|
||||
fontSize: 14.0,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
color: Theme.of(
|
||||
context)
|
||||
.colorScheme
|
||||
.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 22.0),
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (widget.title != null)
|
||||
const SizedBox(width: 6.0),
|
||||
if (widget.icon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0),
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
size: 26.0,
|
||||
color: AppColors.of(context)
|
||||
.text
|
||||
.withOpacity(.75),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.description != null)
|
||||
DefaultTextStyle(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.0,
|
||||
height: 1.0,
|
||||
color: AppColors.of(context)
|
||||
.text
|
||||
.withOpacity(.75),
|
||||
),
|
||||
maxLines:
|
||||
!(widget.nextSubject == null &&
|
||||
widget.progressCurrent ==
|
||||
null &&
|
||||
widget.progressMax == null)
|
||||
? 1
|
||||
: 2,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: widget.description!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!(widget.nextSubject == null &&
|
||||
widget.progressCurrent == null &&
|
||||
widget.progressMax == null))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.nextSubject != null)
|
||||
const Icon(FeatherIcons.arrowRight,
|
||||
size: 12.0),
|
||||
if (widget.nextSubject != null)
|
||||
const SizedBox(width: 4.0),
|
||||
if (widget.nextSubject != null)
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: widget.nextSubject!,
|
||||
style: TextStyle(
|
||||
fontStyle:
|
||||
widget.nextSubjectItalic
|
||||
? FontStyle.italic
|
||||
: null)),
|
||||
if (widget.nextRoom != null)
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(
|
||||
left: 4.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 3.0,
|
||||
vertical: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(.25),
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
4.0),
|
||||
),
|
||||
child: Text(
|
||||
widget.nextRoom!,
|
||||
style: TextStyle(
|
||||
height: 1.1,
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: TextStyle(
|
||||
color: AppColors.of(context)
|
||||
.text
|
||||
.withOpacity(.8),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
if (widget.nextRoom == null &&
|
||||
widget.nextSubject == null)
|
||||
const Spacer(),
|
||||
if (widget.progressCurrent != null &&
|
||||
widget.progressMax != null)
|
||||
GestureDetector(
|
||||
onTap: widget.onProgressTap,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
"remaining ${widget.progressAccuracy == ProgressAccuracy.minutes ? 'min' : 'sec'}"
|
||||
.plural((widget.progressMax! -
|
||||
widget.progressCurrent!)
|
||||
.round()),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.of(context)
|
||||
.text
|
||||
.withOpacity(.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.progressCurrent != null &&
|
||||
widget.progressMax != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: ProgressBar(
|
||||
value: widget.progressCurrent! /
|
||||
widget.progressMax!),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
438
refilc_mobile_ui/lib/pages/home/particle.dart
Normal file
438
refilc_mobile_ui/lib/pages/home/particle.dart
Normal file
@@ -0,0 +1,438 @@
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2018 Norbert Kozsir
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef ParticleBuilder = Particle Function(int index);
|
||||
|
||||
abstract class Particle {
|
||||
void paint(Canvas canvas, Size size, double progress, int seed);
|
||||
}
|
||||
|
||||
class FourRandomSlotParticle extends Particle {
|
||||
final List<Particle> children;
|
||||
|
||||
final double relativeDistanceToMiddle;
|
||||
|
||||
FourRandomSlotParticle({required this.children, this.relativeDistanceToMiddle = 2.0});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
Random random = Random(seed);
|
||||
int side = 0;
|
||||
for (Particle particle in children) {
|
||||
PositionedParticle(
|
||||
position: sideToOffset(side, size, random) * relativeDistanceToMiddle,
|
||||
child: particle,
|
||||
).paint(canvas, size, progress, seed);
|
||||
side++;
|
||||
}
|
||||
}
|
||||
|
||||
Offset sideToOffset(int side, Size size, Random random) {
|
||||
if (side == 0) {
|
||||
return Offset(-random.nextDouble() * (size.width / 2), -random.nextDouble() * (size.height / 2));
|
||||
} else if (side == 1) {
|
||||
return Offset(random.nextDouble() * (size.width / 2), -random.nextDouble() * (size.height / 2));
|
||||
} else if (side == 2) {
|
||||
return Offset(random.nextDouble() * (size.width / 2), random.nextDouble() * (size.height / 2));
|
||||
} else if (side == 3) {
|
||||
return Offset(-random.nextDouble() * (size.width / 2), random.nextDouble() * (size.height / 2));
|
||||
} else {
|
||||
throw Exception();
|
||||
}
|
||||
}
|
||||
|
||||
double randomOffset(Random random, int range) {
|
||||
return range / 2 - random.nextInt(range);
|
||||
}
|
||||
}
|
||||
|
||||
class PoppingCircle extends Particle {
|
||||
final Color color;
|
||||
|
||||
PoppingCircle({required this.color});
|
||||
|
||||
final double radius = 3.0;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
if (progress < 0.5) {
|
||||
canvas.drawCircle(
|
||||
Offset.zero,
|
||||
radius + (progress * 8),
|
||||
Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 5.0 - progress * 2);
|
||||
} else {
|
||||
CircleMirror(
|
||||
numberOfParticles: 4,
|
||||
child: AnimatedPositionedParticle(
|
||||
begin: const Offset(0.0, 5.0),
|
||||
end: const Offset(0.0, 15.0),
|
||||
child: FadingRect(
|
||||
color: color,
|
||||
height: 7.0,
|
||||
width: 2.0,
|
||||
)),
|
||||
initialRotation: pi / 4,
|
||||
).paint(canvas, size, progress, seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Firework extends Particle {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
FourRandomSlotParticle(children: [
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.deepOrangeAccent,
|
||||
),
|
||||
),
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.2, 0.5, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.4, 0.8, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.5, 1.0, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.teal,
|
||||
),
|
||||
),
|
||||
]).paint(canvas, size, progress, seed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors a given particle around a circle.
|
||||
///
|
||||
/// When using the default constructor you specify one [Particle], this particle
|
||||
/// is going to be used on its own, this implies that
|
||||
/// all mirrored particles are identical (expect for the rotation around the circle)
|
||||
class CircleMirror extends Particle {
|
||||
final ParticleBuilder particleBuilder;
|
||||
|
||||
final double initialRotation;
|
||||
|
||||
final int numberOfParticles;
|
||||
|
||||
CircleMirror.builder({required this.particleBuilder, required this.initialRotation, required this.numberOfParticles});
|
||||
|
||||
CircleMirror({required Particle child, required this.initialRotation, required this.numberOfParticles}) : particleBuilder = ((index) => child);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(initialRotation);
|
||||
for (int i = 0; i < numberOfParticles; i++) {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
canvas.rotate(pi / (numberOfParticles / 2));
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors a given particle around a circle.
|
||||
///
|
||||
/// When using the default constructor you specify one [Particle], this particle
|
||||
/// is going to be used on its own, this implies that
|
||||
/// all mirrored particles are identical (expect for the rotation around the circle)
|
||||
class RectangleMirror extends Particle {
|
||||
final ParticleBuilder particleBuilder;
|
||||
|
||||
/// Position of the first particle on the rect
|
||||
final double initialDistance;
|
||||
|
||||
final int numberOfParticles;
|
||||
|
||||
RectangleMirror.builder({required this.particleBuilder, required this.initialDistance, required this.numberOfParticles});
|
||||
|
||||
RectangleMirror({required Particle child, required this.initialDistance, required this.numberOfParticles})
|
||||
: particleBuilder = ((index) => child);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
double totalLength = size.width * 2 + size.height * 2;
|
||||
double distanceBetweenParticles = totalLength / numberOfParticles;
|
||||
|
||||
bool onHorizontalAxis = true;
|
||||
int side = 0;
|
||||
|
||||
assert((distanceBetweenParticles * numberOfParticles).round() == totalLength.round());
|
||||
|
||||
canvas.translate(-size.width / 2, -size.height / 2);
|
||||
|
||||
double currentDistance = initialDistance;
|
||||
for (int i = 0; i < numberOfParticles; i++) {
|
||||
while (true) {
|
||||
if (onHorizontalAxis ? currentDistance > size.width : currentDistance > size.height) {
|
||||
currentDistance -= onHorizontalAxis ? size.width : size.height;
|
||||
onHorizontalAxis = !onHorizontalAxis;
|
||||
side = (++side) % 4;
|
||||
} else {
|
||||
if (side == 0) {
|
||||
assert(onHorizontalAxis);
|
||||
moveTo(canvas, size, 0, currentDistance, 0.0, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
} else if (side == 1) {
|
||||
assert(!onHorizontalAxis);
|
||||
moveTo(canvas, size, 1, size.width, currentDistance, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
} else if (side == 2) {
|
||||
assert(onHorizontalAxis);
|
||||
moveTo(canvas, size, 2, size.width - currentDistance, size.height, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
} else if (side == 3) {
|
||||
assert(!onHorizontalAxis);
|
||||
moveTo(canvas, size, 3, 0.0, size.height - currentDistance, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentDistance += distanceBetweenParticles;
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
void moveTo(Canvas canvas, Size size, int side, double x, double y, VoidCallback painter) {
|
||||
canvas.save();
|
||||
canvas.translate(x, y);
|
||||
canvas.rotate(-atan2(size.width / 2 - x, size.height / 2 - y));
|
||||
painter();
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets a child by a given [Offset]
|
||||
class PositionedParticle extends Particle {
|
||||
PositionedParticle({required this.position, required this.child});
|
||||
|
||||
final Particle child;
|
||||
|
||||
final Offset position;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
canvas.translate(position.dx, position.dy);
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates a childs position based on a Tween<Offset>
|
||||
class AnimatedPositionedParticle extends Particle {
|
||||
AnimatedPositionedParticle({required Offset begin, required Offset end, required this.child}) : offsetTween = Tween<Offset>(begin: begin, end: end);
|
||||
|
||||
final Particle child;
|
||||
|
||||
final Tween<Offset> offsetTween;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
canvas.translate(offsetTween.lerp(progress).dx, offsetTween.lerp(progress).dy);
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies an [Interval] for its child.
|
||||
///
|
||||
/// Instead of applying a curve the the input parameters of the paint method,
|
||||
/// apply it with this Particle.
|
||||
///
|
||||
/// If you want you child to only animate from 0.0 - 0.5 (relative), specify an [Interval] with those values.
|
||||
class IntervalParticle extends Particle {
|
||||
final Interval interval;
|
||||
|
||||
final Particle child;
|
||||
|
||||
IntervalParticle({required this.child, required this.interval});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
if (progress < interval.begin || progress > interval.end) return;
|
||||
child.paint(canvas, size, interval.transform(progress), seed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Does nothing else than holding a list of particles and painting them in that order
|
||||
class CompositeParticle extends Particle {
|
||||
final List<Particle> children;
|
||||
|
||||
CompositeParticle({required this.children});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
for (Particle particle in children) {
|
||||
particle.paint(canvas, size, progress, seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A particle which rotates the child.
|
||||
///
|
||||
/// Does not animate.
|
||||
class RotationParticle extends Particle {
|
||||
final Particle child;
|
||||
|
||||
final double rotation;
|
||||
|
||||
RotationParticle({required this.child, required this.rotation});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(rotation);
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// A particle which rotates a child along a given [Tween]
|
||||
class AnimatedRotationParticle extends Particle {
|
||||
final Particle child;
|
||||
|
||||
final Tween<double> rotation;
|
||||
|
||||
AnimatedRotationParticle({required this.child, required double begin, required double end}) : rotation = Tween<double>(begin: begin, end: end);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(rotation.lerp(progress));
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Geometry
|
||||
///
|
||||
/// These are some basic geometric classes which also fade out as time goes on.
|
||||
/// Each primitive should draw itself at the origin. If the orientation matters it should be directed to the top
|
||||
/// (negative y)
|
||||
///
|
||||
/// A rectangle which also fades out over time.
|
||||
class FadingRect extends Particle {
|
||||
final Color color;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
FadingRect({required this.color, required this.width, required this.height});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.drawRect(Rect.fromLTWH(0.0, 0.0, width, height), Paint()..color = color.withOpacity(1 - progress));
|
||||
}
|
||||
}
|
||||
|
||||
/// A circle which fades out over time
|
||||
class FadingCircle extends Particle {
|
||||
final Color color;
|
||||
final double radius;
|
||||
|
||||
FadingCircle({required this.color, required this.radius});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.drawCircle(Offset.zero, radius, Paint()..color = color.withOpacity(1 - progress));
|
||||
}
|
||||
}
|
||||
|
||||
/// A triangle which also fades out over time
|
||||
class FadingTriangle extends Particle {
|
||||
/// This controls the shape of the triangle.
|
||||
///
|
||||
/// Value between 0 and 1
|
||||
final double variation;
|
||||
|
||||
final Color color;
|
||||
|
||||
/// The size of the base side of the triangle.
|
||||
final double baseSize;
|
||||
|
||||
/// This is the factor of how much bigger then length than the width is
|
||||
final double heightToBaseFactor;
|
||||
|
||||
FadingTriangle({required this.variation, required this.color, required this.baseSize, required this.heightToBaseFactor});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
Path path = Path();
|
||||
path.moveTo(0.0, 0.0);
|
||||
path.lineTo(baseSize * variation, baseSize * heightToBaseFactor);
|
||||
path.lineTo(baseSize, 0.0);
|
||||
path.close();
|
||||
canvas.drawPath(path, Paint()..color = color.withOpacity(1 - progress));
|
||||
}
|
||||
}
|
||||
|
||||
/// An ugly looking "snake"
|
||||
///
|
||||
/// See for yourself
|
||||
class FadingSnake extends Particle {
|
||||
final double width;
|
||||
final double segmentLength;
|
||||
final int segments;
|
||||
final double curvyness;
|
||||
|
||||
final Color color;
|
||||
|
||||
FadingSnake({required this.width, required this.segmentLength, required this.segments, required this.curvyness, required this.color});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(pi / 6);
|
||||
Path path = Path();
|
||||
for (int i = 0; i < segments; i++) {
|
||||
path.quadraticBezierTo(curvyness * i, segmentLength * (i + 1), curvyness * (i + 1), segmentLength * (i + 1));
|
||||
}
|
||||
for (int i = segments - 1; i >= 0; i--) {
|
||||
path.quadraticBezierTo(curvyness * (i + 1), segmentLength * i - curvyness, curvyness * i, segmentLength * i - curvyness);
|
||||
}
|
||||
path.close();
|
||||
canvas.drawPath(path, Paint()..color = color);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user