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'; /// 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) { final s = context.watch().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: background = KeyedSubtree( // imageVersion changes on every replacement, forcing a fresh subtree // alongside the explicit ImageCache evict in the settings handler. key: ValueKey(s.imageVersion), child: _imageLayer( FileImage(File(AppPaths.chatBackgroundImage)), s, dark, isPattern: false, ), ); } return Stack( fit: StackFit.expand, children: [ Positioned.fill(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; } }