feat: Add on/off/blurred options to the background image (#442)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-08-09 09:23:47 +02:00 committed by GitHub
parent ef6780b412
commit 715e707bb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 109 additions and 63 deletions

View file

@ -1316,5 +1316,6 @@
"type": "String" "type": "String"
} }
} }
} },
"blurred": "Blurred"
} }

View file

@ -28,6 +28,28 @@ enum GlobalHotKeys {
} }
} }
enum BackgroundType {
disabled,
enabled,
blurred;
const BackgroundType();
double get opacityValues => switch (this) {
BackgroundType.disabled => 1.0,
BackgroundType.enabled => 0.7,
BackgroundType.blurred => 0.5,
};
String label(BuildContext context) {
return switch (this) {
BackgroundType.disabled => context.localized.off,
BackgroundType.enabled => context.localized.enabled,
BackgroundType.blurred => context.localized.blurred,
};
}
}
@Freezed(copyWith: true) @Freezed(copyWith: true)
class ClientSettingsModel with _$ClientSettingsModel { class ClientSettingsModel with _$ClientSettingsModel {
const ClientSettingsModel._(); const ClientSettingsModel._();
@ -52,7 +74,7 @@ class ClientSettingsModel with _$ClientSettingsModel {
@Default(false) bool showAllCollectionTypes, @Default(false) bool showAllCollectionTypes,
@Default(2) int maxConcurrentDownloads, @Default(2) int maxConcurrentDownloads,
@Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant, @Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant,
@Default(true) bool backgroundPosters, @Default(BackgroundType.blurred) BackgroundType backgroundImage,
@Default(true) bool checkForUpdates, @Default(true) bool checkForUpdates,
@Default(false) bool usePosterForLibrary, @Default(false) bool usePosterForLibrary,
String? lastViewedUpdate, String? lastViewedUpdate,

View file

@ -40,7 +40,7 @@ mixin _$ClientSettingsModel {
bool get showAllCollectionTypes => throw _privateConstructorUsedError; bool get showAllCollectionTypes => throw _privateConstructorUsedError;
int get maxConcurrentDownloads => throw _privateConstructorUsedError; int get maxConcurrentDownloads => throw _privateConstructorUsedError;
DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError; DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError;
bool get backgroundPosters => throw _privateConstructorUsedError; BackgroundType get backgroundImage => throw _privateConstructorUsedError;
bool get checkForUpdates => throw _privateConstructorUsedError; bool get checkForUpdates => throw _privateConstructorUsedError;
bool get usePosterForLibrary => throw _privateConstructorUsedError; bool get usePosterForLibrary => throw _privateConstructorUsedError;
String? get lastViewedUpdate => throw _privateConstructorUsedError; String? get lastViewedUpdate => throw _privateConstructorUsedError;
@ -84,7 +84,7 @@ abstract class $ClientSettingsModelCopyWith<$Res> {
bool showAllCollectionTypes, bool showAllCollectionTypes,
int maxConcurrentDownloads, int maxConcurrentDownloads,
DynamicSchemeVariant schemeVariant, DynamicSchemeVariant schemeVariant,
bool backgroundPosters, BackgroundType backgroundImage,
bool checkForUpdates, bool checkForUpdates,
bool usePosterForLibrary, bool usePosterForLibrary,
String? lastViewedUpdate, String? lastViewedUpdate,
@ -126,7 +126,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
Object? showAllCollectionTypes = null, Object? showAllCollectionTypes = null,
Object? maxConcurrentDownloads = null, Object? maxConcurrentDownloads = null,
Object? schemeVariant = null, Object? schemeVariant = null,
Object? backgroundPosters = null, Object? backgroundImage = null,
Object? checkForUpdates = null, Object? checkForUpdates = null,
Object? usePosterForLibrary = null, Object? usePosterForLibrary = null,
Object? lastViewedUpdate = freezed, Object? lastViewedUpdate = freezed,
@ -210,10 +210,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
? _value.schemeVariant ? _value.schemeVariant
: schemeVariant // ignore: cast_nullable_to_non_nullable : schemeVariant // ignore: cast_nullable_to_non_nullable
as DynamicSchemeVariant, as DynamicSchemeVariant,
backgroundPosters: null == backgroundPosters backgroundImage: null == backgroundImage
? _value.backgroundPosters ? _value.backgroundImage
: backgroundPosters // ignore: cast_nullable_to_non_nullable : backgroundImage // ignore: cast_nullable_to_non_nullable
as bool, as BackgroundType,
checkForUpdates: null == checkForUpdates checkForUpdates: null == checkForUpdates
? _value.checkForUpdates ? _value.checkForUpdates
: checkForUpdates // ignore: cast_nullable_to_non_nullable : checkForUpdates // ignore: cast_nullable_to_non_nullable
@ -266,7 +266,7 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res>
bool showAllCollectionTypes, bool showAllCollectionTypes,
int maxConcurrentDownloads, int maxConcurrentDownloads,
DynamicSchemeVariant schemeVariant, DynamicSchemeVariant schemeVariant,
bool backgroundPosters, BackgroundType backgroundImage,
bool checkForUpdates, bool checkForUpdates,
bool usePosterForLibrary, bool usePosterForLibrary,
String? lastViewedUpdate, String? lastViewedUpdate,
@ -306,7 +306,7 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
Object? showAllCollectionTypes = null, Object? showAllCollectionTypes = null,
Object? maxConcurrentDownloads = null, Object? maxConcurrentDownloads = null,
Object? schemeVariant = null, Object? schemeVariant = null,
Object? backgroundPosters = null, Object? backgroundImage = null,
Object? checkForUpdates = null, Object? checkForUpdates = null,
Object? usePosterForLibrary = null, Object? usePosterForLibrary = null,
Object? lastViewedUpdate = freezed, Object? lastViewedUpdate = freezed,
@ -390,10 +390,10 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
? _value.schemeVariant ? _value.schemeVariant
: schemeVariant // ignore: cast_nullable_to_non_nullable : schemeVariant // ignore: cast_nullable_to_non_nullable
as DynamicSchemeVariant, as DynamicSchemeVariant,
backgroundPosters: null == backgroundPosters backgroundImage: null == backgroundImage
? _value.backgroundPosters ? _value.backgroundImage
: backgroundPosters // ignore: cast_nullable_to_non_nullable : backgroundImage // ignore: cast_nullable_to_non_nullable
as bool, as BackgroundType,
checkForUpdates: null == checkForUpdates checkForUpdates: null == checkForUpdates
? _value.checkForUpdates ? _value.checkForUpdates
: checkForUpdates // ignore: cast_nullable_to_non_nullable : checkForUpdates // ignore: cast_nullable_to_non_nullable
@ -442,7 +442,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
this.showAllCollectionTypes = false, this.showAllCollectionTypes = false,
this.maxConcurrentDownloads = 2, this.maxConcurrentDownloads = 2,
this.schemeVariant = DynamicSchemeVariant.rainbow, this.schemeVariant = DynamicSchemeVariant.rainbow,
this.backgroundPosters = true, this.backgroundImage = BackgroundType.blurred,
this.checkForUpdates = true, this.checkForUpdates = true,
this.usePosterForLibrary = false, this.usePosterForLibrary = false,
this.lastViewedUpdate, this.lastViewedUpdate,
@ -510,7 +510,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
final DynamicSchemeVariant schemeVariant; final DynamicSchemeVariant schemeVariant;
@override @override
@JsonKey() @JsonKey()
final bool backgroundPosters; final BackgroundType backgroundImage;
@override @override
@JsonKey() @JsonKey()
final bool checkForUpdates; final bool checkForUpdates;
@ -532,7 +532,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 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, backgroundPosters: $backgroundPosters, 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, 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)';
} }
@override @override
@ -561,7 +561,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
..add( ..add(
DiagnosticsProperty('maxConcurrentDownloads', maxConcurrentDownloads)) DiagnosticsProperty('maxConcurrentDownloads', maxConcurrentDownloads))
..add(DiagnosticsProperty('schemeVariant', schemeVariant)) ..add(DiagnosticsProperty('schemeVariant', schemeVariant))
..add(DiagnosticsProperty('backgroundPosters', backgroundPosters)) ..add(DiagnosticsProperty('backgroundImage', backgroundImage))
..add(DiagnosticsProperty('checkForUpdates', checkForUpdates)) ..add(DiagnosticsProperty('checkForUpdates', checkForUpdates))
..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary)) ..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary))
..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate)) ..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate))
@ -607,7 +607,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
final bool showAllCollectionTypes, final bool showAllCollectionTypes,
final int maxConcurrentDownloads, final int maxConcurrentDownloads,
final DynamicSchemeVariant schemeVariant, final DynamicSchemeVariant schemeVariant,
final bool backgroundPosters, final BackgroundType backgroundImage,
final bool checkForUpdates, final bool checkForUpdates,
final bool usePosterForLibrary, final bool usePosterForLibrary,
final String? lastViewedUpdate, final String? lastViewedUpdate,
@ -659,7 +659,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
@override @override
DynamicSchemeVariant get schemeVariant; DynamicSchemeVariant get schemeVariant;
@override @override
bool get backgroundPosters; BackgroundType get backgroundImage;
@override @override
bool get checkForUpdates; bool get checkForUpdates;
@override @override

View file

@ -41,7 +41,9 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
schemeVariant: $enumDecodeNullable( schemeVariant: $enumDecodeNullable(
_$DynamicSchemeVariantEnumMap, json['schemeVariant']) ?? _$DynamicSchemeVariantEnumMap, json['schemeVariant']) ??
DynamicSchemeVariant.rainbow, DynamicSchemeVariant.rainbow,
backgroundPosters: json['backgroundPosters'] as bool? ?? true, backgroundImage: $enumDecodeNullable(
_$BackgroundTypeEnumMap, json['backgroundImage']) ??
BackgroundType.blurred,
checkForUpdates: json['checkForUpdates'] as bool? ?? true, checkForUpdates: json['checkForUpdates'] as bool? ?? true,
usePosterForLibrary: json['usePosterForLibrary'] as bool? ?? false, usePosterForLibrary: json['usePosterForLibrary'] as bool? ?? false,
lastViewedUpdate: json['lastViewedUpdate'] as String?, lastViewedUpdate: json['lastViewedUpdate'] as String?,
@ -78,7 +80,7 @@ Map<String, dynamic> _$$ClientSettingsModelImplToJson(
'showAllCollectionTypes': instance.showAllCollectionTypes, 'showAllCollectionTypes': instance.showAllCollectionTypes,
'maxConcurrentDownloads': instance.maxConcurrentDownloads, 'maxConcurrentDownloads': instance.maxConcurrentDownloads,
'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!, 'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!,
'backgroundPosters': instance.backgroundPosters, 'backgroundImage': _$BackgroundTypeEnumMap[instance.backgroundImage]!,
'checkForUpdates': instance.checkForUpdates, 'checkForUpdates': instance.checkForUpdates,
'usePosterForLibrary': instance.usePosterForLibrary, 'usePosterForLibrary': instance.usePosterForLibrary,
'lastViewedUpdate': instance.lastViewedUpdate, 'lastViewedUpdate': instance.lastViewedUpdate,
@ -123,6 +125,12 @@ const _$DynamicSchemeVariantEnumMap = {
DynamicSchemeVariant.fruitSalad: 'fruitSalad', DynamicSchemeVariant.fruitSalad: 'fruitSalad',
}; };
const _$BackgroundTypeEnumMap = {
BackgroundType.disabled: 'disabled',
BackgroundType.enabled: 'enabled',
BackgroundType.blurred: 'blurred',
};
const _$GlobalHotKeysEnumMap = { const _$GlobalHotKeysEnumMap = {
GlobalHotKeys.search: 'search', GlobalHotKeys.search: 'search',
GlobalHotKeys.exit: 'exit', GlobalHotKeys.exit: 'exit',

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/l10n/generated/app_localizations.dart'; import 'package:fladder/l10n/generated/app_localizations.dart';
import 'package:fladder/models/settings/client_settings_model.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
@ -90,13 +91,18 @@ List<Widget> buildClientSettingsVisual(
SettingsListTile( SettingsListTile(
label: Text(context.localized.enableBackgroundPostersTitle), label: Text(context.localized.enableBackgroundPostersTitle),
subLabel: Text(context.localized.enableBackgroundPostersDesc), subLabel: Text(context.localized.enableBackgroundPostersDesc),
onTap: () => ref trailing: EnumBox(
.read(clientSettingsProvider.notifier) current: clientSettings.backgroundImage.label(context),
.update((cb) => cb.copyWith(backgroundPosters: !clientSettings.backgroundPosters)), itemBuilder: (context) => BackgroundType.values
trailing: Switch( .map(
value: clientSettings.backgroundPosters, (e) => PopupMenuItem(
onChanged: (value) => value: e,
ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(backgroundPosters: value)), child: Text(e.label(context)),
onTap: () =>
ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(backgroundImage: e)),
),
)
.toList(),
), ),
), ),
SettingsListTile( SettingsListTile(

View file

@ -96,34 +96,37 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
if (backgroundImage != null) if (backgroundImage != null)
Align( Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: ShaderMask( child: Padding(
shaderCallback: (bounds) => LinearGradient( padding: EdgeInsets.only(left: sideBarPadding),
begin: Alignment.topCenter, child: ShaderMask(
end: Alignment.bottomCenter, shaderCallback: (bounds) => LinearGradient(
colors: [ begin: Alignment.topCenter,
Colors.white, end: Alignment.bottomCenter,
Colors.white, colors: [
Colors.white, Colors.white,
Colors.white, Colors.white,
Colors.white, Colors.white,
Colors.white.withValues(alpha: 0), Colors.white,
], Colors.white,
).createShader(bounds), Colors.white.withValues(alpha: 0),
child: ConstrainedBox( ],
constraints: BoxConstraints( ).createShader(bounds),
minWidth: double.infinity, child: ConstrainedBox(
minHeight: minHeight - 20, constraints: BoxConstraints(
maxHeight: maxHeight.clamp(minHeight, 2500) - 20, minWidth: double.infinity,
), minHeight: minHeight - 20,
child: FadeInImage( maxHeight: maxHeight.clamp(minHeight, 2500) - 20,
placeholder: backgroundImage!.imageProvider, ),
placeholderColor: Colors.transparent, child: FadeInImage(
fit: BoxFit.cover, placeholder: backgroundImage!.imageProvider,
alignment: Alignment.topCenter, placeholderColor: Colors.transparent,
placeholderFit: BoxFit.cover, fit: BoxFit.cover,
excludeFromSemantics: true, alignment: Alignment.topCenter,
placeholderFilterQuality: FilterQuality.low, placeholderFit: BoxFit.cover,
image: backgroundImage!.imageProvider, excludeFromSemantics: true,
placeholderFilterQuality: FilterQuality.low,
image: backgroundImage!.imageProvider,
),
), ),
), ),
), ),

View file

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
class NestedScaffold extends ConsumerWidget { class NestedScaffold extends ConsumerWidget {
final Widget body; final Widget body;
final Widget? background; final Widget? background;
@ -13,6 +15,7 @@ class NestedScaffold extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final backgroundOpacity = ref.watch(clientSettingsProvider.select((value) => value.backgroundImage.opacityValues));
return Stack( return Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
@ -23,8 +26,8 @@ class NestedScaffold extends ConsumerWidget {
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [
Theme.of(context).colorScheme.surface.withValues(alpha: 0.85), Theme.of(context).colorScheme.surface.withValues(alpha: backgroundOpacity),
Theme.of(context).colorScheme.surface.withValues(alpha: 0.7), Theme.of(context).colorScheme.surface.withValues(alpha: backgroundOpacity - 0.15),
], ],
), ),
), ),

View file

@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/settings/client_settings_model.dart';
import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/fladder_image.dart';
@ -36,7 +37,8 @@ class _BackgroundImageState extends ConsumerState<BackgroundImage> {
} }
void updateItems() { void updateItems() {
final enabled = ref.read(clientSettingsProvider.select((value) => value.backgroundPosters)); final enabled =
ref.read(clientSettingsProvider.select((value) => value.backgroundImage != BackgroundType.disabled));
WidgetsBinding.instance.addPostFrameCallback((value) async { WidgetsBinding.instance.addPostFrameCallback((value) async {
if (!enabled && mounted) return; if (!enabled && mounted) return;
@ -69,12 +71,13 @@ class _BackgroundImageState extends ConsumerState<BackgroundImage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final enabled = ref.watch(clientSettingsProvider.select((value) => value.backgroundPosters)); final state = ref.watch(clientSettingsProvider.select((value) => value.backgroundImage));
final enabled = state != BackgroundType.disabled;
return enabled return enabled
? FladderImage( ? FladderImage(
image: backgroundImage, image: backgroundImage,
fit: BoxFit.cover, fit: BoxFit.cover,
blurOnly: false, blurOnly: state == BackgroundType.blurred,
) )
: const SizedBox.shrink(); : const SizedBox.shrink();
} }