127 lines
4.2 KiB
Dart
127 lines
4.2 KiB
Dart
import 'dart:io';
|
|
import 'dart:ui' show ImageFilter, TileMode;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../state/app/modules/settings/bloc/settings_cubit.dart';
|
|
import '../storage/chat_background_settings.dart';
|
|
import '../theming/app_theme.dart';
|
|
import '../utils/app_paths.dart';
|
|
import '../utils/screen_bound_image.dart';
|
|
|
|
/// Renders the configurable chat background behind [child].
|
|
///
|
|
/// Layering (bottom → top): the background source (pattern / image / colour /
|
|
/// none), an optional black dim overlay, then [child]. The pattern source
|
|
/// keeps the legacy dark-mode colour inversion; user images never invert.
|
|
/// Reads [ChatBackgroundSettings] reactively, so the in-app live preview and
|
|
/// the real chat update together as the user drags the sliders.
|
|
class ChatBackground extends StatelessWidget {
|
|
final Widget child;
|
|
const ChatBackground({required this.child, super.key});
|
|
|
|
static const _fallbackColor = Color(0xffefeae2);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Value snapshot: the settings object is mutated in place, so only copied
|
|
// values can tell whether the background actually changed.
|
|
context.select((SettingsCubit c) {
|
|
final s = c.state.chatBackgroundSettings;
|
|
return (s.type, s.fit, s.colorValue, s.imageVersion, s.dim, s.blur);
|
|
});
|
|
final s = context.read<SettingsCubit>().val().chatBackgroundSettings;
|
|
final dark = AppTheme.isDarkMode(context);
|
|
|
|
final Widget background;
|
|
switch (s.type) {
|
|
case ChatBackgroundType.none:
|
|
background = ColoredBox(color: Theme.of(context).colorScheme.surface);
|
|
case ChatBackgroundType.color:
|
|
background = ColoredBox(
|
|
color: Color(s.colorValue ?? _fallbackColor.toARGB32()),
|
|
);
|
|
case ChatBackgroundType.pattern:
|
|
background = _imageLayer(
|
|
const AssetImage('assets/background/chat.png'),
|
|
s,
|
|
dark,
|
|
isPattern: true,
|
|
);
|
|
case ChatBackgroundType.image:
|
|
final image = FileImage(File(AppPaths.chatBackgroundImage));
|
|
background = KeyedSubtree(
|
|
// imageVersion changes on every replacement, forcing a fresh subtree
|
|
// alongside the explicit ImageCache evict in the settings handler.
|
|
key: ValueKey(s.imageVersion),
|
|
// Only "cover" scales the photo to the screen; tile/center render
|
|
// it unscaled, so those keep the natural size.
|
|
child: _imageLayer(
|
|
s.fit == ChatBackgroundFit.cover
|
|
? screenBoundImage(context, image)
|
|
: image,
|
|
s,
|
|
dark,
|
|
isPattern: false,
|
|
),
|
|
);
|
|
}
|
|
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
// Own layer: without it every scroll frame of the message list above
|
|
// repaints the (possibly tiled or blurred) background as well.
|
|
Positioned.fill(child: RepaintBoundary(child: background)),
|
|
if (s.dim > 0)
|
|
Positioned.fill(
|
|
child: ColoredBox(color: Colors.black.withValues(alpha: s.dim)),
|
|
),
|
|
child,
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _imageLayer(
|
|
ImageProvider provider,
|
|
ChatBackgroundSettings s,
|
|
bool dark, {
|
|
required bool isPattern,
|
|
}) {
|
|
final tiled = s.fit == ChatBackgroundFit.tile;
|
|
final fit = switch (s.fit) {
|
|
ChatBackgroundFit.cover => BoxFit.cover,
|
|
ChatBackgroundFit.center => BoxFit.none,
|
|
ChatBackgroundFit.tile => null,
|
|
};
|
|
|
|
Widget layer = DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
image: DecorationImage(
|
|
image: provider,
|
|
fit: fit,
|
|
alignment: Alignment.center,
|
|
// The legacy pattern was rendered at scale 1.5 while tiling.
|
|
scale: isPattern && tiled ? 1.5 : 1.0,
|
|
repeat: tiled ? ImageRepeat.repeat : ImageRepeat.noRepeat,
|
|
invertColors: isPattern && dark,
|
|
),
|
|
),
|
|
);
|
|
|
|
if (s.blur > 0) {
|
|
layer = ImageFiltered(
|
|
// mirror keeps cover-fit edges filled instead of bleeding to transparent.
|
|
imageFilter: ImageFilter.blur(
|
|
sigmaX: s.blur,
|
|
sigmaY: s.blur,
|
|
tileMode: TileMode.mirror,
|
|
),
|
|
child: layer,
|
|
);
|
|
}
|
|
return layer;
|
|
}
|
|
}
|