mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07:00
chore: Improved performance for some widgets (#525)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
10bd34bb20
commit
07972ea5ee
16 changed files with 589 additions and 545 deletions
|
|
@ -72,54 +72,51 @@ class SettingsListTile extends StatelessWidget {
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
minHeight: 50,
|
minHeight: 50,
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: Row(
|
||||||
borderRadius: BorderRadius.circular(12),
|
mainAxisSize: MainAxisSize.max,
|
||||||
child: Row(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.max,
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
DefaultTextStyle.merge(
|
||||||
children: [
|
style: TextStyle(
|
||||||
DefaultTextStyle.merge(
|
color: contentColor ?? Theme.of(context).colorScheme.onSurface,
|
||||||
style: TextStyle(
|
),
|
||||||
|
child: IconTheme(
|
||||||
|
data: IconThemeData(
|
||||||
color: contentColor ?? Theme.of(context).colorScheme.onSurface,
|
color: contentColor ?? Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
child: IconTheme(
|
child: leadingWidget,
|
||||||
data: IconThemeData(
|
|
||||||
color: contentColor ?? Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
child: leadingWidget,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: Column(
|
Expanded(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
textStyle: Theme.of(context).textTheme.titleLarge?.copyWith(color: contentColor),
|
||||||
|
child: label,
|
||||||
|
),
|
||||||
|
if (subLabel != null)
|
||||||
Material(
|
Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
textStyle: Theme.of(context).textTheme.titleLarge?.copyWith(color: contentColor),
|
textStyle: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||||
child: label,
|
color:
|
||||||
|
(contentColor ?? Theme.of(context).colorScheme.onSurface).withValues(alpha: 0.65),
|
||||||
|
),
|
||||||
|
child: subLabel,
|
||||||
),
|
),
|
||||||
if (subLabel != null)
|
],
|
||||||
Opacity(
|
|
||||||
opacity: 0.65,
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
textStyle: Theme.of(context).textTheme.labelLarge?.copyWith(color: contentColor),
|
|
||||||
child: subLabel,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (trailing != null)
|
),
|
||||||
ExcludeFocusTraversal(
|
if (trailing != null)
|
||||||
excluding: onTap != null,
|
ExcludeFocusTraversal(
|
||||||
child: Padding(
|
excluding: onTap != null,
|
||||||
padding: const EdgeInsets.only(left: 16),
|
child: Padding(
|
||||||
child: trailing,
|
padding: const EdgeInsets.only(left: 16),
|
||||||
),
|
child: trailing,
|
||||||
)
|
),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -104,16 +104,30 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
|
||||||
final minHeight = 450.0.clamp(0, size.height).toDouble();
|
final minHeight = 450.0.clamp(0, size.height).toDouble();
|
||||||
final maxHeight = size.height - 10;
|
final maxHeight = size.height - 10;
|
||||||
final sideBarPadding = AdaptiveLayout.of(context).sideBarWidth;
|
final sideBarPadding = AdaptiveLayout.of(context).sideBarWidth;
|
||||||
|
final newColorScheme = dominantColor != null
|
||||||
|
? ColorScheme.fromSeed(
|
||||||
|
seedColor: dominantColor!,
|
||||||
|
brightness: Theme.brightnessOf(context),
|
||||||
|
dynamicSchemeVariant: ref.watch(clientSettingsProvider.select((value) => value.schemeVariant)),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
final amoledBlack = ref.watch(clientSettingsProvider.select((value) => value.amoledBlack));
|
||||||
|
final amoledOverwrite = amoledBlack ? Colors.black : null;
|
||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context).copyWith(
|
data: Theme.of(context)
|
||||||
colorScheme: dominantColor != null
|
.copyWith(
|
||||||
? ColorScheme.fromSeed(
|
colorScheme: newColorScheme,
|
||||||
seedColor: dominantColor!,
|
)
|
||||||
brightness: Theme.brightnessOf(context),
|
.copyWith(
|
||||||
dynamicSchemeVariant: ref.watch(clientSettingsProvider.select((value) => value.schemeVariant)),
|
scaffoldBackgroundColor: amoledOverwrite,
|
||||||
)
|
cardColor: amoledOverwrite,
|
||||||
: null,
|
canvasColor: amoledOverwrite,
|
||||||
),
|
colorScheme: newColorScheme?.copyWith(
|
||||||
|
surface: amoledOverwrite,
|
||||||
|
surfaceContainerHighest: amoledOverwrite,
|
||||||
|
surfaceContainerLow: amoledOverwrite,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Builder(builder: (context) {
|
child: Builder(builder: (context) {
|
||||||
return PullToRefresh(
|
return PullToRefresh(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/item_base_model.dart';
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
import 'package:fladder/screens/shared/flat_button.dart';
|
|
||||||
import 'package:fladder/screens/shared/media/banner_play_button.dart';
|
import 'package:fladder/screens/shared/media/banner_play_button.dart';
|
||||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/fladder_image.dart';
|
import 'package:fladder/util/fladder_image.dart';
|
||||||
|
import 'package:fladder/util/focus_provider.dart';
|
||||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||||
import 'package:fladder/util/list_padding.dart';
|
import 'package:fladder/util/list_padding.dart';
|
||||||
import 'package:fladder/util/themes_data.dart';
|
import 'package:fladder/util/themes_data.dart';
|
||||||
|
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||||
|
|
||||||
|
|
@ -64,148 +65,151 @@ class _CarouselBannerState extends ConsumerState<CarouselBanner> {
|
||||||
itemExtent: itemExtent,
|
itemExtent: itemExtent,
|
||||||
children: [
|
children: [
|
||||||
...widget.items.mapIndexed(
|
...widget.items.mapIndexed(
|
||||||
(index, item) => LayoutBuilder(builder: (context, constraints) {
|
(index, item) => LayoutBuilder(
|
||||||
final opacity = (constraints.maxWidth / maxExtent);
|
builder: (context, constraints) {
|
||||||
return Stack(
|
final opacity = (constraints.maxWidth / maxExtent);
|
||||||
clipBehavior: Clip.none,
|
return FocusButton(
|
||||||
children: [
|
onTap: () => widget.items[index].navigateTo(context),
|
||||||
FladderImage(image: item.bannerImage),
|
onFocusChanged: (hover) {
|
||||||
Opacity(
|
context.ensureVisible();
|
||||||
opacity: opacity.clamp(0, 1),
|
},
|
||||||
child: Stack(
|
onLongPress: AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer
|
||||||
children: [
|
? null
|
||||||
Positioned.fill(
|
: () {
|
||||||
child: Container(
|
final poster = widget.items[index];
|
||||||
decoration: BoxDecoration(
|
showBottomSheetPill(
|
||||||
gradient: LinearGradient(
|
context: context,
|
||||||
begin: Alignment.bottomLeft,
|
item: poster,
|
||||||
end: Alignment.topCenter,
|
content: (scrollContext, scrollController) => ListView(
|
||||||
colors: [
|
shrinkWrap: true,
|
||||||
ThemesData.of(context)
|
controller: scrollController,
|
||||||
.dark
|
children: poster
|
||||||
.colorScheme
|
.generateActions(context, ref)
|
||||||
.primaryContainer
|
.listTileItems(scrollContext, useIcons: true),
|
||||||
.withValues(alpha: 0.85),
|
|
||||||
Colors.transparent,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSecondaryTapDown: AdaptiveLayout.inputDeviceOf(context) == InputDevice.touch
|
||||||
|
? null
|
||||||
|
: (details) async {
|
||||||
|
Offset localPosition = details.globalPosition;
|
||||||
|
RelativeRect position = RelativeRect.fromLTRB(
|
||||||
|
localPosition.dx, 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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
FladderImage(image: item.bannerImage),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomLeft,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
ThemesData.of(context)
|
||||||
|
.dark
|
||||||
|
.colorScheme
|
||||||
|
.primaryContainer
|
||||||
|
.withValues(alpha: opacity.clamp(0, 1)),
|
||||||
|
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)),
|
|
||||||
),
|
),
|
||||||
),
|
Align(
|
||||||
),
|
alignment: Alignment.bottomLeft,
|
||||||
FlatButton(
|
child: Padding(
|
||||||
onTap: () => widget.items[index].navigateTo(context),
|
padding: const EdgeInsets.all(16.0).copyWith(right: constraints.maxWidth * 0.2),
|
||||||
onLongPress: AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer
|
child: Column(
|
||||||
? null
|
mainAxisSize: MainAxisSize.min,
|
||||||
: () {
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
final poster = widget.items[index];
|
children: [
|
||||||
showBottomSheetPill(
|
Text(
|
||||||
context: context,
|
item.title,
|
||||||
item: poster,
|
maxLines: 2,
|
||||||
content: (scrollContext, scrollController) => ListView(
|
softWrap: item.title.length > 25,
|
||||||
shrinkWrap: true,
|
overflow: TextOverflow.fade,
|
||||||
controller: scrollController,
|
style: Theme.of(context)
|
||||||
children: poster
|
.textTheme
|
||||||
.generateActions(context, ref)
|
.headlineMedium
|
||||||
.listTileItems(scrollContext, useIcons: true),
|
?.copyWith(color: Colors.white),
|
||||||
),
|
),
|
||||||
);
|
if (item.label(context) != null || item.subText != null)
|
||||||
},
|
Text(
|
||||||
onSecondaryTapDown: AdaptiveLayout.inputDeviceOf(context) == InputDevice.touch
|
item.label(context) ?? item.subText ?? "",
|
||||||
? null
|
maxLines: 2,
|
||||||
: (details) async {
|
softWrap: false,
|
||||||
Offset localPosition = details.globalPosition;
|
overflow: TextOverflow.fade,
|
||||||
RelativeRect position = RelativeRect.fromLTRB(
|
style: Theme.of(context)
|
||||||
localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
.textTheme
|
||||||
final poster = widget.items[index];
|
.titleMedium
|
||||||
|
?.copyWith(color: Colors.white),
|
||||||
await showMenu(
|
),
|
||||||
context: context,
|
].addInBetween(const SizedBox(height: 4)),
|
||||||
position: position,
|
|
||||||
items: poster.generateActions(context, ref).popupMenuItems(useIcons: true),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ExcludeFocus(
|
|
||||||
child: 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),
|
),
|
||||||
),
|
),
|
||||||
|
ExcludeFocus(
|
||||||
|
child: 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 (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer)
|
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer)
|
||||||
AnimatedOpacity(
|
ExcludeFocus(
|
||||||
duration: const Duration(milliseconds: 250),
|
child: AnimatedOpacity(
|
||||||
opacity: showControls ? 1 : 0,
|
duration: const Duration(milliseconds: 250),
|
||||||
child: IgnorePointer(
|
opacity: showControls ? 1 : 0,
|
||||||
ignoring: !showControls,
|
child: IgnorePointer(
|
||||||
child: Padding(
|
ignoring: !showControls,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
child: Padding(
|
||||||
child: Align(
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
alignment: Alignment.center,
|
child: Align(
|
||||||
child: Row(
|
alignment: Alignment.center,
|
||||||
mainAxisSize: MainAxisSize.max,
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
IconButton.filledTonal(
|
children: [
|
||||||
onPressed: () {
|
IconButton.filledTonal(
|
||||||
final currentPos = carouselController.position;
|
onPressed: () {
|
||||||
carouselController.animateTo(currentPos.pixels - itemExtent,
|
final currentPos = carouselController.position;
|
||||||
curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: 250));
|
carouselController.animateTo(currentPos.pixels - itemExtent,
|
||||||
},
|
curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: 250));
|
||||||
icon: const Icon(IconsaxPlusLinear.arrow_left_1),
|
},
|
||||||
),
|
icon: const Icon(IconsaxPlusLinear.arrow_left_1),
|
||||||
IconButton.filledTonal(
|
),
|
||||||
onPressed: () {
|
IconButton.filledTonal(
|
||||||
final currentPos = carouselController.position;
|
onPressed: () {
|
||||||
carouselController.animateTo(currentPos.pixels + itemExtent,
|
final currentPos = carouselController.position;
|
||||||
curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: 250));
|
carouselController.animateTo(currentPos.pixels + itemExtent,
|
||||||
},
|
curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: 250));
|
||||||
icon: const Icon(IconsaxPlusLinear.arrow_right_3),
|
},
|
||||||
),
|
icon: const Icon(IconsaxPlusLinear.arrow_right_3),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class NextUpEpisode extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final alreadyPlayed = nextEpisode.userData.played;
|
final alreadyPlayed = nextEpisode.userData.played;
|
||||||
final episodeSummary = nextEpisode.overview.summary.maxLength(limitTo: 250);
|
final episodeSummary = nextEpisode.overview.summary.maxLength(limitTo: 250);
|
||||||
|
final style = Theme.of(context).textTheme.titleMedium;
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -27,11 +28,10 @@ class NextUpEpisode extends ConsumerWidget {
|
||||||
StickyHeaderText(
|
StickyHeaderText(
|
||||||
label: alreadyPlayed ? context.localized.reWatch : context.localized.nextUp,
|
label: alreadyPlayed ? context.localized.reWatch : context.localized.nextUp,
|
||||||
),
|
),
|
||||||
Opacity(
|
SelectableText(
|
||||||
opacity: 0.75,
|
nextEpisode.seasonEpisodeLabelFull(context),
|
||||||
child: SelectableText(
|
style: style?.copyWith(
|
||||||
nextEpisode.seasonEpisodeLabelFull(context),
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.75),
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SelectableText(
|
SelectableText(
|
||||||
|
|
|
||||||
|
|
@ -84,205 +84,269 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
||||||
|
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: tag,
|
tag: tag,
|
||||||
child: Card(
|
child: FocusButton(
|
||||||
elevation: 6,
|
onTap: () => pressedWidget(context),
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
onFocusChanged: widget.onFocusChanged,
|
||||||
shape: RoundedRectangleBorder(
|
onLongPress: () {
|
||||||
side: BorderSide(
|
showBottomSheetPill(
|
||||||
width: 1.0,
|
context: context,
|
||||||
color: Colors.white.withValues(alpha: 0.10),
|
item: widget.poster,
|
||||||
|
content: (scrollContext, scrollController) => ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
controller: scrollController,
|
||||||
|
children: widget.poster
|
||||||
|
.generateActions(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
exclude: widget.excludeActions,
|
||||||
|
otherActions: widget.otherActions,
|
||||||
|
onUserDataChanged: widget.onUserDataChanged,
|
||||||
|
onDeleteSuccesFully: widget.onItemRemoved,
|
||||||
|
onItemUpdated: widget.onItemUpdated,
|
||||||
|
)
|
||||||
|
.listTileItems(scrollContext, useIcons: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSecondaryTapDown: (details) async {
|
||||||
|
Offset localPosition = details.globalPosition;
|
||||||
|
RelativeRect position =
|
||||||
|
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||||
|
await showMenu(
|
||||||
|
context: context,
|
||||||
|
position: position,
|
||||||
|
items: widget.poster
|
||||||
|
.generateActions(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
exclude: widget.excludeActions,
|
||||||
|
otherActions: widget.otherActions,
|
||||||
|
onUserDataChanged: widget.onUserDataChanged,
|
||||||
|
onDeleteSuccesFully: widget.onItemRemoved,
|
||||||
|
onItemUpdated: widget.onItemUpdated,
|
||||||
|
)
|
||||||
|
.popupMenuItems(useIcons: true),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Card(
|
||||||
|
elevation: 6,
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: BorderSide(
|
||||||
|
width: 1.0,
|
||||||
|
color: Colors.white.withValues(alpha: 0.10),
|
||||||
|
),
|
||||||
|
borderRadius: posterRadius,
|
||||||
),
|
),
|
||||||
borderRadius: posterRadius,
|
child: Stack(
|
||||||
),
|
fit: StackFit.expand,
|
||||||
child: Stack(
|
children: [
|
||||||
fit: StackFit.expand,
|
FladderImage(
|
||||||
children: [
|
image: widget.primaryPosters
|
||||||
FladderImage(
|
? widget.poster.images?.primary
|
||||||
image: widget.primaryPosters
|
: widget.poster.getPosters?.primary ?? widget.poster.getPosters?.backDrop?.lastOrNull,
|
||||||
? widget.poster.images?.primary
|
placeHolder: PosterPlaceholder(item: widget.poster),
|
||||||
: widget.poster.getPosters?.primary ?? widget.poster.getPosters?.backDrop?.lastOrNull,
|
|
||||||
placeHolder: PosterPlaceholder(item: widget.poster),
|
|
||||||
),
|
|
||||||
if (poster.userData.progress > 0 && widget.poster.type == FladderItemType.book)
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: Padding(
|
|
||||||
padding: padding,
|
|
||||||
child: Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(5.5),
|
|
||||||
child: Text(
|
|
||||||
context.localized.page((widget.poster as BookModel).currentPage),
|
|
||||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (widget.selected == true)
|
if (poster.userData.progress > 0 && widget.poster.type == FladderItemType.book)
|
||||||
Container(
|
Align(
|
||||||
decoration: BoxDecoration(
|
alignment: Alignment.topLeft,
|
||||||
color: Colors.black.withValues(alpha: 0.15),
|
child: Padding(
|
||||||
border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary),
|
padding: padding,
|
||||||
borderRadius: posterRadius,
|
child: Card(
|
||||||
),
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
width: double.infinity,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(2),
|
padding: const EdgeInsets.all(5.5),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.poster.name,
|
context.localized.page((widget.poster as BookModel).currentPage),
|
||||||
maxLines: 2,
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.bold,
|
||||||
style: Theme.of(context)
|
color: Theme.of(context).colorScheme.primary,
|
||||||
.textTheme
|
fontSize: 12,
|
||||||
.labelMedium
|
|
||||||
?.copyWith(color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (widget.poster.userData.isFavourite)
|
|
||||||
const Row(
|
|
||||||
children: [
|
|
||||||
StatusCard(
|
|
||||||
color: Colors.red,
|
|
||||||
child: Icon(
|
|
||||||
IconsaxPlusBold.heart,
|
|
||||||
size: 21,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if ((poster.userData.progress > 0 && poster.userData.progress < 100) &&
|
|
||||||
widget.poster.type != FladderItemType.book) ...{
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 3).copyWith(bottom: 3).add(padding),
|
|
||||||
child: Card(
|
|
||||||
color: Colors.transparent,
|
|
||||||
elevation: 3,
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
child: LinearProgressIndicator(
|
|
||||||
minHeight: 7.5,
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.onPrimary.withValues(alpha: 0.5),
|
|
||||||
value: poster.userData.progress / 100,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.inlineTitle)
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Text(
|
|
||||||
widget.poster.title.maxLength(limitTo: 25),
|
|
||||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if ((widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel) ||
|
|
||||||
(widget.poster.playAble && !widget.poster.unWatched))
|
|
||||||
IgnorePointer(
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topRight,
|
|
||||||
child: StatusCard(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
useFittedBox: widget.poster.unPlayedItemCount != 0,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(6),
|
|
||||||
child: widget.poster.unPlayedItemCount != 0
|
|
||||||
? Container(
|
|
||||||
constraints: const BoxConstraints(minWidth: 16),
|
|
||||||
child: Text(
|
|
||||||
widget.poster.userData.unPlayedItemCount.toString(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
overflow: TextOverflow.visible,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: Icon(
|
),
|
||||||
Icons.check_rounded,
|
|
||||||
size: 20,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (widget.selected == true)
|
||||||
if (widget.poster.overview.runTime != null &&
|
Container(
|
||||||
((widget.poster is PhotoModel) &&
|
decoration: BoxDecoration(
|
||||||
(widget.poster as PhotoModel).internalType == FladderItemType.video)) ...{
|
color: Colors.black.withValues(alpha: 0.15),
|
||||||
Align(
|
border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary),
|
||||||
alignment: Alignment.topRight,
|
borderRadius: posterRadius,
|
||||||
child: Padding(
|
),
|
||||||
padding: padding,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: Card(
|
child: Stack(
|
||||||
elevation: 5,
|
alignment: Alignment.topCenter,
|
||||||
child: Padding(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
Container(
|
||||||
child: Row(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
mainAxisSize: MainAxisSize.min,
|
width: double.infinity,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(2),
|
||||||
Text(
|
child: Text(
|
||||||
widget.poster.overview.runTime.humanizeSmall ?? "",
|
widget.poster.name,
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
maxLines: 2,
|
||||||
fontWeight: FontWeight.bold,
|
textAlign: TextAlign.center,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
style: Theme.of(context)
|
||||||
),
|
.textTheme
|
||||||
|
.labelMedium
|
||||||
|
?.copyWith(color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 2),
|
),
|
||||||
Icon(
|
)
|
||||||
Icons.play_arrow_rounded,
|
],
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (widget.poster.userData.isFavourite)
|
||||||
|
const Row(
|
||||||
|
children: [
|
||||||
|
StatusCard(
|
||||||
|
color: Colors.red,
|
||||||
|
child: Icon(
|
||||||
|
IconsaxPlusBold.heart,
|
||||||
|
size: 21,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if ((poster.userData.progress > 0 && poster.userData.progress < 100) &&
|
||||||
|
widget.poster.type != FladderItemType.book) ...{
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 3).copyWith(bottom: 3).add(padding),
|
||||||
|
child: Card(
|
||||||
|
color: Colors.transparent,
|
||||||
|
elevation: 3,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
minHeight: 7.5,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.onPrimary.withValues(alpha: 0.5),
|
||||||
|
value: poster.userData.progress / 100,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.inlineTitle)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text(
|
||||||
|
widget.poster.title.maxLength(limitTo: 25),
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
if ((widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel) ||
|
||||||
},
|
(widget.poster.playAble && !widget.poster.unWatched))
|
||||||
FocusButton(
|
IgnorePointer(
|
||||||
onTap: () => pressedWidget(context),
|
child: Align(
|
||||||
onFocusChanged: widget.onFocusChanged,
|
alignment: Alignment.topRight,
|
||||||
onLongPress: () {
|
child: StatusCard(
|
||||||
showBottomSheetPill(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
context: context,
|
useFittedBox: widget.poster.unPlayedItemCount != 0,
|
||||||
item: widget.poster,
|
child: Padding(
|
||||||
content: (scrollContext, scrollController) => ListView(
|
padding: const EdgeInsets.all(6),
|
||||||
shrinkWrap: true,
|
child: widget.poster.unPlayedItemCount != 0
|
||||||
controller: scrollController,
|
? Container(
|
||||||
children: widget.poster
|
constraints: const BoxConstraints(minWidth: 16),
|
||||||
|
child: Text(
|
||||||
|
widget.poster.userData.unPlayedItemCount.toString(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
Icons.check_rounded,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.poster.overview.runTime != null &&
|
||||||
|
((widget.poster is PhotoModel) &&
|
||||||
|
(widget.poster as PhotoModel).internalType == FladderItemType.video)) ...{
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: Card(
|
||||||
|
elevation: 5,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.poster.overview.runTime.humanizeSmall ?? "",
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Icon(
|
||||||
|
Icons.play_arrow_rounded,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
overlays: [
|
||||||
|
//Poster Button
|
||||||
|
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
|
||||||
|
// Play Button
|
||||||
|
if (widget.poster.playAble)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: IconButton.filledTonal(
|
||||||
|
onPressed: () => widget.playVideo?.call(false),
|
||||||
|
icon: const Icon(
|
||||||
|
IconsaxPlusBold.play,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
PopupMenuButton(
|
||||||
|
tooltip: "Options",
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.more_vert,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
itemBuilder: (context) => widget.poster
|
||||||
.generateActions(
|
.generateActions(
|
||||||
context,
|
context,
|
||||||
ref,
|
ref,
|
||||||
|
|
@ -292,76 +356,13 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
||||||
onDeleteSuccesFully: widget.onItemRemoved,
|
onDeleteSuccesFully: widget.onItemRemoved,
|
||||||
onItemUpdated: widget.onItemUpdated,
|
onItemUpdated: widget.onItemUpdated,
|
||||||
)
|
)
|
||||||
.listTileItems(scrollContext, useIcons: true),
|
.popupMenuItems(useIcons: true),
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSecondaryTapDown: (details) async {
|
|
||||||
Offset localPosition = details.globalPosition;
|
|
||||||
RelativeRect position =
|
|
||||||
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
|
||||||
await showMenu(
|
|
||||||
context: context,
|
|
||||||
position: position,
|
|
||||||
items: widget.poster
|
|
||||||
.generateActions(
|
|
||||||
context,
|
|
||||||
ref,
|
|
||||||
exclude: widget.excludeActions,
|
|
||||||
otherActions: widget.otherActions,
|
|
||||||
onUserDataChanged: widget.onUserDataChanged,
|
|
||||||
onDeleteSuccesFully: widget.onItemRemoved,
|
|
||||||
onItemUpdated: widget.onItemUpdated,
|
|
||||||
)
|
|
||||||
.popupMenuItems(useIcons: true),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
overlays: [
|
|
||||||
//Poster Button
|
|
||||||
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
|
|
||||||
// Play Button
|
|
||||||
if (widget.poster.playAble)
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: IconButton.filledTonal(
|
|
||||||
onPressed: () => widget.playVideo?.call(false),
|
|
||||||
icon: const Icon(
|
|
||||||
IconsaxPlusBold.play,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
PopupMenuButton(
|
|
||||||
tooltip: "Options",
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.more_vert,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
itemBuilder: (context) => widget.poster
|
|
||||||
.generateActions(
|
|
||||||
context,
|
|
||||||
ref,
|
|
||||||
exclude: widget.excludeActions,
|
|
||||||
otherActions: widget.otherActions,
|
|
||||||
onUserDataChanged: widget.onUserDataChanged,
|
|
||||||
onDeleteSuccesFully: widget.onItemRemoved,
|
|
||||||
onItemUpdated: widget.onItemUpdated,
|
|
||||||
)
|
|
||||||
.popupMenuItems(useIcons: true),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ class PosterPlaceholder extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final color = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.75);
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -15,7 +16,10 @@ class PosterPlaceholder extends StatelessWidget {
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: Opacity(opacity: 0.5, child: Icon(item.type.icon)),
|
child: Icon(
|
||||||
|
item.type.icon,
|
||||||
|
color: color.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
|
@ -34,15 +38,14 @@ class PosterPlaceholder extends StatelessWidget {
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
),
|
),
|
||||||
if (item.label(context) != null) ...[
|
if (item.label(context) != null) ...[
|
||||||
Opacity(
|
Text(
|
||||||
opacity: 0.75,
|
item.label(context)!,
|
||||||
child: Text(
|
maxLines: 2,
|
||||||
item.label(context)!,
|
textAlign: TextAlign.center,
|
||||||
maxLines: 2,
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
textAlign: TextAlign.center,
|
color: color.withValues(alpha: 0.75),
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
),
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ class EpisodeDetailsList extends ConsumerWidget {
|
||||||
((AdaptiveLayout.poster(context).gridRatio * 2) *
|
((AdaptiveLayout.poster(context).gridRatio * 2) *
|
||||||
ref.watch(clientSettingsProvider.select((value) => value.posterSize)));
|
ref.watch(clientSettingsProvider.select((value) => value.posterSize)));
|
||||||
final decimals = size - size.toInt();
|
final decimals = size - size.toInt();
|
||||||
|
final textStyle = Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65),
|
||||||
|
);
|
||||||
return AnimatedSwitcher(
|
return AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
child: switch (viewType) {
|
child: switch (viewType) {
|
||||||
|
|
@ -73,20 +76,14 @@ class EpisodeDetailsList extends ConsumerWidget {
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Opacity(
|
SelectableText(
|
||||||
opacity: 0.65,
|
episode.seasonEpisodeLabel(context),
|
||||||
child: SelectableText(
|
style: textStyle,
|
||||||
episode.seasonEpisodeLabel(context),
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (episode.overview.runTime != null)
|
if (episode.overview.runTime != null)
|
||||||
Opacity(
|
SelectableText(
|
||||||
opacity: 0.65,
|
" - ${episode.overview.runTime!.humanize!}",
|
||||||
child: SelectableText(
|
style: textStyle,
|
||||||
" - ${episode.overview.runTime!.humanize!}",
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -149,13 +149,10 @@ class PosterListItem extends ConsumerWidget {
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
if ((poster.subText ?? poster.subTextShort(context))?.isNotEmpty == true)
|
if ((poster.subText ?? poster.subTextShort(context))?.isNotEmpty == true)
|
||||||
Opacity(
|
Text(
|
||||||
opacity: 0.45,
|
poster.subText ?? poster.subTextShort(context) ?? "",
|
||||||
child: Text(
|
maxLines: 1,
|
||||||
poster.subText ?? poster.subTextShort(context) ?? "",
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -96,10 +96,7 @@ class PosterWidget extends ConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
if (subTitle != null) ...[
|
if (subTitle != null) ...[
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Opacity(
|
child: subTitle!,
|
||||||
opacity: opacity,
|
|
||||||
child: subTitle!,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (poster.subText?.isNotEmpty ?? false)
|
if (poster.subText?.isNotEmpty ?? false)
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ class _OutlinedTextFieldState extends ConsumerState<OutlinedTextField> {
|
||||||
child: KeyboardListener(
|
child: KeyboardListener(
|
||||||
focusNode: _wrapperFocus,
|
focusNode: _wrapperFocus,
|
||||||
onKeyEvent: (KeyEvent event) {
|
onKeyEvent: (KeyEvent event) {
|
||||||
if (keyboardFocus) return;
|
if (keyboardFocus || AdaptiveLayout.inputDeviceOf(context) != InputDevice.dPad) return;
|
||||||
if (event is KeyDownEvent && acceptKeys.contains(event.logicalKey)) {
|
if (event is KeyDownEvent && acceptKeys.contains(event.logicalKey)) {
|
||||||
if (_textFocus.hasFocus) {
|
if (_textFocus.hasFocus) {
|
||||||
_wrapperFocus.requestFocus();
|
_wrapperFocus.requestFocus();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/account_model.dart';
|
import 'package:fladder/models/account_model.dart';
|
||||||
import 'package:fladder/screens/shared/flat_button.dart';
|
import 'package:fladder/screens/shared/flat_button.dart';
|
||||||
import 'package:fladder/theme.dart';
|
|
||||||
import 'package:fladder/util/string_extensions.dart';
|
import 'package:fladder/util/string_extensions.dart';
|
||||||
|
|
||||||
class UserIcon extends ConsumerWidget {
|
class UserIcon extends ConsumerWidget {
|
||||||
|
|
@ -18,7 +17,7 @@ class UserIcon extends ConsumerWidget {
|
||||||
const UserIcon({
|
const UserIcon({
|
||||||
this.size = const Size(50, 50),
|
this.size = const Size(50, 50),
|
||||||
this.labelStyle,
|
this.labelStyle,
|
||||||
this.cornerRadius = 5,
|
this.cornerRadius = 16,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
required this.user,
|
required this.user,
|
||||||
|
|
@ -46,25 +45,23 @@ class UserIcon extends ConsumerWidget {
|
||||||
tag: Key(user?.id ?? "empty-user-avatar"),
|
tag: Key(user?.id ?? "empty-user-avatar"),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: Card(
|
child: Container(
|
||||||
elevation: 0,
|
decoration: BoxDecoration(
|
||||||
surfaceTintColor: Colors.transparent,
|
borderRadius: BorderRadius.circular(cornerRadius),
|
||||||
color: Colors.transparent,
|
),
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: SizedBox.fromSize(
|
child: SizedBox.fromSize(
|
||||||
size: size,
|
size: size,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
CachedNetworkImage(
|
||||||
borderRadius: FladderTheme.smallShape.borderRadius,
|
imageUrl: user?.avatar ?? "",
|
||||||
child: CachedNetworkImage(
|
progressIndicatorBuilder: (context, url, progress) => placeHolder(),
|
||||||
imageUrl: user?.avatar ?? "",
|
errorWidget: (context, url, error) => placeHolder(),
|
||||||
progressIndicatorBuilder: (context, url, progress) => placeHolder(),
|
memCacheHeight: 128,
|
||||||
errorWidget: (context, url, error) => placeHolder(),
|
fit: BoxFit.cover,
|
||||||
memCacheHeight: 128,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
FlatButton(
|
FlatButton(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,8 @@ class FocusButtonState extends State<FocusButton> {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.primaryContainer
|
.surfaceContainerLowest
|
||||||
.withValues(alpha: widget.darkOverlay ? 0.1 : 0),
|
.withValues(alpha: widget.darkOverlay ? 0.35 : 0),
|
||||||
border: Border.all(width: 3, color: Theme.of(context).colorScheme.onPrimaryContainer),
|
border: Border.all(width: 3, color: Theme.of(context).colorScheme.onPrimaryContainer),
|
||||||
borderRadius: widget.borderRadius ?? FladderTheme.smallShape.borderRadius,
|
borderRadius: widget.borderRadius ?? FladderTheme.smallShape.borderRadius,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class SettingsUserIcon extends ConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
UserIcon(
|
UserIcon(
|
||||||
user: user,
|
user: user,
|
||||||
cornerRadius: 200,
|
cornerRadius: 8,
|
||||||
),
|
),
|
||||||
if (hasNewUpdate)
|
if (hasNewUpdate)
|
||||||
Transform.translate(
|
Transform.translate(
|
||||||
|
|
|
||||||
|
|
@ -103,16 +103,16 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||||
if (expandedSideBar) ...[
|
if (expandedSideBar) ...[
|
||||||
Expanded(child: Text(context.localized.navigation)),
|
Expanded(child: Text(context.localized.navigation)),
|
||||||
],
|
],
|
||||||
Opacity(
|
IconButton(
|
||||||
opacity: largeBar && expandedSideBar ? 0.65 : 1.0,
|
onPressed: !largeBar
|
||||||
child: IconButton(
|
? () => widget.scaffoldKey.currentState?.openDrawer()
|
||||||
onPressed: !largeBar
|
: () => setState(() => expandedSideBar = !expandedSideBar),
|
||||||
? () => widget.scaffoldKey.currentState?.openDrawer()
|
icon: Icon(
|
||||||
: () => setState(() => expandedSideBar = !expandedSideBar),
|
largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu,
|
||||||
icon: Icon(
|
|
||||||
largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withValues(
|
||||||
|
alpha: largeBar && expandedSideBar ? 0.65 : 1,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -28,18 +28,19 @@ class _ClickableTextState extends ConsumerState<ClickableText> {
|
||||||
bool hovering = false;
|
bool hovering = false;
|
||||||
|
|
||||||
Widget _textWidget(bool showDecoration) {
|
Widget _textWidget(bool showDecoration) {
|
||||||
return Opacity(
|
final color =
|
||||||
opacity: widget.opacity,
|
(showDecoration ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface).withValues(
|
||||||
child: Text(
|
alpha: widget.opacity,
|
||||||
widget.text,
|
);
|
||||||
maxLines: widget.maxLines,
|
return Text(
|
||||||
overflow: widget.overflow,
|
widget.text,
|
||||||
style: widget.style?.copyWith(
|
maxLines: widget.maxLines,
|
||||||
color: showDecoration ? Theme.of(context).colorScheme.primary : null,
|
overflow: widget.overflow,
|
||||||
decoration: showDecoration ? TextDecoration.underline : TextDecoration.none,
|
style: widget.style?.copyWith(
|
||||||
decorationColor: showDecoration ? Theme.of(context).colorScheme.primary : null,
|
color: color,
|
||||||
decorationThickness: 3,
|
decoration: showDecoration ? TextDecoration.underline : TextDecoration.none,
|
||||||
),
|
decorationColor: color,
|
||||||
|
decorationThickness: 3,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,21 +53,30 @@ class HorizontalList<T> extends ConsumerStatefulWidget {
|
||||||
ConsumerState<ConsumerStatefulWidget> createState() => _HorizontalListState();
|
ConsumerState<ConsumerStatefulWidget> createState() => _HorizontalListState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HorizontalListState extends ConsumerState<HorizontalList> {
|
class _HorizontalListState extends ConsumerState<HorizontalList> with TickerProviderStateMixin {
|
||||||
final FocusNode parentNode = FocusNode();
|
final FocusNode parentNode = FocusNode();
|
||||||
FocusNode? lastFocused;
|
FocusNode? lastFocused;
|
||||||
final GlobalKey _firstItemKey = GlobalKey();
|
final GlobalKey _firstItemKey = GlobalKey();
|
||||||
|
final GlobalKey _listViewKey = GlobalKey();
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
final contentPadding = 8.0;
|
final contentPadding = 8.0;
|
||||||
double? contentWidth;
|
double? contentWidth;
|
||||||
double? _firstItemWidth;
|
double? _firstItemWidth;
|
||||||
|
|
||||||
|
AnimationController? _scrollAnimation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_measureFirstItem();
|
_measureFirstItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollAnimation?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _measureFirstItem() {
|
void _measureFirstItem() {
|
||||||
if (_firstItemWidth != null) return;
|
if (_firstItemWidth != null) return;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
|
@ -87,16 +96,34 @@ class _HorizontalListState extends ConsumerState<HorizontalList> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _scrollToPosition(int index) async {
|
Future<void> _scrollToPosition(int index) async {
|
||||||
if (_firstItemWidth == null) return;
|
if (_firstItemWidth == null || !_scrollController.hasClients) return;
|
||||||
|
|
||||||
final offset = index * (_firstItemWidth! + contentPadding);
|
final target = (index * (_firstItemWidth! + contentPadding)).clamp(0, _scrollController.position.maxScrollExtent);
|
||||||
final clamped = math.min(offset, _scrollController.position.maxScrollExtent);
|
|
||||||
|
|
||||||
await _scrollController.animateTo(
|
// Cancel any ongoing animation
|
||||||
clamped,
|
_scrollAnimation?.stop();
|
||||||
duration: const Duration(milliseconds: 250),
|
|
||||||
curve: Curves.fastOutSlowIn,
|
final controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 125),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_scrollAnimation = controller;
|
||||||
|
|
||||||
|
final tween = Tween<double>(
|
||||||
|
begin: _scrollController.offset,
|
||||||
|
end: target.toDouble(),
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addListener(() {
|
||||||
|
if (_scrollController.hasClients) {
|
||||||
|
_scrollController.jumpTo(tween.evaluate(controller));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.forward().whenComplete(() {
|
||||||
|
if (_scrollAnimation == controller) _scrollAnimation = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scrollToStart() {
|
void _scrollToStart() {
|
||||||
|
|
@ -146,12 +173,11 @@ class _HorizontalListState extends ConsumerState<HorizontalList> {
|
||||||
if (widget.subtext != null)
|
if (widget.subtext != null)
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ExcludeFocus(
|
child: ExcludeFocus(
|
||||||
child: Opacity(
|
child: Text(
|
||||||
opacity: 0.5,
|
widget.subtext!,
|
||||||
child: Text(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
widget.subtext!,
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -223,11 +249,16 @@ class _HorizontalListState extends ConsumerState<HorizontalList> {
|
||||||
|
|
||||||
if (currentNode != null) {
|
if (currentNode != null) {
|
||||||
lastFocused = currentNode;
|
lastFocused = currentNode;
|
||||||
|
final correctIndex = _getCorrectIndexForNode(currentNode);
|
||||||
|
|
||||||
if (widget.onFocused != null) {
|
if (widget.onFocused != null) {
|
||||||
widget.onFocused!(nodesOnSameRow.indexOf(currentNode));
|
if (correctIndex != -1) {
|
||||||
|
widget.onFocused!(correctIndex);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
context.ensureVisible();
|
context.ensureVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
currentNode.requestFocus();
|
currentNode.requestFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,25 +275,17 @@ class _HorizontalListState extends ConsumerState<HorizontalList> {
|
||||||
throttle: Throttler(duration: const Duration(milliseconds: 100)),
|
throttle: Throttler(duration: const Duration(milliseconds: 100)),
|
||||||
onFocused: (node) {
|
onFocused: (node) {
|
||||||
lastFocused = node;
|
lastFocused = node;
|
||||||
final nodesOnSameRow = _nodesInRow(parentNode);
|
final correctIndex = _getCorrectIndexForNode(node);
|
||||||
if (widget.onFocused != null) {
|
if (correctIndex != -1) {
|
||||||
widget.onFocused?.call(nodesOnSameRow.indexOf(node));
|
widget.onFocused?.call(correctIndex);
|
||||||
}
|
_scrollToPosition(correctIndex);
|
||||||
final nodeContext = node.context!;
|
|
||||||
final renderObject = nodeContext.findRenderObject();
|
|
||||||
if (renderObject != null) {
|
|
||||||
final position = _scrollController.position;
|
|
||||||
position.ensureVisible(
|
|
||||||
renderObject,
|
|
||||||
alignment: _calcAlignmentWithPadding(nodeContext),
|
|
||||||
duration: const Duration(milliseconds: 175),
|
|
||||||
curve: Curves.fastOutSlowIn,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
|
key: _listViewKey,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: widget.contentPadding,
|
padding: widget.contentPadding,
|
||||||
itemBuilder: (context, index) => index == widget.items.length
|
itemBuilder: (context, index) => index == widget.items.length
|
||||||
|
|
@ -286,10 +309,24 @@ class _HorizontalListState extends ConsumerState<HorizontalList> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double _calcAlignmentWithPadding(BuildContext context) {
|
int _getCorrectIndexForNode(FocusNode node) {
|
||||||
final viewportWidth = _scrollController.position.viewportDimension;
|
if (!mounted || _firstItemWidth == null || !_scrollController.hasClients || node.context == null) return -1;
|
||||||
final double leftPadding = widget.contentPadding.left + (contentPadding * 2);
|
|
||||||
return leftPadding / viewportWidth;
|
final scrollableContext = _listViewKey.currentContext;
|
||||||
|
if (scrollableContext == null || !scrollableContext.mounted) return -1;
|
||||||
|
|
||||||
|
final scrollableBox = scrollableContext.findRenderObject() as RenderBox?;
|
||||||
|
final itemBox = node.context!.findRenderObject() as RenderBox?;
|
||||||
|
if (scrollableBox == null || itemBox == null) return -1;
|
||||||
|
|
||||||
|
final dx = itemBox.localToGlobal(Offset.zero, ancestor: scrollableBox).dx;
|
||||||
|
|
||||||
|
final totalItemWidth = _firstItemWidth! + contentPadding;
|
||||||
|
final offset = dx + _scrollController.offset - widget.contentPadding.left;
|
||||||
|
|
||||||
|
final index = ((offset + totalItemWidth / 2) ~/ totalItemWidth).clamp(0, widget.items.length - 1);
|
||||||
|
|
||||||
|
return index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -344,12 +381,11 @@ class HorizontalRailFocus extends WidgetOrderTraversalPolicy {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool inDirection(FocusNode currentNode, TraversalDirection direction) {
|
bool inDirection(FocusNode currentNode, TraversalDirection direction) {
|
||||||
if (throttle?.canRun() == false) return true;
|
|
||||||
|
|
||||||
final rowNodes = _nodesInRow(parentNode);
|
final rowNodes = _nodesInRow(parentNode);
|
||||||
final index = rowNodes.indexOf(currentNode);
|
final index = rowNodes.indexOf(currentNode);
|
||||||
|
|
||||||
if (direction == TraversalDirection.left) {
|
if (direction == TraversalDirection.left) {
|
||||||
|
if (throttle?.canRun() == false) return true;
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
final target = rowNodes[index - 1];
|
final target = rowNodes[index - 1];
|
||||||
target.requestFocus();
|
target.requestFocus();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue