feature(Desktop): Usability improvements to top carousel

This commit is contained in:
PartyDonut 2025-01-05 14:32:23 +01:00
parent 6c71a8e63d
commit f445d8908b

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
@ -30,9 +31,15 @@ class CarouselBanner extends ConsumerStatefulWidget {
} }
class _CarouselBannerState extends ConsumerState<CarouselBanner> { class _CarouselBannerState extends ConsumerState<CarouselBanner> {
final carouselController = CarouselController();
bool showControls = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ConstrainedBox( return MouseRegion(
onEnter: (event) => setState(() => showControls = true),
onExit: (event) => setState(() => showControls = false),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: widget.maxHeight), constraints: BoxConstraints(maxHeight: widget.maxHeight),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@ -41,16 +48,21 @@ class _CarouselBannerState extends ConsumerState<CarouselBanner> {
(MediaQuery.sizeOf(context).shortestSide * 0.75).clamp(251.0, double.maxFinite), (MediaQuery.sizeOf(context).shortestSide * 0.75).clamp(251.0, double.maxFinite),
); );
final border = BorderRadius.circular(18); final border = BorderRadius.circular(18);
final itemExtent = widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent;
return Padding( return Padding(
padding: padding: const EdgeInsets.symmetric(horizontal: 4)
const EdgeInsets.symmetric(horizontal: 4).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10), .copyWith(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10),
child: CarouselView( child: Stack(
children: [
CarouselView(
elevation: 3, elevation: 3,
shrinkExtent: 0, shrinkExtent: 0,
controller: carouselController,
shape: RoundedRectangleBorder(borderRadius: border), shape: RoundedRectangleBorder(borderRadius: border),
padding: const EdgeInsets.symmetric(horizontal: 6), padding: const EdgeInsets.symmetric(horizontal: 6),
enableSplash: false, enableSplash: false,
itemExtent: widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent, itemExtent: itemExtent,
children: [ children: [
...widget.items.mapIndexed( ...widget.items.mapIndexed(
(index, item) => LayoutBuilder(builder: (context, constraints) { (index, item) => LayoutBuilder(builder: (context, constraints) {
@ -97,7 +109,8 @@ class _CarouselBannerState extends ConsumerState<CarouselBanner> {
maxLines: 2, maxLines: 2,
softWrap: item.title.length > 25, softWrap: item.title.length > 25,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), style:
Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white),
), ),
if (item.label(context) != null || item.subText != null) if (item.label(context) != null || item.subText != null)
Text( Text(
@ -133,8 +146,8 @@ class _CarouselBannerState extends ConsumerState<CarouselBanner> {
? null ? null
: (details) async { : (details) async {
Offset localPosition = details.globalPosition; Offset localPosition = details.globalPosition;
RelativeRect position = RelativeRect.fromLTRB( RelativeRect position = RelativeRect.fromLTRB(localPosition.dx - 320,
localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy); localPosition.dy, localPosition.dx, localPosition.dy);
final poster = widget.items[index]; final poster = widget.items[index];
await showMenu( await showMenu(
@ -161,9 +174,48 @@ class _CarouselBannerState extends ConsumerState<CarouselBanner> {
) )
], ],
), ),
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer)
AnimatedOpacity(
duration: const Duration(milliseconds: 250),
opacity: showControls ? 1 : 0,
child: IgnorePointer(
ignoring: !showControls,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: Alignment.center,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton.filledTonal(
onPressed: () {
final currentPos = carouselController.position;
carouselController.animateTo(currentPos.pixels - itemExtent,
curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: 250));
},
icon: const Icon(IconsaxOutline.arrow_left_2),
),
IconButton.filledTonal(
onPressed: () {
final currentPos = carouselController.position;
carouselController.animateTo(currentPos.pixels + itemExtent,
curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: 250));
},
icon: const Icon(IconsaxOutline.arrow_right_3),
),
],
),
),
),
),
),
],
),
); );
}, },
), ),
),
); );
} }
} }