mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-13 17:30:31 -07:00
feature(Desktop): Usability improvements to top carousel
This commit is contained in:
parent
6c71a8e63d
commit
f445d8908b
1 changed files with 174 additions and 122 deletions
|
|
@ -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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue