feat: кастомные обои чата (SVG-паттерны, темы, глобальный фон, прозрачные панели)
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class TiledSvgPattern extends StatefulWidget {
|
||||
final String asset;
|
||||
final Color color;
|
||||
final double opacity;
|
||||
final double tileSize;
|
||||
|
||||
const TiledSvgPattern({
|
||||
super.key,
|
||||
required this.asset,
|
||||
required this.color,
|
||||
this.opacity = 0.12,
|
||||
this.tileSize = 120,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TiledSvgPattern> createState() => _TiledSvgPatternState();
|
||||
}
|
||||
|
||||
class _TiledSvgPatternState extends State<TiledSvgPattern> {
|
||||
static final Map<String, ui.Image> _cache = {};
|
||||
static final Map<String, Future<ui.Image>> _pending = {};
|
||||
|
||||
ui.Image? _image;
|
||||
double _dpr = 1;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final dpr = MediaQuery.maybeOf(context)?.devicePixelRatio ?? 1;
|
||||
if (dpr != _dpr || _image == null) {
|
||||
_dpr = dpr;
|
||||
_resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TiledSvgPattern old) {
|
||||
super.didUpdateWidget(old);
|
||||
if (old.asset != widget.asset || old.tileSize != widget.tileSize) {
|
||||
_resolve();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resolve() async {
|
||||
final px = (widget.tileSize * _dpr).clamp(1, 4096).round();
|
||||
final key = '${widget.asset}@$px';
|
||||
final cached = _cache[key];
|
||||
if (cached != null) {
|
||||
if (_image != cached) setState(() => _image = cached);
|
||||
return;
|
||||
}
|
||||
final future = _pending.putIfAbsent(key, () => _rasterize(widget.asset, px));
|
||||
try {
|
||||
final image = await future;
|
||||
_cache[key] = image;
|
||||
_pending.remove(key);
|
||||
if (mounted) setState(() => _image = image);
|
||||
} catch (_) {
|
||||
_pending.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ui.Image> _rasterize(String asset, int px) async {
|
||||
final info = await vg.loadPicture(SvgAssetLoader(asset), null);
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
final size = info.size;
|
||||
if (size.width > 0 && size.height > 0) {
|
||||
canvas.scale(px / size.width, px / size.height);
|
||||
}
|
||||
canvas.drawPicture(info.picture);
|
||||
final picture = recorder.endRecording();
|
||||
final image = await picture.toImage(px, px);
|
||||
info.picture.dispose();
|
||||
picture.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final image = _image;
|
||||
if (image == null) return const SizedBox.expand();
|
||||
return CustomPaint(
|
||||
size: Size.infinite,
|
||||
painter: _PatternPainter(
|
||||
image: image,
|
||||
color: widget.color.withValues(alpha: widget.opacity),
|
||||
tileSize: widget.tileSize,
|
||||
dpr: _dpr,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PatternPainter extends CustomPainter {
|
||||
final ui.Image image;
|
||||
final Color color;
|
||||
final double tileSize;
|
||||
final double dpr;
|
||||
|
||||
const _PatternPainter({
|
||||
required this.image,
|
||||
required this.color,
|
||||
required this.tileSize,
|
||||
required this.dpr,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final s = 1 / dpr;
|
||||
final matrix = Matrix4.identity()..scaleByDouble(s, s, 1, 1);
|
||||
final paint = Paint()
|
||||
..shader = ImageShader(
|
||||
image,
|
||||
TileMode.repeated,
|
||||
TileMode.repeated,
|
||||
matrix.storage,
|
||||
)
|
||||
..colorFilter = ColorFilter.mode(color, BlendMode.srcIn);
|
||||
canvas.drawRect(Offset.zero & size, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_PatternPainter old) =>
|
||||
old.image != image ||
|
||||
old.color != color ||
|
||||
old.tileSize != tileSize ||
|
||||
old.dpr != dpr;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
import '../config/chat_wallpaper_themes.dart';
|
||||
import '../storage/chat_wallpaper_store.dart';
|
||||
|
||||
Future<Color?> computeWallpaperSeed(ChatWallpaper? wallpaper) async {
|
||||
if (wallpaper == null) return null;
|
||||
if (!wallpaper.isImage) {
|
||||
final theme = chatWallpaperThemeById(wallpaper.themeId);
|
||||
if (theme == null) return null;
|
||||
return _mostVivid(theme.colors);
|
||||
}
|
||||
final path = wallpaper.imagePath;
|
||||
if (path == null) return null;
|
||||
try {
|
||||
final bytes = await File(path).readAsBytes();
|
||||
final decoded = img.decodeImage(bytes);
|
||||
if (decoded == null) return null;
|
||||
final small = img.copyResize(decoded, width: 8, height: 8);
|
||||
var r = 0, g = 0, b = 0, n = 0;
|
||||
for (final pixel in small) {
|
||||
r += pixel.r.toInt();
|
||||
g += pixel.g.toInt();
|
||||
b += pixel.b.toInt();
|
||||
n++;
|
||||
}
|
||||
if (n == 0) return null;
|
||||
return Color.fromARGB(255, r ~/ n, g ~/ n, b ~/ n);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Color _mostVivid(List<Color> colors) {
|
||||
var best = colors.first;
|
||||
var bestScore = -1.0;
|
||||
for (final color in colors) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
final score = hsl.saturation * (1 - (hsl.lightness - 0.5).abs());
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = color;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
Reference in New Issue
Block a user