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:
PartyDonut 2025-10-03 13:57:54 +02:00 committed by GitHub
parent 5174bb3a6c
commit 951fc93633
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 335 additions and 235 deletions

View file

@ -1342,5 +1342,7 @@
"quickConnectPostFailed": "Failed to get quick connect code",
"quickConnectLoginUsingCode": "Using quick connect",
"quickConnectEnterCodeDescription": "Enter the code below to login",
"showMore": "Show more"
"showMore": "Show more",
"itemColorsTitle": "Item colors",
"itemColorsDesc": "Use item's primary color to theme the details page"
}

View file

@ -62,6 +62,7 @@ abstract class ClientSettingsModel with _$ClientSettingsModel {
Duration? nextUpDateCutoff,
@Default(ThemeMode.system) ThemeMode themeMode,
ColorThemes? themeColor,
@Default(true) bool deriveColorsFromItem,
@Default(false) bool amoledBlack,
@Default(true) bool blurPlaceHolders,
@Default(false) bool blurUpcomingEpisodes,

View file

@ -21,6 +21,7 @@ mixin _$ClientSettingsModel implements DiagnosticableTreeMixin {
Duration? get nextUpDateCutoff;
ThemeMode get themeMode;
ColorThemes? get themeColor;
bool get deriveColorsFromItem;
bool get amoledBlack;
bool get blurPlaceHolders;
bool get blurUpcomingEpisodes;
@ -63,6 +64,7 @@ mixin _$ClientSettingsModel implements DiagnosticableTreeMixin {
..add(DiagnosticsProperty('nextUpDateCutoff', nextUpDateCutoff))
..add(DiagnosticsProperty('themeMode', themeMode))
..add(DiagnosticsProperty('themeColor', themeColor))
..add(DiagnosticsProperty('deriveColorsFromItem', deriveColorsFromItem))
..add(DiagnosticsProperty('amoledBlack', amoledBlack))
..add(DiagnosticsProperty('blurPlaceHolders', blurPlaceHolders))
..add(DiagnosticsProperty('blurUpcomingEpisodes', blurUpcomingEpisodes))
@ -87,7 +89,7 @@ mixin _$ClientSettingsModel implements DiagnosticableTreeMixin {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)';
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, deriveColorsFromItem: $deriveColorsFromItem, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)';
}
}
@ -105,6 +107,7 @@ abstract mixin class $ClientSettingsModelCopyWith<$Res> {
Duration? nextUpDateCutoff,
ThemeMode themeMode,
ColorThemes? themeColor,
bool deriveColorsFromItem,
bool amoledBlack,
bool blurPlaceHolders,
bool blurUpcomingEpisodes,
@ -145,6 +148,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res>
Object? nextUpDateCutoff = freezed,
Object? themeMode = null,
Object? themeColor = freezed,
Object? deriveColorsFromItem = null,
Object? amoledBlack = null,
Object? blurPlaceHolders = null,
Object? blurUpcomingEpisodes = null,
@ -193,6 +197,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res>
? _self.themeColor
: themeColor // ignore: cast_nullable_to_non_nullable
as ColorThemes?,
deriveColorsFromItem: null == deriveColorsFromItem
? _self.deriveColorsFromItem
: deriveColorsFromItem // ignore: cast_nullable_to_non_nullable
as bool,
amoledBlack: null == amoledBlack
? _self.amoledBlack
: amoledBlack // ignore: cast_nullable_to_non_nullable
@ -370,6 +378,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel {
Duration? nextUpDateCutoff,
ThemeMode themeMode,
ColorThemes? themeColor,
bool deriveColorsFromItem,
bool amoledBlack,
bool blurPlaceHolders,
bool blurUpcomingEpisodes,
@ -402,6 +411,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel {
_that.nextUpDateCutoff,
_that.themeMode,
_that.themeColor,
_that.deriveColorsFromItem,
_that.amoledBlack,
_that.blurPlaceHolders,
_that.blurUpcomingEpisodes,
@ -448,6 +458,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel {
Duration? nextUpDateCutoff,
ThemeMode themeMode,
ColorThemes? themeColor,
bool deriveColorsFromItem,
bool amoledBlack,
bool blurPlaceHolders,
bool blurUpcomingEpisodes,
@ -479,6 +490,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel {
_that.nextUpDateCutoff,
_that.themeMode,
_that.themeColor,
_that.deriveColorsFromItem,
_that.amoledBlack,
_that.blurPlaceHolders,
_that.blurUpcomingEpisodes,
@ -524,6 +536,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel {
Duration? nextUpDateCutoff,
ThemeMode themeMode,
ColorThemes? themeColor,
bool deriveColorsFromItem,
bool amoledBlack,
bool blurPlaceHolders,
bool blurUpcomingEpisodes,
@ -555,6 +568,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel {
_that.nextUpDateCutoff,
_that.themeMode,
_that.themeColor,
_that.deriveColorsFromItem,
_that.amoledBlack,
_that.blurPlaceHolders,
_that.blurUpcomingEpisodes,
@ -591,6 +605,7 @@ class _ClientSettingsModel extends ClientSettingsModel
this.nextUpDateCutoff,
this.themeMode = ThemeMode.system,
this.themeColor,
this.deriveColorsFromItem = true,
this.amoledBlack = false,
this.blurPlaceHolders = true,
this.blurUpcomingEpisodes = false,
@ -634,6 +649,9 @@ class _ClientSettingsModel extends ClientSettingsModel
final ColorThemes? themeColor;
@override
@JsonKey()
final bool deriveColorsFromItem;
@override
@JsonKey()
final bool amoledBlack;
@override
@JsonKey()
@ -717,6 +735,7 @@ class _ClientSettingsModel extends ClientSettingsModel
..add(DiagnosticsProperty('nextUpDateCutoff', nextUpDateCutoff))
..add(DiagnosticsProperty('themeMode', themeMode))
..add(DiagnosticsProperty('themeColor', themeColor))
..add(DiagnosticsProperty('deriveColorsFromItem', deriveColorsFromItem))
..add(DiagnosticsProperty('amoledBlack', amoledBlack))
..add(DiagnosticsProperty('blurPlaceHolders', blurPlaceHolders))
..add(DiagnosticsProperty('blurUpcomingEpisodes', blurUpcomingEpisodes))
@ -741,7 +760,7 @@ class _ClientSettingsModel extends ClientSettingsModel
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)';
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, deriveColorsFromItem: $deriveColorsFromItem, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)';
}
}
@ -761,6 +780,7 @@ abstract mixin class _$ClientSettingsModelCopyWith<$Res>
Duration? nextUpDateCutoff,
ThemeMode themeMode,
ColorThemes? themeColor,
bool deriveColorsFromItem,
bool amoledBlack,
bool blurPlaceHolders,
bool blurUpcomingEpisodes,
@ -801,6 +821,7 @@ class __$ClientSettingsModelCopyWithImpl<$Res>
Object? nextUpDateCutoff = freezed,
Object? themeMode = null,
Object? themeColor = freezed,
Object? deriveColorsFromItem = null,
Object? amoledBlack = null,
Object? blurPlaceHolders = null,
Object? blurUpcomingEpisodes = null,
@ -849,6 +870,10 @@ class __$ClientSettingsModelCopyWithImpl<$Res>
? _self.themeColor
: themeColor // ignore: cast_nullable_to_non_nullable
as ColorThemes?,
deriveColorsFromItem: null == deriveColorsFromItem
? _self.deriveColorsFromItem
: deriveColorsFromItem // ignore: cast_nullable_to_non_nullable
as bool,
amoledBlack: null == amoledBlack
? _self.amoledBlack
: amoledBlack // ignore: cast_nullable_to_non_nullable

View file

@ -24,6 +24,7 @@ _ClientSettingsModel _$ClientSettingsModelFromJson(Map<String, dynamic> json) =>
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
ThemeMode.system,
themeColor: $enumDecodeNullable(_$ColorThemesEnumMap, json['themeColor']),
deriveColorsFromItem: json['deriveColorsFromItem'] as bool? ?? true,
amoledBlack: json['amoledBlack'] as bool? ?? false,
blurPlaceHolders: json['blurPlaceHolders'] as bool? ?? true,
blurUpcomingEpisodes: json['blurUpcomingEpisodes'] as bool? ?? false,
@ -64,6 +65,7 @@ Map<String, dynamic> _$ClientSettingsModelToJson(
'nextUpDateCutoff': instance.nextUpDateCutoff?.inMicroseconds,
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'themeColor': _$ColorThemesEnumMap[instance.themeColor],
'deriveColorsFromItem': instance.deriveColorsFromItem,
'amoledBlack': instance.amoledBlack,
'blurPlaceHolders': instance.blurPlaceHolders,
'blurUpcomingEpisodes': instance.blurUpcomingEpisodes,

View file

@ -39,6 +39,8 @@ class ClientSettingsNotifier extends StateNotifier<ClientSettingsModel> {
void setAmoledBlack(bool? value) => state = state.copyWith(amoledBlack: value ?? false);
void setDerivedColorsFromItem(bool? value) => state = state.copyWith(deriveColorsFromItem: value ?? false);
void setBlurPlaceholders(bool value) => state = state.copyWith(blurPlaceHolders: value);
void setTimeOut(Duration? duration) => state = state.copyWith(timeOut: duration);

View file

@ -113,5 +113,15 @@ List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value),
),
),
SettingsListTile(
label: Text(context.localized.itemColorsTitle),
subLabel: Text(context.localized.itemColorsDesc),
onTap: () =>
ref.read(clientSettingsProvider.notifier).setDerivedColorsFromItem(!clientSettings.deriveColorsFromItem),
trailing: Switch(
value: clientSettings.deriveColorsFromItem,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setDerivedColorsFromItem(value),
),
),
]);
}

View file

@ -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),
)),
],
),
),
),
],
),
),
),
],
),
),
);
}),
);
}
}

View file

@ -1373,6 +1373,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.1"
palette_generator_master:
dependency: "direct main"
description:
name: palette_generator_master
sha256: "2b27a3d9f773c5bc407ed828589488777f73fa23cd24abe6a9b90249a41a7df0"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
path:
dependency: "direct main"
description:

View file

@ -91,6 +91,7 @@ dependencies:
overflow_view: ^0.5.0
flutter_sticky_header: ^0.8.0
markdown_widget: ^2.3.2+8
palette_generator_master: ^1.0.1
# Navigation
auto_route: ^10.1.2
@ -202,6 +203,7 @@ flutter:
- assets/fonts/
- config/
- assets/mp-font.ttf
- assets/
fonts:
- family: Rubik