Files
Client/lib/widget/chat_background.dart
T

111 lines
3.4 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';
/// 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<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:
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;
}
}