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,139 +31,190 @@ 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(
constraints: BoxConstraints(maxHeight: widget.maxHeight), onEnter: (event) => setState(() => showControls = true),
child: LayoutBuilder( onExit: (event) => setState(() => showControls = false),
builder: (context, constraints) { child: ConstrainedBox(
final maxExtent = (constraints.maxHeight * 2.1).clamp( constraints: BoxConstraints(maxHeight: widget.maxHeight),
250.0, child: LayoutBuilder(
(MediaQuery.sizeOf(context).shortestSide * 0.75).clamp(251.0, double.maxFinite), builder: (context, constraints) {
); final maxExtent = (constraints.maxHeight * 2.1).clamp(
final border = BorderRadius.circular(18); 250.0,
return Padding( (MediaQuery.sizeOf(context).shortestSide * 0.75).clamp(251.0, double.maxFinite),
padding: );
const EdgeInsets.symmetric(horizontal: 4).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10), final border = BorderRadius.circular(18);
child: CarouselView( final itemExtent = widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent;
elevation: 3,
shrinkExtent: 0, return Padding(
shape: RoundedRectangleBorder(borderRadius: border), padding: const EdgeInsets.symmetric(horizontal: 4)
padding: const EdgeInsets.symmetric(horizontal: 6), .copyWith(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10),
enableSplash: false, child: Stack(
itemExtent: widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent, children: [
children: [ CarouselView(
...widget.items.mapIndexed( elevation: 3,
(index, item) => LayoutBuilder(builder: (context, constraints) { shrinkExtent: 0,
final opacity = (constraints.maxWidth / maxExtent); controller: carouselController,
return Stack( shape: RoundedRectangleBorder(borderRadius: border),
clipBehavior: Clip.none, padding: const EdgeInsets.symmetric(horizontal: 6),
children: [ enableSplash: false,
FladderImage(image: item.bannerImage), itemExtent: itemExtent,
Opacity( children: [
opacity: opacity.clamp(0, 1), ...widget.items.mapIndexed(
child: Stack( (index, item) => LayoutBuilder(builder: (context, constraints) {
final opacity = (constraints.maxWidth / maxExtent);
return Stack(
clipBehavior: Clip.none,
children: [ children: [
Positioned.fill( FladderImage(image: item.bannerImage),
child: Container( Opacity(
decoration: BoxDecoration( opacity: opacity.clamp(0, 1),
gradient: LinearGradient( child: Stack(
begin: Alignment.bottomLeft, children: [
end: Alignment.topCenter, Positioned.fill(
colors: [ child: Container(
ThemesData.of(context) decoration: BoxDecoration(
.dark gradient: LinearGradient(
.colorScheme begin: Alignment.bottomLeft,
.primaryContainer end: Alignment.topCenter,
.withValues(alpha: 0.85), colors: [
Colors.transparent, ThemesData.of(context)
], .dark
.colorScheme
.primaryContainer
.withValues(alpha: 0.85),
Colors.transparent,
],
),
),
),
), ),
],
),
),
Align(
alignment: Alignment.bottomLeft,
child: Padding(
padding: const EdgeInsets.all(16.0).copyWith(right: constraints.maxWidth * 0.2),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
maxLines: 2,
softWrap: item.title.length > 25,
overflow: TextOverflow.fade,
style:
Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white),
),
if (item.label(context) != null || item.subText != null)
Text(
item.label(context) ?? item.subText ?? "",
maxLines: 2,
softWrap: false,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white),
),
].addInBetween(const SizedBox(height: 4)),
), ),
), ),
), ),
], FlatButton(
), onTap: () => widget.items[index].navigateTo(context),
), onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer
Align( ? null
alignment: Alignment.bottomLeft, : () {
child: Padding( final poster = widget.items[index];
padding: const EdgeInsets.all(16.0).copyWith(right: constraints.maxWidth * 0.2), showBottomSheetPill(
child: Column( context: context,
mainAxisSize: MainAxisSize.min, item: poster,
crossAxisAlignment: CrossAxisAlignment.start, content: (scrollContext, scrollController) => ListView(
children: [ shrinkWrap: true,
Text( controller: scrollController,
item.title, children: poster
maxLines: 2, .generateActions(context, ref)
softWrap: item.title.length > 25, .listTileItems(scrollContext, useIcons: true),
overflow: TextOverflow.fade, ),
style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), );
},
onSecondaryTapDown: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
? null
: (details) async {
Offset localPosition = details.globalPosition;
RelativeRect position = RelativeRect.fromLTRB(localPosition.dx - 320,
localPosition.dy, localPosition.dx, localPosition.dy);
final poster = widget.items[index];
await showMenu(
context: context,
position: position,
items: poster.generateActions(context, ref).popupMenuItems(useIcons: true),
);
},
),
BannerPlayButton(item: widget.items[index]),
IgnorePointer(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1.0,
),
borderRadius: border),
), ),
if (item.label(context) != null || item.subText != null) ),
Text( ],
item.label(context) ?? item.subText ?? "", );
maxLines: 2, }),
softWrap: false, )
overflow: TextOverflow.fade, ],
style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white), ),
), if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer)
].addInBetween(const SizedBox(height: 4)), 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),
),
],
), ),
), ),
), ),
FlatButton( ),
onTap: () => widget.items[index].navigateTo(context), ),
onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer ],
? null ),
: () { );
final poster = widget.items[index]; },
showBottomSheetPill( ),
context: context,
item: poster,
content: (scrollContext, scrollController) => ListView(
shrinkWrap: true,
controller: scrollController,
children: poster
.generateActions(context, ref)
.listTileItems(scrollContext, useIcons: true),
),
);
},
onSecondaryTapDown: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
? null
: (details) async {
Offset localPosition = details.globalPosition;
RelativeRect position = RelativeRect.fromLTRB(
localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy);
final poster = widget.items[index];
await showMenu(
context: context,
position: position,
items: poster.generateActions(context, ref).popupMenuItems(useIcons: true),
);
},
),
BannerPlayButton(item: widget.items[index]),
IgnorePointer(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1.0,
),
borderRadius: border),
),
),
],
);
}),
)
],
),
);
},
), ),
); );
} }