1
0
forked from firka/firka

fix + ref: nav tap animation

This commit is contained in:
checkedear
2026-03-26 20:03:18 +01:00
parent 68035140b9
commit e732168a1b

View File

@@ -287,7 +287,7 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
active = -1; active = -1;
const double cardWidth = 40.0; const double cardWidth = 40.0;
const double spacing = 8.0; const double spacing = 18.0;
final double totalCardWidth = cardWidth + spacing; final double totalCardWidth = cardWidth + spacing;
// Calculate animation positions based on real display indices // Calculate animation positions based on real display indices
@@ -365,193 +365,167 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
} }
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
if (lessons != null && tests != null && events != null && dates != null) { if (lessons == null || tests == null || events == null || dates == null) {
List<Widget> ttWidgets = []; return Column(
List<Widget> ttDays = []; mainAxisAlignment: MainAxisAlignment.center,
final showABTimetable = widget.data.settings children: [
.group("settings") Row(
.subGroup("timetable_toast") mainAxisAlignment: MainAxisAlignment.center,
.boolean("ab_timetable"); children: [DelayedSpinnerWidget()],
// Build navigation icons using original dates ),
for (var i = 0; i < dates!.length; i++) { ],
final date = dates![i]; );
final realIndex = i; // Always use real index for nav icons }
final testsOnDate = tests! List<Widget> ttWidgets = [];
.where( List<Widget> ttDays = [];
(test) => final showABTimetable = widget.data.settings
test.date.isAfter(date.subtract(Duration(seconds: 1))) && .group("settings")
test.date.isBefore( .subGroup("timetable_toast")
date.add(Duration(hours: 23, minutes: 59)), .boolean("ab_timetable");
), // Build navigation icons using original dates
) for (var i = 0; i < dates!.length; i++) {
.toList(); final date = dates![i];
final realIndex = i; // Always use real index for nav icons
if (testsOnDate.isNotEmpty) { final testsOnDate = tests!
ttWidgets.add( .where(
Stack( (test) =>
children: [ test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
BottomTimeTableNavIconWidget( test.date.isBefore(date.add(Duration(hours: 23, minutes: 59))),
widget.data.l10n, )
() { .toList();
_handleNavTap(active, realIndex);
},
active == i,
date,
),
Transform.translate(
offset: Offset(38, -10),
child: BubbleTest(),
),
],
),
);
} else {
ttWidgets.add(
BottomTimeTableNavIconWidget(
widget.data.l10n,
() {
_handleNavTap(active, realIndex);
},
active == i,
date,
),
);
}
}
// Build carousel pages using animation dates Widget ttWidget = BottomTimeTableNavIconWidget(
for (var i = 0; i < _animationDates!.length; i++) { widget.data.l10n,
final date = _animationDates![i]; () {
_handleNavTap(active, realIndex);
final lessonsOnDate = lessons! },
.where( active == i,
(lesson) => date,
lesson.start.isAfter(date) && );
lesson.end.isBefore(date.add(Duration(hours: 24))), if (testsOnDate.isNotEmpty) {
) ttWidgets.add(
.toList(); Stack(
final lessonsOnDay = lessons! children: [
.where( ttWidget,
(lesson) => Transform.translate(offset: Offset(38, -10), child: BubbleTest()),
lesson.start.getMidnight().millisecondsSinceEpoch == ],
date.getMidnight().millisecondsSinceEpoch,
)
.toList();
final eventsOnDate = events!
.where(
(lesson) =>
lesson.start.isAfter(date.subtract(Duration(seconds: 1))) &&
lesson.end.isBefore(
date.add(Duration(hours: 23, minutes: 59)),
),
)
.toList();
final testsOnDate = tests!
.where(
(test) =>
test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
test.date.isBefore(
date.add(Duration(hours: 23, minutes: 59)),
),
)
.toList();
ttDays.add(
TimeTableDayWidget(
widget.data,
date,
lessons!,
lessonsOnDate,
eventsOnDate,
testsOnDate,
lessonsOnDay,
), ),
); );
} else {
ttWidgets.add(ttWidget);
} }
}
List<Widget> ttEmptyCards = List.empty(growable: true); // Build carousel pages using animation dates
for (var i = 0; i < _animationDates!.length; i++) {
final date = _animationDates![i];
if (animating || _showAnimatedCard) { final lessonsOnDate = lessons!
for (var i = 0; i < ttDays.length; i++) { .where(
if (i == 0) { (lesson) =>
Widget cardWidget = Card( lesson.start.isAfter(date) &&
lesson.end.isBefore(date.add(Duration(hours: 24))),
)
.toList();
final lessonsOnDay = lessons!
.where(
(lesson) =>
lesson.start.getMidnight().millisecondsSinceEpoch ==
date.getMidnight().millisecondsSinceEpoch,
)
.toList();
final eventsOnDate = events!
.where(
(lesson) =>
lesson.start.isAfter(date.subtract(Duration(seconds: 1))) &&
lesson.end.isBefore(date.add(Duration(hours: 23, minutes: 59))),
)
.toList();
final testsOnDate = tests!
.where(
(test) =>
test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
test.date.isBefore(date.add(Duration(hours: 23, minutes: 59))),
)
.toList();
ttDays.add(
TimeTableDayWidget(
widget.data,
date,
lessons!,
lessonsOnDate,
eventsOnDate,
testsOnDate,
lessonsOnDay,
),
);
}
Widget ttAnimatedCard = Card(
color: Colors.transparent,
shadowColor: Colors.transparent,
child: SizedBox(width: 40, height: 54),
);
if (_cardOffsetAnimation != null && _showAnimatedCard) {
ttAnimatedCard = AnimatedBuilder(
animation: _cardOffsetAnimation!,
builder: (context, child) {
return Transform.translate(
offset: _cardOffsetAnimation!.value,
child: Card(
color: appStyle.colors.buttonSecondaryFill, color: appStyle.colors.buttonSecondaryFill,
shadowColor: Colors.transparent, shadowColor: Colors.transparent,
child: SizedBox(width: 40, height: 54), child: SizedBox(width: 40, height: 54),
); ),
);
},
);
}
if (_showAnimatedCard && _cardOffsetAnimation != null) { return Stack(
ttEmptyCards.add( children: [
AnimatedBuilder( Column(
animation: _cardOffsetAnimation!, children: [
builder: (context, child) { SizedBox(
return Transform.translate( width: MediaQuery.of(context).size.width,
offset: _cardOffsetAnimation!.value, height: 74 + 16,
child: cardWidget, child: Padding(
); padding: const EdgeInsets.symmetric(horizontal: 20),
}, child: Column(
), children: [
); Row(
} else { mainAxisAlignment: MainAxisAlignment.spaceBetween,
ttEmptyCards.add( children: [
Transform.translate(offset: Offset(0, 0), child: cardWidget), Text(
); widget.data.l10n.timetable,
} style: appStyle.fonts.H_H2.apply(
} else { color: appStyle.colors.textPrimary,
ttEmptyCards.add(
Card(
color: Colors.transparent,
shadowColor: Colors.transparent,
child: SizedBox(width: 40, height: 54),
),
);
}
}
} else {
ttEmptyCards.clear();
}
return Stack(
children: [
Column(
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 74 + 16,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.data.l10n.timetable,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
), ),
Row( ),
children: [ Row(
GestureDetector( children: [
child: Card( GestureDetector(
color: appStyle.colors.buttonSecondaryFill, child: Card(
child: Padding( color: appStyle.colors.buttonSecondaryFill,
padding: const EdgeInsets.all(8), child: Padding(
child: FirkaIconWidget( padding: const EdgeInsets.all(8),
FirkaIconType.majesticons, child: FirkaIconWidget(
Majesticon.tableSolid, FirkaIconType.majesticons,
size: 26.0, Majesticon.tableSolid,
color: appStyle.colors.accent, size: 26.0,
), color: appStyle.colors.accent,
), ),
), ),
onTap: () {
context.push('/timetable/monthly');
},
), ),
/* TODO: 1.1.0 onTap: () {
context.push('/timetable/monthly');
},
),
/* TODO: 1.1.0
Card( Card(
color: appStyle.colors.buttonSecondaryFill, color: appStyle.colors.buttonSecondaryFill,
@@ -566,205 +540,192 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
), ),
), ),
*/ */
GestureDetector( GestureDetector(
child: Card( child: Card(
color: appStyle.colors.buttonSecondaryFill, color: appStyle.colors.buttonSecondaryFill,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: FirkaIconWidget( child: FirkaIconWidget(
FirkaIconType.majesticons, FirkaIconType.majesticons,
Majesticon.settingsCogSolid, Majesticon.settingsCogSolid,
size: 26.0, size: 26.0,
color: appStyle.colors.accent, color: appStyle.colors.accent,
),
), ),
), ),
onTap: () {
showSettingsSheet(
context,
MediaQuery.of(context).size.height * 0.4,
widget.data,
widget.data.settings
.group("settings")
.subGroup("timetable_toast"),
);
},
),
],
),
],
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 24,
height: 24,
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownLeft",
size: 24,
color: appStyle.colors.accent,
), ),
onTap: () {
showSettingsSheet(
context,
MediaQuery.of(context).size.height * 0.4,
widget.data,
widget.data.settings
.group("settings")
.subGroup("timetable_toast"),
);
},
),
],
),
],
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 24,
height: 24,
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownLeft",
size: 24,
color: appStyle.colors.accent,
), ),
onTap: () async {
var newNow = now!.subtract(Duration(days: 7));
if (!mounted) return;
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForWeek(newNow);
setState(() {
now = newNow;
});
},
), ),
GestureDetector( onTap: () async {
child: Row( var newNow = now!.subtract(Duration(days: 7));
children: [ if (!mounted) return;
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForWeek(newNow);
setState(() {
now = newNow;
});
},
),
GestureDetector(
child: Row(
children: [
Text(
now!.format(
widget.data.l10n,
FormatMode.yyyymmddwedd,
),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(width: 4),
if (showABTimetable) ...[
Text( Text(
now!.format( "",
widget.data.l10n, style: appStyle.fonts.B_16R.apply(
FormatMode.yyyymmddwedd, color: appStyle.colors.accent,
), ),
),
SizedBox(width: 4),
Text(
now!.isAWeek()
? widget.data.l10n.a_week
: widget.data.l10n.b_week,
style: appStyle.fonts.B_16R.apply( style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary, color: appStyle.colors.textPrimary,
), ),
), ),
SizedBox(width: 4),
if (showABTimetable) ...[
Text(
"",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.accent,
),
),
SizedBox(width: 4),
Text(
now!.isAWeek()
? widget.data.l10n.a_week
: widget.data.l10n.b_week,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
], ],
), ],
onTap: () {
now = timeNow();
setActiveToToday();
_controller.jumpToPage(active);
},
), ),
GestureDetector( onTap: () {
behavior: HitTestBehavior.translucent, now = timeNow();
child: SizedBox( setActiveToToday();
width: 24, _controller.jumpToPage(active);
height: 24, },
child: FirkaIconWidget( ),
FirkaIconType.icons, GestureDetector(
"dropdownRight", behavior: HitTestBehavior.translucent,
size: 24, child: SizedBox(
color: appStyle.colors.accent, width: 24,
), height: 24,
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownRight",
size: 24,
color: appStyle.colors.accent,
), ),
onTap: () async {
var newNow = now!.add(Duration(days: 7));
if (!mounted) return;
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForWeek(newNow);
setState(() {
now = newNow;
});
},
), ),
], onTap: () async {
), var newNow = now!.add(Duration(days: 7));
], if (!mounted) return;
), setState(() {
), now = newNow;
), lessons = null;
], dates = null;
), });
Column( await initForWeek(newNow);
mainAxisAlignment: MainAxisAlignment.end, setState(() {
children: [ now = newNow;
Expanded( });
child: TransparentPointer( },
child: CarouselSlider( ),
items: ttDays, ],
carouselController: _controller,
options: CarouselOptions(
height: MediaQuery.of(context).size.height,
viewportFraction: 1,
enableInfiniteScroll: false,
initialPage: active,
onPageChanged: (i, _) {
if (animating || !mounted) return;
HapticFeedback.mediumImpact();
// Convert animation index to display index
final displayIndex = dates!.indexOf(
_animationDates![i],
);
maybeCacheNextWeek(displayIndex);
maybeCachePreviousWeek(displayIndex);
setState(() {
active = displayIndex;
});
},
),
),
),
),
Container(
padding: EdgeInsets.only(bottom: 12),
decoration: ShapeDecoration(
color: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
shadows: [
BoxShadow(
color: appStyle.colors.background,
blurRadius: 30,
spreadRadius: 20,
offset: Offset(0, -25),
), ),
], ],
), ),
child: Stack( ),
children: [ ),
Wrap(spacing: 10, children: ttEmptyCards), ],
Wrap(spacing: 10, children: ttWidgets), ),
], Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
child: TransparentPointer(
child: CarouselSlider(
items: ttDays,
carouselController: _controller,
options: CarouselOptions(
height: MediaQuery.of(context).size.height,
viewportFraction: 1,
enableInfiniteScroll: false,
initialPage: active,
onPageChanged: (i, _) {
if (animating || !mounted) return;
HapticFeedback.mediumImpact();
// Convert animation index to display index
final displayIndex = dates!.indexOf(_animationDates![i]);
maybeCacheNextWeek(displayIndex);
maybeCachePreviousWeek(displayIndex);
setState(() {
active = displayIndex;
});
},
),
), ),
), ),
], ),
), Container(
], padding: EdgeInsets.only(bottom: 12),
); decoration: ShapeDecoration(
} else { color: Colors.transparent,
return Column( shape: RoundedRectangleBorder(
mainAxisAlignment: MainAxisAlignment.center, borderRadius: BorderRadius.circular(0),
children: [ ),
Row( shadows: [
mainAxisAlignment: MainAxisAlignment.center, BoxShadow(
children: [DelayedSpinnerWidget()], color: appStyle.colors.background,
), blurRadius: 30,
], spreadRadius: 20,
); offset: Offset(0, -25),
} ),
],
),
child: Stack(
children: [
ttAnimatedCard,
Wrap(spacing: 10, children: ttWidgets),
],
),
),
],
),
],
);
} }
} }