mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07:00
feat: Option to use item's primary colors in details screen (#509)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
5174bb3a6c
commit
951fc93633
9 changed files with 335 additions and 235 deletions
|
|
@ -3,9 +3,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:palette_generator_master/palette_generator_master.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/images_models.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
|
|
@ -23,6 +25,16 @@ import 'package:fladder/widgets/shared/item_actions.dart';
|
|||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
|
||||
Future<Color?> getDominantColor(ImageProvider imageProvider) async {
|
||||
final paletteGenerator = await PaletteGeneratorMaster.fromImageProvider(
|
||||
imageProvider,
|
||||
size: const Size(200, 200),
|
||||
maximumColorCount: 20,
|
||||
);
|
||||
|
||||
return paletteGenerator.dominantColor?.color;
|
||||
}
|
||||
|
||||
class DetailScaffold extends ConsumerStatefulWidget {
|
||||
final String label;
|
||||
final ItemBaseModel? item;
|
||||
|
|
@ -49,18 +61,41 @@ class DetailScaffold extends ConsumerStatefulWidget {
|
|||
class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
|
||||
List<ImageData>? lastImages;
|
||||
ImageData? backgroundImage;
|
||||
Color? dominantColor;
|
||||
|
||||
ImageProvider? _lastRequestedImage;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant DetailScaffold oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
updateImage();
|
||||
_updateDominantColor();
|
||||
}
|
||||
|
||||
void updateImage() {
|
||||
if (lastImages == null) {
|
||||
lastImages = widget.backDrops?.backDrop;
|
||||
setState(() {
|
||||
backgroundImage = widget.backDrops?.randomBackDrop;
|
||||
});
|
||||
backgroundImage = widget.backDrops?.randomBackDrop;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateDominantColor() async {
|
||||
if (!ref.read(clientSettingsProvider.select((value) => value.deriveColorsFromItem))) return;
|
||||
final newImage = widget.item?.getPosters?.logo;
|
||||
if (newImage == null) return;
|
||||
|
||||
final provider = newImage.imageProvider;
|
||||
_lastRequestedImage = provider;
|
||||
|
||||
final newColor = await getDominantColor(provider);
|
||||
|
||||
if (!mounted || _lastRequestedImage != provider) return;
|
||||
|
||||
setState(() {
|
||||
dominantColor = newColor;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
|
|
@ -69,250 +104,263 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
|
|||
final minHeight = 450.0.clamp(0, size.height).toDouble();
|
||||
final maxHeight = size.height - 10;
|
||||
final sideBarPadding = AdaptiveLayout.of(context).sideBarWidth;
|
||||
return PullToRefresh(
|
||||
onRefresh: () async {
|
||||
await widget.onRefresh?.call();
|
||||
setState(() {
|
||||
if (context.mounted) {
|
||||
if (widget.backDrops?.backDrop?.contains(backgroundImage) == true) {
|
||||
backgroundImage = widget.backDrops?.randomBackDrop;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
refreshOnStart: true,
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: maxHeight,
|
||||
width: size.width,
|
||||
child: FladderImage(
|
||||
image: backgroundImage,
|
||||
blurOnly: true,
|
||||
),
|
||||
),
|
||||
if (backgroundImage != null)
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: sideBarPadding),
|
||||
child: ShaderMask(
|
||||
shaderCallback: (bounds) => LinearGradient(
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: dominantColor != null
|
||||
? ColorScheme.fromSeed(
|
||||
seedColor: dominantColor!,
|
||||
brightness: Theme.brightnessOf(context),
|
||||
dynamicSchemeVariant: ref.watch(clientSettingsProvider.select((value) => value.schemeVariant)),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Builder(builder: (context) {
|
||||
return PullToRefresh(
|
||||
onRefresh: () async {
|
||||
await widget.onRefresh?.call();
|
||||
setState(() {
|
||||
if (context.mounted) {
|
||||
if (widget.backDrops?.backDrop?.contains(backgroundImage) == true) {
|
||||
backgroundImage = widget.backDrops?.randomBackDrop;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
refreshOnStart: true,
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: maxHeight,
|
||||
width: size.width,
|
||||
child: FladderImage(
|
||||
image: backgroundImage,
|
||||
blurOnly: true,
|
||||
),
|
||||
),
|
||||
if (backgroundImage != null)
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: sideBarPadding),
|
||||
child: ShaderMask(
|
||||
shaderCallback: (bounds) => LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white.withValues(alpha: 0),
|
||||
],
|
||||
).createShader(bounds),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: double.infinity,
|
||||
minHeight: minHeight - 20,
|
||||
maxHeight: maxHeight.clamp(minHeight, 2500) - 20,
|
||||
),
|
||||
child: FadeInImage(
|
||||
placeholder: ResizeImage(
|
||||
backgroundImage!.imageProvider,
|
||||
height: maxHeight ~/ 1.5,
|
||||
),
|
||||
placeholderColor: Colors.transparent,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
placeholderFit: BoxFit.cover,
|
||||
excludeFromSemantics: true,
|
||||
image: ResizeImage(
|
||||
backgroundImage!.imageProvider,
|
||||
height: maxHeight ~/ 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: maxHeight + 10,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white.withValues(alpha: 0),
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0),
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.10),
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.35),
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.85),
|
||||
Theme.of(context).colorScheme.surface,
|
||||
],
|
||||
).createShader(bounds),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
color: widget.backgroundColor,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 0,
|
||||
top: MediaQuery.of(context).padding.top,
|
||||
),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: double.infinity,
|
||||
minHeight: minHeight - 20,
|
||||
maxHeight: maxHeight.clamp(minHeight, 2500) - 20,
|
||||
minHeight: size.height,
|
||||
maxWidth: size.width,
|
||||
),
|
||||
child: FadeInImage(
|
||||
placeholder: ResizeImage(
|
||||
backgroundImage!.imageProvider,
|
||||
height: maxHeight ~/ 1.5,
|
||||
),
|
||||
placeholderColor: Colors.transparent,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
placeholderFit: BoxFit.cover,
|
||||
excludeFromSemantics: true,
|
||||
image: ResizeImage(
|
||||
backgroundImage!.imageProvider,
|
||||
height: maxHeight ~/ 1.5,
|
||||
child: widget.content(
|
||||
padding.copyWith(
|
||||
left: sideBarPadding + 25 + MediaQuery.paddingOf(context).left,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: maxHeight + 10,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0),
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.10),
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.35),
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.85),
|
||||
Theme.of(context).colorScheme.surface,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
color: widget.backgroundColor,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 0,
|
||||
top: MediaQuery.of(context).padding.top,
|
||||
),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: size.height,
|
||||
maxWidth: size.width,
|
||||
),
|
||||
child: widget.content(
|
||||
padding.copyWith(
|
||||
left: sideBarPadding + 25 + MediaQuery.paddingOf(context).left,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
//Top row buttons
|
||||
if (AdaptiveLayout.of(context).viewSize < ViewSize.desktop)
|
||||
IconTheme(
|
||||
data: IconThemeData(color: Theme.of(context).colorScheme.onSurface),
|
||||
child: Padding(
|
||||
padding: MediaQuery.paddingOf(context)
|
||||
.copyWith(left: sideBarPadding + MediaQuery.paddingOf(context).left)
|
||||
.add(
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: backGroundColor,
|
||||
),
|
||||
onPressed: () => context.router.popBack(),
|
||||
icon: Padding(
|
||||
padding:
|
||||
EdgeInsets.all(AdaptiveLayout.of(context).inputDevice == InputDevice.pointer ? 0 : 4),
|
||||
child: const BackButtonIcon(),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backGroundColor, borderRadius: FladderTheme.defaultShape.borderRadius),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.item != null) ...[
|
||||
ref.watch(syncedItemProvider(widget.item)).when(
|
||||
error: (error, stackTrace) => const SizedBox.shrink(),
|
||||
data: (syncedItem) {
|
||||
if (syncedItem == null &&
|
||||
ref.read(userProvider.select(
|
||||
(value) => value?.canDownload ?? false,
|
||||
)) &&
|
||||
widget.item?.syncAble == true) {
|
||||
return IconButton(
|
||||
onPressed: () =>
|
||||
ref.read(syncProvider.notifier).addSyncItem(context, widget.item!),
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.arrow_down_2,
|
||||
),
|
||||
);
|
||||
} else if (syncedItem != null) {
|
||||
return IconButton(
|
||||
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
|
||||
icon: SyncButton(item: widget.item!, syncedItem: syncedItem),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final newActions = widget.actions?.call(context);
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) {
|
||||
return PopupMenuButton(
|
||||
tooltip: context.localized.moreOptions,
|
||||
enabled: newActions?.isNotEmpty == true,
|
||||
icon: Icon(
|
||||
widget.item!.type.icon,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
itemBuilder: (context) => newActions?.popupMenuItems(useIcons: true) ?? [],
|
||||
);
|
||||
} else {
|
||||
return IconButton(
|
||||
onPressed: () => showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
children: newActions?.listTileItems(context, useIcons: true) ?? [],
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
widget.item!.type.icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer)
|
||||
Builder(
|
||||
builder: (context) => Tooltip(
|
||||
message: context.localized.refresh,
|
||||
child: IconButton(
|
||||
onPressed: () => context.refreshData(),
|
||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single ||
|
||||
AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: const SizedBox(
|
||||
height: 30,
|
||||
width: 30,
|
||||
child: SettingsUserIcon(),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single)
|
||||
Tooltip(
|
||||
message: context.localized.home,
|
||||
child: IconButton(
|
||||
onPressed: () => context.navigateTo(const DashboardRoute()),
|
||||
icon: const Icon(IconsaxPlusLinear.home),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
//Top row buttons
|
||||
if (AdaptiveLayout.of(context).viewSize < ViewSize.desktop)
|
||||
IconTheme(
|
||||
data: IconThemeData(color: Theme.of(context).colorScheme.onSurface),
|
||||
child: Padding(
|
||||
padding: MediaQuery.paddingOf(context)
|
||||
.copyWith(left: sideBarPadding + MediaQuery.paddingOf(context).left)
|
||||
.add(
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: backGroundColor,
|
||||
),
|
||||
onPressed: () => context.router.popBack(),
|
||||
icon: Padding(
|
||||
padding:
|
||||
EdgeInsets.all(AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer ? 0 : 4),
|
||||
child: const BackButtonIcon(),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backGroundColor, borderRadius: FladderTheme.defaultShape.borderRadius),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.item != null) ...[
|
||||
ref.watch(syncedItemProvider(widget.item)).when(
|
||||
error: (error, stackTrace) => const SizedBox.shrink(),
|
||||
data: (syncedItem) {
|
||||
if (syncedItem == null &&
|
||||
ref.read(userProvider.select(
|
||||
(value) => value?.canDownload ?? false,
|
||||
)) &&
|
||||
widget.item?.syncAble == true) {
|
||||
return IconButton(
|
||||
onPressed: () =>
|
||||
ref.read(syncProvider.notifier).addSyncItem(context, widget.item!),
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.arrow_down_2,
|
||||
),
|
||||
);
|
||||
} else if (syncedItem != null) {
|
||||
return IconButton(
|
||||
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
|
||||
icon: SyncButton(item: widget.item!, syncedItem: syncedItem),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final newActions = widget.actions?.call(context);
|
||||
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) {
|
||||
return PopupMenuButton(
|
||||
tooltip: context.localized.moreOptions,
|
||||
enabled: newActions?.isNotEmpty == true,
|
||||
icon: Icon(
|
||||
widget.item!.type.icon,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
itemBuilder: (context) => newActions?.popupMenuItems(useIcons: true) ?? [],
|
||||
);
|
||||
} else {
|
||||
return IconButton(
|
||||
onPressed: () => showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
children: newActions?.listTileItems(context, useIcons: true) ?? [],
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
widget.item!.type.icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer)
|
||||
Builder(
|
||||
builder: (context) => Tooltip(
|
||||
message: context.localized.refresh,
|
||||
child: IconButton(
|
||||
onPressed: () => context.refreshData(),
|
||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single ||
|
||||
AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: const SizedBox(
|
||||
height: 30,
|
||||
width: 30,
|
||||
child: SettingsUserIcon(),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single)
|
||||
Tooltip(
|
||||
message: context.localized.home,
|
||||
child: IconButton(
|
||||
onPressed: () => context.navigateTo(const DashboardRoute()),
|
||||
icon: const Icon(IconsaxPlusLinear.home),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue