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

View File

@@ -1,357 +1,357 @@
// ignore_for_file: dead_code
import 'dart:math';
import 'package:filcnaplo/api/providers/live_card_provider.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:animated_list_plus/animated_list_plus.dart';
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo/api/providers/sync.dart';
import 'package:confetti/confetti.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo_mobile_ui/common/empty.dart';
import 'package:filcnaplo_mobile_ui/common/filter_bar.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_button.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo_mobile_ui/pages/home/live_card/live_card.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_screen.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'home_page.i18n.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo/ui/filter/widgets.dart';
import 'package:filcnaplo/ui/filter/sort.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: 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 = 4;
@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();
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.month == DateTime.december && now.day >= 24 && now.day <= 26) {
greeting = "merryxmas";
} else if (now.month == DateTime.january && now.day == 1) {
greeting = "happynewyear";
} else if (now.hour >= 18) {
greeting = "goodevening";
} else if (now.hour >= 10) {
greeting = "goodafternoon";
} else if (now.hour >= 4) {
greeting = "goodmorning";
} else {
greeting = "goodevening";
}
}
@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();
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
if (!settings.presentationMode) {
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
} else {
firstName = "Béla";
}
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: Text(
greeting.i18n.fill([firstName]),
overflow: TextOverflow.fade,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18.0,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
actions: [
// Profile Icon
Padding(
padding: const EdgeInsets.only(right: 24.0),
child: ProfileButton(
child: ProfileImage(
heroTag: "profile",
name: firstName,
backgroundColor: !settings.presentationMode
? ColorUtils.stringToColor(user.displayName ?? "?")
: Theme.of(context).colorScheme.secondary,
badge: updateProvider.available,
role: user.role,
profilePictureString: user.picture,
),
),
),
],
expandedHeight: _liveCardAnimation.value * 234.0,
// Live Card
flexibleSpace: FlexibleSpaceBar(
background: Padding(
padding: EdgeInsets.only(
left: 24.0,
right: 24.0,
top: 58.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: "Messages".i18n),
Tab(text: "Absences".i18n),
],
controller: _tabController,
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!),
],
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: 4,
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.bottomCenter,
child: ConfettiWidget(
confettiController: _confettiController!,
blastDirection: -pi / 2,
emissionFrequency: 0.01,
numberOfParticles: 80,
maxBlastForce: 100,
minBlastForce: 90,
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),
),
),
);
}
}
// ignore_for_file: dead_code
import 'dart:math';
import 'package:filcnaplo/api/providers/live_card_provider.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:animated_list_plus/animated_list_plus.dart';
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo/api/providers/sync.dart';
import 'package:confetti/confetti.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo_mobile_ui/common/empty.dart';
import 'package:filcnaplo_mobile_ui/common/filter_bar.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_button.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo_mobile_ui/pages/home/live_card/live_card.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_screen.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'home_page.i18n.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo/ui/filter/widgets.dart';
import 'package:filcnaplo/ui/filter/sort.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: 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 = 4;
@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();
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.month == DateTime.december && now.day >= 24 && now.day <= 26) {
greeting = "merryxmas";
} else if (now.month == DateTime.january && now.day == 1) {
greeting = "happynewyear";
} else if (now.hour >= 18) {
greeting = "goodevening";
} else if (now.hour >= 10) {
greeting = "goodafternoon";
} else if (now.hour >= 4) {
greeting = "goodmorning";
} else {
greeting = "goodevening";
}
}
@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();
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
if (!settings.presentationMode) {
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
} else {
firstName = "Béla";
}
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: Text(
greeting.i18n.fill([firstName]),
overflow: TextOverflow.fade,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18.0,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
actions: [
// Profile Icon
Padding(
padding: const EdgeInsets.only(right: 24.0),
child: ProfileButton(
child: ProfileImage(
heroTag: "profile",
name: firstName,
backgroundColor: !settings.presentationMode
? ColorUtils.stringToColor(user.displayName ?? "?")
: Theme.of(context).colorScheme.secondary,
badge: updateProvider.available,
role: user.role,
profilePictureString: user.picture,
),
),
),
],
expandedHeight: _liveCardAnimation.value * 234.0,
// Live Card
flexibleSpace: FlexibleSpaceBar(
background: Padding(
padding: EdgeInsets.only(
left: 24.0,
right: 24.0,
top: 58.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: "Messages".i18n),
Tab(text: "Absences".i18n),
],
controller: _tabController,
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!),
],
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: 4,
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.bottomCenter,
child: ConfettiWidget(
confettiController: _confettiController!,
blastDirection: -pi / 2,
emissionFrequency: 0.01,
numberOfParticles: 80,
maxBlastForce: 100,
minBlastForce: 90,
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),
),
),
);
}
}

View File

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

View File

@@ -1,102 +1,102 @@
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({Key? key, required this.maxTime, required this.elapsedTime}) : super(key: key);
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),
),
],
),
),
);
}
}
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({Key? key, required this.maxTime, required this.elapsedTime}) : super(key: key);
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),
),
],
),
),
);
}
}

View File

@@ -1,197 +1,197 @@
import 'package:animations/animations.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/icons/filc_icons.dart';
import 'package:filcnaplo_mobile_ui/pages/home/live_card/heads_up_countdown.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo/api/providers/live_card_provider.dart';
import 'package:filcnaplo_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:provider/provider.dart';
import 'live_card.i18n.dart';
class LiveCard extends StatefulWidget {
const LiveCard({Key? key}) : super(key: key);
@override
_LiveCardState createState() => _LiveCardState();
}
class _LiveCardState 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);
if (!liveCard.show) return Container();
Widget child;
Duration bellDelay = liveCard.delay;
switch (liveCard.currentState) {
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,
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 ? 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,
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 ?? false,
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 ?? false,
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,
child: child,
fillColor: Theme.of(context).scaffoldBackgroundColor,
);
},
child: child,
);
}
}
import 'package:animations/animations.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/icons/filc_icons.dart';
import 'package:filcnaplo_mobile_ui/pages/home/live_card/heads_up_countdown.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo/api/providers/live_card_provider.dart';
import 'package:filcnaplo_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:provider/provider.dart';
import 'live_card.i18n.dart';
class LiveCard extends StatefulWidget {
const LiveCard({Key? key}) : super(key: key);
@override
_LiveCardState createState() => _LiveCardState();
}
class _LiveCardState 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);
if (!liveCard.show) return Container();
Widget child;
Duration bellDelay = liveCard.delay;
switch (liveCard.currentState) {
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,
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 ? 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,
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 ?? false,
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 ?? false,
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,
child: child,
fillColor: Theme.of(context).scaffoldBackgroundColor,
);
},
child: child,
);
}
}

View File

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

View File

@@ -1,247 +1,247 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_mobile_ui/common/progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'live_card.i18n.dart';
enum ProgressAccuracy { minutes, seconds }
class LiveCardWidget extends StatefulWidget {
const LiveCardWidget({
Key? key,
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,
}) : super(key: key);
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;
@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),
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: [
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: 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: 11.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!),
)
],
),
),
),
),
),
);
}
}
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_mobile_ui/common/progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'live_card.i18n.dart';
enum ProgressAccuracy { minutes, seconds }
class LiveCardWidget extends StatefulWidget {
const LiveCardWidget({
Key? key,
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,
}) : super(key: key);
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;
@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),
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: [
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: 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: 11.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!),
)
],
),
),
),
),
),
);
}
}

View File

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